kit

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

native.c (250345B)


      1 /* aa64 NativeTarget production-readiness checklist:
      2  * - ABI completeness: finish AAPCS64/Linux va_list and register-save-area
      3  *   lowering, verify Apple/AAPCS64/Windows arm64 differences, handle all
      4  *   homogeneous aggregates, indirect/byval/sret corner cases, small aggregate
      5  *   splitting, multi-register returns, stack alignment, and ABI diagnostics.
      6  * - Calls and returns: replace call-plus-return tail handling with true direct
      7  *   and indirect sibling calls, preserve musttail ABI guarantees, support stack
      8  *   argument reshuffling without clobbering live inputs, and cover all sret,
      9  *   variadic, FP, aggregate, and many-argument combinations.
     10  * - Frame lowering: implement known-frame/prologue integration for optimized
     11  *   emission, spill/reload hooks, callee-save tracking for integer and FP/SIMD
     12  *   registers, large-frame probing/materialization as needed by each platform,
     13  *   dynamic alloca restoration, and unwind/debug frame metadata.
     14  * - Operations and intrinsics: fill remaining scalar, FP, conversion, rounding,
     15  *   overflow, bit, vector/SIMD, trap, prefetch, and target-specific intrinsics;
     16  *   validate NaN/ordered/unordered FP compare semantics and integer narrowing
     17  *   behavior for every supported width.
     18  * - Aggregates and memory: support large constants, overlap-safe memmove,
     19  *   optimized bulk copy/set selection, bitfield load/store, packed/unaligned
     20  *   accesses, volatile access constraints, and record/slice edge cases across
     21  *   direct and optimized lowering.
     22  * - Atomics: replace ordinary load/store RMW/CAS sequences with correct LL/SC
     23  *   or LSE loops, implement acquire/release/seq_cst mappings precisely, handle
     24  *   failure ordering, byte/halfword/word/dword widths, and retry/clobber rules.
     25  * - Inline and file-scope asm: complete register/memory/immediate constraints,
     26  *   named operands, tied operands, early-clobber and clobber validation, hard
     27  *   register conflicts, memory barriers, outputs for aggregates/FP values, and
     28  *   file-scope asm integration. */
     29 
     30 #include <string.h>
     31 
     32 #include "abi/abi.h"
     33 #include "arch/aa64/aa64.h"
     34 #include "arch/aa64/asm.h"
     35 #include "arch/aa64/isa.h"
     36 #include "arch/aa64/regs.h"
     37 #include "asm/asm.h"
     38 #include "asm/asm_lex.h"
     39 #include "cg/native_argmove.h"
     40 #include "cg/native_asm.h"
     41 #include "cg/native_direct_target.h"
     42 #include "cg/native_frame.h"
     43 #include "cg/type.h"
     44 #include "core/arena.h"
     45 #include "core/bytes.h"
     46 #include "core/core.h"
     47 #include "core/pool.h"
     48 #include "core/slice.h"
     49 #include "obj/obj.h"
     50 
     51 #if defined(__GNUC__) || defined(__clang__)
     52 #define AA_UNUSED_FN __attribute__((unused))
     53 #else
     54 #define AA_UNUSED_FN
     55 #endif
     56 
     57 enum {
     58   AA_X8 = 8u, /* indirect-result (sret) register; usable as a copy base that
     59                  aa_copy_bytes (which scratches only x16/x17) never clobbers */
     60   /* Tail epilogues restore x30 and may use x16/x17 while rebuilding the frame,
     61    * so an indirect tail target is moved here only after argument marshalling.
     62    * x9 is an instruction-scoped emitter temp and no later operand phase runs
     63    * between marshal_call and the tail branch. */
     64   AA_TAIL_TARGET = 9u,
     65   AA_TMP0 = 16u,
     66   AA_TMP1 = 17u,
     67   /* Callee-saved frame-base anchor. The single-pass far-slot fast path
     68    * (aa_emit_mem / AA_PATCH_SLOT) addresses fixed slots at positive scaled
     69    * offsets from a stable bottom anchor: sp itself when the function never
     70    * moves sp, else this register, set to the frame base in the prologue and
     71    * saved / restored like a callee-save (only when the function calls alloca).
     72    */
     73   AA_FRAME_BASE = 28u,
     74   AA_FP = 29u,
     75   AA_LR = 30u,
     76   AA_SP = 31u,
     77   AA_FRAME_SAVE_SIZE = 16u,
     78   /* Worst-case reserved prologue region (NDT single-pass path patches it in
     79    * place; the optimizer path reserves exactly what it emits). Sized to hold
     80    * the fat prologue plus the Windows large-frame stack probe (≤7 words, see
     81    * aa_words_stack_probe). */
     82   /* Worst-case prologue/epilogue word counts. The W1.1 bottom-record general
     83    * path can stack: a Windows page probe (≤7), the robust `sub sp` (≤5), the
     84    * far saved-pair address build + stp (≤3), the x29 anchor (≤3), a callee-save
     85    * base build for huge frames (≤4), and up to 18 callee-save store/restores
     86    * (~5 int stp pairs + an int single + 8 fp singles ≈ 14 insns) — round up. */
     87   AA_PROLOGUE_WORDS = 48u,
     88   AA_TAIL_WORDS = 48u,
     89   /* Single-pass (-O0) tcc-style prologue. The frame-independent entry
     90    * `stp x29,x30,[sp,#-(16+top_home)]! ; mov x29,sp` (AA_NDT_FIXED_ENTRY_WORDS)
     91    * is emitted live in aa_func_begin; only the `sub sp` that grows the rest of
     92    * the frame below the saved pair is deferred (patched once frame_size is
     93    * final), plus — on stack-probing targets (Windows) — the page probe. The
     94    * single-pass path uses no callee-saved registers (enforced in
     95    * nd_scratch_acquire), so nothing else is deferred except the optional
     96    * alloca frame-base anchor setup: worst-case sub = load_imm(2)+3 = 5; +probe
     97    * load_imm(2)+5 = 7; +frame-base save/mov = 2. */
     98   AA_NDT_FIXED_ENTRY_WORDS = 2u,
     99   AA_NDT_SUB_WORDS = 14u,        /* worst case (probe + sub + frame-base) */
    100   AA_NDT_SUB_WORDS_NOPROBE = 7u, /* reserved region when no stack probe */
    101 };
    102 
    103 /* Windows/AArch64 TLS Local-Exec. The TEB pointer lives in the reserved
    104  * platform register x18 (never allocated; see AA_PHYS_INT_RESERVED(18)), and
    105  * the thread's TLS-array pointer (TEB.ThreadLocalStoragePointer) sits at
    106  * TEB+0x58 — same offset as on Win64/x86-64. */
    107 enum {
    108   AA_WIN_TEB_REG = 18u,
    109   AA_WIN_TEB_TLS_PTR_OFF = 0x58u,
    110 };
    111 
    112 /* ============================================================================
    113  * AAPCS64 frame layout
    114  *
    115  * Two layouts. Every fp- or sp-relative offset in this file is computed via one
    116  * of the aa_fp_off_ / aa_sp_off_ helpers below — no site does bare arithmetic
    117  * on AA_FP / AA_SP, and no site outside those helpers branches on the layout.
    118  *
    119  * TOP-RECORD (default — single-pass -O0, fat frames, and out_stack>0 small
    120  * frames). fp anchors at the caller's saved-pair address near the top; sp at
    121  * the bottom of the outgoing-arg area. Offsets are frame-size-independent.
    122  *
    123  *   high addr   caller's stack frame
    124  *               +------------------------------+
    125  *               | incoming stack args          |  aa_fp_off_in_arg(a,i) = 16+i
    126  *               +------------------------------+
    127  *      fp  -->  | saved x29 (prev fp)          |  aa_fp_off_saved_fp() = 0
    128  *               | saved x30 (prev lr)          |  aa_fp_off_saved_lr() = 8
    129  *               +------------------------------+
    130  *               | frame slots                  |  aa_fp_off_slot(a,off) = -off
    131  *               |   (callee-saves + locals     |
    132  *               |    + spills + sret/variadic) |
    133  *               +------------------------------+
    134  *               | outgoing args                |  aa_sp_off_out_arg(i)
    135  *      sp  -->  +------------------------------+
    136  *   low addr                                       CFA = fp + 16
    137  *
    138  * BOTTOM-RECORD (fp_at_bottom — the W1.1 uniform x29-at-bottom layout: every
    139  * known frame on the -O1 path EXCEPT slim Tier A and the Windows-variadic
    140  * top_home case). The frame record + slots stack ABOVE x29, anchored just above
    141  * the outgoing-arg area: x29 = sp + out_stack. Every slot is then a positive
    142  * `ldr/str [x29,#k]` — one instruction within the 32 KB scaled reach (add-build
    143  * `add x16,x29,#hi ; ldr [x16,#lo]` only past it), replacing the top-record
    144  * `sub x17,x29,#k ; ldur` fallback. x29 is the frame pointer (reserved,
    145  * alloca-stable, set once) so alloca falls out for free and outgoing args stay
    146  * sp-relative (calls after an alloca still address [sp,#k]). Offsets depend on
    147  * frame_size (hence known-frame only, where the frame is final before the body).
    148  * Let N = frame_size, os = out_stack:
    149  *
    150  *   high addr   caller's stack frame
    151  *               +------------------------------+
    152  *               | incoming stack args          |  aa_fp_off_in_arg(a,i)=(N-os)+i
    153  *               +------------------------------+   <- caller's sp = CFA = x29+(N-os)
    154  *               | frame slots (+ align pad)    |  aa_fp_off_slot(a,off)=(N-os)-off
    155  *               |   (callee-saves + locals …)  |     (in [16, N-os), above record)
    156  *               +------------------------------+
    157  *  x29 = sp+os->| saved x29 (prev fp)          |  aa_fp_off_saved_fp() = 0
    158  *               | saved x30 (prev lr)          |  aa_fp_off_saved_lr() = 8
    159  *               +------------------------------+
    160  *               | outgoing args (os)           |  aa_sp_off_out_arg(i)  ([sp,#i])
    161  *      sp  -->  +------------------------------+
    162  *   low addr
    163  *
    164  * frame_size (N) = align16(AA_FRAME_SAVE_SIZE + slot_bytes + out_stack).
    165  * The saved pair sits AT x29 so [x29]=caller fp, [x29+8]=caller lr — the
    166  * frame-pointer chain kit's unwinder / __kit_backtrace walks (uniform
    167  * fp[0]/fp[1]); non-negotiable. When os==0 this reduces to fp = sp and the
    168  * earlier folded `stp [sp,#-N]! ; mov x29,sp` encoding (the original
    169  * fp_at_bottom fast path). Tail calls write outgoing args into the caller's
    170  * incoming-args window — physically the same address, expressed via
    171  * aa_fp_off_tail_out_arg.
    172  * ========================================================================== */
    173 
    174 typedef struct AAFrameLayout {
    175   u32 slot_bytes; /* sum of aa_frame_slot reservations (callee-saves + locals
    176                    * + spills + sret/variadic) */
    177   u32 out_stack;  /* max outgoing-arg bytes across all calls in this function */
    178   u32 top_home;   /* Windows-variadic GP register home area, reserved between
    179                    * the saved pair and the incoming stack args so the
    180                    * plain-pointer va_list walks register then stack varargs as
    181                    * one contiguous block (0 on every other ABI). */
    182   u32 frame_size; /* align16(AA_FRAME_SAVE_SIZE + top_home + slot_bytes +
    183                    * out_stack) */
    184 } AAFrameLayout;
    185 
    186 static inline AAFrameLayout aa_build_layout(u32 slot_bytes, u32 out_stack,
    187                                             u32 top_home) {
    188   AAFrameLayout L;
    189   L.slot_bytes = slot_bytes;
    190   L.out_stack = out_stack;
    191   L.top_home = top_home;
    192   L.frame_size =
    193       align_up_u32(AA_FRAME_SAVE_SIZE + top_home + slot_bytes + out_stack, 16u);
    194   return L;
    195 }
    196 
    197 /* FP-relative byte offsets. The saved-pair is at [fp]/[fp+8] in both the
    198  * top-record and bottom-record (fp_at_bottom) layouts, so these two are
    199  * layout-independent. The frame-size-dependent helpers — aa_fp_off_in_arg,
    200  * aa_fp_off_slot, aa_fp_off_tail_out_arg — branch on a->fp_at_bottom and are
    201  * defined after AANativeTarget (see aa_fp_off_* below aa_of). */
    202 static inline i32 aa_fp_off_saved_fp(void) { return 0; }
    203 static inline i32 aa_fp_off_saved_lr(void) { return 8; }
    204 
    205 /* SP-relative byte offsets. */
    206 static inline i32 aa_sp_off_out_arg(u32 byte_off) { return (i32)byte_off; }
    207 static inline u32 aa_sp_off_saved_pair(const AAFrameLayout* L) {
    208   return L->frame_size - AA_FRAME_SAVE_SIZE - L->top_home;
    209 }
    210 
    211 /* Frame slots and callee-save records are owned by the shared NativeFrame
    212  * bookkeeping (cg/native_frame.h); these aliases keep the aa64-local spellings.
    213  */
    214 typedef NativeFrameSlotEntry AANativeSlot;
    215 
    216 /* Deferred in-function patches, all resolved in aa_func_end once the frame
    217  * layout (max_outgoing, callee-saves) is final. One growable list carries both
    218  * kinds; each entry patches a disjoint, fixed code position, so insertion order
    219  * is irrelevant. The prologue region is patched separately (exactly one per
    220  * function, fixed position) and is not a list entry. */
    221 typedef enum AAPatchKind {
    222   AA_PATCH_ALLOCA, /* single instr: add dst, sp, #max_outgoing */
    223   AA_PATCH_TAIL,   /* AA_TAIL_WORDS region: callee restores + frame + br/b */
    224   AA_PATCH_SLOT,   /* single instr: far fixed-slot ldr/str at a positive scaled
    225                     * offset from the frame base (sp, or AA_FRAME_BASE under
    226                     * alloca), resolved once frame_size is final. */
    227 } AAPatchKind;
    228 
    229 /* A deferred far fixed-slot access. The body emits a one-word placeholder
    230  * `ldr/str [sp,#0]`; aa_apply_patches rewrites the base register and scaled
    231  * offset once the frame is final. slot_off + extra is the slot's byte position
    232  * (as fed to aa_fp_off_slot); the positive frame-base offset is
    233  * aa_sp_off_saved_pair(L) + aa_fp_off_slot(slot)+extra (top-record). */
    234 typedef struct AASlotPatch {
    235   u32 slot_off; /* aa_slot(...)->off */
    236   i32 extra;    /* addr.offset added to the slot base */
    237   u8 sz;        /* size_idx: 0 (byte) / 1 (half) / 2 (word) / 3 (dword). sz<2
    238                  * reserves a 2nd placeholder word (overflow fallback). */
    239   u8 vbit;      /* SIMD/FP register (always 0 for sz<2) */
    240   u8 load;      /* 1 = ldr, 0 = str */
    241   u8 rt;        /* transfer register */
    242 } AASlotPatch;
    243 
    244 typedef struct AAPatch {
    245   AAPatchKind kind;
    246   u32 pos;
    247   union {
    248     u32 dst_reg;      /* AA_PATCH_ALLOCA */
    249     NativeLoc callee; /* AA_PATCH_TAIL */
    250     AASlotPatch slot; /* AA_PATCH_SLOT */
    251   } u;
    252 } AAPatch;
    253 
    254 typedef NativeFrameCalleeSave AACalleeSave;
    255 
    256 typedef struct AANativeTarget {
    257   NativeTarget base;
    258   SrcLoc loc;
    259   const CGFuncDesc* func;
    260 
    261   /* Shared frame bookkeeping: slot table, cumulative offset, max-outgoing,
    262    * callee-save set, and the known_frame / has_alloca / frame_final flags. */
    263   NativeFrame frame;
    264   /* Final frame size, set once in aa_func_begin_known_frame when fp_at_bottom
    265    * is decided. Read by the fp-relative offset helpers in the bottom-record
    266    * layout (where slot/incoming-arg offsets depend on frame_size); meaningless
    267    * and unread on the single-pass path, which never sets fp_at_bottom. */
    268   u32 frame_size_final;
    269   /* Final outgoing-arg area (= max_outgoing), set with frame_size_final in
    270    * aa_func_begin_known_frame. In the bottom-record layout x29 is anchored
    271    * `out_stack` bytes above sp, so every fp-relative offset is measured from
    272    * `frame_size - out_stack` (the bytes above the anchor). Zero on the
    273    * single-pass path (top-record) and on os==0 known frames (where the formula
    274    * reduces to the original fp_at_bottom one). */
    275   u32 out_stack_final;
    276   u32 incoming_stack_size;
    277   /* Windows-variadic GP register home area size (gp_reg_count * gp_slot_size,
    278    * 64 today; 0 on every other ABI). When nonzero the function takes the fat
    279    * top-record layout and homes x0..x7 into [fp + AA_FRAME_SAVE_SIZE ..] so the
    280    * plain-pointer va_list can walk register then stack varargs contiguously. */
    281   u32 top_home_bytes;
    282   u32 next_param_int;
    283   u32 next_param_fp;
    284   u32 next_param_stack;
    285   NativeFrameSlot sret_ptr_slot;
    286   NativeFrameSlot saved_tmp_slot;
    287   NativeFrameSlot va_gr_slot;
    288   NativeFrameSlot va_vr_slot;
    289 
    290   AAPatch* patches;
    291   u32 npatches;
    292   u32 patches_cap;
    293   u32 nalloca; /* count of AA_PATCH_ALLOCA entries; gates slim prologue/frame */
    294 
    295   u32 func_start;
    296   u32 prologue_pos;
    297   u32 prologue_region_words;  /* single-pass: reserved deferred-`sub` region */
    298   u32 minimal_prologue_words; /* opt path: exact prologue length, else 0 */
    299   MCLabel epilogue_label;
    300 
    301   /* Set at func_end when this function qualifies for the slim prologue/epilogue
    302    * (Tier A: no body locals/spills, no callee-saves, no alloca, no outgoing
    303    * stack args, no sret/variadic). When set, the prologue patch and epilogue
    304    * emit a 2-insn `stp x29,x30,[sp,#-16]! ; mov x29,sp` and matching `ldp
    305    * x29,x30,[sp],#16 ; ret` instead of the fat 4+3-insn FP-frame form. */
    306   u8 slim_prologue;
    307   /* Set by aa_func_begin_known_frame for the W1.1 uniform x29-at-bottom layout:
    308    * every known frame except slim Tier A and the Windows-variadic top_home
    309    * case. x29 is anchored out_stack bytes above sp with the saved pair AT x29;
    310    * every slot is a positive `ldr/str [x29,#k]`, slots/callee-saves stack ABOVE
    311    * the record, incoming args at x29 + (frame_size - out_stack) = CFA. Covers
    312    * out_stack>0, alloca (x29 is stable across the floated sp), and arbitrarily
    313    * large frames. Two prologue/epilogue encodings: a folded `stp [sp,#-N]!`
    314    * fast path when out_stack==0 && frame_size<=504 && !alloca, and the general
    315    * `sub sp ; stp [sp,#os] ; add x29,sp,#os` / `mov x16,x29 ; ldp [x16] ;
    316    * add sp,x16,#(N-os)` form otherwise. The frame-size-dependent offsets are
    317    * the reason this is only available on the known-frame path (frame final
    318    * before the body). Mutually exclusive with slim_prologue (Tier A). */
    319   u8 fp_at_bottom;
    320   /* L10: a stricter case of slim Tier A — a true frameless leaf. Set by
    321    * aa_func_begin_known_frame when, on top of the slim conditions (no
    322    * callee-saves, no alloca, no body slots, no outgoing stack args, no
    323    * sret/variadic), the function is also a LEAF (no call of any kind, so x30/LR
    324    * is never clobbered), contains no inline asm (which could clobber LR or make
    325    * a call opaquely), and never reads its own frame chain
    326    * (__builtin_frame_address / __builtin_return_address). Such a function needs
    327    * no frame record at all: the prologue/epilogue emit nothing but `ret`, and
    328    * the CFA stays at sp with the return address live in LR (the aa64 CIE
    329    * default). Implies slim_prologue=0 and fp_at_bottom=0. */
    330   u8 frameless;
    331 
    332   /* Single-pass far-slot fast path (Lever 1 / Fix B). When set, fixed-slot
    333    * loads/stores whose top-record fp offset falls outside stur's ±256 range are
    334    * emitted as a one-word positive scaled `ldr/str [base,#scaled]` placeholder
    335    * plus an AA_PATCH_SLOT, instead of the multi-insn `sub xN,x29,#off ; ldur`
    336    * address build. The base is a stable bottom anchor: sp (the common case) or
    337    * AA_FRAME_BASE when the function moves sp via alloca. Decided at func_begin
    338    * (off when a Windows GP home area shifts the layout); the frame-base
    339    * fallback is armed lazily on the first alloca. Only 4/8-byte int/fp slots
    340    * take this path — byte/half stay on the existing path (their positive scaled
    341    * reach, 4 KB/8 KB, is too small to guarantee a one-word patch). */
    342   u8 slot_sp_base;
    343   u8 uses_frame_base; /* an alloca fired: patch AA_PATCH_SLOT against
    344                        * AA_FRAME_BASE and set it up in the prologue. */
    345   NativeFrameSlot frame_base_slot; /* home for the saved caller AA_FRAME_BASE */
    346 } AANativeTarget;
    347 
    348 static AANativeTarget* aa_of(NativeTarget* t) { return (AANativeTarget*)t; }
    349 
    350 /* Layout-aware FP-relative offsets. Every frame use site goes through these;
    351  * the fp_at_bottom test lives here and nowhere else.
    352  *
    353  *   top-record (default):  record near the top, fp anchored at the saved pair.
    354  *     incoming args at fp+16+b, slots below fp at -off.  CFA = fp+16.
    355  *   bottom-record (fp_at_bottom): x29 = sp + out_stack, the saved pair AT x29,
    356  *     slots above the record at (frame_size - out_stack) - off, incoming args
    357  *     at x29 + (frame_size - out_stack).  CFA = x29 + (frame_size - out_stack).
    358  *     The "bytes above the anchor" is bsz = frame_size - out_stack = align16(16
    359  *     + slot_bytes) padding aside, so slots land in [16, bsz) and never overlap
    360  *     the 16-byte record. When out_stack==0, bsz = frame_size (fp = sp). */
    361 static inline u32 aa_fp_bottom_above(const AANativeTarget* a) {
    362   return a->frame_size_final - a->out_stack_final;
    363 }
    364 static inline i32 aa_fp_off_in_arg(const AANativeTarget* a, u32 byte_off) {
    365   /* top-record incoming args sit above the saved pair and the (usually empty)
    366    * Windows-variadic GP home area; bottom-record never carries a home area. */
    367   u32 base = a->fp_at_bottom ? aa_fp_bottom_above(a)
    368                              : AA_FRAME_SAVE_SIZE + a->top_home_bytes;
    369   return (i32)(base + byte_off);
    370 }
    371 static inline i32 aa_fp_off_slot(const AANativeTarget* a, u32 slot_off) {
    372   return a->fp_at_bottom ? (i32)aa_fp_bottom_above(a) - (i32)slot_off
    373                          : -(i32)slot_off;
    374 }
    375 /* Outgoing stack args on a tail call land in the caller's incoming-arg window —
    376  * the same physical address the tail-callee will read via aa_fp_off_in_arg.
    377  * Same helper, distinct name for site-side intent. */
    378 static inline i32 aa_fp_off_tail_out_arg(const AANativeTarget* a,
    379                                          u32 byte_off) {
    380   return aa_fp_off_in_arg(a, byte_off);
    381 }
    382 /* CFA = caller's sp, expressed as an fp-relative offset (fp+16 top-record,
    383  * x29 + (frame_size - out_stack) bottom-record). Named so the CFI emit site
    384  * stays layout-blind. */
    385 static inline i32 aa_cfa_off(const AANativeTarget* a) {
    386   return a->fp_at_bottom ? (i32)aa_fp_bottom_above(a)
    387                          : (i32)(AA_FRAME_SAVE_SIZE + a->top_home_bytes);
    388 }
    389 
    390 /* fp-relative offset of GP home slot `i` (Windows variadic only). The home area
    391  * sits just above the saved pair and just below the incoming stack args, so
    392  * slot gp_reg_count coincides with incoming-arg byte 0 (top-record only — a
    393  * function with a home area never takes a slim/bottom layout). */
    394 static inline i32 aa_fp_off_home_slot(u32 i) {
    395   return (i32)(AA_FRAME_SAVE_SIZE + i * 8u);
    396 }
    397 
    398 static _Noreturn void aa_panic(AANativeTarget* a, const char* msg) {
    399   compiler_panic(a->base.c, a->loc, "aarch64 native target: %s", msg);
    400 }
    401 
    402 /* Declared locally rather than pulling in debug/debug.h, keeping the
    403  * backend's dependency on the Debug producer to this one entry point —
    404  * same pattern as the x64/rv64 emit TUs (see arch/mc.h). */
    405 extern void debug_emit_row(Debug*, ObjSecId text_section, u32 offset, SrcLoc);
    406 extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs,
    407                                 u32 end_ofs);
    408 
    409 static void aa_emit32(MCEmitter* mc, u32 word) {
    410   /* obj_pos is a section lookup + buf_pos; only the -g line table needs it, so
    411    * skip it on the common no-debug compile (one fewer lookup per instruction).
    412    * The pre-write offset must be read BEFORE mc_emit32 advances the cursor. */
    413   if (mc->debug) {
    414     u32 ofs = obj_pos(mc->obj, mc->section_id);
    415     mc_emit32(mc, word);
    416     debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
    417   } else {
    418     mc_emit32(mc, word);
    419   }
    420 }
    421 
    422 static void aa_patch32(ObjBuilder* obj, ObjSecId sec, u32 off, u32 word) {
    423   u8 b[4];
    424   wr_u32_le(b, word);
    425   obj_patch(obj, sec, off, b, sizeof b);
    426 }
    427 
    428 static u32 type_size32(NativeTarget* t, KitCgTypeId type) {
    429   u64 n = type ? cg_type_size(t->c, type) : 8u;
    430   if (n == 0) n = 8u;
    431   if (n > 16u)
    432     compiler_panic(t->c, (SrcLoc){0, 0, 0},
    433                    "aarch64 native target: scalar too large");
    434   return (u32)n;
    435 }
    436 
    437 static u32 type_align32(NativeTarget* t, KitCgTypeId type) {
    438   u64 n = type ? cg_type_align(t->c, type) : 8u;
    439   if (n == 0) n = 1u;
    440   if (n > 16u) n = 16u;
    441   return (u32)n;
    442 }
    443 
    444 static u32 size_idx(u32 n) {
    445   if (n <= 1u) return 0u;
    446   if (n <= 2u) return 1u;
    447   if (n <= 4u) return 2u;
    448   return 3u;
    449 }
    450 
    451 static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0x1fu; }
    452 
    453 /* Scalar byte width of a register operand. Reads the NDT-stamped szinfo
    454  * descriptor (one byte) when present, else falls back to the live type query —
    455  * so partial adoption stays byte-identical. */
    456 static u32 loc_size32(NativeTarget* t, NativeLoc loc) {
    457   if (loc.szinfo & NATIVE_SZINFO_VALID) return native_szinfo_size(loc.szinfo);
    458   return type_size32(t, loc.type);
    459 }
    460 
    461 /* The original predicate exactly, but sourcing the width from the cached
    462  * descriptor when stamped: `size == 8 || is_ptr`. is_ptr is short-circuited
    463  * away whenever the cached size is already 8 (the pointer case on this arch),
    464  * so the common path becomes a single byte test. Byte-identical to the old
    465  * type_size32+cg_type_is_ptr form. */
    466 static int loc_is_64(NativeTarget* t, NativeLoc loc) {
    467   return loc_size32(t, loc) == 8u || cg_type_is_ptr(t->c, loc.type);
    468 }
    469 
    470 /* native_loc_is_fp is shared in native_target.h. */
    471 
    472 static int aa_use_got_for_sym(NativeTarget* t, ObjSymId sym) {
    473   return obj_symbol_extern_via_got(t->c, t->obj, sym);
    474 }
    475 
    476 static RelocKind aa_ldst_reloc_for_size(u32 size) {
    477   switch (size) {
    478     case 0:
    479       return R_AARCH64_LDST8_ABS_LO12_NC;
    480     case 1:
    481       return R_AARCH64_LDST16_ABS_LO12_NC;
    482     case 2:
    483       return R_AARCH64_LDST32_ABS_LO12_NC;
    484     case 3:
    485       return R_AARCH64_LDST64_ABS_LO12_NC;
    486     default:
    487       return R_AARCH64_LDST64_ABS_LO12_NC;
    488   }
    489 }
    490 
    491 static u32 aa_load_imm_words(u32* out, u32 cap, u32 sf, u32 rd, i64 imm) {
    492   u64 v = (u64)imm;
    493   u32 words = sf ? 4u : 2u;
    494   u32 n = 0;
    495   for (u32 i = 0; i < words; ++i) {
    496     u32 part = (u32)((v >> (i * 16u)) & 0xffffu);
    497     if (!part && n) continue;
    498     if (n >= cap) return 0;
    499     out[n] = n ? aa64_movk(sf, rd, part, i) : aa64_movz(sf, rd, part, i);
    500     ++n;
    501   }
    502   if (!n) {
    503     if (!cap) return 0;
    504     out[n++] = aa64_movz(sf, rd, 0, 0);
    505   }
    506   return n;
    507 }
    508 
    509 static void aa_emit_load_imm(MCEmitter* mc, u32 sf, u32 rd, i64 imm) {
    510   u32 words[4];
    511   u32 n = aa_load_imm_words(words, 4u, sf, rd, imm);
    512   for (u32 i = 0; i < n; ++i) aa_emit32(mc, words[i]);
    513 }
    514 
    515 static void aa_emit_add_imm(AANativeTarget* a, u32 rd, u32 rn, i32 off) {
    516   u32 imm12, sh;
    517   MCEmitter* mc = a->base.mc;
    518   if (off >= 0 && aa64_addsub_imm_fits(off, &imm12, &sh)) {
    519     aa_emit32(mc, aa64_add_imm(1, rd, rn, imm12, sh));
    520     return;
    521   }
    522   if (off < 0 && aa64_addsub_imm_fits(-(i64)off, &imm12, &sh)) {
    523     aa_emit32(mc, aa64_sub_imm(1, rd, rn, imm12, sh));
    524     return;
    525   }
    526   u32 tmp = rd == rn ? (rd == AA_TMP0 ? AA_TMP1 : AA_TMP0) : rd;
    527   aa_emit_load_imm(mc, 1, tmp, off);
    528   aa_emit32(mc, aa64_add(1, rd, rn, tmp));
    529 }
    530 
    531 static void aa_emit_add_i64(AANativeTarget* a, u32 rd, u32 rn, i64 off) {
    532   u32 imm12, sh;
    533   MCEmitter* mc = a->base.mc;
    534   if (off >= 0 && aa64_addsub_imm_fits(off, &imm12, &sh)) {
    535     aa_emit32(mc, aa64_add_imm(1, rd, rn, imm12, sh));
    536     return;
    537   }
    538   if (off < 0 && aa64_addsub_imm_fits(-off, &imm12, &sh)) {
    539     aa_emit32(mc, aa64_sub_imm(1, rd, rn, imm12, sh));
    540     return;
    541   }
    542   u32 tmp = rd == rn ? (rd == AA_TMP0 ? AA_TMP1 : AA_TMP0) : rd;
    543   aa_emit_load_imm(mc, 1, tmp, off);
    544   aa_emit32(mc, aa64_add(1, rd, rn, tmp));
    545 }
    546 
    547 /* Unscaled load with an explicit load opcode (AA64_LDST_OPC_LDR for a plain
    548  * zero-extending load, AA64_LDST_OPC_LDRS_X for a sign-extending ldursb/ldursh
    549  * into the X register). */
    550 static u32 aa_ldur_op_v(u32 size, u32 v, u32 ld_opc, u32 rt, u32 rn,
    551                         i32 simm9) {
    552   return aa64_ldst_simm9_pack((AA64LdStSimm9){.size = size,
    553                                               .V = v,
    554                                               .opc = ld_opc,
    555                                               .imm9 = (u32)simm9 & 0x1ffu,
    556                                               .Rn = rn,
    557                                               .Rt = rt});
    558 }
    559 
    560 static u32 aa_ldur_v(u32 size, u32 v, u32 rt, u32 rn, i32 simm9) {
    561   return aa_ldur_op_v(size, v, AA64_LDST_OPC_LDR, rt, rn, simm9);
    562 }
    563 
    564 static u32 aa_stur_v(u32 size, u32 v, u32 rt, u32 rn, i32 simm9) {
    565   return aa64_ldst_simm9_pack((AA64LdStSimm9){.size = size,
    566                                               .V = v,
    567                                               .opc = AA64_LDST_OPC_STR,
    568                                               .imm9 = (u32)simm9 & 0x1ffu,
    569                                               .Rn = rn,
    570                                               .Rt = rt});
    571 }
    572 
    573 /* Scaled load with an explicit load opcode (see aa_ldur_op_v). The scale shift
    574  * is the access size for plain LDR; a sign-extending narrow load (opc=LDRS_X)
    575  * uses the same byte-size scale (the encoded imm12 is byte_off >> size). */
    576 static u32 aa_ldr_uimm_op_v(u32 size, u32 v, u32 ld_opc, u32 rt, u32 rn,
    577                             u32 byte_off) {
    578   u32 sc = byte_off >> size;
    579   return aa64_ldst_uimm_pack((AA64LdStUimm){
    580       .size = size, .V = v, .opc = ld_opc, .imm12 = sc, .Rn = rn, .Rt = rt});
    581 }
    582 
    583 static u32 aa_ldr_uimm_v(u32 size, u32 v, u32 rt, u32 rn, u32 byte_off) {
    584   return aa_ldr_uimm_op_v(size, v, AA64_LDST_OPC_LDR, rt, rn, byte_off);
    585 }
    586 
    587 static u32 aa_str_uimm_v(u32 size, u32 v, u32 rt, u32 rn, u32 byte_off) {
    588   u32 sc = byte_off >> size;
    589   return aa64_ldst_uimm_pack((AA64LdStUimm){.size = size,
    590                                             .V = v,
    591                                             .opc = AA64_LDST_OPC_STR,
    592                                             .imm12 = sc,
    593                                             .Rn = rn,
    594                                             .Rt = rt});
    595 }
    596 
    597 static u32 aa_ldr_uimm(u32 size, u32 rt, u32 rn, u32 byte_off) {
    598   return aa_ldr_uimm_v(size, 0, rt, rn, byte_off);
    599 }
    600 
    601 static u32 aa_str_uimm(u32 size, u32 rt, u32 rn, u32 byte_off) {
    602   return aa_str_uimm_v(size, 0, rt, rn, byte_off);
    603 }
    604 
    605 /* PRFM (immediate), zero byte offset. prfop is the architectural Rt field:
    606  * type[4:3] | level[2:1] | policy[0]. */
    607 static u32 aa_prfm(u32 prfop, u32 rn) {
    608   return 0xF9800000u | ((rn & 31u) << 5) | (prfop & 31u);
    609 }
    610 
    611 /* Register-offset load/store with an explicit load opcode (ld_opc is consulted
    612  * only when load != 0; AA64_LDST_OPC_LDRS_X gives a sign-extending ldrsb/ldrsh
    613  * into the X register) and an explicit index-extend `option`
    614  * (AA64_LDST_OPTION_*): LSL/UXTX (full X index, the default), or SXTW/UXTW for
    615  * a 32-bit W index widened by the addressing mode (O1-PATTERNS L8). */
    616 static u32 aa_ldst_regoff_opt_v(u32 size, u32 v, u32 load, u32 ld_opc, u32 rt,
    617                                 u32 rn, u32 rm, u32 option, u32 scaled) {
    618   return aa64_ldst_regoff_pack((AA64LdStRegOff){
    619       .size = size & 3u,
    620       .V = v & 1u,
    621       .opc = (load ? ld_opc : AA64_LDST_OPC_STR),
    622       .Rm = rm & 0x1fu,
    623       .option = option & 7u,
    624       .S = scaled & 1u,
    625       .Rn = rn & 0x1fu,
    626       .Rt = rt & 0x1fu});
    627 }
    628 
    629 static u32 aa_ldst_regoff_op_v(u32 size, u32 v, u32 load, u32 ld_opc, u32 rt,
    630                                u32 rn, u32 rm, u32 scaled) {
    631   return aa_ldst_regoff_opt_v(size, v, load, ld_opc, rt, rn, rm,
    632                               AA64_LDST_OPTION_LSL, scaled);
    633 }
    634 
    635 static u32 aa_ldst_regoff_v(u32 size, u32 v, u32 load, u32 rt, u32 rn, u32 rm,
    636                             u32 scaled) {
    637   return aa_ldst_regoff_op_v(size, v, load, AA64_LDST_OPC_LDR, rt, rn, rm,
    638                              scaled);
    639 }
    640 
    641 static u32 aa_mrs_tpidr_el0(u32 rt) { return 0xd53bd040u | (rt & 0x1fu); }
    642 
    643 /* The scalar-FP / bit-packing encoders below delegate to the single-source
    644  * isa.h encoders so encode/decode stay in lockstep. ftype 0=single, 1=double;
    645  * the historical aa_* signatures (and call sites) are preserved as thin
    646  * wrappers. */
    647 static u32 aa_fp_bin(u32 op, u32 is_double, u32 rd, u32 rn, u32 rm) {
    648   return aa64_fp_dp2(is_double, op, rd, rn, rm);
    649 }
    650 
    651 static u32 aa_fcmp(u32 is_double, u32 rn, u32 rm) {
    652   return aa64_fcmp_reg(is_double, rn, rm);
    653 }
    654 
    655 static u32 aa_fneg(u32 is_double, u32 rd, u32 rn) {
    656   return aa64_fp_dp1(is_double, AA64_FP_DP1_FNEG, rd, rn);
    657 }
    658 
    659 static u32 aa_fmov_fp(u32 is_double, u32 rd, u32 rn) {
    660   return aa64_fp_dp1(is_double, AA64_FP_DP1_FMOV, rd, rn);
    661 }
    662 
    663 /* MOV Vd.16B, Vn.16B (alias of ORR Vd.16B, Vn.16B, Vn.16B): a full 128-bit
    664  * SIMD register copy. Used to move binary128 / long double values, which fmov
    665  * (scalar, max 64-bit) would truncate. */
    666 static u32 aa_mov_vec16(u32 rd, u32 rn) {
    667   return 0x4ea01c00u | ((rn & 0x1fu) << 16) | ((rn & 0x1fu) << 5) |
    668          (rd & 0x1fu);
    669 }
    670 
    671 /* The FP<->int conversion family: sf selects the GPR width (1=64-bit), ftype
    672  * the FP width (1=double). Their roles flip between the convert-from-int
    673  * (s/ucvtf: sf=src GPR, ftype=dst FP) and convert-to-int (fcvtz*: sf=dst GPR,
    674  * ftype=src FP) directions, but the encoding is the same shape. */
    675 static u32 aa_scvtf(u32 is_double_dst, u32 is64_src, u32 fd, u32 rn) {
    676   return aa64_fp_int_cvt(is64_src, is_double_dst, AA64_FP_ICVT_SCVTF, fd, rn);
    677 }
    678 
    679 static u32 aa_ucvtf(u32 is_double_dst, u32 is64_src, u32 fd, u32 rn) {
    680   return aa64_fp_int_cvt(is64_src, is_double_dst, AA64_FP_ICVT_UCVTF, fd, rn);
    681 }
    682 
    683 static u32 aa_fcvtzs(u32 is64_dst, u32 is_double_src, u32 rd, u32 fn) {
    684   return aa64_fp_int_cvt(is64_dst, is_double_src, AA64_FP_ICVT_FCVTZS, rd, fn);
    685 }
    686 
    687 static u32 aa_fcvtzu(u32 is64_dst, u32 is_double_src, u32 rd, u32 fn) {
    688   return aa64_fp_int_cvt(is64_dst, is_double_src, AA64_FP_ICVT_FCVTZU, rd, fn);
    689 }
    690 
    691 static u32 aa_fcvt_d_s(u32 rd, u32 rn) {
    692   return aa64_fcvt_prec(/*src=single*/ 0, /*dst=double*/ 1, rd, rn);
    693 }
    694 
    695 static u32 aa_fcvt_s_d(u32 rd, u32 rn) {
    696   return aa64_fcvt_prec(/*src=double*/ 1, /*dst=single*/ 0, rd, rn);
    697 }
    698 
    699 /* fmov between GPR and FP reg: the FP ftype tracks the operand width (64-bit
    700  * GPR <-> double, 32-bit GPR <-> single), so sf and ftype move together. */
    701 static u32 aa_fmov_gpr_to_fp(u32 is64, u32 fd, u32 rn) {
    702   return aa64_fp_int_cvt(is64, is64, AA64_FP_ICVT_FMOV_TO_FP, fd, rn);
    703 }
    704 
    705 static u32 aa_fmov_fp_to_gpr(u32 is64, u32 rd, u32 fn) {
    706   return aa64_fp_int_cvt(is64, is64, AA64_FP_ICVT_FMOV_TO_GPR, rd, fn);
    707 }
    708 
    709 static u32 aa_clz(u32 sf, u32 rd, u32 rn) {
    710   return aa64_dp1(sf, AA64_DP1_CLZ, rd, rn);
    711 }
    712 
    713 static u32 aa_rbit(u32 sf, u32 rd, u32 rn) {
    714   return aa64_dp1(sf, AA64_DP1_RBIT, rd, rn);
    715 }
    716 
    717 /* REV reverses all bytes of the operand, so the 32-bit form is REV(32) and the
    718  * 64-bit form is REV(64) — the opcode2 follows sf rather than being constant.
    719  */
    720 static u32 aa_rev(u32 sf, u32 rd, u32 rn) {
    721   return aa64_dp1(sf, sf ? AA64_DP1_REV64 : AA64_DP1_REV32, rd, rn);
    722 }
    723 
    724 static u32 aa_sbfm(u32 sf, u32 rd, u32 rn, u32 immr, u32 imms) {
    725   return aa64_bitfield(sf, /*SBFM*/ 0u, immr, imms, rd, rn);
    726 }
    727 
    728 static u32 aa_ubfm(u32 sf, u32 rd, u32 rn, u32 immr, u32 imms) {
    729   return aa64_bitfield(sf, /*UBFM*/ 2u, immr, imms, rd, rn);
    730 }
    731 
    732 static u32 aa_ldaxr(u32 size, u32 rt, u32 rn) {
    733   return (size << 30) | 0x085ffc00u | ((rn & 0x1fu) << 5) | (rt & 0x1fu);
    734 }
    735 
    736 static u32 aa_ldxr(u32 size, u32 rt, u32 rn) {
    737   return (size << 30) | 0x085f7c00u | ((rn & 0x1fu) << 5) | (rt & 0x1fu);
    738 }
    739 
    740 static u32 aa_stlxr(u32 size, u32 rs, u32 rt, u32 rn) {
    741   return (size << 30) | 0x0800fc00u | ((rs & 0x1fu) << 16) |
    742          ((rn & 0x1fu) << 5) | (rt & 0x1fu);
    743 }
    744 
    745 static u32 aa_stxr(u32 size, u32 rs, u32 rt, u32 rn) {
    746   return (size << 30) | 0x08007c00u | ((rs & 0x1fu) << 16) |
    747          ((rn & 0x1fu) << 5) | (rt & 0x1fu);
    748 }
    749 
    750 static u32 aa_ldar(u32 size, u32 rt, u32 rn) {
    751   return (size << 30) | 0x08dffc00u | ((rn & 0x1fu) << 5) | (rt & 0x1fu);
    752 }
    753 
    754 static u32 aa_stlr(u32 size, u32 rt, u32 rn) {
    755   return (size << 30) | 0x089ffc00u | ((rn & 0x1fu) << 5) | (rt & 0x1fu);
    756 }
    757 
    758 static u32 aa_umaddl(u32 rd, u32 rn, u32 rm, u32 ra) {
    759   return 0x9ba00000u | ((rm & 0x1fu) << 16) | ((ra & 0x1fu) << 10) |
    760          ((rn & 0x1fu) << 5) | (rd & 0x1fu);
    761 }
    762 
    763 static u32 aa_smaddl(u32 rd, u32 rn, u32 rm, u32 ra) {
    764   return 0x9b200000u | ((rm & 0x1fu) << 16) | ((ra & 0x1fu) << 10) |
    765          ((rn & 0x1fu) << 5) | (rd & 0x1fu);
    766 }
    767 
    768 static u32 aa_smulh(u32 rd, u32 rn, u32 rm) {
    769   return 0x9b407c00u | ((rm & 0x1fu) << 16) | ((rn & 0x1fu) << 5) |
    770          (rd & 0x1fu);
    771 }
    772 
    773 static u32 aa_umulh(u32 rd, u32 rn, u32 rm) {
    774   return 0x9bc07c00u | ((rm & 0x1fu) << 16) | ((rn & 0x1fu) << 5) |
    775          (rd & 0x1fu);
    776 }
    777 
    778 static u32 aa_subs_reg(u32 sf, u32 rd, u32 rn, u32 rm) {
    779   return aa64_addsubsr_pack(
    780       (AA64AddSubSR){.sf = sf, .op = 1, .S = 1, .Rm = rm, .Rn = rn, .Rd = rd});
    781 }
    782 
    783 static void aa_emit_cmp_to_flags(NativeTarget* t, NativeLoc lhs, NativeLoc rhs);
    784 
    785 static u32 aa_add_lsl(u32 rd, u32 rn, u32 rm, u32 shift) {
    786   return aa64_addsubsr_pack((AA64AddSubSR){.sf = 1,
    787                                            .op = 0,
    788                                            .S = 0,
    789                                            .shift = 0,
    790                                            .Rm = rm,
    791                                            .imm6 = shift,
    792                                            .Rn = rn,
    793                                            .Rd = rd});
    794 }
    795 
    796 /* L7 shifted-register ALU forms: rd = rn <op> (rm << shift). shift type is
    797  * always LSL (the .shift field, 0); the shift amount is imm6. add/sub use the
    798  * add/sub shifted-register family, and/orr/eor the logical shifted-register
    799  * family. `sf` selects the 32- vs 64-bit operand width. */
    800 static u32 aa_addsub_lsl(u32 sf, u32 op, u32 rd, u32 rn, u32 rm, u32 shift) {
    801   return aa64_addsubsr_pack((AA64AddSubSR){.sf = sf,
    802                                            .op = op,
    803                                            .S = 0,
    804                                            .shift = 0,
    805                                            .Rm = rm,
    806                                            .imm6 = shift,
    807                                            .Rn = rn,
    808                                            .Rd = rd});
    809 }
    810 
    811 static u32 aa_logsr_lsl(u32 sf, u32 opc, u32 rd, u32 rn, u32 rm, u32 shift) {
    812   return aa64_logsr_pack((AA64LogSR){.sf = sf,
    813                                      .opc = opc,
    814                                      .shift = 0,
    815                                      .N = 0,
    816                                      .Rm = rm,
    817                                      .imm6 = shift,
    818                                      .Rn = rn,
    819                                      .Rd = rd});
    820 }
    821 
    822 static u32 aa_cset(u32 sf, u32 rd, u32 cond) {
    823   return aa64_csinc_enc(sf, rd, AA64_ZR, AA64_ZR, cond ^ 1u);
    824 }
    825 
    826 static u32 cmp_cond(CmpOp op) {
    827   switch (op) {
    828     case CMP_EQ:
    829       return 0x0u;
    830     case CMP_NE:
    831       return 0x1u;
    832     case CMP_LT_U:
    833       return 0x3u;
    834     case CMP_LE_U:
    835       return 0x9u;
    836     case CMP_GT_U:
    837       return 0x8u;
    838     case CMP_GE_U:
    839       return 0x2u;
    840     case CMP_LT_S:
    841       return 0xbu;
    842     case CMP_LE_S:
    843       return 0xdu;
    844     case CMP_GT_S:
    845       return 0xcu;
    846     case CMP_GE_S:
    847       return 0xau;
    848     /* FP predicates after FCMP set NZCV as: a<b -> N; a==b -> Z,C; a>b -> C;
    849      * unordered -> C,V. Each maps to a single condition except CMP_ONE_F /
    850      * CMP_UEQ_F (synthesized with two instructions in aa_cmp/aa_cmp_branch,
    851      * which intercept them before calling cmp_cond). */
    852     case CMP_OEQ_F:
    853       return 0x0u; /* EQ */
    854     case CMP_OLT_F:
    855       return 0x4u; /* MI */
    856     case CMP_OLE_F:
    857       return 0x9u; /* LS */
    858     case CMP_OGT_F:
    859       return 0xcu; /* GT */
    860     case CMP_OGE_F:
    861       return 0xau; /* GE */
    862     case CMP_UNE_F:
    863       return 0x1u; /* NE  (unordered or not-equal) */
    864     case CMP_ULT_F:
    865       return 0xbu; /* LT  (unordered or less-than) */
    866     case CMP_ULE_F:
    867       return 0xdu; /* LE  (unordered or less-or-equal) */
    868     case CMP_UGT_F:
    869       return 0x8u; /* HI  (unordered or greater-than) */
    870     case CMP_UGE_F:
    871       return 0x2u; /* CS  (unordered or greater-or-equal) */
    872     default:
    873       return 0x0u;
    874   }
    875 }
    876 
    877 static AANativeSlot* aa_slot(AANativeTarget* a, NativeFrameSlot slot) {
    878   return native_frame_slot_at(&a->frame, slot);
    879 }
    880 
    881 /* x16/x17 are the backend-private integer temporaries available to memory
    882  * primitives. Address formation must not silently overwrite a store payload
    883  * or a register index that the final memory instruction still consumes. Keep
    884  * those conflicts explicit at the address-materialization boundary. */
    885 typedef u8 AATmpAvoid;
    886 enum {
    887   AA_TMP_AVOID_0 = 1u << 0,
    888   AA_TMP_AVOID_1 = 1u << 1,
    889 };
    890 
    891 static AATmpAvoid aa_tmp_avoid_reg(Reg reg) {
    892   if (reg == AA_TMP0) return AA_TMP_AVOID_0;
    893   if (reg == AA_TMP1) return AA_TMP_AVOID_1;
    894   return 0u;
    895 }
    896 
    897 static Reg aa_addr_tmp(AANativeTarget* a, AATmpAvoid avoid) {
    898   if (!(avoid & AA_TMP_AVOID_0)) return AA_TMP0;
    899   if (!(avoid & AA_TMP_AVOID_1)) return AA_TMP1;
    900   aa_panic(a, "address formation has no scratch register");
    901   return AA_TMP0;
    902 }
    903 
    904 static Reg aa_addr_adjust_tmp(AANativeTarget* a, Reg base,
    905                               AATmpAvoid avoid) {
    906   AATmpAvoid base_bit = aa_tmp_avoid_reg(base);
    907   if (base_bit && !(avoid & base_bit)) return base;
    908   return aa_addr_tmp(a, avoid);
    909 }
    910 
    911 static AATmpAvoid aa_mem_addr_avoid(int load, NativeLoc reg,
    912                                     NativeAddr addr) {
    913   AATmpAvoid avoid = 0u;
    914   /* v16/v17 and x16/x17 are separate register files. Only an integer store
    915    * keeps its Rt live across integer address materialization. */
    916   if (!load && !native_loc_is_fp(reg))
    917     avoid |= aa_tmp_avoid_reg(loc_reg(reg));
    918   if (addr.index_kind == NATIVE_ADDR_INDEX_REG)
    919     avoid |= aa_tmp_avoid_reg(addr.index.reg);
    920   return avoid;
    921 }
    922 
    923 static void aa_addr_base(AANativeTarget* a, NativeAddr addr,
    924                          AATmpAvoid avoid, u32* base_out, i32* off_out) {
    925   *base_out = AA_TMP0;
    926   *off_out = addr.offset;
    927   switch ((NativeAddrBaseKind)addr.base_kind) {
    928     case NATIVE_ADDR_BASE_REG:
    929       *base_out = addr.base.reg;
    930       return;
    931     case NATIVE_ADDR_BASE_FRAME: {
    932       AANativeSlot* s = aa_slot(a, addr.base.frame);
    933       *base_out = AA_FP;
    934       *off_out = aa_fp_off_slot(a, s->off) + addr.offset;
    935       return;
    936     }
    937     case NATIVE_ADDR_BASE_FRAME_VALUE: {
    938       NativeLoc tmp;
    939       NativeAddr base_addr = addr;
    940       Reg addr_tmp = aa_addr_tmp(a, avoid);
    941       if (!addr.base_type)
    942         aa_panic(a, "frame-value base has no exact type");
    943       base_addr.index_kind = NATIVE_ADDR_INDEX_NONE;
    944       base_addr.index_ext = NATIVE_ADDR_IDX_EXT_NONE;
    945       base_addr.log2_scale = 0;
    946       memset(&tmp, 0, sizeof tmp);
    947       tmp.kind = NATIVE_LOC_REG;
    948       tmp.cls = NATIVE_REG_INT;
    949       tmp.type = addr.base_type;
    950       tmp.v.reg = addr_tmp;
    951       a->base.load_addr(&a->base, tmp, base_addr);
    952       *base_out = addr_tmp;
    953       *off_out = 0;
    954       return;
    955     }
    956     case NATIVE_ADDR_BASE_GLOBAL: {
    957       NativeLoc tmp;
    958       NativeAddr base_addr = addr;
    959       Reg addr_tmp = aa_addr_tmp(a, avoid);
    960       base_addr.index_kind = NATIVE_ADDR_INDEX_NONE;
    961       base_addr.index_ext = NATIVE_ADDR_IDX_EXT_NONE;
    962       base_addr.log2_scale = 0;
    963       memset(&tmp, 0, sizeof tmp);
    964       tmp.kind = NATIVE_LOC_REG;
    965       tmp.cls = NATIVE_REG_INT;
    966       tmp.type = builtin_id(KIT_CG_BUILTIN_I64);
    967       tmp.v.reg = addr_tmp;
    968       a->base.load_addr(&a->base, tmp, base_addr);
    969       *base_out = addr_tmp;
    970       *off_out = 0;
    971       return;
    972     }
    973     default:
    974       aa_panic(a, "unsupported address base");
    975   }
    976 }
    977 
    978 static u32 aa_ldst_q_uimm(int load, u32 rt, u32 rn, u32 byte_off);
    979 static u32 aa_ldst_q_simm9(int load, u32 rt, u32 rn, i32 byte_off);
    980 static AAPatch* aa_patch_alloc(AANativeTarget* a); /* far-slot deferral */
    981 
    982 static void aa_emit_mem_q(AANativeTarget* a, int load, NativeLoc reg,
    983                           NativeAddr addr) {
    984   u32 base, rt;
    985   i32 off;
    986   MCEmitter* mc = a->base.mc;
    987   if (addr.index_kind != NATIVE_ADDR_INDEX_NONE)
    988     aa_panic(a, "unsupported q-register indexed memory access");
    989   aa_addr_base(a, addr, 0u, &base, &off);
    990   rt = loc_reg(reg);
    991   if (off >= 0 && (((u32)off & 15u) == 0) && ((u32)off >> 4) <= 0xfffu) {
    992     aa_emit32(mc, aa_ldst_q_uimm(load, rt, base, (u32)off));
    993     return;
    994   }
    995   if (off >= -256 && off <= 255) {
    996     aa_emit32(mc, aa_ldst_q_simm9(load, rt, base, off));
    997     return;
    998   }
    999   aa_emit_add_imm(a, AA_TMP1, base, off);
   1000   aa_emit32(mc, aa_ldst_q_uimm(load, rt, AA_TMP1, 0));
   1001 }
   1002 
   1003 static void aa_emit_mem_native(AANativeTarget* a, int load, NativeLoc reg,
   1004                                NativeAddr addr, MemAccess mem) {
   1005   u32 base, rt, sz, bytes, ld_opc;
   1006   AATmpAvoid addr_avoid;
   1007   i32 off;
   1008   MCEmitter* mc = a->base.mc;
   1009   rt = loc_reg(reg);
   1010   addr_avoid = aa_mem_addr_avoid(load, reg, addr);
   1011   bytes = mem.size
   1012               ? mem.size
   1013               : type_size32(&a->base, reg.type ? reg.type : mem.type);
   1014   if (native_loc_is_fp(reg)) {
   1015     if (bytes != 4u && bytes != 8u && bytes != 16u)
   1016       aa_panic(a, "unsupported native fp memory width");
   1017   } else if (bytes != 1u && bytes != 2u && bytes != 4u && bytes != 8u) {
   1018     aa_panic(a, "unsupported native integer memory width");
   1019   }
   1020   sz = size_idx(bytes);
   1021   if (native_loc_is_fp(reg) && bytes == 16u) {
   1022     aa_emit_mem_q(a, load, reg, addr);
   1023     return;
   1024   }
   1025   if (native_loc_is_fp(reg) && sz < 2u) sz = 2u;
   1026   /* Lever 4: a signed narrow integer load (MF_SEXT_LOAD, byte/half, integer
   1027    * register) becomes a sign-extending load into the X register (ldrsb/ldrsh,
   1028    * opc=10) so it fills the whole register in one instruction — the cg layer
   1029    * then drops the redundant CV_SEXT. Every other load (incl. fp, word, dword)
   1030    * keeps the plain zero-extending LDR. */
   1031   ld_opc =
   1032       (load && (mem.flags & MF_SEXT_LOAD) && !native_loc_is_fp(reg) && sz <= 1u)
   1033           ? AA64_LDST_OPC_LDRS_X
   1034           : AA64_LDST_OPC_LDR;
   1035   /* Far fixed-slot fast path (Lever 1/3 / Fix B). A plain frame slot whose
   1036    * top-record fp offset is past stur's ±256 range would otherwise cost `sub
   1037    * xN,x29,#off (+movk) ; ldur` (2-4 insns). Instead emit a positive scaled
   1038    * `ldr/str [sp,#0]` placeholder and defer the offset: once the frame is
   1039    * final, aa_apply_patches rewrites it to `[base, #frame_size-...]`, base = sp
   1040    * (stable) or AA_FRAME_BASE (alloca).
   1041    *
   1042    * The base-relative offset is frame_size - 16 - slot_off, unknown here
   1043    * (single-pass: the frame is still growing) and able to exceed the scaled
   1044    * ldr/str reach (4 KB/8 KB/16 KB/32 KB by size) for ANY access size — a
   1045    * top-of-frame 8-byte slot sits ~frame_size off sp, and kit's own
   1046    * src/api/package.c reaches a ~69 KB frame. So every size reserves TWO words:
   1047    * when the resolved offset fits it is `ldr/str [base,#scaled] ; nop`, and when
   1048    * it overflows the resolver emits the address-build fallback `add x17,base,#hi
   1049    * ; ldr/str [x17,#lo]` (correct for any frame < 16 MB).
   1050    *
   1051    * Lever 3/4 interaction: a far SIGNED narrow load (ld_opc==LDRS_X) is
   1052    * excluded from this scaled-slot path — the slot patch records only a plain
   1053    * LDR opcode, so it would zero-extend while the cg layer dropped the CV_SEXT.
   1054    * Such loads fall through to the general path below, which honors ld_opc
   1055    * (ldrsb/ldrsh with an address build). Rare (a far signed char/short slot);
   1056    * correctness over the one-insn win. */
   1057   int sext_far = load && ld_opc == AA64_LDST_OPC_LDRS_X;
   1058   if (a->slot_sp_base && addr.base_kind == NATIVE_ADDR_BASE_FRAME &&
   1059       addr.index_kind == NATIVE_ADDR_INDEX_NONE && sz <= 3u && !sext_far) {
   1060     AANativeSlot* s = aa_slot(a, addr.base.frame);
   1061     i32 fp_off = aa_fp_off_slot(a, s->off) + addr.offset;
   1062     /* The placeholder resolves to a scaled `ldr/str [base,#imm<<sz]`, whose
   1063      * immediate must be a multiple of the access size. The frame base
   1064      * (sp / AA_FRAME_BASE) is 16-aligned at this point (slot_sp_base is off
   1065      * whenever a Windows GP home area shifts the layout), so the resolved
   1066      * offset is a multiple of the access size exactly when fp_off is. An
   1067      * aggregate copy chunks an under-aligned record (e.g. a 4-aligned 12-byte
   1068      * struct copied as 8+4 bytes) into an access whose frame offset is not a
   1069      * multiple of the chunk size; such an access can't use the scaled form, so
   1070      * it falls through to the fp-relative address-build path below, which
   1071      * encodes any offset. */
   1072     u32 acc = 1u << sz;
   1073     if (fp_off < -256 && ((u32)fp_off & (acc - 1u)) == 0u) {
   1074       u32 vbit = native_loc_is_fp(reg) ? 1u : 0u;
   1075       AAPatch* p = aa_patch_alloc(a);
   1076       p->kind = AA_PATCH_SLOT;
   1077       p->pos = mc_pos(mc);
   1078       p->u.slot.slot_off = s->off;
   1079       p->u.slot.extra = addr.offset;
   1080       p->u.slot.sz = (u8)sz;
   1081       p->u.slot.vbit = (u8)vbit;
   1082       p->u.slot.load = (u8)(load ? 1u : 0u);
   1083       p->u.slot.rt = (u8)rt;
   1084       aa_emit32(mc, load ? aa_ldr_uimm_v(sz, vbit, rt, AA_SP, 0)
   1085                          : aa_str_uimm_v(sz, vbit, rt, AA_SP, 0));
   1086       /* Reserve a second word for the address-build fallback. The final
   1087        * base-relative offset is frame_size - 16 - slot_off, which is unknown
   1088        * here (single-pass: the frame keeps growing) and can exceed the scaled
   1089        * ldr/str reach for ANY access size in a large frame (a top-of-frame
   1090        * 8-byte slot sits ~frame_size off sp; kit's own src/api/package.c has a
   1091        * ~69 KB frame). So every size reserves the fallback word; aa_apply_patches
   1092        * leaves it a nop when the one-word scaled form fits. */
   1093       aa_emit32(mc, aa64_nop());
   1094       return;
   1095     }
   1096   }
   1097   if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL &&
   1098       addr.index_kind == NATIVE_ADDR_INDEX_NONE) {
   1099     i64 addend = addr.base.global.addend + (i64)addr.offset;
   1100     u32 scratch = aa_addr_tmp(a, addr_avoid);
   1101     u32 pos = mc_pos(mc);
   1102     if (aa_use_got_for_sym(&a->base, addr.base.global.sym)) {
   1103       aa_emit32(mc, aa64_adrp(scratch, 0, 0));
   1104       mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_ADR_GOT_PAGE,
   1105                        addr.base.global.sym, 0, 0, 0);
   1106       pos = mc_pos(mc);
   1107       aa_emit32(mc, aa_ldr_uimm(3, scratch, scratch, 0));
   1108       mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_LD64_GOT_LO12_NC,
   1109                        addr.base.global.sym, 0, 0, 0);
   1110       if (addend) aa_emit_add_i64(a, scratch, scratch, addend);
   1111       aa_emit32(
   1112           mc,
   1113           load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, scratch, 0)
   1114                : aa_stur_v(sz, native_loc_is_fp(reg), rt, scratch, 0));
   1115       return;
   1116     }
   1117     aa_emit32(mc, aa64_adrp(scratch, 0, 0));
   1118     mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_ADR_PREL_PG_HI21,
   1119                      addr.base.global.sym, addend, 0, 0);
   1120     pos = mc_pos(mc);
   1121     aa_emit32(mc,
   1122               load ? aa_ldr_uimm_op_v(sz, native_loc_is_fp(reg), ld_opc, rt,
   1123                                       scratch, 0)
   1124                    : aa_str_uimm_v(sz, native_loc_is_fp(reg), rt, scratch, 0));
   1125     mc_emit_reloc_at(mc, mc->section_id, pos, aa_ldst_reloc_for_size(sz),
   1126                      addr.base.global.sym, addend, 0, 0);
   1127     return;
   1128   }
   1129   aa_addr_base(a, addr, addr_avoid, &base, &off);
   1130   if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) {
   1131     u32 use_base = base;
   1132     u32 scaled = 0;
   1133     /* L8 index extend: a 32-bit W index widened by the addressing mode. The
   1134      * Rm field still names the same register number; only the option bits
   1135      * change (010=UXTW, 110=SXTW). NONE keeps the full-width LSL/UXTX form. */
   1136     u32 option = AA64_LDST_OPTION_LSL;
   1137     if (addr.index_kind != NATIVE_ADDR_INDEX_REG)
   1138       aa_panic(a, "unsupported address index");
   1139     if (addr.index_ext == NATIVE_ADDR_IDX_EXT_SXTW)
   1140       option = AA64_LDST_OPTION_SXTW;
   1141     else if (addr.index_ext == NATIVE_ADDR_IDX_EXT_UXTW)
   1142       option = AA64_LDST_OPTION_UXTW;
   1143     if (off) {
   1144       use_base = aa_addr_adjust_tmp(a, base, addr_avoid);
   1145       aa_emit_add_imm(a, use_base, base, off);
   1146     }
   1147     if (addr.log2_scale == 0) {
   1148       scaled = 0;
   1149     } else if (addr.log2_scale == sz) {
   1150       scaled = 1;
   1151     } else {
   1152       aa_panic(a, "unsupported memory address scale");
   1153     }
   1154     aa_emit32(mc, aa_ldst_regoff_opt_v(sz, native_loc_is_fp(reg), load, ld_opc,
   1155                                        rt, use_base, addr.index.reg, option,
   1156                                        scaled));
   1157     return;
   1158   }
   1159   if (off >= 0 && (((u32)off & ((1u << sz) - 1u)) == 0) &&
   1160       ((u32)off >> sz) <= 0xfffu) {
   1161     aa_emit32(mc, load ? aa_ldr_uimm_op_v(sz, native_loc_is_fp(reg), ld_opc, rt,
   1162                                           base, (u32)off)
   1163                        : aa_str_uimm_v(sz, native_loc_is_fp(reg), rt, base,
   1164                                        (u32)off));
   1165     return;
   1166   }
   1167   if (off >= -256 && off <= 255) {
   1168     aa_emit32(mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt,
   1169                                       base, off)
   1170                        : aa_stur_v(sz, native_loc_is_fp(reg), rt, base, off));
   1171     return;
   1172   }
   1173   {
   1174     u32 scratch = aa_addr_adjust_tmp(a, base, addr_avoid);
   1175     aa_emit_add_imm(a, scratch, base, off);
   1176     aa_emit32(mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt,
   1177                                       scratch, 0)
   1178                        : aa_stur_v(sz, native_loc_is_fp(reg), rt, scratch, 0));
   1179   }
   1180 }
   1181 
   1182 static u32 aa_part_transfer_tmp(AANativeTarget* a, NativeLoc reg,
   1183                                 NativeAddr addr) {
   1184   u32 used = 1u << loc_reg(reg);
   1185   if (addr.base_kind == NATIVE_ADDR_BASE_REG) used |= 1u << addr.base.reg;
   1186   if (addr.index_kind == NATIVE_ADDR_INDEX_REG) used |= 1u << addr.index.reg;
   1187   if (!(used & (1u << AA_TMP0))) return AA_TMP0;
   1188   if (!(used & (1u << AA_TMP1))) return AA_TMP1;
   1189   aa_panic(a, "exact ABI part transfer has no scratch register");
   1190   return AA_TMP0;
   1191 }
   1192 
   1193 /* A register carrier is 8 bytes, but the final part of a small aggregate may
   1194  * carry any byte count from 1 through 8. Keep the one-instruction native-width
   1195  * path; decompose 3/5/6/7-byte memory transfers into exact low-to-high chunks.
   1196  * This is deliberately below call marshalling so incoming binds, outgoing
   1197  * args, and generic NativeCallPhaseRet write phases all share the same rule. */
   1198 static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg,
   1199                         NativeAddr addr, MemAccess mem) {
   1200   u32 bytes = mem.size
   1201                   ? mem.size
   1202                   : type_size32(&a->base, reg.type ? reg.type : mem.type);
   1203   NativePartChunkIter it;
   1204   NativeLoc lane;
   1205   u32 chunk_off, chunk_size, regno, tmp;
   1206   if (bytes == 1u || bytes == 2u || bytes == 4u || bytes == 8u ||
   1207       (native_loc_is_fp(reg) && bytes == 16u)) {
   1208     aa_emit_mem_native(a, load, reg, addr, mem);
   1209     return;
   1210   }
   1211   if (native_loc_is_fp(reg) || bytes == 0u || bytes > 8u)
   1212     aa_panic(a, "unsupported exact ABI part memory width");
   1213 
   1214   regno = loc_reg(reg);
   1215   /* A native-width load may overwrite its address register because it is one
   1216    * instruction (`ldr x0, [x0]`). An exact-width load is a sequence, so retain
   1217    * the effective address in a backend-private register when dst aliases its
   1218    * base or index. The other private register remains available for packing
   1219    * later chunks into dst. */
   1220   if (load &&
   1221       ((addr.base_kind == NATIVE_ADDR_BASE_REG && addr.base.reg == regno) ||
   1222        (addr.index_kind == NATIVE_ADDR_INDEX_REG &&
   1223         addr.index.reg == regno))) {
   1224     NativeLoc addr_loc;
   1225     NativeAddr retained;
   1226     u32 addr_reg = regno == AA_TMP0 ? AA_TMP1 : AA_TMP0;
   1227     if (regno == AA_TMP0 || regno == AA_TMP1)
   1228       aa_panic(a, "exact load destination aliases private address register");
   1229     addr_loc = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64),
   1230                               NATIVE_REG_INT, addr_reg);
   1231     a->base.load_addr(&a->base, addr_loc, addr);
   1232     memset(&retained, 0, sizeof retained);
   1233     retained.base_kind = NATIVE_ADDR_BASE_REG;
   1234     retained.base.reg = addr_reg;
   1235     retained.base_type = addr_loc.type;
   1236     addr = retained;
   1237   }
   1238   tmp = aa_part_transfer_tmp(a, reg, addr);
   1239   it = native_part_chunks(bytes, 8u);
   1240   while (native_part_chunk_next(&it, &chunk_off, &chunk_size)) {
   1241     NativeAddr chunk_addr = addr;
   1242     MemAccess chunk_mem = mem;
   1243     chunk_addr.offset += (i32)chunk_off;
   1244     chunk_mem.size = chunk_size;
   1245     if (chunk_mem.align > chunk_size) chunk_mem.align = chunk_size;
   1246     lane = reg;
   1247     if (chunk_off == 0u) {
   1248       lane.v.reg = regno;
   1249     } else {
   1250       lane.v.reg = tmp;
   1251       if (!load)
   1252         aa_emit32(a->base.mc,
   1253                   aa_ubfm(1u, tmp, regno, chunk_off * 8u, 63u));
   1254     }
   1255     aa_emit_mem_native(a, load, lane, chunk_addr, chunk_mem);
   1256     if (load && chunk_off != 0u)
   1257       aa_emit32(a->base.mc,
   1258                 aa_logsr_lsl(1u, 1u, regno, regno, tmp, chunk_off * 8u));
   1259   }
   1260 }
   1261 
   1262 static NativeAllocClass aa_class_for_type(NativeTarget* t, KitCgTypeId type) {
   1263   if (type && cg_type_is_float(t->c, type) && cg_type_size(t->c, type) <= 8u)
   1264     return NATIVE_REG_FP;
   1265   return NATIVE_REG_INT;
   1266 }
   1267 
   1268 static int aa_addr_legal(NativeTarget* t, const NativeAddr* addr,
   1269                          MemAccess mem) {
   1270   u32 sz;
   1271   (void)t;
   1272   if (!addr) return 0;
   1273   if (addr->index_kind == NATIVE_ADDR_INDEX_NONE) return 1;
   1274   if (addr->index_kind != NATIVE_ADDR_INDEX_REG) return 0;
   1275   /* The SXTW/UXTW index-extend (L8) is encoded in the same regoff form as the
   1276    * plain LSL/UXTX index, so the scale legality is identical: scale 0 (no
   1277    * shift) or scale == access size (S bit). */
   1278   if (addr->log2_scale == 0) return 1;
   1279   sz = size_idx(mem.size ? mem.size : 8u);
   1280   return addr->log2_scale == sz;
   1281 }
   1282 
   1283 /* O1-PATTERNS rider capabilities (see native_target.h). aa64 emits both folded
   1284  * forms: the shifted-register ALU op (L7, aa_binop) and the SXTW/UXTW
   1285  * index-extend addressing mode (L8, aa_emit_mem). */
   1286 static int aa_can_fold_shift_into_alu(NativeTarget* t) {
   1287   (void)t;
   1288   return 1;
   1289 }
   1290 
   1291 static int aa_can_fold_extend_into_addr(NativeTarget* t) {
   1292   (void)t;
   1293   return 1;
   1294 }
   1295 
   1296 /* True if `mul Rd, Rn, #c` can be replaced by a single non-mul aarch64
   1297  * instruction using only Rn as a source (no extra scratch reg). Constants
   1298  * that match: 0, 1, -1, +/-2^k, 2^k+1, 1-2^k for k in [1..width-1]. The
   1299  * shift exponent must fit imm6 for the operand width (width = 32 if !sf
   1300  * else 64). The emit side is aa_emit_mul_const_imm. */
   1301 static int aa64_imul_strength_reducible(u32 sf, i64 imm) {
   1302   u32 max_sh = sf ? 63u : 31u;
   1303   u64 a;
   1304   if (imm == 0 || imm == 1 || imm == -1) return 1;
   1305   /* +2^k */
   1306   a = (u64)imm;
   1307   if (imm > 0 && (a & (a - 1u)) == 0u) {
   1308     u32 k = (u32)__builtin_ctzll(a);
   1309     return k <= max_sh;
   1310   }
   1311   /* -2^k */
   1312   if (imm < 0) {
   1313     a = (u64)(-imm);
   1314     if (a && (a & (a - 1u)) == 0u) {
   1315       u32 k = (u32)__builtin_ctzll(a);
   1316       return k >= 1u && k <= max_sh;
   1317     }
   1318   }
   1319   /* 2^k + 1 (k >= 1, so c >= 3) */
   1320   if (imm >= 3) {
   1321     u64 m = (u64)(imm - 1);
   1322     if ((m & (m - 1u)) == 0u) {
   1323       u32 k = (u32)__builtin_ctzll(m);
   1324       return k >= 1u && k <= max_sh;
   1325     }
   1326   }
   1327   /* 1 - 2^k (k >= 1, so c <= -1) */
   1328   if (imm <= -1) {
   1329     u64 m = (u64)(1 - imm);
   1330     if (m && (m & (m - 1u)) == 0u) {
   1331       u32 k = (u32)__builtin_ctzll(m);
   1332       return k >= 1u && k <= max_sh;
   1333     }
   1334   }
   1335   return 0;
   1336 }
   1337 
   1338 /* Which constant operands the backend can fold directly into an instruction
   1339  * (so the optimizer can leave them as immediates instead of materializing a
   1340  * register). Currently: add/sub/cmp 12-bit immediates (optionally <<12),
   1341  * any value for a plain register move (movz/movk synthesizes it), and
   1342  * strength-reducible mul constants (handled in aa_binop via shift / shifted
   1343  * add or sub). */
   1344 static int aa_imm_legal(NativeTarget* t, NativeImmUse use, u32 op,
   1345                         KitCgTypeId type, i64 imm) {
   1346   u32 imm12, sh;
   1347   switch (use) {
   1348     case NATIVE_IMM_BINOP:
   1349       if ((BinOp)op == BO_IADD || (BinOp)op == BO_ISUB)
   1350         return aa64_addsub_imm_fits(imm < 0 ? -imm : imm, &imm12, &sh);
   1351       if ((BinOp)op == BO_IMUL) {
   1352         u32 sf = type_size32(t, type) == 8u ? 1u : 0u;
   1353         return aa64_imul_strength_reducible(sf, imm);
   1354       }
   1355       /* LSL/LSR/ASR #imm via the UBFM/SBFM aliases: shift count in range. */
   1356       if ((BinOp)op == BO_SHL || (BinOp)op == BO_SHR_S ||
   1357           (BinOp)op == BO_SHR_U) {
   1358         u32 bits = type_size32(t, type) == 8u ? 64u : 32u;
   1359         return imm >= 0 && (u64)imm < (u64)bits;
   1360       }
   1361       /* AND/ORR/EOR #bitmask: encodable as an AArch64 logical immediate. */
   1362       if ((BinOp)op == BO_AND || (BinOp)op == BO_OR || (BinOp)op == BO_XOR) {
   1363         u32 sf = type_size32(t, type) == 8u ? 1u : 0u;
   1364         u32 N, immr, imms;
   1365         return aa64_logimm_encode((u64)imm, sf, &N, &immr, &imms);
   1366       }
   1367       return 0;
   1368     case NATIVE_IMM_CMP:
   1369       /* cmp lowers to subs #imm12; cmn (negative) is not wired, so require a
   1370        * non-negative immediate. */
   1371       return imm >= 0 && aa64_addsub_imm_fits(imm, &imm12, &sh);
   1372     case NATIVE_IMM_ADDR_OFFSET:
   1373       return aa64_addsub_imm_fits(imm < 0 ? -imm : imm, &imm12, &sh);
   1374     case NATIVE_IMM_MOVE:
   1375       return 1;
   1376   }
   1377   return 0;
   1378 }
   1379 
   1380 static int aa_addr_index_is_reg(const NativeAddr* addr, u32 reg) {
   1381   return addr->index_kind == NATIVE_ADDR_INDEX_REG && addr->index.reg == reg;
   1382 }
   1383 
   1384 static u32 aa_tmp_avoiding(u32 reg) {
   1385   return reg == AA_TMP0 ? AA_TMP1 : AA_TMP0;
   1386 }
   1387 
   1388 static void aa_load_addr_from_base(AANativeTarget* a, u32 rd, u32 base, i64 off,
   1389                                    const NativeAddr* addr) {
   1390   if (addr->index_kind == NATIVE_ADDR_INDEX_NONE) {
   1391     if (rd != base || off) aa_emit_add_i64(a, rd, base, off);
   1392     return;
   1393   }
   1394   if (addr->index_kind != NATIVE_ADDR_INDEX_REG)
   1395     aa_panic(a, "unsupported address index");
   1396   if (addr->log2_scale > 4u) aa_panic(a, "unsupported address scale");
   1397 
   1398   /* Read the index before writing rd. O1 can legally select the same physical
   1399    * register for an address result and its dead-after-use index; materializing
   1400    * the base into rd first would turn `base + index` into `base + base`. */
   1401   u32 idx_reg = addr->index.reg;
   1402   if (addr->index_ext != NATIVE_ADDR_IDX_EXT_NONE) {
   1403     /* L8 extend rider on an address-materialization (LEA-shaped) use: the
   1404      * shifted-register add takes a full-width X index, so first widen the
   1405      * 32-bit W index into a scratch (sxtw/uxtw) before the scaled add. The
   1406      * regoff memory form (aa_emit_mem) folds the extend directly; this path is
   1407      * the fallback when the indexed indirect is materialized as an address. */
   1408     u32 tmp = aa_tmp_avoiding(rd == base ? base : rd);
   1409     if (tmp == base) tmp = aa_tmp_avoiding(base);
   1410     if (addr->index_ext == NATIVE_ADDR_IDX_EXT_SXTW)
   1411       aa_emit32(a->base.mc, aa_sbfm(1, tmp, idx_reg, 0, 31));
   1412     else
   1413       aa_emit32(a->base.mc, aa_ubfm(1, tmp, idx_reg, 0, 31));
   1414     idx_reg = tmp;
   1415   }
   1416   aa_emit32(a->base.mc, aa_add_lsl(rd, base, idx_reg, addr->log2_scale));
   1417   if (off) aa_emit_add_i64(a, rd, rd, off);
   1418 }
   1419 
   1420 static void aa_materialize_frame_index(AANativeTarget* a, NativeAddr* addr,
   1421                                        u32 avoid_reg) {
   1422   NativeAddr load;
   1423   NativeLoc idx;
   1424   MemAccess mem;
   1425   u32 reg;
   1426   if (addr->index_kind != NATIVE_ADDR_INDEX_FRAME_VALUE) return;
   1427   if (!addr->index_type)
   1428     aa_panic(a, "frame-value index has no exact type");
   1429   reg = avoid_reg == AA_TMP1 ? AA_TMP0 : AA_TMP1;
   1430   memset(&load, 0, sizeof load);
   1431   load.base_kind = NATIVE_ADDR_BASE_FRAME;
   1432   load.base.frame = addr->index.frame;
   1433   load.base_type = addr->index_type;
   1434   memset(&idx, 0, sizeof idx);
   1435   idx.kind = NATIVE_LOC_REG;
   1436   idx.cls = NATIVE_REG_INT;
   1437   idx.type = load.base_type;
   1438   idx.v.reg = reg;
   1439   memset(&mem, 0, sizeof mem);
   1440   mem.type = load.base_type;
   1441   mem.size = type_size32(&a->base, load.base_type);
   1442   mem.align = type_align32(&a->base, load.base_type);
   1443   aa_emit_mem(a, 1, idx, load, mem);
   1444   addr->index_kind = NATIVE_ADDR_INDEX_REG;
   1445   addr->index.reg = reg;
   1446 }
   1447 
   1448 static NativeLoc native_loc_reg(KitCgTypeId type, NativeAllocClass cls,
   1449                                 Reg reg);
   1450 
   1451 static u32 aa_ldst_q_uimm(int load, u32 rt, u32 rn, u32 byte_off) {
   1452   return aa64_ldst_uimm_pack((AA64LdStUimm){.size = 0,
   1453                                             .V = 1,
   1454                                             .opc = load ? 3u : 2u,
   1455                                             .imm12 = byte_off >> 4,
   1456                                             .Rn = rn,
   1457                                             .Rt = rt});
   1458 }
   1459 
   1460 static u32 aa_ldst_q_simm9(int load, u32 rt, u32 rn, i32 byte_off) {
   1461   return aa64_ldst_simm9_pack((AA64LdStSimm9){.size = 0,
   1462                                               .V = 1,
   1463                                               .opc = load ? 3u : 2u,
   1464                                               .imm9 = (u32)byte_off & 0x1ffu,
   1465                                               .Rn = rn,
   1466                                               .Rt = rt});
   1467 }
   1468 
   1469 static void aa_emit_q_frame(AANativeTarget* a, int load, u32 qreg,
   1470                             NativeFrameSlot slot, u32 offset) {
   1471   AANativeSlot* s = aa_slot(a, slot);
   1472   i32 off = aa_fp_off_slot(a, s->off) + (i32)offset;
   1473   MCEmitter* mc = a->base.mc;
   1474   if (off >= 0 && ((u32)off & 15u) == 0 && ((u32)off >> 4) <= 0xfffu) {
   1475     aa_emit32(mc, aa_ldst_q_uimm(load, qreg, AA_FP, (u32)off));
   1476     return;
   1477   }
   1478   if (off >= -256 && off <= 255) {
   1479     aa_emit32(mc, aa_ldst_q_simm9(load, qreg, AA_FP, off));
   1480     return;
   1481   }
   1482   aa_emit_add_imm(a, AA_TMP1, AA_FP, off);
   1483   aa_emit32(mc, aa_ldst_q_uimm(load, qreg, AA_TMP1, 0));
   1484 }
   1485 
   1486 /* Reserve the variadic register-save-area frame slots (gp then fp). Split from
   1487  * the store emission so the known-frame path can fix the full frame — including
   1488  * these slots — before the prologue, then emit the stores after it. */
   1489 static void aa_reserve_variadic_reg_saves(AANativeTarget* a) {
   1490   NativeFrameSlotDesc sd;
   1491   KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   1492   ABIVaListInfo vai = abi_va_list_layout(a->base.c->abi);
   1493   if (vai.kind != ABI_VA_LIST_AAPCS64) return;
   1494   memset(&sd, 0, sizeof sd);
   1495   sd.type = i64;
   1496   sd.size = vai.gp_reg_count * vai.gp_slot_size;
   1497   sd.align = 8;
   1498   sd.kind = NATIVE_FRAME_SLOT_SAVE;
   1499   a->va_gr_slot = a->base.frame_slot(&a->base, &sd);
   1500   sd.size = vai.fp_reg_count * vai.fp_slot_size;
   1501   sd.align = 16;
   1502   a->va_vr_slot = a->base.frame_slot(&a->base, &sd);
   1503 }
   1504 
   1505 /* Emit the stores into the variadic register-save area. For AAPCS64 these land
   1506  * in the reserved gr/vr frame slots (aa_reserve_variadic_reg_saves); for the
   1507  * Windows GP home area they land in [fp + AA_FRAME_SAVE_SIZE ..], the
   1508  * top-of-frame block contiguous with the incoming stack args. */
   1509 static void aa_emit_variadic_reg_save_stores(AANativeTarget* a) {
   1510   NativeAddr addr;
   1511   MemAccess mem;
   1512   KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   1513   ABIVaListInfo vai = abi_va_list_layout(a->base.c->abi);
   1514   if (vai.kind == ABI_VA_LIST_POINTER && a->top_home_bytes) {
   1515     /* Windows: home x0..x{gp_reg_count-1} so the plain-pointer va_list walks
   1516      * register then stack varargs as one block. The named leading registers are
   1517      * homed too (harmless): va_start skips past them. */
   1518     memset(&mem, 0, sizeof mem);
   1519     mem.type = i64;
   1520     mem.size = 8;
   1521     mem.align = 8;
   1522     memset(&addr, 0, sizeof addr);
   1523     addr.base_kind = NATIVE_ADDR_BASE_REG;
   1524     addr.base.reg = AA_FP;
   1525     addr.base_type = i64;
   1526     for (u32 r = 0; r < vai.gp_reg_count && r < 8u; ++r) {
   1527       NativeLoc src = native_loc_reg(i64, NATIVE_REG_INT, r);
   1528       addr.offset = aa_fp_off_home_slot(r);
   1529       aa_emit_mem(a, 0, src, addr, mem);
   1530     }
   1531     return;
   1532   }
   1533   if (vai.kind != ABI_VA_LIST_AAPCS64) return;
   1534   memset(&mem, 0, sizeof mem);
   1535   mem.type = i64;
   1536   mem.size = 8;
   1537   mem.align = 8;
   1538   memset(&addr, 0, sizeof addr);
   1539   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   1540   addr.base.frame = a->va_gr_slot;
   1541   addr.base_type = i64;
   1542   for (u32 r = 0; r < vai.gp_reg_count && r < 8u; ++r) {
   1543     NativeLoc src = native_loc_reg(i64, NATIVE_REG_INT, r);
   1544     addr.offset = (i32)(r * vai.gp_slot_size);
   1545     aa_emit_mem(a, 0, src, addr, mem);
   1546   }
   1547   for (u32 r = 0; r < vai.fp_reg_count && r < 8u; ++r)
   1548     aa_emit_q_frame(a, 0, r, a->va_vr_slot, r * vai.fp_slot_size);
   1549 }
   1550 
   1551 static void aa_emit_entry_saves(AANativeTarget* a);
   1552 
   1553 /* Per-function state reset + function-symbol / cfi / prologue-anchor setup
   1554  * shared by both entry points (aa_func_begin for the single-pass path,
   1555  * aa_func_begin_known_frame for the optimizer path). Emits no prologue. */
   1556 static void aa_func_begin_common(NativeTarget* t, const CGFuncDesc* fd) {
   1557   AANativeTarget* a = aa_of(t);
   1558   MCEmitter* mc = t->mc;
   1559   a->func = fd;
   1560   /* Shared frame bookkeeping: clears the slot table, cum_off, max_outgoing,
   1561    * callee-save set, and known_frame/has_alloca/frame_final. cum_off counts
   1562    * frame-slot bytes below fp; the saved fp/lr pair (16 bytes at [fp, fp+8]) is
   1563    * *not* part of it — aa_build_layout adds it in aa_func_end. */
   1564   native_frame_reset(&a->frame);
   1565   a->incoming_stack_size = 0;
   1566   a->next_param_int = 0;
   1567   a->next_param_fp = 0;
   1568   /* 0-based byte cursor for incoming stack args (also reported as the
   1569    * caller's incoming_stack_size for tail-call realizability). bind_param
   1570    * forms its fp-relative address via aa_fp_off_in_arg(next_param_stack),
   1571    * which adds the saved-pair offset. */
   1572   a->next_param_stack = 0;
   1573   a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE;
   1574   a->saved_tmp_slot = NATIVE_FRAME_SLOT_NONE;
   1575   a->va_gr_slot = NATIVE_FRAME_SLOT_NONE;
   1576   a->va_vr_slot = NATIVE_FRAME_SLOT_NONE;
   1577   a->npatches = 0;
   1578   a->nalloca = 0;
   1579   a->slim_prologue = 0;
   1580   a->fp_at_bottom = 0;
   1581   a->frameless = 0;
   1582   a->frame_size_final = 0;
   1583   a->out_stack_final = 0;
   1584   a->slot_sp_base = 0;
   1585   a->uses_frame_base = 0;
   1586   a->frame_base_slot = NATIVE_FRAME_SLOT_NONE;
   1587   /* Windows variadic functions reserve a GP register home area at the top of
   1588    * the frame (just below the incoming stack args). The plain-pointer va_list
   1589    * then walks register-passed then stack-passed varargs as one block. Other
   1590    * ABIs leave gp_reg_count 0 here: Apple ARM64 routes all varargs to the
   1591    * stack, AAPCS64 uses a struct va_list with separate reg-save pointers. */
   1592   {
   1593     const ABIFuncInfo* fi = abi_cg_func_info(t->c->abi, fd->fn_type);
   1594     ABIVaListInfo vai = abi_va_list_layout(t->c->abi);
   1595     a->top_home_bytes = (fi && fi->variadic && vai.kind == ABI_VA_LIST_POINTER)
   1596                             ? vai.gp_reg_count * vai.gp_slot_size
   1597                             : 0u;
   1598   }
   1599   mc_set_section(mc, fd->text_section_id);
   1600   mc_emit_align(mc, 4, 0);
   1601   a->func_start = mc_pos(mc);
   1602   mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start);
   1603   mc_cfi_startproc(mc);
   1604   a->prologue_pos = mc_pos(mc);
   1605   a->minimal_prologue_words = 0;
   1606   a->epilogue_label = mc_label_new(mc);
   1607 }
   1608 
   1609 /* Single-pass (NativeDirectTarget) entry point: the frame is not known up
   1610  * front, so reserve a worst-case prologue region (patched in aa_func_end once
   1611  * max_outgoing / callee-saves are final) and emit the entry saves now. */
   1612 static void aa_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
   1613   AANativeTarget* a = aa_of(t);
   1614   MCEmitter* mc = t->mc;
   1615   u32 region;
   1616   aa_func_begin_common(t, fd);
   1617   /* tcc-style prologue. Emit the frame-independent entry live: save the fp/lr
   1618    * pair via a (16 + top_home)-byte pre-decrement and anchor fp at the pair.
   1619    * top_home (the Windows-variadic GP home area, 0 elsewhere) is known here, so
   1620    * the pair and fp land at exactly the addresses every fp-relative offset
   1621    * already assumes — the frame is byte-identical to the old single big-`sub`
   1622    * prologue, only the instruction sequence changes. */
   1623   aa_emit32(mc, aa64_stp64_pre(
   1624                     AA_FP, AA_LR, AA_SP,
   1625                     -(i32)((AA_FRAME_SAVE_SIZE + a->top_home_bytes) / 8u)));
   1626   aa_emit32(mc, aa64_add_imm(1, AA_FP, AA_SP, 0, 0)); /* mov x29, sp */
   1627   /* Arm the far-slot positive-scaled fast path for the common top-record
   1628    * layout. A Windows GP home area (top_home_bytes) places incoming args/home
   1629    * above the saved pair and is rare; leave those on the fp-relative path. */
   1630   a->slot_sp_base = (a->top_home_bytes == 0u);
   1631   /* Reserve only the deferred `sub sp` (+ Windows probe), patched in
   1632    * aa_func_end. The region starts here; record it for the patch and CFI. */
   1633   region = abi_stack_probe_interval(a->base.c->abi) ? AA_NDT_SUB_WORDS
   1634                                                     : AA_NDT_SUB_WORDS_NOPROBE;
   1635   a->prologue_pos = mc_pos(mc);
   1636   a->prologue_region_words = region;
   1637   {
   1638     u8 nops[AA_NDT_SUB_WORDS * 4u];
   1639     for (u32 i = 0; i < region; ++i) wr_u32_le(nops + i * 4u, aa64_nop());
   1640     if (mc->debug) {
   1641       u32 ofs = obj_pos(mc->obj, mc->section_id);
   1642       mc_emit_bytes(mc, nops, region * 4u);
   1643       for (u32 i = 0; i < region; ++i)
   1644         debug_emit_row(mc->debug, mc->section_id, ofs + i * 4u, mc->loc);
   1645     } else {
   1646       mc_emit_bytes(mc, nops, region * 4u);
   1647     }
   1648   }
   1649   aa_emit_entry_saves(a);
   1650 }
   1651 
   1652 /* Reserve the entry-save frame slots: the sret-pointer home (x8) and, for
   1653  * variadic functions, the argument register-save area. Reserving is split from
   1654  * emitting so the known-frame path can fix the full frame before the prologue;
   1655  * the single-pass path runs both back to back via aa_emit_entry_saves. */
   1656 static void aa_reserve_entry_saves(AANativeTarget* a) {
   1657   NativeTarget* t = &a->base;
   1658   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type);
   1659   if (abi && abi->has_sret) {
   1660     NativeFrameSlotDesc sd;
   1661     memset(&sd, 0, sizeof sd);
   1662     sd.type = builtin_id(KIT_CG_BUILTIN_I64);
   1663     sd.size = 8;
   1664     sd.align = 8;
   1665     sd.kind = NATIVE_FRAME_SLOT_SAVE;
   1666     a->sret_ptr_slot = t->frame_slot(t, &sd);
   1667   }
   1668   if (abi && abi->variadic) aa_reserve_variadic_reg_saves(a);
   1669 }
   1670 
   1671 /* Emit the entry-save stores (x8 → sret slot, then the variadic reg-save area).
   1672  * Slots must already be reserved (aa_reserve_entry_saves). */
   1673 static void aa_emit_entry_save_stores(AANativeTarget* a) {
   1674   NativeTarget* t = &a->base;
   1675   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type);
   1676   if (abi && abi->has_sret) {
   1677     NativeAddr addr;
   1678     NativeLoc src;
   1679     MemAccess mem;
   1680     KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   1681     memset(&addr, 0, sizeof addr);
   1682     addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   1683     addr.base.frame = a->sret_ptr_slot;
   1684     addr.base_type = i64;
   1685     memset(&src, 0, sizeof src);
   1686     src.kind = NATIVE_LOC_REG;
   1687     src.cls = NATIVE_REG_INT;
   1688     src.type = i64;
   1689     src.v.reg = 8u;
   1690     memset(&mem, 0, sizeof mem);
   1691     mem.type = i64;
   1692     mem.size = 8;
   1693     mem.align = 8;
   1694     aa_emit_mem(a, 0, src, addr, mem);
   1695   }
   1696   if (abi && abi->variadic) aa_emit_variadic_reg_save_stores(a);
   1697 }
   1698 
   1699 /* Reserve + emit the entry saves back to back. Single-pass (NativeDirectTarget)
   1700  * path, where the prologue region is a reserved worst-case block and slot
   1701  * offsets need not be final before it. */
   1702 static void aa_emit_entry_saves(AANativeTarget* a) {
   1703   aa_reserve_entry_saves(a);
   1704   aa_emit_entry_save_stores(a);
   1705 }
   1706 
   1707 static void aa_note_frame_state(NativeTarget* t,
   1708                                 const NativeFramePatchState* state) {
   1709   AANativeTarget* a = aa_of(t);
   1710   if (state && state->max_outgoing > a->frame.max_outgoing)
   1711     a->frame.max_outgoing = state->max_outgoing;
   1712 }
   1713 
   1714 /* Reserve a save slot for each callee-saved register the allocator used. Runs
   1715  * before frame-slot mapping so these slots get the lowest offsets, keeping the
   1716  * prologue stores within stur's signed-9-bit range. The prologue/epilogue
   1717  * save/restore is emitted from this list in aa_patch_prologue / aa_func_end. */
   1718 static void aa_reserve_callee_saves(NativeTarget* t, const u32* used,
   1719                                     u32 nclasses) {
   1720   AANativeTarget* a = aa_of(t);
   1721   /* aa64 homes each callee-save in its own 8-byte frame slot (reserved before
   1722    * the body slots so they sit nearest fp, in stur range), so alloc_slots=1.
   1723    * Adjacent integer slots are later paired into stp/ldp. */
   1724   NativeFrameSaveSpec spec[NATIVE_REG_VEC + 1];
   1725   memset(spec, 0, sizeof spec);
   1726   spec[NATIVE_REG_INT].size = 8;
   1727   spec[NATIVE_REG_INT].align = 8;
   1728   spec[NATIVE_REG_INT].type = builtin_id(KIT_CG_BUILTIN_I64);
   1729   spec[NATIVE_REG_FP].size = 8;
   1730   spec[NATIVE_REG_FP].align = 8;
   1731   spec[NATIVE_REG_FP].type = builtin_id(KIT_CG_BUILTIN_F64);
   1732   native_frame_set_callee_saves(&a->frame, used, nclasses, spec,
   1733                                 NATIVE_REG_VEC + 1, 1);
   1734 }
   1735 
   1736 static MemAccess aa_mem_for_type(NativeTarget* t, KitCgTypeId type, u32 size);
   1737 static void aa_words_callee_saves(AANativeTarget* a, int save, u32* words,
   1738                                   u32 cap, u32* n);
   1739 
   1740 static void aa_emit_callee_restores(AANativeTarget* a) {
   1741   u32 words[AA_PROLOGUE_WORDS];
   1742   u32 n = 0;
   1743   aa_words_callee_saves(a, 0, words, AA_PROLOGUE_WORDS, &n);
   1744   for (u32 i = 0; i < n; ++i) aa_emit32(a->base.mc, words[i]);
   1745 }
   1746 
   1747 static void aa_words_load_imm(AANativeTarget* a, u32* words, u32 cap, u32* n,
   1748                               u32 rd, i64 imm) {
   1749   u32 tmp[4];
   1750   u32 m = aa_load_imm_words(tmp, 4u, 1, rd, imm);
   1751   if (!m || *n + m > cap) aa_panic(a, "instruction patch too small");
   1752   for (u32 i = 0; i < m; ++i) words[(*n)++] = tmp[i];
   1753 }
   1754 
   1755 /* Windows large-frame stack probe. kit's prologue reserves the whole frame in
   1756  * one `sub sp, sp, #N`, but Windows grows a thread stack one guard page at a
   1757  * time: a sub that jumps SP more than a page past the guard page leaves the
   1758  * skipped pages uncommitted, and the first store into them faults (and, since
   1759  * SP itself is then in uncommitted memory, the fault can't even be delivered).
   1760  * Touch every page the frame spans, top-down, so each guard page commits in
   1761  * turn before the sub. Inlined (no external __chkstk symbol / no reloc in the
   1762  * patched prologue region); mirrors the linker's aa64_coff_chkstk body. Only
   1763  * x16/x17 are clobbered — the following sub-sp / saved-pair material re-derives
   1764  * both. Emitted only when frame_size > interval (one page). */
   1765 static void aa_words_stack_probe(AANativeTarget* a, u32* words, u32 cap, u32* n,
   1766                                  u32 frame_size, u32 interval) {
   1767   u32 imm12, sh;
   1768   if (!aa64_addsub_imm_fits(interval, &imm12, &sh))
   1769     aa_panic(a, "stack-probe interval not an addsub immediate");
   1770   /* x16 = frame_size ; x17 = sp */
   1771   aa_words_load_imm(a, words, cap, n, AA_TMP0, frame_size);
   1772   if (*n + 5u > cap) aa_panic(a, "instruction patch too small");
   1773   words[(*n)++] = aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0); /* mov x17, sp */
   1774   /* loop: x17 -= page ; x16 -= page (sets flags) ; touch [x17] ; b.gt loop */
   1775   words[(*n)++] = aa64_sub_imm(1, AA_TMP1, AA_TMP1, imm12, sh);
   1776   words[(*n)++] = aa64_subs_imm12(1, AA_TMP0, AA_TMP0, imm12, sh);
   1777   words[(*n)++] = aa64_ldr64_uimm12(31, AA_TMP1, 0); /* ldr xzr, [x17] */
   1778   /* branch back to the `sub x17` three words above while x16 stays positive */
   1779   words[(*n)++] =
   1780       aa64_brcond_pack((AA64BrCond){.imm19 = (u32)(-3), .cond = 0xcu /* GT */});
   1781 }
   1782 
   1783 static void aa_words_sub_sp_frame(AANativeTarget* a, u32* words, u32 cap,
   1784                                   u32* n, u32 frame_size) {
   1785   u32 imm12, sh;
   1786   if (aa64_addsub_imm_fits(frame_size, &imm12, &sh)) {
   1787     if (*n >= cap) aa_panic(a, "instruction patch too small");
   1788     words[(*n)++] = aa64_sub_imm(1, AA_SP, AA_SP, imm12, sh);
   1789     return;
   1790   }
   1791   aa_words_load_imm(a, words, cap, n, AA_TMP0, frame_size);
   1792   if (*n + 3u > cap) aa_panic(a, "instruction patch too small");
   1793   words[(*n)++] = aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0);
   1794   words[(*n)++] = aa64_sub(1, AA_TMP1, AA_TMP1, AA_TMP0);
   1795   words[(*n)++] = aa64_add_imm(1, AA_SP, AA_TMP1, 0, 0);
   1796 }
   1797 
   1798 /* Anchor fp at the AAPCS64 saved-pair address (= sp + saved-pair offset).
   1799  * The slim_prologue path achieves the same anchor in a single insn via
   1800  * `add x29, sp, #0` after the pre-decrement stp moves sp to the saved-pair. */
   1801 static void aa_words_frame_ptr_from_sp(AANativeTarget* a, u32* words, u32 cap,
   1802                                        u32* n, const AAFrameLayout* L) {
   1803   u32 imm12, sh;
   1804   u32 anchor = aa_sp_off_saved_pair(L);
   1805   if (aa64_addsub_imm_fits(anchor, &imm12, &sh)) {
   1806     if (*n >= cap) aa_panic(a, "instruction patch too small");
   1807     words[(*n)++] = aa64_add_imm(1, AA_FP, AA_SP, imm12, sh);
   1808     return;
   1809   }
   1810   aa_words_load_imm(a, words, cap, n, AA_TMP0, anchor);
   1811   if (*n + 2u > cap) aa_panic(a, "instruction patch too small");
   1812   words[(*n)++] = aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0);
   1813   words[(*n)++] = aa64_add(1, AA_FP, AA_TMP1, AA_TMP0);
   1814 }
   1815 
   1816 /* x17 = address of the saved-pair slot (= sp + saved-pair offset). Used by
   1817  * the fat prologue to materialize the stp destination when the offset
   1818  * doesn't fit stp's signed-7-bit-scaled immediate. */
   1819 static void aa_words_saved_pair_addr(AANativeTarget* a, u32* words, u32 cap,
   1820                                      u32* n, const AAFrameLayout* L) {
   1821   u32 save_off = aa_sp_off_saved_pair(L);
   1822   u32 imm12, sh;
   1823   if (aa64_addsub_imm_fits(save_off, &imm12, &sh)) {
   1824     if (*n >= cap) aa_panic(a, "instruction patch too small");
   1825     words[(*n)++] = aa64_add_imm(1, AA_TMP1, AA_SP, imm12, sh);
   1826     return;
   1827   }
   1828   aa_words_load_imm(a, words, cap, n, AA_TMP0, save_off);
   1829   if (*n + 2u > cap) aa_panic(a, "instruction patch too small");
   1830   words[(*n)++] = aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0);
   1831   words[(*n)++] = aa64_add(1, AA_TMP1, AA_TMP1, AA_TMP0);
   1832 }
   1833 
   1834 /* Positive frame-base offset of the slot homing the caller's AA_FRAME_BASE
   1835  * (alloca functions only). Same positive-scaled form the far-slot patches use,
   1836  * so it is one word and fits any frame within dword scaled reach (32 KB). */
   1837 static u32 aa_frame_base_save_off(AANativeTarget* a, const AAFrameLayout* L) {
   1838   i32 off = (i32)aa_sp_off_saved_pair(L) +
   1839             aa_fp_off_slot(a, aa_slot(a, a->frame_base_slot)->off);
   1840   if (off < 0 || ((u32)off >> 3) > 0xfffu)
   1841     aa_panic(a, "frame-base save slot out of positive scaled range");
   1842   return (u32)off;
   1843 }
   1844 
   1845 static void aa_words_restore_frame(AANativeTarget* a, u32* words, u32 cap,
   1846                                    u32* n, const AAFrameLayout* L) {
   1847   /* L10: frameless leaf — nothing to tear down (no record, sp untouched). The
   1848    * caller emits the bare `ret`. */
   1849   if (a->frameless) return;
   1850   if (!L->frame_size) return;
   1851   /* Restore the caller's AA_FRAME_BASE before tearing the frame down. It still
   1852    * holds the frame base here (untouched by the body), so the slot is reachable
   1853    * as a positive scaled offset off itself; the load then overwrites it with
   1854    * the saved caller value. Alloca single-pass functions only. */
   1855   if (a->uses_frame_base) {
   1856     u32 off = aa_frame_base_save_off(a, L);
   1857     if (*n >= cap) aa_panic(a, "epilogue too small for frame-base restore");
   1858     words[(*n)++] = aa_ldr_uimm_v(3, 0, AA_FRAME_BASE, AA_FRAME_BASE, off);
   1859   }
   1860   if (a->slim_prologue) {
   1861     if (*n + 1u > cap) aa_panic(a, "instruction patch too small");
   1862     /* `ldp x29, x30, [sp], #16` — pop saved pair, restore sp. */
   1863     words[(*n)++] = aa64_ldp64_post(AA_FP, AA_LR, AA_SP, 2);
   1864     return;
   1865   }
   1866   if (a->fp_at_bottom) {
   1867     u32 os = L->out_stack;
   1868     if (os == 0u && !a->frame.has_alloca && L->frame_size <= 504u) {
   1869       /* Folded fast path (matches the os==0 prologue fold): the saved pair sits
   1870        * at the very bottom (= sp, since no alloca floated it), so
   1871        * `ldp x29,x30,[sp],#N` reloads the pair AND releases the whole frame in
   1872        * one insn. Callee-saves were already restored by aa_emit_callee_restores.
   1873        * N <= 504 holds the post-index imm. */
   1874       if (*n + 1u > cap) aa_panic(a, "instruction patch too small");
   1875       words[(*n)++] =
   1876           aa64_ldp64_post(AA_FP, AA_LR, AA_SP, (i32)(L->frame_size / 8u));
   1877       return;
   1878     }
   1879     /* General bottom-record teardown, correct with OR without alloca (the
   1880      * post-indexed `ldp [sp],#N` can't be used once alloca floated sp): recover
   1881      * sp from the stable anchor x29, not from sp. `mov x16,x29 ; ldp x29,x30,
   1882      * [x16] ; add sp,x16,#(fs-os)` — x16 holds the anchor across the pair reload
   1883      * so the final add restores sp = x29 + (fs-os) = caller's sp = CFA. */
   1884     {
   1885       u32 bsz = L->frame_size - os; /* = aa_cfa_off in this layout */
   1886       u32 imm12, sh;
   1887       if (*n + 2u > cap) aa_panic(a, "instruction patch too small");
   1888       words[(*n)++] = aa64_add_imm(1, AA_TMP0, AA_FP, 0, 0); /* mov x16, x29 */
   1889       words[(*n)++] = aa64_ldp64_soff(AA_FP, AA_LR, AA_TMP0, 0);
   1890       if (aa64_addsub_imm_fits(bsz, &imm12, &sh)) {
   1891         if (*n >= cap) aa_panic(a, "instruction patch too small");
   1892         words[(*n)++] = aa64_add_imm(1, AA_SP, AA_TMP0, imm12, sh);
   1893       } else {
   1894         /* bsz exceeds the addsub-imm window. The 3-register add cannot target
   1895          * SP (Rd=31 means XZR there), so sum into x16 (a normal register) then
   1896          * copy to sp via the immediate `add sp, x16, #0` form (Rd=31 == SP). */
   1897         aa_words_load_imm(a, words, cap, n, AA_TMP1, bsz);
   1898         if (*n + 2u > cap) aa_panic(a, "instruction patch too small");
   1899         words[(*n)++] = aa64_add(1, AA_TMP0, AA_TMP0, AA_TMP1);
   1900         words[(*n)++] = aa64_add_imm(1, AA_SP, AA_TMP0, 0, 0);
   1901       }
   1902       return;
   1903     }
   1904   }
   1905   if (aa_cfa_off(a) == AA_FRAME_SAVE_SIZE) {
   1906     /* Common top-record case (CFA == 16: no Windows-variadic GP home area). The
   1907      * saved pair sits at [fp] and the caller's sp is fp+16, so `mov sp,x29`
   1908      * followed by a post-indexed `ldp x29,x30,[sp],#16` restores the pair and
   1909      * sp in two insns with no scratch — vs the fat path's three (`mov x16,fp;
   1910      * ldp [x16]; add sp,x16,#16`). Correct under alloca: `mov sp,x29` resets sp
   1911      * from the fp anchor before the post-index pops the pair. */
   1912     if (*n + 2u > cap) aa_panic(a, "instruction patch too small");
   1913     words[(*n)++] = aa64_add_imm(1, AA_SP, AA_FP, 0, 0);
   1914     words[(*n)++] = aa64_ldp64_post(AA_FP, AA_LR, AA_SP, 2);
   1915     return;
   1916   }
   1917   if (*n + 3u > cap) aa_panic(a, "instruction patch too small");
   1918   /* AAPCS64: fp is the saved-pair address. Reload pair from [fp], then restore
   1919    * sp to fp + CFA-offset (= caller's original sp = CFA). The CFA offset is
   1920    * AA_FRAME_SAVE_SIZE normally, plus the Windows-variadic GP home area when
   1921    * present (the only case that still reaches here). */
   1922   words[(*n)++] = aa64_add_imm(1, AA_TMP0, AA_FP, 0, 0);
   1923   words[(*n)++] = aa64_ldp64_soff(AA_FP, AA_LR, AA_TMP0, 0);
   1924   words[(*n)++] = aa64_add_imm(1, AA_SP, AA_TMP0, (u32)aa_cfa_off(a), 0);
   1925 }
   1926 
   1927 /* Emit callee-save store (save=1) or restore (save=0) words into `words`,
   1928  * pairing adjacent integer registers into a single stp/ldp.
   1929  * reserve_callee_saves allocates consecutive 8-byte slots in order, so
   1930  * callee_saves[i] sits 8 bytes above callee_saves[i+1]; for an int pair the
   1931  * lower-addressed reg[i+1] is the stp's Rt and reg[i] is Rt2. FP registers (and
   1932  * an unpaired trailing int) use the single-register stur/ldur form.
   1933  *
   1934  * Layout reach. Top-record callee-saves sit nearest fp at small NEGATIVE offsets
   1935  * (reserved first), so they always fit stur/stp directly. Bottom-record (W1.1)
   1936  * anchors x29 at the bottom, so callee-saves stack near the TOP at large
   1937  * POSITIVE offsets — past stp's +504 reach in any frame above ~512 bytes (and
   1938  * past the scaled str reach of 32 KB in a huge frame). When the highest
   1939  * callee-save offset exceeds stp's reach we materialize a base register
   1940  * `x16 = x29 + adj` once (adj 16-aligned so it stays addsub-encodable) and
   1941  * address the whole callee-save block relative to it at small offsets — x16 is
   1942  * dead here on both the prologue (after the x29 anchor) and epilogue (callee
   1943  * restores precede the teardown that reuses x16) paths. */
   1944 static void aa_words_callee_saves(AANativeTarget* a, int save, u32* words,
   1945                                   u32 cap, u32* n) {
   1946   u32 base = AA_FP;
   1947   i32 adj = 0;
   1948   if (a->fp_at_bottom && a->frame.ncallee_saves) {
   1949     /* The first reserved callee-save (smallest slot.off) gets the largest fp
   1950      * offset; that bound decides whether a base register is needed. */
   1951     i32 hi = aa_fp_off_slot(a, aa_slot(a, a->frame.callee_saves[0].slot)->off);
   1952     if (hi > 504) {
   1953       u32 imm12, sh;
   1954       adj = (i32)((u32)(hi - 504) & ~15u); /* 16-aligned, leaves hi-adj <= 504 */
   1955       if (!aa64_addsub_imm_fits((u32)adj, &imm12, &sh)) {
   1956         /* adj is a multiple of 4096 worth of frame; build it via x16 load_imm. */
   1957         aa_words_load_imm(a, words, cap, n, AA_TMP0, adj);
   1958         if (*n + 1u > cap) aa_panic(a, "prologue too large");
   1959         words[(*n)++] = aa64_add(1, AA_TMP0, AA_FP, AA_TMP0);
   1960       } else {
   1961         if (*n >= cap) aa_panic(a, "prologue too large");
   1962         words[(*n)++] = aa64_add_imm(1, AA_TMP0, AA_FP, imm12, sh);
   1963       }
   1964       base = AA_TMP0;
   1965     }
   1966   }
   1967   for (u32 i = 0; i < a->frame.ncallee_saves;) {
   1968     const AACalleeSave* cs = &a->frame.callee_saves[i];
   1969     i32 off = aa_fp_off_slot(a, aa_slot(a, cs->slot)->off) - adj;
   1970     if (i + 1u < a->frame.ncallee_saves && cs->cls == (u8)NATIVE_REG_INT &&
   1971         a->frame.callee_saves[i + 1u].cls == (u8)NATIVE_REG_INT) {
   1972       const AACalleeSave* cs2 = &a->frame.callee_saves[i + 1u];
   1973       i32 off2 = aa_fp_off_slot(a, aa_slot(a, cs2->slot)->off) - adj;
   1974       /* cs2 is reserved after cs (larger slot.off), so it is the lower address
   1975        * in both layouts (off2 = off - 8): stp's Rt = cs2, Rt2 = cs, base off2.
   1976        * stp/ldp's signed-7-bit scaled immediate reaches ±504. */
   1977       if (off2 < -512 || off2 > 504)
   1978         aa_panic(a, "callee-save pair offset out of prologue range");
   1979       if (*n >= cap) aa_panic(a, "prologue too large");
   1980       words[(*n)++] = save
   1981                           ? aa64_stp64_soff(cs2->reg, cs->reg, base, off2 / 8)
   1982                           : aa64_ldp64_soff(cs2->reg, cs->reg, base, off2 / 8);
   1983       i += 2u;
   1984     } else {
   1985       u32 v = cs->cls == (u8)NATIVE_REG_FP ? 1u : 0u;
   1986       if (*n >= cap) aa_panic(a, "prologue too large");
   1987       if (a->fp_at_bottom) {
   1988         /* Positive, 8-aligned offset (after the optional base adjustment): the
   1989          * unscaled stur (±256) can't reach it, so use the scaled unsigned-imm
   1990          * str/ldr (reach 32 KB; adj keeps offsets in range for huge frames). */
   1991         if (off < 0 || (u32)off > 0x7ff8u)
   1992           aa_panic(a, "callee-save offset out of prologue range");
   1993         words[(*n)++] = save ? aa_str_uimm_v(3, v, cs->reg, base, (u32)off)
   1994                              : aa_ldr_uimm_v(3, v, cs->reg, base, (u32)off);
   1995       } else {
   1996         if (off < -256 || off > 255)
   1997           aa_panic(a, "callee-save offset out of prologue range");
   1998         words[(*n)++] = save ? aa_stur_v(3, v, cs->reg, base, off)
   1999                              : aa_ldur_v(3, v, cs->reg, base, off);
   2000       }
   2001       i += 1u;
   2002     }
   2003   }
   2004 }
   2005 
   2006 /* Build the prologue instruction words for `L` into `words` (capacity `cap`),
   2007  * returning the count. Shared by the NativeDirectTarget patch path (reserves
   2008  * a fixed worst-case region, then patches it here) and the optimizer path
   2009  * (aa_func_begin_known_frame emits exactly these words up front).
   2010  *
   2011  * All variants establish a post-prologue state defined by L: saved x29/x30 at
   2012  * [fp]/[fp+8], callee-saves at aa_fp_off_slot of each. The top-record variants
   2013  * leave fp = sp + aa_sp_off_saved_pair(L) (saved-pair near the top); the
   2014  * bottom-record variant leaves fp = sp (saved-pair at the bottom). */
   2015 static u32 aa_build_prologue_words(AANativeTarget* a, const AAFrameLayout* L,
   2016                                    u32* words, u32 cap) {
   2017   u32 n = 0;
   2018   /* L10: frameless leaf — no record, no sp adjustment. The body uses only
   2019    * allocated registers + incoming args; sp is untouched and LR holds the live
   2020    * return address (no call clobbered it). */
   2021   if (a->frameless) return 0;
   2022   if (!L->frame_size) return 0;
   2023   if (a->slim_prologue) {
   2024     if (cap < 2u) aa_panic(a, "prologue too large");
   2025     /* `stp x29, x30, [sp, #-16]!; add x29, sp, #0` — the pre-decrement stp
   2026      * moves sp down to the saved-pair address, so a no-op add anchors fp
   2027      * there directly. AAPCS64 frame record. */
   2028     words[n++] = aa64_stp64_pre(AA_FP, AA_LR, AA_SP, -2);
   2029     words[n++] = aa64_add_imm(1, AA_FP, AA_SP, 0, 0);
   2030     return n;
   2031   }
   2032   if (a->fp_at_bottom) {
   2033     /* W1.1 uniform x29-at-bottom: anchor x29 = sp + out_stack, saved pair AT
   2034      * x29, slots above it. Two encodings: */
   2035     u32 os = L->out_stack;
   2036     u32 imm12, sh;
   2037     if (os == 0u && L->frame_size <= 504u) {
   2038       /* Folded fast path (the original fp_at_bottom): the saved pair is at the
   2039        * very bottom, so `stp x29,x30,[sp,#-N]!` decrements sp by the whole frame
   2040        * AND saves the pair in one insn; `mov x29,sp` anchors fp there. The
   2041        * post-index ldp on exit needs N <= 504. */
   2042       if (n + 2u > cap) aa_panic(a, "prologue too large");
   2043       words[n++] =
   2044           aa64_stp64_pre(AA_FP, AA_LR, AA_SP, -(i32)(L->frame_size / 8u));
   2045       words[n++] = aa64_add_imm(1, AA_FP, AA_SP, 0, 0);
   2046       aa_words_callee_saves(a, 1, words, cap, &n);
   2047       return n;
   2048     }
   2049     /* General bottom-record. `sub sp,sp,#fs` (robust / Windows-probed); save the
   2050      * pair at [sp,#os] and anchor x29 = sp + os; callee-saves at positive x29
   2051      * offsets. Outgoing args occupy [sp, sp+os); the body addresses them
   2052      * sp-relative, so anchoring x29 above them keeps every slot positive while
   2053      * leaving the arg area reachable from the (possibly alloca-floated) sp. */
   2054     {
   2055       u32 interval = abi_stack_probe_interval(a->base.c->abi);
   2056       if (interval && L->frame_size > interval)
   2057         aa_words_stack_probe(a, words, cap, &n, L->frame_size, interval);
   2058     }
   2059     aa_words_sub_sp_frame(a, words, cap, &n, L->frame_size);
   2060     if (os <= 504u) {
   2061       /* `stp x29,x30,[sp,#os]` — os fits stp's signed-7-bit scaled imm. */
   2062       if (n >= cap) aa_panic(a, "prologue too large");
   2063       words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_SP, (i32)(os / 8u));
   2064     } else {
   2065       /* Far outgoing area: build the saved-pair address (sp + os) into x17. The
   2066        * 3-register add cannot read SP (Rn=31 means XZR), so copy sp into x17 via
   2067        * the immediate `add x17, sp, #0` form first, then add os. */
   2068       if (aa64_addsub_imm_fits(os, &imm12, &sh)) {
   2069         if (n >= cap) aa_panic(a, "prologue too large");
   2070         words[n++] = aa64_add_imm(1, AA_TMP1, AA_SP, imm12, sh);
   2071       } else {
   2072         aa_words_load_imm(a, words, cap, &n, AA_TMP0, os);
   2073         if (n + 2u > cap) aa_panic(a, "prologue too large");
   2074         words[n++] = aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0);
   2075         words[n++] = aa64_add(1, AA_TMP1, AA_TMP1, AA_TMP0);
   2076       }
   2077       if (n >= cap) aa_panic(a, "prologue too large");
   2078       words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_TMP1, 0);
   2079     }
   2080     /* x29 = sp + os (the anchor). Same SP-as-Rn restriction: build via immediate
   2081      * copy + register add for a far os. */
   2082     if (aa64_addsub_imm_fits(os, &imm12, &sh)) {
   2083       if (n >= cap) aa_panic(a, "prologue too large");
   2084       words[n++] = aa64_add_imm(1, AA_FP, AA_SP, imm12, sh);
   2085     } else {
   2086       aa_words_load_imm(a, words, cap, &n, AA_TMP0, os);
   2087       if (n + 2u > cap) aa_panic(a, "prologue too large");
   2088       words[n++] = aa64_add_imm(1, AA_FP, AA_SP, 0, 0);
   2089       words[n++] = aa64_add(1, AA_FP, AA_FP, AA_TMP0);
   2090     }
   2091     aa_words_callee_saves(a, 1, words, cap, &n);
   2092     return n;
   2093   }
   2094   /* On targets that don't auto-grow the stack (Windows), probe each page the
   2095    * frame spans before the single large `sub sp` jumps past the guard page.
   2096    * slim_prologue/fp_at_bottom returned above — their frames are bounded to
   2097    * one page (≤16 / ≤504 bytes), so only this path can exceed `interval`. */
   2098   {
   2099     u32 interval = abi_stack_probe_interval(a->base.c->abi);
   2100     if (interval && L->frame_size > interval)
   2101       aa_words_stack_probe(a, words, cap, &n, L->frame_size, interval);
   2102   }
   2103   aa_words_sub_sp_frame(a, words, cap, &n, L->frame_size);
   2104   /* Fat top-record prologue: build the saved-pair address (sp + saved_pair_off)
   2105    * into x17, store the pair there, then anchor fp. Reached only by the
   2106    * single-pass (-O0) path and the Windows-variadic top_home case — the known-
   2107    * frame -O1 path takes slim_prologue or the fp_at_bottom layout above. */
   2108   aa_words_saved_pair_addr(a, words, cap, &n, L);
   2109   if (n >= cap) aa_panic(a, "prologue too large");
   2110   words[n++] = aa64_stp64_soff(AA_FP, AA_LR, AA_TMP1, 0); /* fp,lr @ [x17] */
   2111   aa_words_frame_ptr_from_sp(a, words, cap, &n, L);
   2112   /* Save callee-saved registers the allocator used (fp-relative; their slots
   2113    * were reserved first by aa_reserve_callee_saves so offsets fit stur). */
   2114   aa_words_callee_saves(a, 1, words, cap, &n);
   2115   return n;
   2116 }
   2117 
   2118 /* Build the deferred portion of the single-pass (-O0) prologue: the optional
   2119  * Windows page probe and the `sub sp` that grows the frame below the fp/lr pair
   2120  * the live entry (aa_func_begin) already saved and anchored fp at. Only
   2121  * `frame_size - 16 - top_home` (= aa_sp_off_saved_pair) remains to subtract. */
   2122 static u32 aa_build_ndt_sub_words(AANativeTarget* a, const AAFrameLayout* L,
   2123                                   u32* words, u32 cap) {
   2124   u32 n = 0;
   2125   u32 sub_bytes = aa_sp_off_saved_pair(L);
   2126   if (!sub_bytes) return 0;
   2127   {
   2128     u32 interval = abi_stack_probe_interval(a->base.c->abi);
   2129     if (interval && sub_bytes > interval)
   2130       aa_words_stack_probe(a, words, cap, &n, sub_bytes, interval);
   2131   }
   2132   aa_words_sub_sp_frame(a, words, cap, &n, sub_bytes);
   2133   /* Set up the frame-base anchor for alloca functions: sp now points at the
   2134    * frame base, so save the caller's AA_FRAME_BASE into its slot (positive
   2135    * scaled, valid before the move) and anchor AA_FRAME_BASE = sp. The body
   2136    * never touches AA_FRAME_BASE (callee-saved; the single-pass path allocates
   2137    * caller-saved only), so it stays the frame base across any alloca. */
   2138   if (a->uses_frame_base) {
   2139     u32 off = aa_frame_base_save_off(a, L);
   2140     if (n + 2u > cap) aa_panic(a, "prologue too small for frame-base setup");
   2141     words[n++] = aa_str_uimm_v(3, 0, AA_FRAME_BASE, AA_SP, off);
   2142     words[n++] = aa64_add_imm(1, AA_FRAME_BASE, AA_SP, 0, 0);
   2143   }
   2144   return n;
   2145 }
   2146 
   2147 /* Patch the reserved deferred-`sub` region (prologue_region_words at
   2148  * prologue_pos) once the frame is final. Single-pass (NativeDirectTarget) path
   2149  * only — the optimizer path emits its prologue final and never patches. The
   2150  * fixed entry (pair save + fp anchor) was already emitted live, so this writes
   2151  * just the probe + `sub sp` and branches over any unused tail. */
   2152 static void aa_patch_prologue(AANativeTarget* a, const AAFrameLayout* L) {
   2153   u32 words[AA_NDT_SUB_WORDS];
   2154   u32 region = a->prologue_region_words;
   2155   u32 n;
   2156   ObjSecId sec = a->func->text_section_id;
   2157   if (region > AA_NDT_SUB_WORDS) aa_panic(a, "prologue region too large");
   2158   memset(words, 0, sizeof words);
   2159   n = aa_build_ndt_sub_words(a, L, words, region);
   2160   /* If the deferred prologue is shorter than the reserved region, branch
   2161    * straight to the entry saves rather than executing the trailing NOPs. */
   2162   if (n < region) {
   2163     words[n] = aa64_b(region - n);
   2164     for (u32 i = n + 1u; i < region; ++i) words[i] = 0xd503201fu;
   2165   }
   2166   {
   2167     u8 pbytes[AA_NDT_SUB_WORDS * 4u];
   2168     for (u32 i = 0; i < region; ++i) wr_u32_le(pbytes + i * 4u, words[i]);
   2169     obj_patch(a->base.obj, sec, a->prologue_pos, pbytes, (size_t)region * 4u);
   2170   }
   2171 }
   2172 
   2173 static void aa_emit_restore_frame(AANativeTarget* a, const AAFrameLayout* L) {
   2174   MCEmitter* mc = a->base.mc;
   2175   u32 words[AA_PROLOGUE_WORDS];
   2176   u32 n = 0;
   2177   if (!L->frame_size) return;
   2178   aa_words_restore_frame(a, words, AA_PROLOGUE_WORDS, &n, L);
   2179   for (u32 i = 0; i < n; ++i) aa_emit32(mc, words[i]);
   2180 }
   2181 
   2182 /* Reserve one entry in the deferred-patch list, growing (arena-doubling) as
   2183  * needed. The returned pointer is stable until the next aa_patch_alloc. */
   2184 static AAPatch* aa_patch_alloc(AANativeTarget* a) {
   2185   if (a->npatches == a->patches_cap) {
   2186     u32 cap = a->patches_cap ? a->patches_cap * 2u : 8u;
   2187     AAPatch* nb = arena_zarray(a->base.c->tu, AAPatch, cap);
   2188     if (a->patches) memcpy(nb, a->patches, sizeof(*nb) * a->npatches);
   2189     a->patches = nb;
   2190     a->patches_cap = cap;
   2191   }
   2192   return &a->patches[a->npatches++];
   2193 }
   2194 
   2195 /* Append FP-relative loads that restore the saved callee registers (stp/ldp
   2196  * paired, same as the prologue saves). Shared by the tail-call patch; the
   2197  * function epilogue uses aa_emit_callee_restores. */
   2198 static void aa_words_callee_restores(AANativeTarget* a, u32* words, u32 cap,
   2199                                      u32* n) {
   2200   aa_words_callee_saves(a, 0, words, cap, n);
   2201 }
   2202 
   2203 /* Drain the deferred-patch list. Each entry targets a disjoint, fixed code
   2204  * position, so insertion order does not affect output. */
   2205 static void aa_apply_patches(AANativeTarget* a, const AAFrameLayout* L) {
   2206   ObjSecId sec = a->func->text_section_id;
   2207   for (u32 i = 0; i < a->npatches; ++i) {
   2208     AAPatch* p = &a->patches[i];
   2209     if (p->kind == AA_PATCH_ALLOCA) {
   2210       u32 imm12, sh;
   2211       if (!aa64_addsub_imm_fits(a->frame.max_outgoing, &imm12, &sh))
   2212         aa_panic(a, "outgoing area too large for alloca result");
   2213       aa_patch32(a->base.obj, sec, p->pos,
   2214                  aa64_add_imm(1, p->u.dst_reg, AA_SP, imm12, sh));
   2215     } else if (p->kind == AA_PATCH_SLOT) {
   2216       /* Resolve a far fixed-slot access to a positive scaled load/store off the
   2217        * stable bottom anchor. The slot sits at fp_off below fp (top-record);
   2218        * adding the bytes the prologue subtracted to reach the frame base
   2219        * (aa_sp_off_saved_pair) gives its offset from sp / AA_FRAME_BASE. */
   2220       const AASlotPatch* sl = &p->u.slot;
   2221       i32 fp_off = aa_fp_off_slot(a, sl->slot_off) + sl->extra;
   2222       i32 base_off = (i32)aa_sp_off_saved_pair(L) + fp_off;
   2223       u32 base = a->uses_frame_base ? AA_FRAME_BASE : AA_SP;
   2224       u32 ubo = (u32)base_off;
   2225       /* Natural alignment is guaranteed by the slot allocator (and the byte
   2226        * case is unconditionally aligned); a misalignment would mean a layout
   2227        * bug. */
   2228       if (base_off < 0 || (ubo & ((1u << sl->sz) - 1u)) != 0u)
   2229         aa_panic(a, "far slot offset negative or misaligned");
   2230       if ((ubo >> sl->sz) <= 0xfffu) {
   2231         /* Scaled immediate fits: one-word ldr/str; the reserved second
   2232          * placeholder word stays the nop emitted at body time. */
   2233         aa_patch32(a->base.obj, sec, p->pos,
   2234                    sl->load
   2235                        ? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, base, ubo)
   2236                        : aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, base, ubo));
   2237       } else {
   2238         /* The scaled reach (4 KB/8 KB/16 KB/32 KB by size) is below the largest
   2239          * frames, so build the address into the reserved second word instead:
   2240          * `add x17,base,#(off & ~0xfff) ; ldr/str rt,[x17,#(off & 0xfff)]`. The
   2241          * high part is a multiple of 4096 (fits the shift-12 addsub imm for any
   2242          * frame < 16 MB); the low part is <4096 and, being naturally aligned to
   2243          * the access size (base_off is, and hi is 4096-aligned), scale-divides
   2244          * cleanly. */
   2245         u32 hi = ubo & ~0xfffu;
   2246         u32 lo = ubo & 0xfffu;
   2247         if ((hi >> 12) > 0xfffu)
   2248           aa_panic(a, "far slot offset out of address-build range");
   2249         aa_patch32(a->base.obj, sec, p->pos,
   2250                    aa64_add_imm(1, AA_TMP1, base, hi >> 12, 1));
   2251         aa_patch32(a->base.obj, sec, p->pos + 4u,
   2252                    sl->load
   2253                        ? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, AA_TMP1, lo)
   2254                        : aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, AA_TMP1, lo));
   2255       }
   2256     } else { /* AA_PATCH_TAIL */
   2257       NativeLoc callee = p->u.callee;
   2258       u32 words[AA_TAIL_WORDS];
   2259       u32 n = 0;
   2260       memset(words, 0, sizeof words);
   2261       aa_words_callee_restores(a, words, AA_TAIL_WORDS, &n);
   2262       aa_words_restore_frame(a, words, AA_TAIL_WORDS, &n, L);
   2263       if (n >= AA_TAIL_WORDS) aa_panic(a, "tail patch too small");
   2264       if (callee.kind == NATIVE_LOC_REG) {
   2265         words[n++] = aa64_br(loc_reg(callee));
   2266       } else if (callee.kind == NATIVE_LOC_GLOBAL) {
   2267         while (n + 1u < AA_TAIL_WORDS) words[n++] = 0xd503201fu;
   2268         words[n++] = aa64_b(0);
   2269       } else {
   2270         aa_panic(a, "unsupported tail target");
   2271       }
   2272       while (n < AA_TAIL_WORDS) words[n++] = 0xd503201fu;
   2273       for (u32 w = 0; w < AA_TAIL_WORDS; ++w)
   2274         aa_patch32(a->base.obj, sec, p->pos + w * 4u, words[w]);
   2275     }
   2276   }
   2277 }
   2278 
   2279 static void aa_func_end(NativeTarget* t) {
   2280   AANativeTarget* a = aa_of(t);
   2281   MCEmitter* mc = t->mc;
   2282   AAFrameLayout L = aa_build_layout(a->frame.cum_off, a->frame.max_outgoing,
   2283                                     a->top_home_bytes);
   2284   /* known_frame (optimizer): prologue, allocas, and tail epilogues were emitted
   2285    * final and slim eligibility was settled in aa_func_begin_known_frame — there
   2286    * is nothing to patch. Single-pass (NDT): the frame-independent entry was
   2287    * emitted live and only the deferred `sub` region + patches remain; resolve
   2288    * them now that the frame is final. The advance past the prologue (for CFI)
   2289    * is the live fixed entry plus the reserved deferred region. */
   2290   u32 prologue_advance_words =
   2291       a->frame.known_frame
   2292           ? a->minimal_prologue_words
   2293           : (AA_NDT_FIXED_ENTRY_WORDS + a->prologue_region_words);
   2294   mc_label_place(mc, a->epilogue_label);
   2295   aa_emit_callee_restores(a);
   2296   aa_emit_restore_frame(a, &L);
   2297   aa_emit32(mc, aa64_ret(AA_LR));
   2298   if (a->frame.known_frame) {
   2299     /* The frame-planning pre-pass plus final prologue/alloca/tail emission must
   2300      * leave nothing deferred; a stray patch would mean a body-time frame change
   2301      * the final prologue never saw. */
   2302     if (a->npatches != 0) aa_panic(a, "known-frame path left deferred patches");
   2303   } else {
   2304     aa_patch_prologue(a, &L);
   2305     aa_apply_patches(a, &L);
   2306   }
   2307   if (a->frameless) {
   2308     /* L10: frameless leaf — CFA = sp (unchanged from entry) and the return
   2309      * address stays live in LR (the aa64 CIE default), so no saved-register
   2310      * rules. The state holds from the first instruction (offset 0). */
   2311     mc_cfi_set_next_pc_offset(mc, 0);
   2312     mc_cfi_def_cfa(mc, AA_SP, 0);
   2313   } else {
   2314     i32 cfa = aa_cfa_off(a);
   2315     mc_cfi_set_next_pc_offset(mc, prologue_advance_words * 4u);
   2316     /* CFA = caller's sp, an fp-relative offset that depends on the layout:
   2317      * fp+16 (top-record) or fp+frame_size (bottom-record). saved fp/lr live at
   2318      * [fp]/[fp+8] in both, hence at CFA-cfa / CFA-cfa+8. */
   2319     mc_cfi_def_cfa(mc, AA_FP, cfa);
   2320     mc_cfi_offset(mc, AA_FP, aa_fp_off_saved_fp() - cfa);
   2321     mc_cfi_offset(mc, AA_LR, aa_fp_off_saved_lr() - cfa);
   2322   }
   2323   obj_symbol_define(t->obj, a->func->sym, a->func->text_section_id,
   2324                     a->func_start, mc_pos(mc) - a->func_start);
   2325   if (a->func->atomize) {
   2326     obj_atom_define(t->obj, a->func->text_section_id, a->func_start,
   2327                     mc_pos(mc) - a->func_start, a->func->sym, 0);
   2328   }
   2329   /* Hand the function's PC range to the Debug producer so its line program
   2330    * (and DW_AT_low_pc/high_pc) cover this function — emit_section_line skips
   2331    * functions without a recorded range. */
   2332   if (mc->debug)
   2333     debug_func_pc_range(mc->debug, a->func->text_section_id, a->func_start,
   2334                         mc_pos(mc));
   2335   mc_cfi_endproc(mc);
   2336   mc_end_function(mc);
   2337   a->func = NULL;
   2338 }
   2339 
   2340 static NativeFrameSlot aa_frame_slot(NativeTarget* t,
   2341                                      const NativeFrameSlotDesc* d) {
   2342   return native_frame_slot_alloc(&aa_of(t)->frame, d);
   2343 }
   2344 
   2345 static void aa_release_frame_slot(NativeTarget* t, NativeFrameSlot slot) {
   2346   native_frame_release_slot(&aa_of(t)->frame, slot);
   2347 }
   2348 
   2349 static int aa_frame_slot_debug_loc(NativeTarget* t, NativeFrameSlot slot,
   2350                                    CGDebugLoc* out) {
   2351   AANativeTarget* a = aa_of(t);
   2352   AANativeSlot* s;
   2353   i32 fp_off;
   2354   if (!out) return 0;
   2355   memset(out, 0, sizeof *out);
   2356   if (slot == NATIVE_FRAME_SLOT_NONE || slot > a->frame.nslots) return 0;
   2357   s = aa_slot(a, slot);
   2358   fp_off = aa_fp_off_slot(a, s->off);
   2359   out->kind = CG_DEBUG_LOC_FRAME;
   2360   /* The hosted dbg stop snapshot currently carries x29/fp as the frame base
   2361    * for variable materialization, so report the same FP-relative slot offset
   2362    * used by native memory operands. */
   2363   out->v.frame_ofs = fp_off;
   2364   return 1;
   2365 }
   2366 
   2367 static void aa_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers,
   2368                                  u32 nclob, u32* int_mask, u32* fp_mask);
   2369 /* abi_clobber_masks is shared as native_asm_abi_clobber_masks
   2370  * (cg/native_asm.h); it reads the masks from t->regs->classes. */
   2371 
   2372 /* Build the callee-saved set the prologue must preserve: the allocator-assigned
   2373  * callee-saved registers (frame->callee_saved_used) plus any an inline-asm
   2374  * block clobbers. The latter are opaque to the optimizer's operand scan, so it
   2375  * forwards the raw clobber names (frame->asm_clobbers) and the arch-neutral
   2376  * clobber-ABI sets (frame->asm_clobber_abi_sets); we resolve both into masks
   2377  * and keep only the callee-saved ones, per AAPCS64: x19..x28 and the low 64
   2378  * bits of v8..v15 (x29/x30 are the frame pointer and link register, handled by
   2379  * the prologue head, not as ordinary callee-saves). This is the same register
   2380  * selection the per-block spill used, hoisted into the prologue. */
   2381 static int aa_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls,
   2382                                       Reg r) {
   2383   (void)t;
   2384   return (cls == NATIVE_REG_INT && r >= 19u && r <= 28u) ||
   2385          (cls == NATIVE_REG_FP && r >= 8u && r <= 15u);
   2386 }
   2387 
   2388 static u32 aa_known_callee_saves(NativeTarget* t,
   2389                                  const NativeKnownFrameDesc* frame, u32* out,
   2390                                  u32 cap) {
   2391   AANativeTarget* a = aa_of(t);
   2392   SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0};
   2393   return native_asm_known_callee_saves(
   2394       t, loc, frame, out, cap, aa_asm_clobber_masks,
   2395       aa_asm_reg_is_callee_saved);
   2396 }
   2397 
   2398 static u32 aa_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
   2399                                     int* variadic, u32* nparams);
   2400 
   2401 /* Optimizer entry point: the full frame is supplied up front, so the prologue,
   2402  * entry saves, slim-form eligibility, allocas, and tail epilogues are all final
   2403  * the moment they are emitted — no back-patching (aa_func_end skips the patch
   2404  * passes when a->frame.known_frame). Slot creation order matches the
   2405  * single-pass path (callee-saves first for stur range, then the static slots,
   2406  * then sret/variadic entry saves) so offsets are identical to what the patch
   2407  * path would produce. */
   2408 static void aa_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd,
   2409                                       const NativeKnownFrameDesc* frame,
   2410                                       NativeFrameSlot* out_slots) {
   2411   AANativeTarget* a = aa_of(t);
   2412   AAFrameLayout L;
   2413   u32 words[AA_PROLOGUE_WORDS];
   2414   u32 n;
   2415   aa_func_begin_common(t, fd);
   2416   a->frame.known_frame = 1;
   2417   if (frame) {
   2418     u32 cs[NATIVE_REG_CLASS_COUNT];
   2419     u32 ncs = aa_known_callee_saves(t, frame, cs, NATIVE_REG_CLASS_COUNT);
   2420     a->frame.has_alloca = frame->has_alloca;
   2421     if (ncs) aa_reserve_callee_saves(t, cs, ncs);
   2422     for (u32 i = 0; i < frame->nslots; ++i) {
   2423       NativeFrameSlot slot = aa_frame_slot(t, &frame->slots[i]);
   2424       if (out_slots) out_slots[i] = slot;
   2425     }
   2426     aa_reserve_entry_saves(a);
   2427     /* Reserve the atomic-RMW scratch spill last (matching its lazy position in
   2428      * the single-pass path), so aa_saved_tmp_spill reuses it instead of growing
   2429      * the frame mid-body. */
   2430     if (frame->needs_scratch_spill) {
   2431       NativeFrameSlotDesc sd;
   2432       memset(&sd, 0, sizeof sd);
   2433       sd.type = builtin_id(KIT_CG_BUILTIN_I64);
   2434       sd.size = 8;
   2435       sd.align = 8;
   2436       sd.kind = NATIVE_FRAME_SLOT_SPILL;
   2437       a->saved_tmp_slot = a->base.frame_slot(&a->base, &sd);
   2438     }
   2439     if (frame->max_outgoing > a->frame.max_outgoing)
   2440       a->frame.max_outgoing = frame->max_outgoing;
   2441   }
   2442   /* Frame is final: slot_bytes (cum_off) and out_stack (max_outgoing) are both
   2443    * known, so the prologue immediates and slim-form choice are settled here.
   2444    * frame_size_final + out_stack_final must be set before aa_build_prologue_words
   2445    * / entry saves, since the bottom-record offset helpers read both. */
   2446   L = aa_build_layout(a->frame.cum_off, a->frame.max_outgoing,
   2447                       a->top_home_bytes);
   2448   a->frame_size_final = L.frame_size;
   2449   a->out_stack_final = L.out_stack;
   2450   /* Layout choice (W1.1). Slim Tier A: no callee-saves, no alloca, no body
   2451    * slots, no outgoing stack args — the whole frame is the 16-byte record, with
   2452    * its own folded `stp [sp,#-16]!` encoding. Otherwise the uniform x29-at-bottom
   2453    * (fp_at_bottom) layout: x29 anchored out_stack bytes above sp, every slot a
   2454    * positive [x29,#k], for ALL remaining known frames (out_stack>0, alloca, and
   2455    * arbitrarily large frames included). The only exception is a Windows-variadic
   2456    * GP home area, which must sit ABOVE the saved pair (the plain-pointer va_list
   2457    * walks register then stack varargs as one contiguous block) — bottom-record
   2458    * has no room for it, so that case keeps the fat top-record layout.
   2459    * (See aa_func_end for the single-pass path, which never takes any slim/bottom
   2460    * form.) */
   2461   a->slim_prologue = a->frame.ncallee_saves == 0 && !a->frame.has_alloca &&
   2462                      L.slot_bytes == 0 && L.out_stack == 0 &&
   2463                      !a->top_home_bytes;
   2464   /* L10: a slim Tier A function that is additionally a leaf (no call clobbers
   2465    * x30/LR), has no inline asm (which could clobber LR / call opaquely), and
   2466    * never reads its own frame chain (__builtin_frame_address /
   2467    * __builtin_return_address) needs no frame record at all. Emit no prologue /
   2468    * epilogue / saved-pair CFI; the return address rides LR and the CFA stays at
   2469    * sp. A frameless function has frame_size 0 in this layout (slim's whole frame
   2470    * is the 16-byte record we are eliding), so it takes neither slim nor
   2471    * fp_at_bottom encoding. Additionally exclude any function with incoming
   2472    * stack arguments: those are addressed fp-relative (aa_fp_off_in_arg, off
   2473    * x29), so a function that reads them needs the x29 anchor a frameless
   2474    * prologue never sets up. (Stack args do not show up in L.slot_bytes /
   2475    * L.out_stack — they sit above the saved pair — so this guard is separate.) */
   2476   a->frameless = a->slim_prologue && frame && frame->is_leaf &&
   2477                  !frame->has_asm && !frame->reads_frame &&
   2478                  aa_signature_stack_bytes(t, fd->fn_type, NULL, NULL) == 0u;
   2479   if (a->frameless) a->slim_prologue = 0;
   2480   a->fp_at_bottom = !a->slim_prologue && !a->frameless && !a->top_home_bytes;
   2481   n = aa_build_prologue_words(a, &L, words, AA_PROLOGUE_WORDS);
   2482   for (u32 i = 0; i < n; ++i) aa_emit32(t->mc, words[i]);
   2483   a->minimal_prologue_words = n;
   2484   a->frame.frame_final = 1;
   2485   aa_emit_entry_save_stores(a);
   2486 }
   2487 
   2488 static void aa_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot,
   2489                      MemAccess mem) {
   2490   NativeAddr addr;
   2491   memset(&addr, 0, sizeof addr);
   2492   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   2493   addr.base.frame = slot;
   2494   addr.base_type = src.type;
   2495   aa_emit_mem(aa_of(t), 0, src, addr, mem);
   2496 }
   2497 
   2498 static void aa_reload(NativeTarget* t, NativeLoc dst, NativeFrameSlot slot,
   2499                       MemAccess mem) {
   2500   NativeAddr addr;
   2501   memset(&addr, 0, sizeof addr);
   2502   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   2503   addr.base.frame = slot;
   2504   addr.base_type = dst.type;
   2505   aa_emit_mem(aa_of(t), 1, dst, addr, mem);
   2506 }
   2507 
   2508 static MCLabel aa_label_new(NativeTarget* t) { return mc_label_new(t->mc); }
   2509 
   2510 static void aa_label_place(NativeTarget* t, MCLabel label) {
   2511   mc_label_place(t->mc, label);
   2512 }
   2513 
   2514 static void aa_jump(NativeTarget* t, MCLabel label) {
   2515   aa_emit32(t->mc, aa64_b(0));
   2516   mc_emit_label_ref(t->mc, label, R_AARCH64_JUMP26, 4, 0);
   2517 }
   2518 
   2519 static void aa_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc lhs,
   2520                           NativeLoc rhs, MCLabel label) {
   2521   /* Fuse `<reg> == 0` / `!= 0` tests into a single cbz/cbnz, dropping the
   2522    * separate `cmp <reg>,#0`. Only the GPR integer-equality case qualifies: the
   2523    * rhs must be an immediate zero (the cg layer passes IMM 0 for null/zero
   2524    * tests) and lhs a register (always true on the -O0 path). FP compares route
   2525    * through FCMP and keep cmp+b.cond. */
   2526   if ((op == CMP_EQ || op == CMP_NE) && !native_loc_is_fp(lhs) &&
   2527       lhs.kind == NATIVE_LOC_REG && rhs.kind == NATIVE_LOC_IMM &&
   2528       rhs.v.imm == 0) {
   2529     u32 sf = loc_is_64(t, lhs) ? 1u : 0u;
   2530     aa_emit32(t->mc, op == CMP_EQ ? aa64_cbz(sf, loc_reg(lhs), 0)
   2531                                   : aa64_cbnz_imm(sf, loc_reg(lhs), 0));
   2532     mc_emit_label_ref(t->mc, label, R_AARCH64_CONDBR19, 4, 0);
   2533     return;
   2534   }
   2535   aa_emit_cmp_to_flags(t, lhs, rhs);
   2536   /* CMP_ONE_F / CMP_UEQ_F have no single FP condition: take the branch from a
   2537    * pair of conditional branches to the same label (no scratch register). */
   2538   if (op == CMP_ONE_F) {
   2539     /* ordered & !=: branch if a<b (MI) or a>b (GT). */
   2540     aa_emit32(t->mc, aa64_brcond_pack((AA64BrCond){.cond = 0x4u})); /* MI */
   2541     mc_emit_label_ref(t->mc, label, R_AARCH64_CONDBR19, 4, 0);
   2542     aa_emit32(t->mc, aa64_brcond_pack((AA64BrCond){.cond = 0xcu})); /* GT */
   2543     mc_emit_label_ref(t->mc, label, R_AARCH64_CONDBR19, 4, 0);
   2544     return;
   2545   }
   2546   if (op == CMP_UEQ_F) {
   2547     /* unordered | ==: branch if a==b (EQ) or unordered (VS). */
   2548     aa_emit32(t->mc, aa64_brcond_pack((AA64BrCond){.cond = 0x0u})); /* EQ */
   2549     mc_emit_label_ref(t->mc, label, R_AARCH64_CONDBR19, 4, 0);
   2550     aa_emit32(t->mc, aa64_brcond_pack((AA64BrCond){.cond = 0x6u})); /* VS */
   2551     mc_emit_label_ref(t->mc, label, R_AARCH64_CONDBR19, 4, 0);
   2552     return;
   2553   }
   2554   aa_emit32(t->mc, aa64_brcond_pack((AA64BrCond){.cond = cmp_cond(op)}));
   2555   mc_emit_label_ref(t->mc, label, R_AARCH64_CONDBR19, 4, 0);
   2556 }
   2557 
   2558 static void aa_indirect_branch(NativeTarget* t, NativeLoc addr,
   2559                                const MCLabel* valid_targets, u32 ntargets) {
   2560   (void)valid_targets;
   2561   (void)ntargets;
   2562   aa_emit32(t->mc, aa64_br(loc_reg(addr)));
   2563 }
   2564 
   2565 static void aa_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel target) {
   2566   /* `&&label` address-take: adrp/add with the ADR_PREL_PG_HI21 +
   2567    * ADD_ABS_LO12_NC relocation pair against the label's per-block local symbol
   2568    * — the same form used to address a global — so the reference is genuinely
   2569    * relocatable (reaches ±4 GiB) and any assembler resolves it from the symbol.
   2570    * Replaces the old 16-byte INTRA-label sequence with a baked offset. */
   2571   MCEmitter* mc = t->mc;
   2572   u32 rd = loc_reg(dst);
   2573   ObjSymId sym = mc_label_symbol(mc, target);
   2574   u32 pos = mc_pos(mc);
   2575   aa_emit32(mc, aa64_adrp(rd, 0, 0));
   2576   mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_ADR_PREL_PG_HI21, sym, 0,
   2577                    0, 0);
   2578   pos = mc_pos(mc);
   2579   aa_emit32(mc, aa64_add_imm(1, rd, rd, 0, 0));
   2580   mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_ADD_ABS_LO12_NC, sym, 0,
   2581                    0, 0);
   2582 }
   2583 
   2584 static void aa_move(NativeTarget* t, NativeLoc dst, NativeLoc src) {
   2585   /* Identity move elision: same-class same-reg is a no-op on aarch64
   2586    * regardless of width (mov xN,xN and mov wN,wN both leave the low bits
   2587    * untouched). Catches no-op IR_CONVERT (BITCAST, ZEXT/SEXT with
   2588    * src_bits>=dst_bits, FEXT/FTRUNC across-class) when the allocator put
   2589    * dst and src in the same hard reg — common post #2.5 return-reg
   2590    * coalescing, e.g. `convert opnds=[v0,v0]` after a pointer-returning call
   2591    * was emitting `mov x0,x0`. Cross-class (fp<->gpr) bitcasts are not
   2592    * elided here even when the reg numbers match — the register files are
   2593    * disjoint. */
   2594   if (dst.kind == NATIVE_LOC_REG && src.kind == NATIVE_LOC_REG &&
   2595       native_loc_is_fp(dst) == native_loc_is_fp(src) && dst.v.reg == src.v.reg)
   2596     return;
   2597   if (native_loc_is_fp(dst) && native_loc_is_fp(src)) {
   2598     if (loc_size32(t, dst) == 16u)
   2599       aa_emit32(t->mc, aa_mov_vec16(loc_reg(dst), loc_reg(src)));
   2600     else
   2601       aa_emit32(t->mc, aa_fmov_fp(loc_size32(t, dst) == 8u, loc_reg(dst),
   2602                                   loc_reg(src)));
   2603   } else if (native_loc_is_fp(dst)) {
   2604     aa_emit32(t->mc,
   2605               aa_fmov_gpr_to_fp(loc_is_64(t, src), loc_reg(dst), loc_reg(src)));
   2606   } else if (native_loc_is_fp(src)) {
   2607     aa_emit32(t->mc,
   2608               aa_fmov_fp_to_gpr(loc_is_64(t, dst), loc_reg(dst), loc_reg(src)));
   2609   } else {
   2610     aa_emit32(t->mc,
   2611               aa64_mov_reg(loc_is_64(t, dst), loc_reg(dst), loc_reg(src)));
   2612   }
   2613 }
   2614 
   2615 static NativeLoc aa_tmp_loc(KitCgTypeId type, Reg reg);
   2616 
   2617 static void aa_load_imm_native(NativeTarget* t, NativeLoc dst, i64 imm) {
   2618   aa_emit_load_imm(t->mc, loc_is_64(t, dst), loc_reg(dst), imm);
   2619 }
   2620 
   2621 static void aa_load_const(NativeTarget* t, NativeLoc dst, ConstBytes cbytes) {
   2622   u64 v = 0;
   2623   if (cbytes.size > 8u)
   2624     compiler_panic(t->c, ((AANativeTarget*)t)->loc,
   2625                    "aarch64 native target: byte constant too large");
   2626   for (u32 i = 0; i < cbytes.size; ++i) v |= (u64)cbytes.bytes[i] << (i * 8u);
   2627   if (native_loc_is_fp(dst)) {
   2628     NativeLoc tmp = aa_tmp_loc(cbytes.type, AA_TMP0);
   2629     aa_emit_load_imm(t->mc, cbytes.size == 8u, AA_TMP0, (i64)v);
   2630     aa_move(t, dst, tmp);
   2631   } else {
   2632     aa_emit_load_imm(t->mc, loc_is_64(t, dst), loc_reg(dst), (i64)v);
   2633   }
   2634 }
   2635 
   2636 static void aa_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) {
   2637   AANativeTarget* a = aa_of(t);
   2638   u32 rd = loc_reg(dst);
   2639   aa_materialize_frame_index(a, &addr, rd);
   2640   switch ((NativeAddrBaseKind)addr.base_kind) {
   2641     case NATIVE_ADDR_BASE_FRAME: {
   2642       AANativeSlot* s = aa_slot(a, addr.base.frame);
   2643       aa_load_addr_from_base(a, rd, AA_FP,
   2644                              aa_fp_off_slot(a, s->off) + addr.offset, &addr);
   2645       return;
   2646     }
   2647     case NATIVE_ADDR_BASE_FRAME_VALUE: {
   2648       NativeAddr load;
   2649       MemAccess mem;
   2650       u32 base_reg = aa_addr_index_is_reg(&addr, rd) ? aa_tmp_avoiding(rd) : rd;
   2651       memset(&load, 0, sizeof load);
   2652       load.base_kind = NATIVE_ADDR_BASE_FRAME;
   2653       load.base.frame = addr.base.frame;
   2654       load.base_type =
   2655           addr.base_type ? addr.base_type : builtin_id(KIT_CG_BUILTIN_I64);
   2656       memset(&mem, 0, sizeof mem);
   2657       mem.type = load.base_type;
   2658       mem.size = type_size32(t, load.base_type);
   2659       mem.align = type_align32(t, load.base_type);
   2660       aa_emit_mem(a, 1, native_loc_reg(dst.type, NATIVE_REG_INT, base_reg),
   2661                   load, mem);
   2662       aa_load_addr_from_base(a, rd, base_reg, addr.offset, &addr);
   2663       return;
   2664     }
   2665     case NATIVE_ADDR_BASE_REG:
   2666       aa_load_addr_from_base(a, rd, addr.base.reg, addr.offset, &addr);
   2667       return;
   2668     case NATIVE_ADDR_BASE_GLOBAL: {
   2669       i64 addend = addr.base.global.addend + (i64)addr.offset;
   2670       u32 base_reg = aa_addr_index_is_reg(&addr, rd) ? aa_tmp_avoiding(rd) : rd;
   2671       u32 pos = mc_pos(t->mc);
   2672       if (aa_use_got_for_sym(t, addr.base.global.sym)) {
   2673         aa_emit32(t->mc, aa64_adrp(base_reg, 0, 0));
   2674         mc_emit_reloc_at(t->mc, t->mc->section_id, pos, R_AARCH64_ADR_GOT_PAGE,
   2675                          addr.base.global.sym, 0, 0, 0);
   2676         pos = mc_pos(t->mc);
   2677         aa_emit32(t->mc, aa_ldr_uimm(3, base_reg, base_reg, 0));
   2678         mc_emit_reloc_at(t->mc, t->mc->section_id, pos,
   2679                          R_AARCH64_LD64_GOT_LO12_NC, addr.base.global.sym, 0, 0,
   2680                          0);
   2681         if (addend) aa_emit_add_i64(a, base_reg, base_reg, addend);
   2682         aa_load_addr_from_base(a, rd, base_reg, 0, &addr);
   2683         return;
   2684       }
   2685       aa_emit32(t->mc, aa64_adrp(base_reg, 0, 0));
   2686       mc_emit_reloc_at(t->mc, t->mc->section_id, pos,
   2687                        R_AARCH64_ADR_PREL_PG_HI21, addr.base.global.sym, addend,
   2688                        0, 0);
   2689       pos = mc_pos(t->mc);
   2690       aa_emit32(t->mc, aa64_add_imm(1, base_reg, base_reg, 0, 0));
   2691       mc_emit_reloc_at(t->mc, t->mc->section_id, pos, R_AARCH64_ADD_ABS_LO12_NC,
   2692                        addr.base.global.sym, addend, 0, 0);
   2693       aa_load_addr_from_base(a, rd, base_reg, 0, &addr);
   2694       return;
   2695     }
   2696     default:
   2697       aa_panic(a, "unsupported load_addr");
   2698   }
   2699 }
   2700 
   2701 static void aa_load_native(NativeTarget* t, NativeLoc dst, NativeAddr addr,
   2702                            MemAccess mem) {
   2703   aa_emit_mem(aa_of(t), 1, dst, addr, mem);
   2704 }
   2705 
   2706 static void aa_store_native(NativeTarget* t, NativeAddr addr, NativeLoc src,
   2707                             MemAccess mem) {
   2708   aa_emit_mem(aa_of(t), 0, src, addr, mem);
   2709 }
   2710 
   2711 /* Windows/AArch64 TLS Local-Exec (PE-COFF). Mirrors x64_tls_addr_of_win64:
   2712  *   ldr  rd,  [x18, #0x58]            ; TEB.ThreadLocalStoragePointer
   2713  *   adrp x16, _tls_index              ; PAGEBASE_REL21
   2714  *   add  x16, x16, :lo12:_tls_index   ; PAGEOFFSET_12A
   2715  *   ldr  w16, [x16]                   ; module's TLS index
   2716  *   ldr  rd,  [rd, x16, lsl #3]       ; this module's TLS block base
   2717  *   add  rd,  rd, #:secrel_hi12:sym   ; SECREL_HIGH12A (sh=1)
   2718  *   add  rd,  rd, #:secrel_lo12:sym   ; SECREL_LOW12A  (sh=0)
   2719  * We materialize &_tls_index via ADRP+ADD (not LDR :lo12:) on purpose: the
   2720  * COFF reader collapses LDST32→LDST64 width, so an LDR :lo12: form would be
   2721  * mis-scaled at link time; ADD_ABS_LO12_NC carries no width and round-trips
   2722  * cleanly. AA_TMP0 (x16) is the reserved scratch; rd is an allocated reg
   2723  * distinct from x16/x17/x18. */
   2724 static void aa_tls_addr_of_win(NativeTarget* t, NativeLoc dst, ObjSymId sym,
   2725                                i64 addend) {
   2726   MCEmitter* mc = t->mc;
   2727   u32 sec = mc->section_id;
   2728   u32 rd = loc_reg(dst);
   2729   u32 pos;
   2730   Sym idx_name = pool_intern_slice(t->c->global, SLICE_LIT("_tls_index"));
   2731   ObjSymId idx_sym = obj_symbol_find(t->obj, idx_name);
   2732   if (idx_sym == 0)
   2733     idx_sym =
   2734         obj_symbol(t->obj, idx_name, SB_GLOBAL, SK_UNDEF, OBJ_SEC_NONE, 0, 0);
   2735   /* (1) rd = TEB.ThreadLocalStoragePointer. */
   2736   aa_emit32(mc, aa_ldr_uimm(3, rd, AA_WIN_TEB_REG, AA_WIN_TEB_TLS_PTR_OFF));
   2737   /* (2)+(3) x16 = &_tls_index via ADRP + ADD. */
   2738   pos = mc_pos(mc);
   2739   aa_emit32(mc, aa64_adrp(AA_TMP0, 0, 0));
   2740   mc_emit_reloc_at(mc, sec, pos, R_AARCH64_ADR_PREL_PG_HI21, idx_sym, 0, 0, 0);
   2741   pos = mc_pos(mc);
   2742   aa_emit32(mc, aa64_add_imm(1, AA_TMP0, AA_TMP0, 0, 0));
   2743   mc_emit_reloc_at(mc, sec, pos, R_AARCH64_ADD_ABS_LO12_NC, idx_sym, 0, 0, 0);
   2744   /* (4) w16 = _tls_index (the loaded value). */
   2745   aa_emit32(mc, aa_ldr_uimm(2, AA_TMP0, AA_TMP0, 0));
   2746   /* (5) rd = TLS array slot for this module: ldr rd, [rd, x16, lsl #3]. */
   2747   aa_emit32(mc, aa_ldst_regoff_v(3, 0, 1, rd, rd, AA_TMP0, 1));
   2748   /* (6) rd += :secrel_hi12:sym  (ADD with sh=1; linker patches imm12). */
   2749   pos = mc_pos(mc);
   2750   aa_emit32(mc, aa64_add_imm(1, rd, rd, 0, 1));
   2751   mc_emit_reloc_at(mc, sec, pos, R_COFF_AARCH64_SECREL_HIGH12A, sym, addend, 1,
   2752                    0);
   2753   /* (7) rd += :secrel_lo12:sym  (ADD with sh=0). */
   2754   pos = mc_pos(mc);
   2755   aa_emit32(mc, aa64_add_imm(1, rd, rd, 0, 0));
   2756   mc_emit_reloc_at(mc, sec, pos, R_COFF_AARCH64_SECREL_LOW12A, sym, addend, 1,
   2757                    0);
   2758 }
   2759 
   2760 static void aa_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym,
   2761                            i64 addend) {
   2762   AANativeTarget* a = aa_of(t);
   2763   MCEmitter* mc = t->mc;
   2764   u32 rd = loc_reg(dst);
   2765   u32 pos;
   2766   if (obj_format_tls_via_descriptor(t->c)) {
   2767     aa_emit32(mc, aa64_adrp(0, 0, 0));
   2768     pos = mc_pos(mc) - 4u;
   2769     mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_TLVP_LOAD_PAGE21, sym,
   2770                      0, 0, 0);
   2771     aa_emit32(mc, aa_ldr_uimm(3, 0, 0, 0));
   2772     pos = mc_pos(mc) - 4u;
   2773     mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_TLVP_LOAD_PAGEOFF12,
   2774                      sym, 0, 0, 0);
   2775     aa_emit32(mc, aa_ldr_uimm(3, AA_TMP0, 0, 0));
   2776     aa_emit32(mc, aa64_blr(AA_TMP0));
   2777     if (addend) aa_emit_add_i64(a, 0, 0, addend);
   2778     if (rd != 0) aa_emit32(mc, aa64_mov_reg(1, rd, 0));
   2779     return;
   2780   }
   2781   if (obj_format_tls_model(t->c) == OBJ_TLS_WINDOWS_TEB) {
   2782     aa_tls_addr_of_win(t, dst, sym, addend);
   2783     return;
   2784   }
   2785   if (t->c->target.obj != KIT_OBJ_ELF) {
   2786     aa_panic(a, "unsupported TLS object format");
   2787   }
   2788   aa_emit32(mc, aa_mrs_tpidr_el0(rd));
   2789   pos = mc_pos(mc);
   2790   aa_emit32(mc, aa64_add_imm(1, rd, rd, 0, 1));
   2791   mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_TLSLE_ADD_TPREL_HI12, sym,
   2792                    addend, 0, 0);
   2793   pos = mc_pos(mc);
   2794   aa_emit32(mc, aa64_add_imm(1, rd, rd, 0, 0));
   2795   mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_TLSLE_ADD_TPREL_LO12_NC,
   2796                    sym, addend, 0, 0);
   2797 }
   2798 
   2799 static NativeLoc aa_tmp_loc(KitCgTypeId type, Reg reg) {
   2800   NativeLoc loc;
   2801   memset(&loc, 0, sizeof loc);
   2802   loc.kind = NATIVE_LOC_REG;
   2803   loc.cls = NATIVE_REG_INT;
   2804   loc.type = type;
   2805   loc.v.reg = reg;
   2806   return loc;
   2807 }
   2808 
   2809 static NativeAddr aa_addr_plus(NativeAddr addr, u32 off) {
   2810   addr.offset += (i32)off;
   2811   return addr;
   2812 }
   2813 
   2814 static void aa_copy_bytes_dir(NativeTarget* t, NativeAddr dst, NativeAddr src,
   2815                               AggregateAccess access, int backward) {
   2816   KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   2817   KitCgTypeId i32 = builtin_id(KIT_CG_BUILTIN_I32);
   2818   KitCgTypeId i16 = builtin_id(KIT_CG_BUILTIN_I16);
   2819   KitCgTypeId i8 = builtin_id(KIT_CG_BUILTIN_I8);
   2820   NativeLoc tmp = aa_tmp_loc(i64, AA_TMP0);
   2821   u32 off = 0;
   2822   while (off < access.size) {
   2823     u32 rem = access.size - off;
   2824     u32 pos;
   2825     MemAccess mem = access.mem;
   2826     if (rem >= 8u) {
   2827       mem.type = i64;
   2828       mem.size = 8u;
   2829     } else if (rem >= 4u) {
   2830       mem.type = i32;
   2831       mem.size = 4u;
   2832       tmp.type = i32;
   2833     } else if (rem >= 2u) {
   2834       mem.type = i16;
   2835       mem.size = 2u;
   2836       tmp.type = i16;
   2837     } else {
   2838       mem.type = i8;
   2839       mem.size = 1u;
   2840       tmp.type = i8;
   2841     }
   2842     mem.align = mem.size;
   2843     pos = backward ? access.size - off - mem.size : off;
   2844     aa_load_native(t, tmp, aa_addr_plus(src, pos), mem);
   2845     aa_store_native(t, aa_addr_plus(dst, pos), tmp, mem);
   2846     off += mem.size;
   2847     tmp.type = i64;
   2848   }
   2849 }
   2850 
   2851 static void aa_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src,
   2852                           AggregateAccess access) {
   2853   aa_copy_bytes_dir(t, dst, src, access, 0);
   2854 }
   2855 
   2856 /* L3: widen the AGG_SET (whole-struct/array zero-init, memset) expander. The
   2857  * old form emitted one `strb` per byte even for an aligned whole-struct zero
   2858  * (cjson had a 64-long strb run). Instead, splat the fill byte across a 64-bit
   2859  * register once and store the widest aligned chunk covering the remaining run
   2860  * (`str x` -> `str w` -> `strh` -> `strb` tail), exactly mirroring the
   2861  * aa_copy_bytes_dir width ladder. This is a pure per-call expansion (it only
   2862  * removes emitted instructions); correctness holds for any fill byte (the splat
   2863  * replicates it to all 8 bytes), for unaligned bases/sizes (the ladder narrows
   2864  * the chunk to the remaining run, which the store offset never over-runs), and
   2865  * for the exact byte count.
   2866  *
   2867  * The splat costs a few up-front instructions, so for tiny runs the per-byte
   2868  * loop is still cheaper; gate the widening on a size threshold. */
   2869 #define AA_SET_BYTES_WIDEN_MIN 8u
   2870 
   2871 static void aa_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc byte_value,
   2872                          AggregateAccess access) {
   2873   KitCgTypeId i8 = builtin_id(KIT_CG_BUILTIN_I8);
   2874   NativeLoc byte = byte_value;
   2875   MemAccess mem = access.mem;
   2876   mem.type = i8;
   2877   mem.size = 1u;
   2878   mem.align = 1u;
   2879   byte.type = i8;
   2880   if (access.size < AA_SET_BYTES_WIDEN_MIN || native_loc_is_fp(byte_value)) {
   2881     /* Small run (or a non-integer fill value): the original per-byte loop. */
   2882     for (u32 off = 0; off < access.size; ++off)
   2883       aa_store_native(t, aa_addr_plus(dst, off), byte, mem);
   2884     return;
   2885   }
   2886   /* Splat the fill byte to every byte of a 64-bit scratch (AA_TMP0), distinct
   2887    * from the address base/index (IR_AGG_SET materializes the value clear of
   2888    * them). `and w,b,#0xff` clears any high bits, then ORR-shift doublings build
   2889    * bb -> bbbb -> bbbbbbbb. For a zero fill these collapse to zeros, but stay
   2890    * correct; the win is replacing N byte stores with N/8 dword stores. */
   2891   {
   2892     MCEmitter* mc = t->mc;
   2893     KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   2894     KitCgTypeId i32 = builtin_id(KIT_CG_BUILTIN_I32);
   2895     KitCgTypeId i16 = builtin_id(KIT_CG_BUILTIN_I16);
   2896     u32 b = loc_reg(byte_value);
   2897     NativeLoc val = aa_tmp_loc(i64, AA_TMP0);
   2898     u32 immr = 0, imms = 0, N = 0;
   2899     u32 off = 0;
   2900     (void)aa64_logimm_encode(0xffu, 0u, &N, &immr, &imms);
   2901     aa_emit32(mc, aa64_and_imm(0u, AA_TMP0, b, N, immr, imms)); /* w16 = b&0xff */
   2902     aa_emit32(mc, aa64_logsr_pack((AA64LogSR){.sf = 0u,
   2903                                               .opc = AA64_LOG_ORR_OPC,
   2904                                               .Rm = AA_TMP0,
   2905                                               .imm6 = 8u,
   2906                                               .Rn = AA_TMP0,
   2907                                               .Rd = AA_TMP0})); /* orr w,w,w<<8 */
   2908     aa_emit32(mc, aa64_logsr_pack((AA64LogSR){
   2909                       .sf = 0u,
   2910                       .opc = AA64_LOG_ORR_OPC,
   2911                       .Rm = AA_TMP0,
   2912                       .imm6 = 16u,
   2913                       .Rn = AA_TMP0,
   2914                       .Rd = AA_TMP0})); /* orr w,w,w<<16 -> low 32 = bbbb */
   2915     aa_emit32(mc, aa64_logsr_pack((AA64LogSR){
   2916                       .sf = 1u,
   2917                       .opc = AA64_LOG_ORR_OPC,
   2918                       .Rm = AA_TMP0,
   2919                       .imm6 = 32u,
   2920                       .Rn = AA_TMP0,
   2921                       .Rd = AA_TMP0})); /* orr x,x,x<<32 -> all 8 bytes = b */
   2922     while (off < access.size) {
   2923       u32 rem = access.size - off;
   2924       if (rem >= 8u) {
   2925         mem.type = i64;
   2926         mem.size = 8u;
   2927         val.type = i64;
   2928       } else if (rem >= 4u) {
   2929         mem.type = i32;
   2930         mem.size = 4u;
   2931         val.type = i32;
   2932       } else if (rem >= 2u) {
   2933         mem.type = i16;
   2934         mem.size = 2u;
   2935         val.type = i16;
   2936       } else {
   2937         mem.type = i8;
   2938         mem.size = 1u;
   2939         val.type = i8;
   2940       }
   2941       mem.align = mem.size;
   2942       aa_store_native(t, aa_addr_plus(dst, off), val, mem);
   2943       off += mem.size;
   2944     }
   2945   }
   2946 }
   2947 
   2948 static void aa_lsl_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn, u32 sh);
   2949 static void aa_lsr_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn, u32 sh);
   2950 static void aa_asr_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn, u32 sh);
   2951 
   2952 /* Strength-reduce `mul rd, rn, #imm` for the constants accepted by
   2953  * aa64_imul_strength_reducible into a single non-mul instruction. Callers
   2954  * must gate on aa64_imul_strength_reducible(sf, imm) — this routine panics
   2955  * on unhandled constants. */
   2956 static void aa_emit_mul_const_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn,
   2957                                   i64 imm) {
   2958   u64 a;
   2959   if (imm == 0) {
   2960     aa_emit32(t->mc, aa64_mov_reg(sf, rd, AA64_ZR));
   2961     return;
   2962   }
   2963   if (imm == 1) {
   2964     if (rd != rn) aa_emit32(t->mc, aa64_mov_reg(sf, rd, rn));
   2965     return;
   2966   }
   2967   if (imm == -1) {
   2968     aa_emit32(t->mc, aa64_neg(sf, rd, rn));
   2969     return;
   2970   }
   2971   /* +2^k: lsl rd, rn, #k */
   2972   a = (u64)imm;
   2973   if (imm > 0 && (a & (a - 1u)) == 0u) {
   2974     u32 k = (u32)__builtin_ctzll(a);
   2975     aa_lsl_imm(t, sf, rd, rn, k);
   2976     return;
   2977   }
   2978   /* -2^k: sub rd, xzr, rn, lsl #k */
   2979   if (imm < 0) {
   2980     a = (u64)(-imm);
   2981     if (a && (a & (a - 1u)) == 0u) {
   2982       u32 k = (u32)__builtin_ctzll(a);
   2983       aa_emit32(t->mc, aa64_addsubsr_pack((AA64AddSubSR){.sf = sf,
   2984                                                          .op = 1u,
   2985                                                          .S = 0u,
   2986                                                          .shift = 0u,
   2987                                                          .Rm = rn,
   2988                                                          .imm6 = k,
   2989                                                          .Rn = AA64_ZR,
   2990                                                          .Rd = rd}));
   2991       return;
   2992     }
   2993   }
   2994   /* 2^k + 1: add rd, rn, rn, lsl #k */
   2995   if (imm >= 3) {
   2996     u64 m = (u64)(imm - 1);
   2997     if ((m & (m - 1u)) == 0u) {
   2998       u32 k = (u32)__builtin_ctzll(m);
   2999       aa_emit32(t->mc, aa64_addsubsr_pack((AA64AddSubSR){.sf = sf,
   3000                                                          .op = 0u,
   3001                                                          .S = 0u,
   3002                                                          .shift = 0u,
   3003                                                          .Rm = rn,
   3004                                                          .imm6 = k,
   3005                                                          .Rn = rn,
   3006                                                          .Rd = rd}));
   3007       return;
   3008     }
   3009   }
   3010   /* 1 - 2^k: sub rd, rn, rn, lsl #k */
   3011   if (imm <= -1) {
   3012     u64 m = (u64)(1 - imm);
   3013     if (m && (m & (m - 1u)) == 0u) {
   3014       u32 k = (u32)__builtin_ctzll(m);
   3015       aa_emit32(t->mc, aa64_addsubsr_pack((AA64AddSubSR){.sf = sf,
   3016                                                          .op = 1u,
   3017                                                          .S = 0u,
   3018                                                          .shift = 0u,
   3019                                                          .Rm = rn,
   3020                                                          .imm6 = k,
   3021                                                          .Rn = rn,
   3022                                                          .Rd = rd}));
   3023       return;
   3024     }
   3025   }
   3026   aa_panic(aa_of(t), "aa_emit_mul_const_imm: unhandled constant");
   3027 }
   3028 
   3029 static void aa_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc lhs,
   3030                      NativeLoc rhs) {
   3031   u32 sf = loc_is_64(t, dst) ? 1u : 0u;
   3032   u32 rd = loc_reg(dst), rn = loc_reg(lhs), rm = loc_reg(rhs);
   3033   if (native_loc_is_fp(dst)) {
   3034     u32 d = loc_size32(t, dst) == 8u;
   3035     switch (op) {
   3036       case BO_FADD:
   3037         aa_emit32(t->mc, aa_fp_bin(0x002800u, d, rd, rn, rm));
   3038         return;
   3039       case BO_FSUB:
   3040         aa_emit32(t->mc, aa_fp_bin(0x003800u, d, rd, rn, rm));
   3041         return;
   3042       case BO_FMUL:
   3043         aa_emit32(t->mc, aa_fp_bin(0x000800u, d, rd, rn, rm));
   3044         return;
   3045       case BO_FDIV:
   3046         aa_emit32(t->mc, aa_fp_bin(0x001800u, d, rd, rn, rm));
   3047         return;
   3048       default:
   3049         aa_panic(aa_of(t), "unsupported floating binary op");
   3050     }
   3051   }
   3052   if (rhs.kind == NATIVE_LOC_IMM && (op == BO_IADD || op == BO_ISUB)) {
   3053     i64 imm = rhs.v.imm;
   3054     int is_add = (op == BO_IADD);
   3055     u32 imm12, sh;
   3056     if (imm < 0) {
   3057       is_add = !is_add;
   3058       imm = -imm;
   3059     }
   3060     if (!aa64_addsub_imm_fits(imm, &imm12, &sh))
   3061       aa_panic(aa_of(t), "binop immediate not encodable");
   3062     aa_emit32(t->mc, is_add ? aa64_add_imm(sf, rd, rn, imm12, sh)
   3063                             : aa64_sub_imm(sf, rd, rn, imm12, sh));
   3064     return;
   3065   }
   3066   if (rhs.kind == NATIVE_LOC_IMM && op == BO_IMUL) {
   3067     aa_emit_mul_const_imm(t, sf, rd, rn, rhs.v.imm);
   3068     return;
   3069   }
   3070   if (rhs.kind == NATIVE_LOC_IMM &&
   3071       (op == BO_SHL || op == BO_SHR_U || op == BO_SHR_S)) {
   3072     u32 shamt = (u32)rhs.v.imm; /* imm_legal guarantees 0 <= imm < datasize */
   3073     if (op == BO_SHL)
   3074       aa_lsl_imm(t, sf, rd, rn, shamt);
   3075     else if (op == BO_SHR_U)
   3076       aa_lsr_imm(t, sf, rd, rn, shamt);
   3077     else
   3078       aa_asr_imm(t, sf, rd, rn, shamt);
   3079     return;
   3080   }
   3081   if (rhs.kind == NATIVE_LOC_IMM &&
   3082       (op == BO_AND || op == BO_OR || op == BO_XOR)) {
   3083     u32 N, immr, imms;
   3084     if (!aa64_logimm_encode((u64)rhs.v.imm, sf, &N, &immr, &imms))
   3085       aa_panic(aa_of(t), "logical immediate not encodable");
   3086     if (op == BO_AND)
   3087       aa_emit32(t->mc, aa64_and_imm(sf, rd, rn, N, immr, imms));
   3088     else if (op == BO_OR)
   3089       aa_emit32(t->mc, aa64_orr_imm(sf, rd, rn, N, immr, imms));
   3090     else
   3091       aa_emit32(t->mc, aa64_eor_imm(sf, rd, rn, N, immr, imms));
   3092     return;
   3093   }
   3094   /* L7 shifted-register ALU: a single-use `lsl rm,#k` (k in 1..4) folded into
   3095    * this op's second source. Emit `<op> rd,rn,rm,lsl #k` in one instruction
   3096    * instead of `lsl rT,rm,#k; <op> rd,rn,rT`. Only an integer register rhs can
   3097    * carry a shift rider (immediates never do, and the recognition pass restricts
   3098    * it to add/sub/and/orr/eor). */
   3099   if (rhs.kind == NATIVE_LOC_REG && rhs.shift) {
   3100     u32 k = rhs.shift;
   3101     switch (op) {
   3102       case BO_IADD:
   3103         aa_emit32(t->mc, aa_addsub_lsl(sf, /*op=add*/ 0u, rd, rn, rm, k));
   3104         return;
   3105       case BO_ISUB:
   3106         aa_emit32(t->mc, aa_addsub_lsl(sf, /*op=sub*/ 1u, rd, rn, rm, k));
   3107         return;
   3108       case BO_AND:
   3109         aa_emit32(t->mc, aa_logsr_lsl(sf, AA64_LOG_AND_OPC, rd, rn, rm, k));
   3110         return;
   3111       case BO_OR:
   3112         aa_emit32(t->mc, aa_logsr_lsl(sf, AA64_LOG_ORR_OPC, rd, rn, rm, k));
   3113         return;
   3114       case BO_XOR:
   3115         aa_emit32(t->mc, aa_logsr_lsl(sf, AA64_LOG_EOR_OPC, rd, rn, rm, k));
   3116         return;
   3117       default:
   3118         aa_panic(aa_of(t), "shift rider on unsupported binop");
   3119     }
   3120   }
   3121   switch (op) {
   3122     case BO_IADD:
   3123       aa_emit32(t->mc, aa64_add(sf, rd, rn, rm));
   3124       return;
   3125     case BO_ISUB:
   3126       aa_emit32(t->mc, aa64_sub(sf, rd, rn, rm));
   3127       return;
   3128     case BO_IMUL:
   3129       aa_emit32(t->mc, aa64_mul(sf, rd, rn, rm));
   3130       return;
   3131     case BO_SDIV:
   3132       aa_emit32(t->mc, aa64_sdiv(sf, rd, rn, rm));
   3133       return;
   3134     case BO_UDIV:
   3135       aa_emit32(t->mc, aa64_udiv(sf, rd, rn, rm));
   3136       return;
   3137     case BO_SREM:
   3138       aa_emit32(t->mc, aa64_sdiv(sf, AA_TMP0, rn, rm));
   3139       aa_emit32(t->mc, aa64_mul(sf, AA_TMP0, AA_TMP0, rm));
   3140       aa_emit32(t->mc, aa64_sub(sf, rd, rn, AA_TMP0));
   3141       return;
   3142     case BO_UREM:
   3143       aa_emit32(t->mc, aa64_udiv(sf, AA_TMP0, rn, rm));
   3144       aa_emit32(t->mc, aa64_mul(sf, AA_TMP0, AA_TMP0, rm));
   3145       aa_emit32(t->mc, aa64_sub(sf, rd, rn, AA_TMP0));
   3146       return;
   3147     case BO_AND:
   3148       aa_emit32(t->mc, aa64_and(sf, rd, rn, rm));
   3149       return;
   3150     case BO_OR:
   3151       aa_emit32(t->mc, aa64_orr(sf, rd, rn, rm));
   3152       return;
   3153     case BO_XOR:
   3154       aa_emit32(t->mc, aa64_eor(sf, rd, rn, rm));
   3155       return;
   3156     case BO_SHL:
   3157       aa_emit32(t->mc, aa64_lslv(sf, rd, rn, rm));
   3158       return;
   3159     case BO_SHR_U:
   3160       aa_emit32(t->mc, aa64_lsrv(sf, rd, rn, rm));
   3161       return;
   3162     case BO_SHR_S:
   3163       aa_emit32(t->mc, aa64_asrv(sf, rd, rn, rm));
   3164       return;
   3165     default:
   3166       aa_panic(aa_of(t), "unsupported binary op");
   3167   }
   3168 }
   3169 
   3170 static void aa_unop(NativeTarget* t, UnOp op, NativeLoc dst, NativeLoc src) {
   3171   u32 sf = loc_is_64(t, dst) ? 1u : 0u;
   3172   if (native_loc_is_fp(dst)) {
   3173     switch (op) {
   3174       case UO_FNEG:
   3175       case UO_NEG:
   3176         aa_emit32(t->mc, aa_fneg(loc_size32(t, dst) == 8u, loc_reg(dst),
   3177                                  loc_reg(src)));
   3178         return;
   3179       default:
   3180         aa_panic(aa_of(t), "unsupported floating unary op");
   3181     }
   3182   }
   3183   switch (op) {
   3184     case UO_NEG:
   3185       aa_emit32(t->mc, aa64_neg(sf, loc_reg(dst), loc_reg(src)));
   3186       return;
   3187     case UO_BNOT:
   3188       aa_emit32(t->mc, aa64_mvn(sf, loc_reg(dst), loc_reg(src)));
   3189       return;
   3190     case UO_NOT:
   3191       aa_emit32(t->mc, aa64_subs_imm12(sf, AA64_ZR, loc_reg(src), 0, 0));
   3192       aa_emit32(t->mc, aa_cset(sf, loc_reg(dst), 0x0u));
   3193       return;
   3194     default:
   3195       aa_panic(aa_of(t), "unsupported unary op");
   3196   }
   3197 }
   3198 
   3199 static void aa_emit_cmp_to_flags(NativeTarget* t, NativeLoc lhs,
   3200                                  NativeLoc rhs) {
   3201   if (native_loc_is_fp(lhs)) {
   3202     aa_emit32(t->mc,
   3203               aa_fcmp(loc_size32(t, lhs) == 8u, loc_reg(lhs), loc_reg(rhs)));
   3204     return;
   3205   }
   3206   {
   3207     u32 sf = loc_is_64(t, lhs) ? 1u : 0u;
   3208     if (rhs.kind == NATIVE_LOC_IMM) {
   3209       u32 imm12 = 0, sh = 0;
   3210       if (rhs.v.imm < 0 || !aa64_addsub_imm_fits(rhs.v.imm, &imm12, &sh))
   3211         aa_panic(aa_of(t), "cmp immediate not encodable");
   3212       aa_emit32(t->mc, aa64_subs_imm12(sf, AA64_ZR, loc_reg(lhs), imm12, sh));
   3213       return;
   3214     }
   3215     aa_emit32(t->mc, aa_subs_reg(sf, AA64_ZR, loc_reg(lhs), loc_reg(rhs)));
   3216   }
   3217 }
   3218 
   3219 static void aa_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc lhs,
   3220                    NativeLoc rhs) {
   3221   u32 sf = loc_is_64(t, dst);
   3222   u32 rd = loc_reg(dst);
   3223   aa_emit_cmp_to_flags(t, lhs, rhs);
   3224   /* CMP_ONE_F (ordered & !=) and CMP_UEQ_F (unordered | ==) have no single
   3225    * AArch64 FP condition. After FCMP, unordered sets V (and Z=0), so VC
   3226    * (V==0) selects "ordered". */
   3227   if (op == CMP_ONE_F) {
   3228     /* ordered & not-equal: NE masked to the ordered case. */
   3229     aa_emit32(t->mc, aa_cset(sf, rd, 0x1u)); /* cset  rd, NE */
   3230     aa_emit32(t->mc,
   3231               aa64_csel_enc(sf, rd, rd, AA64_ZR, 0x7u)); /* csel  rd,rd,zr,VC */
   3232     return;
   3233   }
   3234   if (op == CMP_UEQ_F) {
   3235     /* equal, or forced to 1 when unordered. */
   3236     aa_emit32(t->mc, aa_cset(sf, rd, 0x0u)); /* cset  rd, EQ */
   3237     aa_emit32(t->mc, aa64_csinc_enc(sf, rd, rd, AA64_ZR,
   3238                                     0x7u)); /* csinc rd,rd,zr,VC */
   3239     return;
   3240   }
   3241   aa_emit32(t->mc, aa_cset(sf, rd, cmp_cond(op)));
   3242 }
   3243 
   3244 static void aa_convert(NativeTarget* t, ConvKind op, NativeLoc dst,
   3245                        NativeLoc src) {
   3246   int dst_fp = native_loc_is_fp(dst);
   3247   int src_fp = native_loc_is_fp(src);
   3248   switch (op) {
   3249     case CV_TRUNC:
   3250     case CV_BITCAST:
   3251       aa_move(t, dst, src);
   3252       return;
   3253     case CV_ZEXT: {
   3254       u32 src_bits = loc_size32(t, src) * 8u;
   3255       u32 dst_bits = loc_size32(t, dst) * 8u;
   3256       u32 sf = dst_bits > 32u;
   3257       if (src_bits >= dst_bits) {
   3258         aa_move(t, dst, src);
   3259       } else if (src_bits >= 32u) {
   3260         /* w-reg mov zero-extends into the x-reg. When dst and src are the same
   3261          * hard register (the -O0 convert-coalescing rename targets the source's
   3262          * own register) it is a no-op: the value already arrived there from a
   3263          * 32-bit-writing op, which itself cleared the upper 32 bits. */
   3264         if (!(dst.kind == NATIVE_LOC_REG && src.kind == NATIVE_LOC_REG &&
   3265               dst.v.reg == src.v.reg))
   3266           aa_emit32(t->mc, aa64_mov_reg(0, loc_reg(dst), loc_reg(src)));
   3267       } else {
   3268         aa_emit32(t->mc,
   3269                   aa_ubfm(sf, loc_reg(dst), loc_reg(src), 0, src_bits - 1u));
   3270       }
   3271       return;
   3272     }
   3273     case CV_SEXT: {
   3274       u32 src_bits = loc_size32(t, src) * 8u;
   3275       u32 dst_bits = loc_size32(t, dst) * 8u;
   3276       u32 sf = dst_bits > 32u;
   3277       if (src_bits >= dst_bits) {
   3278         aa_move(t, dst, src);
   3279       } else {
   3280         aa_emit32(t->mc,
   3281                   aa_sbfm(sf, loc_reg(dst), loc_reg(src), 0, src_bits - 1u));
   3282       }
   3283       return;
   3284     }
   3285     case CV_ITOF_S:
   3286       aa_emit32(t->mc, aa_scvtf(loc_size32(t, dst) == 8u, loc_is_64(t, src),
   3287                                 loc_reg(dst), loc_reg(src)));
   3288       return;
   3289     case CV_ITOF_U:
   3290       aa_emit32(t->mc, aa_ucvtf(loc_size32(t, dst) == 8u, loc_is_64(t, src),
   3291                                 loc_reg(dst), loc_reg(src)));
   3292       return;
   3293     case CV_FTOI_S:
   3294       aa_emit32(t->mc, aa_fcvtzs(loc_is_64(t, dst), loc_size32(t, src) == 8u,
   3295                                  loc_reg(dst), loc_reg(src)));
   3296       return;
   3297     case CV_FTOI_U:
   3298       aa_emit32(t->mc, aa_fcvtzu(loc_is_64(t, dst), loc_size32(t, src) == 8u,
   3299                                  loc_reg(dst), loc_reg(src)));
   3300       return;
   3301     case CV_FEXT:
   3302       if (dst_fp && src_fp)
   3303         aa_emit32(t->mc, aa_fcvt_d_s(loc_reg(dst), loc_reg(src)));
   3304       else
   3305         aa_move(t, dst, src);
   3306       return;
   3307     case CV_FTRUNC:
   3308       if (dst_fp && src_fp)
   3309         aa_emit32(t->mc, aa_fcvt_s_d(loc_reg(dst), loc_reg(src)));
   3310       else
   3311         aa_move(t, dst, src);
   3312       return;
   3313     default:
   3314       aa_panic(aa_of(t), "unsupported conversion");
   3315   }
   3316 }
   3317 
   3318 /* §E.3 narrow register-only entry points. The NDT crosses 16 B NativeRegLoc
   3319  * here; these reconstruct the NativeLoc the fat hook expects and delegate, so
   3320  * the emitted bytes are identical. Used only on the -O0 NDT path; the opt
   3321  * replay path calls aa_binop/aa_move/aa_cmp/aa_convert directly. */
   3322 static void aa_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst,
   3323                         NativeRegLoc a, NativeRegLoc b) {
   3324   aa_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
   3325            native_loc_from_reg(b));
   3326 }
   3327 
   3328 static void aa_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) {
   3329   aa_move(t, native_loc_from_reg(dst), native_loc_from_reg(src));
   3330 }
   3331 
   3332 static void aa_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst,
   3333                       NativeRegLoc a, NativeRegLoc b) {
   3334   aa_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
   3335          native_loc_from_reg(b));
   3336 }
   3337 
   3338 static void aa_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst,
   3339                           NativeRegLoc src) {
   3340   aa_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src));
   3341 }
   3342 
   3343 static void aa_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size,
   3344                       u32 align) {
   3345   AANativeTarget* a = aa_of(t);
   3346   u32 use_align = align < 16u ? 16u : align;
   3347   if (use_align & (use_align - 1u)) aa_panic(a, "alloca alignment not pow2");
   3348   aa_emit_add_imm(a, AA_TMP0, loc_reg(size), (i32)(use_align - 1u));
   3349   aa_emit_load_imm(t->mc, 1, AA_TMP1, -(i64)use_align);
   3350   aa_emit32(t->mc, aa64_and(1, AA_TMP0, AA_TMP0, AA_TMP1));
   3351   aa_emit32(t->mc, aa64_add_imm(1, AA_TMP1, AA_SP, 0, 0));
   3352   aa_emit32(t->mc, aa64_sub(1, AA_TMP1, AA_TMP1, AA_TMP0));
   3353   aa_emit32(t->mc, aa64_add_imm(1, AA_SP, AA_TMP1, 0, 0));
   3354   /* The alloca result is sp + outgoing-area bytes. On the known-frame path
   3355    * max_outgoing is already final, so emit the final `add dst, sp, #N` here; on
   3356    * the single-pass path it is not known yet, so record a patch. */
   3357   if (a->frame.known_frame) {
   3358     u32 imm12, sh;
   3359     if (!aa64_addsub_imm_fits(a->frame.max_outgoing, &imm12, &sh))
   3360       aa_panic(a, "outgoing area too large for alloca result");
   3361     aa_emit32(t->mc, aa64_add_imm(1, loc_reg(dst), AA_SP, imm12, sh));
   3362   } else {
   3363     AAPatch* p;
   3364     /* First alloca arms the frame-base anchor. The far-slot fast path can no
   3365      * longer key off sp (this `sub sp` floats it), so deferred AA_PATCH_SLOTs —
   3366      * those already emitted and any still to come — resolve against
   3367      * AA_FRAME_BASE instead. Reserve a static-frame home for the caller's
   3368      * value; the prologue saves it and sets AA_FRAME_BASE = frame base (before
   3369      * the body runs, so every slot access sees a valid anchor), the epilogue
   3370      * restores it. Only needed when slot_sp_base armed the fast path in the
   3371      * first place. */
   3372     if (a->slot_sp_base && !a->uses_frame_base) {
   3373       NativeFrameSlotDesc sd;
   3374       memset(&sd, 0, sizeof sd);
   3375       sd.type = builtin_id(KIT_CG_BUILTIN_I64);
   3376       sd.size = 8;
   3377       sd.align = 8;
   3378       sd.kind = NATIVE_FRAME_SLOT_SAVE;
   3379       a->frame_base_slot = t->frame_slot(t, &sd);
   3380       a->uses_frame_base = 1;
   3381     }
   3382     p = aa_patch_alloc(a);
   3383     p->kind = AA_PATCH_ALLOCA;
   3384     p->pos = mc_pos(t->mc);
   3385     p->u.dst_reg = loc_reg(dst);
   3386     a->nalloca++;
   3387     aa_emit32(t->mc, aa64_add_imm(1, loc_reg(dst), AA_SP, 0, 0));
   3388   }
   3389 }
   3390 
   3391 static MemAccess aa_mem_for_type(NativeTarget* t, KitCgTypeId type, u32 size) {
   3392   MemAccess mem;
   3393   memset(&mem, 0, sizeof mem);
   3394   mem.type = type;
   3395   mem.size = size ? size : type_size32(t, type);
   3396   mem.align = type_align32(t, type);
   3397   if (mem.align > mem.size && mem.size) mem.align = mem.size;
   3398   return mem;
   3399 }
   3400 
   3401 /* native_loc_reg / native_loc_stack are shared in native_target.h. */
   3402 
   3403 static NativeAddr aa_storage_addr(AANativeTarget* a, NativeLoc loc,
   3404                                   u32 offset) {
   3405   NativeAddr addr;
   3406   if (!native_loc_storage_addr(loc, (i32)offset, &addr))
   3407     aa_panic(a, "location is not storage");
   3408   return addr;
   3409 }
   3410 
   3411 static void aa_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) {
   3412   AANativeTarget* a = aa_of(t);
   3413   NativeAddr addr = aa_storage_addr(a, src, 0);
   3414   aa_load_addr(t, dst, addr);
   3415 }
   3416 
   3417 static void aa_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src,
   3418                          u32 offset, u32 size) {
   3419   AANativeTarget* a = aa_of(t);
   3420   MemAccess mem = aa_mem_for_type(t, dst.type, size);
   3421   if (src.kind == NATIVE_LOC_REG) {
   3422     aa_move(t, dst, src);
   3423     return;
   3424   }
   3425   if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) {
   3426     NativeAddr addr;
   3427     if (offset != 0u || size != t->c->target.ptr_size ||
   3428         !native_loc_address_value(src, &addr))
   3429       aa_panic(a, "split or invalid address-value argument");
   3430     aa_load_addr(t, dst, addr);
   3431     return;
   3432   }
   3433   if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_STORAGE) {
   3434     NativeAddr addr = aa_storage_addr(a, src, offset);
   3435     addr.base_type = dst.type;
   3436     aa_emit_mem(a, 1, dst, addr, mem);
   3437     return;
   3438   }
   3439   if (src.kind == NATIVE_LOC_IMM) {
   3440     i64 part;
   3441     if (!native_loc_imm_part(src, offset, size, &part))
   3442       aa_panic(a, "invalid immediate argument part");
   3443     aa_emit_load_imm(t->mc, loc_is_64(t, dst), loc_reg(dst), part);
   3444     return;
   3445   }
   3446   aa_panic(a, "unsupported call argument source");
   3447 }
   3448 
   3449 static void aa_store_part(NativeTarget* t, NativeLoc dst, NativeLoc src,
   3450                           u32 offset, u32 size) {
   3451   AANativeTarget* a = aa_of(t);
   3452   MemAccess mem = aa_mem_for_type(t, src.type, size);
   3453   if (native_loc_addr_role(dst) == NATIVE_LOC_ADDR_ROLE_STORAGE) {
   3454     NativeAddr addr = aa_storage_addr(a, dst, offset);
   3455     addr.base_type = src.type;
   3456     aa_emit_mem(a, 0, src, addr, mem);
   3457     return;
   3458   }
   3459   if (dst.kind == NATIVE_LOC_REG) {
   3460     aa_move(t, dst, src);
   3461     return;
   3462   }
   3463   aa_panic(a, "unsupported call return destination");
   3464 }
   3465 
   3466 static void aa_store_outgoing_part(NativeTarget* t, int tail_call,
   3467                                    u32 stack_off, NativeLoc src, u32 size) {
   3468   NativeAddr addr;
   3469   MemAccess mem = aa_mem_for_type(t, src.type, size);
   3470   memset(&addr, 0, sizeof addr);
   3471   addr.base_kind = NATIVE_ADDR_BASE_REG;
   3472   addr.base.reg = tail_call ? AA_FP : AA_SP;
   3473   addr.base_type = src.type;
   3474   /* Tail calls write outgoing args into the caller's incoming-args window
   3475    * (= [fp + 16 + off], same address the tail-callee will read via
   3476    * aa_fp_off_in_arg). Non-tail calls write to the sp-anchored outgoing
   3477    * area at the bottom of the caller's frame. */
   3478   addr.offset = tail_call ? aa_fp_off_tail_out_arg(aa_of(t), stack_off)
   3479                           : aa_sp_off_out_arg(stack_off);
   3480   aa_emit_mem(aa_of(t), 0, src, addr, mem);
   3481 }
   3482 
   3483 /* Copy only the bytes that belong to a stack-routed value. The caller owns the
   3484  * separately rounded ABI slot and advances its physical stack cursor itself;
   3485  * this helper never treats that carrier/padding size as readable source data. */
   3486 static void aa_store_outgoing_value_bytes(NativeTarget* t, int tail_call,
   3487                                           u32 stack_off, NativeLoc src,
   3488                                           u32 value_size) {
   3489   NativeLoc tmp =
   3490       native_loc_reg(src.type, NATIVE_REG_INT, AA_TMP0);
   3491   u32 off = 0;
   3492   while (off < value_size) {
   3493     u32 chunk = value_size - off;
   3494     if (chunk > 8u) chunk = 8u;
   3495     aa_load_part(t, tmp, src, off, chunk);
   3496     aa_store_outgoing_part(t, tail_call, stack_off + off, tmp, chunk);
   3497     off += chunk;
   3498   }
   3499 }
   3500 
   3501 static const ABIArgInfo* aa_param_abi(NativeTarget* t, const ABIFuncInfo* abi,
   3502                                       const NativeCallDesc* desc, u32 i,
   3503                                       ABIArgInfo* scratch) {
   3504   if (abi && i < abi->nparams) return &abi->params[i];
   3505   memset(scratch, 0, sizeof *scratch);
   3506   scratch->kind = ABI_ARG_DIRECT;
   3507   scratch->flags = ABI_AF_NONE;
   3508   scratch->nparts = 1;
   3509   scratch->parts = arena_zarray(t->c->tu, ABIArgPart, 1);
   3510   /* Windows ARM64 routes variadic floating-point arguments through the integer
   3511    * registers/stack (the classifier's remap_fp_parts_to_int does the same for
   3512    * the *named* params of a variadic function); the value's bit pattern moves
   3513    * via fmov x,d. Every other ABI keeps the `...` FP args in v registers. */
   3514   ((ABIArgPart*)scratch->parts)[0].cls =
   3515       (cg_type_is_float(t->c, desc->args[i].type) &&
   3516        !(abi && abi->vararg_fp_via_int))
   3517           ? ABI_CLASS_FP
   3518           : ABI_CLASS_INT;
   3519   ((ABIArgPart*)scratch->parts)[0].loc = ABI_LOC_REG;
   3520   ((ABIArgPart*)scratch->parts)[0].size = type_size32(t, desc->args[i].type);
   3521   ((ABIArgPart*)scratch->parts)[0].align = type_align32(t, desc->args[i].type);
   3522   ((ABIArgPart*)scratch->parts)[0].src_offset = 0;
   3523   return scratch;
   3524 }
   3525 
   3526 /* Stack footprint of a single argument part. AAPCS64 uses 8-byte slots. Apple
   3527  * ARM64 uses compact 4-byte slots for fixed stack-passed int32-sized values,
   3528  * but its forced stack variadics still use 8-byte slots. */
   3529 static u32 aa_stack_arg_min_align(const ABIFuncInfo* abi) {
   3530   return (abi && abi->stack_arg_min_align) ? abi->stack_arg_min_align : 8u;
   3531 }
   3532 
   3533 static u32 aa_vararg_stack_arg_min_align(const ABIFuncInfo* abi) {
   3534   if (abi && abi->vararg_stack_arg_min_align)
   3535     return abi->vararg_stack_arg_min_align;
   3536   return aa_stack_arg_min_align(abi);
   3537 }
   3538 
   3539 static u32 aa_vararg_stack_start(const ABIFuncInfo* abi, u32 cursor) {
   3540   return align_up_u32(cursor, aa_vararg_stack_arg_min_align(abi));
   3541 }
   3542 
   3543 /* Natural stack alignment of a part, capped at 16 (binary128). */
   3544 static u32 aa_part_stack_align_min(u32 min_align, const ABIArgPart* part) {
   3545   u32 al = part->align ? part->align : 8u;
   3546   if (al < min_align) al = min_align;
   3547   if (al > 16u) al = 16u;
   3548   return al;
   3549 }
   3550 
   3551 static u32 aa_part_stack_align(const ABIFuncInfo* abi, const ABIArgPart* part) {
   3552   return aa_part_stack_align_min(aa_stack_arg_min_align(abi), part);
   3553 }
   3554 
   3555 static u32 aa_part_vararg_stack_align(const ABIFuncInfo* abi,
   3556                                       const ABIArgPart* part) {
   3557   return aa_part_stack_align_min(aa_vararg_stack_arg_min_align(abi), part);
   3558 }
   3559 
   3560 static u32 aa_part_stack_size(const ABIFuncInfo* abi, const ABIArgPart* part) {
   3561   return align_up_u32(part->size ? part->size : 8u,
   3562                       aa_part_stack_align(abi, part));
   3563 }
   3564 
   3565 static u32 aa_part_vararg_stack_size(const ABIFuncInfo* abi,
   3566                                      const ABIArgPart* part) {
   3567   return align_up_u32(part->size ? part->size : 8u,
   3568                       aa_part_vararg_stack_align(abi, part));
   3569 }
   3570 
   3571 /* The scalar type used to move one ABI part through a register. Aggregate
   3572  * args/results are split into parts; each part must move at its own width, not
   3573  * the (possibly >8-byte) aggregate width. */
   3574 static KitCgTypeId aa_part_scalar_type(const ABIArgPart* part) {
   3575   if (part->cls == ABI_CLASS_FP) {
   3576     if (part->size <= 4u) return builtin_id(KIT_CG_BUILTIN_F32);
   3577     if (part->size <= 8u) return builtin_id(KIT_CG_BUILTIN_F64);
   3578     return builtin_id(KIT_CG_BUILTIN_F128);
   3579   }
   3580   switch (part->size) {
   3581     case 1u:
   3582       return builtin_id(KIT_CG_BUILTIN_I8);
   3583     case 2u:
   3584       return builtin_id(KIT_CG_BUILTIN_I16);
   3585     case 4u:
   3586       return builtin_id(KIT_CG_BUILTIN_I32);
   3587     default:
   3588       return builtin_id(KIT_CG_BUILTIN_I64);
   3589   }
   3590 }
   3591 
   3592 static u32 aa_class_vararg_stack_size(const ABIFuncInfo* abi,
   3593                                       const ABIArgInfo* ai) {
   3594   u32 total = 0;
   3595   u32 min_align = aa_vararg_stack_arg_min_align(abi);
   3596   if (!ai || ai->kind == ABI_ARG_IGNORE) return 0;
   3597   if (ai->kind == ABI_ARG_INDIRECT) return 8u;
   3598   for (u32 p = 0; p < ai->nparts; ++p) {
   3599     total = align_up_u32(total, aa_part_vararg_stack_align(abi, &ai->parts[p]));
   3600     total += aa_part_vararg_stack_size(abi, &ai->parts[p]);
   3601   }
   3602   return align_up_u32(total ? total : min_align, min_align);
   3603 }
   3604 
   3605 static u32 aa_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) {
   3606   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   3607   u32 next_int = 0, next_fp = 0, stack = 0;
   3608   for (u32 i = 0; i < desc->nargs; ++i) {
   3609     ABIArgInfo tmp;
   3610     const ABIArgInfo* ai = aa_param_abi(t, abi, desc, i, &tmp);
   3611     int force_stack =
   3612         abi && abi->variadic && abi->vararg_on_stack && i >= abi->nparams;
   3613     if (ai->kind == ABI_ARG_IGNORE) continue;
   3614     if (force_stack) {
   3615       stack = aa_vararg_stack_start(abi, stack);
   3616       stack += aa_class_vararg_stack_size(abi, ai);
   3617       continue;
   3618     }
   3619     if (ai->kind == ABI_ARG_INDIRECT) {
   3620       if (next_int < 8u)
   3621         next_int++;
   3622       else
   3623         stack += 8u;
   3624       continue;
   3625     }
   3626     for (u32 p = 0; p < ai->nparts; ++p) {
   3627       const ABIArgPart* part = &ai->parts[p];
   3628       if (part->cls == ABI_CLASS_FP) {
   3629         if (next_fp < 8u)
   3630           next_fp++;
   3631         else {
   3632           stack = align_up_u32(stack, aa_part_stack_align(abi, part));
   3633           stack += aa_part_stack_size(abi, part);
   3634         }
   3635       } else {
   3636         if (next_int < 8u)
   3637           next_int++;
   3638         else {
   3639           stack = align_up_u32(stack, aa_part_stack_align(abi, part));
   3640           stack += aa_part_stack_size(abi, part);
   3641         }
   3642       }
   3643     }
   3644   }
   3645   return align_up_u32(stack, 16u);
   3646 }
   3647 
   3648 /* Stack-argument bytes a call with `fn_type`'s fixed parameters uses. Reuses
   3649  * aa_call_stack_size by routing the declared params through it (their ABI
   3650  * classification is independent of the actual operand locations, which
   3651  * aa_call_stack_size ignores for register/stack placement). */
   3652 static u32 aa_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
   3653                                     int* variadic, u32* nparams) {
   3654   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fn_type);
   3655   NativeCallDesc d;
   3656   if (variadic) *variadic = abi ? (int)abi->variadic : 0;
   3657   if (nparams) *nparams = abi ? abi->nparams : 0u;
   3658   memset(&d, 0, sizeof d);
   3659   d.fn_type = fn_type;
   3660   d.nargs = abi ? abi->nparams : 0u;
   3661   if (d.nargs) d.args = arena_zarray(t->c->tu, NativeLoc, d.nargs);
   3662   return aa_call_stack_size(t, &d);
   3663 }
   3664 
   3665 /* Pure NativeTarget.call_stack_bytes: outgoing stack bytes for a full call
   3666  * descriptor (handles variadic stack args, unlike signature_stack_bytes which
   3667  * sees only the fixed params). aa_call_stack_size reads only fn_type and each
   3668  * args[i].type, so the frame-planning pre-pass can call this before emitting.
   3669  */
   3670 static u32 aa_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) {
   3671   return aa_call_stack_size(t, desc);
   3672 }
   3673 
   3674 /* One register-passed call argument: write `src` (or its address) into the
   3675  * argument register `dst`. Collected during planning and emitted as a batch so
   3676  * the backend can order them as a parallel copy (see aa_emit_reg_arg_moves). */
   3677 typedef NativeArgMove AAArgMove;
   3678 
   3679 /* AAPCS64/Apple permit at most 8 GP + 8 FP register-passed argument slots. */
   3680 #define AA_MAX_REG_ARG_MOVES 16u
   3681 
   3682 static void aa_emit_one_arg_move(NativeTarget* t, const NativeArgMove* m) {
   3683   if (m->is_addr)
   3684     aa_addr_of_loc(t, m->dst, m->src);
   3685   else
   3686     aa_load_part(t, m->dst, m->src, m->src_offset, m->size);
   3687 }
   3688 
   3689 /* Emit register-argument moves as a parallel copy via the shared scheduler:
   3690  * every register is read by all moves that source it before any move overwrites
   3691  * it; a true cycle is broken through a scratch. The allocator usually arranges
   3692  * a conflict-free order, but not always (notably variadic args, where it can
   3693  * leave a prior call's result in x0 even though x0 is this call's first arg
   3694  * register), so the backend must not assume a safe order. Cycle scratch is
   3695  * AA_TMP1 (x17) for int and v16 for fp — distinct from x16 (AA_TMP0), which may
   3696  * hold a stashed indirect callee. */
   3697 static void aa_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves,
   3698                                   u32 n) {
   3699   NativeArgShuffle s;
   3700   if (n > AA_MAX_REG_ARG_MOVES)
   3701     aa_panic(aa_of(t), "too many register arguments");
   3702   memset(&s, 0, sizeof s);
   3703   s.t = t;
   3704   s.emit_one = aa_emit_one_arg_move;
   3705   s.reg_move = aa_move;
   3706   s.scratch[NATIVE_REG_INT] = AA_TMP1;
   3707   s.scratch[NATIVE_REG_FP] = 16u;
   3708   s.scratch_class_mask = (1u << NATIVE_REG_INT) | (1u << NATIVE_REG_FP);
   3709   native_arg_shuffle(&s, moves, n);
   3710 }
   3711 
   3712 static void aa_marshal_call(NativeTarget* t, const NativeCallDesc* desc,
   3713                             NativeCallPhase* plan) {
   3714   NativeCallPhaseRet* rets;
   3715   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   3716   NativeLoc saved_indirect_callee;
   3717   int preserve_indirect_callee = 0;
   3718   /* Right-size the result scratch to the exact number of entries the ret loops
   3719    * below write: nparts on a DIRECT register return, 1 on the !abi fallback,
   3720    * 0 (NULL) otherwise (IGNORE / sret / no results). */
   3721   u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults)
   3722                       ? abi->ret.nparts
   3723                       : ((!abi && desc->nresults) ? 1u : 0u);
   3724   memset(plan, 0, sizeof *plan);
   3725   rets =
   3726       nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL;
   3727   plan->callee = desc->callee;
   3728   plan->rets = rets;
   3729   plan->flags = desc->flags;
   3730   plan->has_sret = abi && abi->has_sret;
   3731   plan->is_variadic = abi && abi->variadic;
   3732   plan->stack_arg_size = aa_call_stack_size(t, desc);
   3733   native_frame_note_outgoing(&aa_of(t)->frame, plan->stack_arg_size);
   3734   /* An indirect target in an argument/result or backend-private register can
   3735    * be overwritten while this hook fills x0..x7, x8 (sret), or uses x16/x17
   3736    * for exact-width stack arguments. Preserve it in LR, whose incoming value
   3737    * is already frame-saved for a non-tail call and is dead on a tail path.
   3738    * After all argument work, move it to the final call/branch register. */
   3739   if (plan->callee.kind == NATIVE_LOC_REG &&
   3740       (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT &&
   3741       (plan->callee.v.reg <= AA_X8 || plan->callee.v.reg == AA_TMP0 ||
   3742        plan->callee.v.reg == AA_TMP1)) {
   3743     saved_indirect_callee =
   3744         native_loc_reg(plan->callee.type, NATIVE_REG_INT, AA_LR);
   3745     aa_move(t, saved_indirect_callee, plan->callee);
   3746     preserve_indirect_callee = 1;
   3747   }
   3748   {
   3749     u32 next_int = 0, next_fp = 0, stack = 0, nmoves = 0;
   3750     int tail_call = (desc->flags & CG_CALL_TAIL) != 0;
   3751     AAArgMove moves[AA_MAX_REG_ARG_MOVES];
   3752     /* Stack-passed arguments are stored inline as we walk, *before* any
   3753      * argument register is written, so a stack-arg source that the allocator
   3754      * left in an arg register (e.g. a prior call's result still in x0, consumed
   3755      * as a variadic stack arg) is read while it is still live. Stack stores
   3756      * only touch memory and the AA_TMP0/v16 scratch, never an arg-register
   3757      * source, so emitting them first cannot clobber a register-arg source.
   3758      * Register-passed arguments are collected and emitted afterward as a
   3759      * parallel copy (aa_emit_reg_arg_moves) so they likewise never overwrite a
   3760      * register another argument still needs to read. */
   3761     for (u32 i = 0; i < desc->nargs; ++i) {
   3762       ABIArgInfo tmp;
   3763       const ABIArgInfo* ai = aa_param_abi(t, abi, desc, i, &tmp);
   3764       int force_stack =
   3765           abi && abi->variadic && abi->vararg_on_stack && i >= abi->nparams;
   3766       if (ai->kind == ABI_ARG_IGNORE) continue;
   3767       if (force_stack) {
   3768         u32 n = aa_class_vararg_stack_size(abi, ai);
   3769         stack = aa_vararg_stack_start(abi, stack);
   3770         if (ai->kind == ABI_ARG_DIRECT && ai->nparts == 1 && n <= 8u) {
   3771           /* Scalar variadic argument in one <=8-byte slot. Load the value at
   3772            * its OWN width — a sub-slot integer load zero-extends into the
   3773            * 64-bit temp — then store the whole slot. Loading the slot width
   3774            * from the value's home would pull garbage high bits, and storing
   3775            * only the value width would leave them undefined; either way a
   3776            * callee that va_arg's a wider type than was passed (the common
   3777            * `(int)0` read back as a null pointer) would see garbage. Zero-
   3778            * extend-then-store-the-slot is what clang emits and makes it
   3779            * well-defined. */
   3780           const ABIArgPart* part = &ai->parts[0];
   3781           NativeAllocClass cls =
   3782               part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   3783           NativeLoc tmpreg = native_loc_reg(
   3784               desc->args[i].type, cls, cls == NATIVE_REG_FP ? 16u : AA_TMP0);
   3785           aa_load_part(t, tmpreg, desc->args[i], part->src_offset, part->size);
   3786           aa_store_outgoing_part(t, tail_call, stack, tmpreg, n);
   3787           stack += n;
   3788           continue;
   3789         }
   3790         {
   3791           /* Aggregate / multi-word variadic argument: `n` is the physical ABI
   3792            * carrier (Apple rounds a 12-byte unnamed aggregate to 16), not a
   3793            * source access width. Copy the value's exact object bytes, leaving
   3794            * carrier padding untouched, then advance by the full slot. */
   3795           u32 value_size = type_size32(t, desc->args[i].type);
   3796           aa_store_outgoing_value_bytes(t, tail_call, stack, desc->args[i],
   3797                                         value_size);
   3798           stack += n;
   3799         }
   3800         continue;
   3801       }
   3802       if (ai->kind == ABI_ARG_INDIRECT) {
   3803         if (next_int < 8u) {
   3804           AAArgMove* m = &moves[nmoves++];
   3805           m->dst = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64),
   3806                                   NATIVE_REG_INT, next_int++);
   3807           m->src = desc->args[i];
   3808           m->src_offset = 0;
   3809           m->size = 8;
   3810           m->is_addr = 1;
   3811         } else {
   3812           NativeLoc ptr = native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64),
   3813                                          NATIVE_REG_INT, AA_TMP0);
   3814           aa_addr_of_loc(t, ptr, desc->args[i]);
   3815           aa_store_outgoing_part(t, tail_call, stack, ptr, 8);
   3816           stack += 8u;
   3817         }
   3818         continue;
   3819       }
   3820       for (u32 p = 0; p < ai->nparts; ++p) {
   3821         const ABIArgPart* part = &ai->parts[p];
   3822         NativeAllocClass cls =
   3823             part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   3824         if ((cls == NATIVE_REG_FP && next_fp < 8u) ||
   3825             (cls == NATIVE_REG_INT && next_int < 8u)) {
   3826           AAArgMove* m = &moves[nmoves++];
   3827           m->dst =
   3828               native_loc_reg(desc->args[i].type, cls,
   3829                              cls == NATIVE_REG_FP ? next_fp++ : next_int++);
   3830           m->src = desc->args[i];
   3831           m->src_offset = part->src_offset;
   3832           m->size = part->size;
   3833           m->is_addr = 0;
   3834         } else {
   3835           NativeLoc tmpreg = native_loc_reg(
   3836               desc->args[i].type, cls, cls == NATIVE_REG_FP ? 16u : AA_TMP0);
   3837           aa_load_part(t, tmpreg, desc->args[i], part->src_offset, part->size);
   3838           stack = align_up_u32(stack, aa_part_stack_align(abi, part));
   3839           aa_store_outgoing_part(t, tail_call, stack, tmpreg, part->size);
   3840           stack += aa_part_stack_size(abi, part);
   3841         }
   3842       }
   3843     }
   3844     aa_emit_reg_arg_moves(t, moves, nmoves);
   3845     /* Set the indirect-result register (x8) *after* the argument loads: an
   3846      * argument source may have been allocated to x8, and the sret pointer load
   3847      * would otherwise clobber it before it is moved into its argument
   3848      * register. */
   3849     if (abi && abi->has_sret) {
   3850       NativeLoc x8 =
   3851           native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, 8u);
   3852       if (desc->flags & CG_CALL_TAIL) {
   3853         AANativeTarget* a = aa_of(t);
   3854         NativeLoc saved = native_loc_stack(x8.type, a->sret_ptr_slot, 0);
   3855         aa_load_part(t, x8, saved, 0, 8);
   3856       } else if (desc->nresults) {
   3857         aa_addr_of_loc(t, x8, desc->results[0]);
   3858       }
   3859     }
   3860     if (preserve_indirect_callee) {
   3861       NativeLoc final_callee = native_loc_reg(
   3862           saved_indirect_callee.type, NATIVE_REG_INT,
   3863           tail_call ? AA_TAIL_TARGET : AA_TMP0);
   3864       aa_move(t, final_callee, saved_indirect_callee);
   3865       plan->callee = final_callee;
   3866     }
   3867   }
   3868   if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) {
   3869     u32 nr = 0, ni = 0, nf = 0;
   3870     for (u32 p = 0; p < abi->ret.nparts; ++p) {
   3871       const ABIArgPart* part = &abi->ret.parts[p];
   3872       NativeAllocClass cls =
   3873           part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   3874       KitCgTypeId pty = aa_part_scalar_type(part);
   3875       rets[nr].src =
   3876           native_loc_reg(pty, cls, cls == NATIVE_REG_FP ? nf++ : ni++);
   3877       rets[nr].dst = desc->results[0];
   3878       if (rets[nr].dst.kind == NATIVE_LOC_FRAME)
   3879         rets[nr].dst = native_loc_stack(pty, desc->results[0].v.frame,
   3880                                         (i32)part->src_offset);
   3881       else if (rets[nr].dst.kind == NATIVE_LOC_STACK) {
   3882         rets[nr].dst.v.stack.offset += (i32)part->src_offset;
   3883         rets[nr].dst.type = pty;
   3884       } else if (rets[nr].dst.kind == NATIVE_LOC_ADDR) {
   3885         rets[nr].dst.v.addr.offset += (i32)part->src_offset;
   3886         rets[nr].dst.type = pty;
   3887       }
   3888       rets[nr].mem = aa_mem_for_type(t, pty, part->size);
   3889       nr++;
   3890     }
   3891     plan->nrets = nr;
   3892   } else if (abi && abi->ret.kind == ABI_ARG_IGNORE) {
   3893     plan->nrets = 0;
   3894   } else if (!abi && desc->nresults) {
   3895     rets[0].src = native_loc_reg(desc->results[0].type, NATIVE_REG_INT, 0);
   3896     rets[0].dst = desc->results[0];
   3897     rets[0].mem = aa_mem_for_type(t, desc->results[0].type, 0);
   3898     plan->nrets = 1;
   3899   }
   3900 }
   3901 
   3902 static void aa_ret(NativeTarget* t);
   3903 
   3904 static void aa_emit_tail_site(NativeTarget* t, NativeLoc callee) {
   3905   AANativeTarget* a = aa_of(t);
   3906   if (a->frame.known_frame) {
   3907     /* Frame is final: emit the tail epilogue (callee restores + frame restore +
   3908      * branch) directly, exactly the words aa_apply_patches would patch in but
   3909      * without the reserved NOP padding. */
   3910     AAFrameLayout L = aa_build_layout(a->frame.cum_off, a->frame.max_outgoing,
   3911                                       a->top_home_bytes);
   3912     u32 words[AA_TAIL_WORDS];
   3913     u32 n = 0;
   3914     aa_words_callee_restores(a, words, AA_TAIL_WORDS, &n);
   3915     aa_words_restore_frame(a, words, AA_TAIL_WORDS, &n, &L);
   3916     if (n >= AA_TAIL_WORDS) aa_panic(a, "tail epilogue too large");
   3917     for (u32 i = 0; i < n; ++i) aa_emit32(t->mc, words[i]);
   3918     if (callee.kind == NATIVE_LOC_REG) {
   3919       aa_emit32(t->mc, aa64_br(loc_reg(callee)));
   3920     } else if (callee.kind == NATIVE_LOC_GLOBAL) {
   3921       u32 pos = mc_pos(t->mc);
   3922       aa_emit32(t->mc, aa64_b(0));
   3923       mc_emit_reloc_at(t->mc, t->mc->section_id, pos, R_AARCH64_JUMP26,
   3924                        callee.v.global.sym, callee.v.global.addend, 0, 0);
   3925     } else {
   3926       aa_panic(a, "unsupported tail target");
   3927     }
   3928     return;
   3929   }
   3930   /* Single-pass: reserve a worst-case region and record a patch; the callee
   3931    * restores and frame restore depend on the not-yet-final frame layout. */
   3932   AAPatch* p = aa_patch_alloc(a);
   3933   p->kind = AA_PATCH_TAIL;
   3934   p->pos = mc_pos(t->mc);
   3935   p->u.callee = callee;
   3936   for (u32 i = 0; i < AA_TAIL_WORDS; ++i) aa_emit32(t->mc, 0xd503201fu);
   3937   if (callee.kind == NATIVE_LOC_GLOBAL) {
   3938     mc_emit_reloc_at(t->mc, t->mc->section_id,
   3939                      p->pos + (AA_TAIL_WORDS - 1u) * 4u, R_AARCH64_JUMP26,
   3940                      callee.v.global.sym, callee.v.global.addend, 0, 0);
   3941   }
   3942 }
   3943 
   3944 static void aa_emit_call(NativeTarget* t, const NativeCallPhase* plan) {
   3945   int is_tail = (plan->flags & CG_CALL_TAIL) != 0;
   3946   if (is_tail) {
   3947     if (plan->callee.kind != NATIVE_LOC_GLOBAL &&
   3948         plan->callee.kind != NATIVE_LOC_REG)
   3949       aa_panic(aa_of(t), "unsupported tail target");
   3950     aa_emit_tail_site(t, plan->callee);
   3951     return;
   3952   }
   3953   if (plan->callee.kind == NATIVE_LOC_GLOBAL) {
   3954     aa_emit32(t->mc, aa64_bl(0));
   3955     mc_emit_reloc_at(t->mc, t->mc->section_id, mc_pos(t->mc) - 4u,
   3956                      R_AARCH64_CALL26, plan->callee.v.global.sym,
   3957                      plan->callee.v.global.addend, 0, 0);
   3958     return;
   3959   }
   3960   if (plan->callee.kind == NATIVE_LOC_REG) {
   3961     aa_emit32(t->mc, aa64_blr(loc_reg(plan->callee)));
   3962     return;
   3963   }
   3964   aa_panic(aa_of(t), "unsupported call target");
   3965 }
   3966 
   3967 static void aa_marshal_ret(NativeTarget* t, const CGFuncDesc* fd,
   3968                            const NativeLoc* value,
   3969                            NativeCallPhaseRet** out_rets, u32* out_nrets) {
   3970   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
   3971   NativeCallPhaseRet* rets = NULL;
   3972   u32 nr = 0;
   3973   if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) {
   3974     AANativeTarget* a = aa_of(t);
   3975     /* Hold the sret destination pointer in x8, not AA_TMP1: aa_copy_bytes
   3976      * materializes out-of-range source/dest frame offsets into AA_TMP1, which
   3977      * would clobber the destination base mid-copy (only triggered once a frame
   3978      * is large enough that the source offset escapes stur's signed-9 range). */
   3979     NativeLoc dstp =
   3980         native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, AA_X8);
   3981     NativeLoc saved = native_loc_stack(dstp.type, a->sret_ptr_slot, 0);
   3982     NativeAddr dst_addr, src_addr;
   3983     AggregateAccess access;
   3984     aa_load_part(t, dstp, saved, 0, 8);
   3985     memset(&dst_addr, 0, sizeof dst_addr);
   3986     dst_addr.base_kind = NATIVE_ADDR_BASE_REG;
   3987     dst_addr.base.reg = AA_X8;
   3988     dst_addr.base_type = value->type;
   3989     src_addr = aa_storage_addr(a, *value, 0);
   3990     src_addr.base_type = value->type;
   3991     memset(&access, 0, sizeof access);
   3992     access.type = value->type;
   3993     access.size = (u32)cg_type_size(t->c, value->type);
   3994     access.align = type_align32(t, value->type);
   3995     aa_copy_bytes(t, dst_addr, src_addr, access);
   3996     *out_rets = NULL;
   3997     *out_nrets = 0;
   3998     return;
   3999   }
   4000   if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) {
   4001     u32 ni = 0, nf = 0;
   4002     int exact_parts = 0;
   4003     for (u32 p = 0; p < abi->ret.nparts; ++p) {
   4004       const ABIArgPart* part = &abi->ret.parts[p];
   4005       if (part->cls == ABI_CLASS_INT && part->size != 1u &&
   4006           part->size != 2u && part->size != 4u && part->size != 8u) {
   4007         exact_parts = 1;
   4008         break;
   4009       }
   4010     }
   4011 
   4012     /* The generic return write phase materializes a memory source using the
   4013      * scalar carrier type, so an I64 carrier for a 3/5/6/7-byte aggregate part
   4014      * becomes an eight-byte load before NativeCallPhaseRet.mem can narrow the
   4015      * final move. Once one exact-width part is present, marshal every return
   4016      * part here in ABI order. aa_load_part retains the exact byte count and
   4017      * uses only backend-private temporaries, so already-filled x0/x1 or v0..v3
   4018      * return registers remain live while later parts are loaded. */
   4019     if (exact_parts) {
   4020       for (u32 p = 0; p < abi->ret.nparts; ++p) {
   4021         const ABIArgPart* part = &abi->ret.parts[p];
   4022         NativeAllocClass cls =
   4023             part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   4024         KitCgTypeId pty = aa_part_scalar_type(part);
   4025         Reg rreg = cls == NATIVE_REG_FP ? nf++ : ni++;
   4026         NativeLoc dst = native_loc_reg(pty, cls, rreg);
   4027         aa_load_part(t, dst, *value, part->src_offset, part->size);
   4028       }
   4029       *out_rets = NULL;
   4030       *out_nrets = 0;
   4031       return;
   4032     }
   4033 
   4034     rets = arena_zarray(t->c->tu, NativeCallPhaseRet, abi->ret.nparts);
   4035     for (u32 p = 0; p < abi->ret.nparts; ++p) {
   4036       const ABIArgPart* part = &abi->ret.parts[p];
   4037       NativeAllocClass cls =
   4038           part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   4039       KitCgTypeId pty = aa_part_scalar_type(part);
   4040       rets[nr].src = *value;
   4041       if (rets[nr].src.kind == NATIVE_LOC_FRAME)
   4042         rets[nr].src =
   4043             native_loc_stack(pty, value->v.frame, (i32)part->src_offset);
   4044       else if (rets[nr].src.kind == NATIVE_LOC_STACK) {
   4045         rets[nr].src.v.stack.offset += (i32)part->src_offset;
   4046         rets[nr].src.type = pty;
   4047       } else if (rets[nr].src.kind == NATIVE_LOC_ADDR) {
   4048         rets[nr].src.v.addr.offset += (i32)part->src_offset;
   4049         rets[nr].src.type = pty;
   4050       }
   4051       rets[nr].dst =
   4052           native_loc_reg(pty, cls, cls == NATIVE_REG_FP ? nf++ : ni++);
   4053       rets[nr].mem = aa_mem_for_type(t, pty, part->size);
   4054       nr++;
   4055     }
   4056   } else if (value) {
   4057     rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 1);
   4058     rets[0].src = *value;
   4059     rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, 0);
   4060     rets[0].mem = aa_mem_for_type(t, value->type, 0);
   4061     nr = 1;
   4062   }
   4063   *out_rets = rets;
   4064   *out_nrets = nr;
   4065 }
   4066 
   4067 static void aa_ret(NativeTarget* t) {
   4068   AANativeTarget* a = aa_of(t);
   4069   aa_jump(t, a->epilogue_label);
   4070 }
   4071 
   4072 static u32 aa_bit_storage_reg_bits(u32 storage_bytes) {
   4073   return storage_bytes == 8u ? 64u : 32u;
   4074 }
   4075 
   4076 static void aa_lsl_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn, u32 sh) {
   4077   u32 bits = sf ? 64u : 32u;
   4078   if (!sh) {
   4079     if (rd != rn) aa_emit32(t->mc, aa64_mov_reg(sf, rd, rn));
   4080     return;
   4081   }
   4082   aa_emit32(t->mc, aa_ubfm(sf, rd, rn, bits - sh, bits - 1u - sh));
   4083 }
   4084 
   4085 static void aa_lsr_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn, u32 sh) {
   4086   if (!sh) {
   4087     if (rd != rn) aa_emit32(t->mc, aa64_mov_reg(sf, rd, rn));
   4088     return;
   4089   }
   4090   aa_emit32(t->mc, aa_ubfm(sf, rd, rn, sh, sf ? 63u : 31u));
   4091 }
   4092 
   4093 static void aa_asr_imm(NativeTarget* t, u32 sf, u32 rd, u32 rn, u32 sh) {
   4094   if (!sh) {
   4095     if (rd != rn) aa_emit32(t->mc, aa64_mov_reg(sf, rd, rn));
   4096     return;
   4097   }
   4098   aa_emit32(t->mc, aa_sbfm(sf, rd, rn, sh, sf ? 63u : 31u));
   4099 }
   4100 
   4101 static void aa_bitfield_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
   4102                              BitFieldAccess bf) {
   4103   u32 storage = bf.storage.size ? bf.storage.size : 4u;
   4104   u32 bits = aa_bit_storage_reg_bits(storage);
   4105   u32 width = bf.bit_width ? bf.bit_width : 1u;
   4106   u32 sf = bits == 64u;
   4107   NativeAddr saddr = aa_addr_plus(addr, bf.storage_offset);
   4108   NativeLoc tmp = dst;
   4109   tmp.type = bf.storage.type ? bf.storage.type : dst.type;
   4110   aa_load_native(t, tmp, saddr, bf.storage);
   4111   aa_lsl_imm(t, sf, loc_reg(dst), loc_reg(dst),
   4112              bits - (u32)bf.bit_offset - width);
   4113   if (bf.signed_)
   4114     aa_asr_imm(t, sf, loc_reg(dst), loc_reg(dst), bits - width);
   4115   else
   4116     aa_lsr_imm(t, sf, loc_reg(dst), loc_reg(dst), bits - width);
   4117 }
   4118 
   4119 static void aa_bitfield_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
   4120                               BitFieldAccess bf) {
   4121   u32 storage = bf.storage.size ? bf.storage.size : 4u;
   4122   u32 bits = aa_bit_storage_reg_bits(storage);
   4123   u32 width = bf.bit_width ? bf.bit_width : 1u;
   4124   u32 sf = bits == 64u;
   4125   u64 ones = width >= 64u ? ~(u64)0 : ((1ull << width) - 1ull);
   4126   u64 field_mask = ones << bf.bit_offset;
   4127   NativeAddr saddr = aa_addr_plus(addr, bf.storage_offset);
   4128   NativeLoc word =
   4129       aa_tmp_loc(bf.storage.type ? bf.storage.type : src.type, AA_TMP0);
   4130   aa_load_native(t, word, saddr, bf.storage);
   4131   aa_emit_load_imm(t->mc, sf, AA_TMP1, (i64)~field_mask);
   4132   aa_emit32(t->mc, aa64_and(sf, AA_TMP0, AA_TMP0, AA_TMP1));
   4133   aa_emit32(t->mc, aa_ubfm(sf, AA_TMP1, loc_reg(src), 0, width - 1u));
   4134   aa_lsl_imm(t, sf, AA_TMP1, AA_TMP1, bf.bit_offset);
   4135   aa_emit32(t->mc, aa64_orr(sf, AA_TMP0, AA_TMP0, AA_TMP1));
   4136   aa_store_native(t, saddr, word, bf.storage);
   4137 }
   4138 
   4139 static void aa_trap(NativeTarget* t);
   4140 
   4141 static int aa_order_acquire(KitCgMemOrder order) {
   4142   return order == KIT_CG_MO_CONSUME || order == KIT_CG_MO_ACQUIRE ||
   4143          order == KIT_CG_MO_ACQ_REL || order == KIT_CG_MO_SEQ_CST;
   4144 }
   4145 
   4146 static int aa_order_release(KitCgMemOrder order) {
   4147   return order == KIT_CG_MO_RELEASE || order == KIT_CG_MO_ACQ_REL ||
   4148          order == KIT_CG_MO_SEQ_CST;
   4149 }
   4150 
   4151 static NativeLoc aa_i64_reg_loc(u32 reg) {
   4152   return native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, reg);
   4153 }
   4154 
   4155 static void aa_atomic_addr_reg(NativeTarget* t, NativeAddr addr, u32 reg) {
   4156   NativeLoc dst = aa_i64_reg_loc(reg);
   4157   t->load_addr(t, dst, addr);
   4158 }
   4159 
   4160 static u32 aa_saved_tmp_pick(u32 a, u32 b, u32 c) {
   4161   static const u32 regs[] = {11u, 12u, 13u, 14u, 15u};
   4162   for (u32 i = 0; i < sizeof regs / sizeof regs[0]; ++i) {
   4163     if (regs[i] != a && regs[i] != b && regs[i] != c) return regs[i];
   4164   }
   4165   return 15u;
   4166 }
   4167 
   4168 static void aa_saved_tmp_spill(AANativeTarget* a, u32 reg) {
   4169   NativeFrameSlotDesc sd;
   4170   NativeAddr addr;
   4171   MemAccess mem;
   4172   memset(&sd, 0, sizeof sd);
   4173   if (a->saved_tmp_slot == NATIVE_FRAME_SLOT_NONE) {
   4174     sd.type = builtin_id(KIT_CG_BUILTIN_I64);
   4175     sd.size = 8;
   4176     sd.align = 8;
   4177     sd.kind = NATIVE_FRAME_SLOT_SPILL;
   4178     a->saved_tmp_slot = a->base.frame_slot(&a->base, &sd);
   4179   }
   4180   memset(&addr, 0, sizeof addr);
   4181   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4182   addr.base.frame = a->saved_tmp_slot;
   4183   addr.base_type = builtin_id(KIT_CG_BUILTIN_I64);
   4184   mem = aa_mem_for_type(&a->base, addr.base_type, 8);
   4185   aa_store_native(&a->base, addr, aa_i64_reg_loc(reg), mem);
   4186 }
   4187 
   4188 static void aa_saved_tmp_restore(AANativeTarget* a, u32 reg) {
   4189   NativeAddr addr;
   4190   MemAccess mem;
   4191   memset(&addr, 0, sizeof addr);
   4192   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   4193   addr.base.frame = a->saved_tmp_slot;
   4194   addr.base_type = builtin_id(KIT_CG_BUILTIN_I64);
   4195   mem = aa_mem_for_type(&a->base, addr.base_type, 8);
   4196   aa_load_native(&a->base, aa_i64_reg_loc(reg), addr, mem);
   4197 }
   4198 
   4199 static void aa_atomic_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
   4200                            MemAccess mem, KitCgMemOrder order) {
   4201   u32 base = AA_TMP0;
   4202   u32 sz = size_idx(mem.size ? mem.size : loc_size32(t, dst));
   4203   aa_atomic_addr_reg(t, addr, base);
   4204   aa_emit32(t->mc, aa_order_acquire(order)
   4205                        ? aa_ldar(sz, loc_reg(dst), base)
   4206                        : aa_ldr_uimm(sz, loc_reg(dst), base, 0));
   4207   if (order == KIT_CG_MO_SEQ_CST)
   4208     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4209 }
   4210 
   4211 static void aa_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
   4212                             MemAccess mem, KitCgMemOrder order) {
   4213   u32 base = AA_TMP0;
   4214   u32 sz = size_idx(mem.size ? mem.size : loc_size32(t, src));
   4215   if (order == KIT_CG_MO_SEQ_CST)
   4216     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4217   aa_atomic_addr_reg(t, addr, base);
   4218   aa_emit32(t->mc, aa_order_release(order)
   4219                        ? aa_stlr(sz, loc_reg(src), base)
   4220                        : aa_str_uimm(sz, loc_reg(src), base, 0));
   4221   if (order == KIT_CG_MO_SEQ_CST)
   4222     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4223 }
   4224 
   4225 static void aa_atomic_rmw(NativeTarget* t, KitCgAtomicOp op, NativeLoc dst,
   4226                           NativeAddr addr, NativeLoc val, MemAccess mem,
   4227                           KitCgMemOrder order) {
   4228   AANativeTarget* a = aa_of(t);
   4229   u32 base = AA_TMP0;
   4230   u32 next_reg = AA_TMP1;
   4231   u32 status = aa_saved_tmp_pick(loc_reg(dst), loc_reg(val), base);
   4232   NativeLoc next = aa_tmp_loc(dst.type, next_reg);
   4233   MCLabel retry = mc_label_new(t->mc);
   4234   u32 sz = size_idx(mem.size ? mem.size : loc_size32(t, dst));
   4235   if (order == KIT_CG_MO_SEQ_CST)
   4236     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4237   aa_saved_tmp_spill(a, status);
   4238   aa_atomic_addr_reg(t, addr, base);
   4239   mc_label_place(t->mc, retry);
   4240   aa_emit32(t->mc, aa_order_acquire(order) ? aa_ldaxr(sz, loc_reg(dst), base)
   4241                                            : aa_ldxr(sz, loc_reg(dst), base));
   4242   switch (op) {
   4243     case KIT_CG_ATOMIC_XCHG:
   4244       aa_move(t, next, val);
   4245       break;
   4246     case KIT_CG_ATOMIC_ADD:
   4247       aa_binop(t, BO_IADD, next, dst, val);
   4248       break;
   4249     case KIT_CG_ATOMIC_SUB:
   4250       aa_binop(t, BO_ISUB, next, dst, val);
   4251       break;
   4252     case KIT_CG_ATOMIC_AND:
   4253       aa_binop(t, BO_AND, next, dst, val);
   4254       break;
   4255     case KIT_CG_ATOMIC_OR:
   4256       aa_binop(t, BO_OR, next, dst, val);
   4257       break;
   4258     case KIT_CG_ATOMIC_XOR:
   4259       aa_binop(t, BO_XOR, next, dst, val);
   4260       break;
   4261     case KIT_CG_ATOMIC_NAND:
   4262       aa_binop(t, BO_AND, next, dst, val);
   4263       aa_unop(t, UO_BNOT, next, next);
   4264       break;
   4265     default:
   4266       aa_panic(a, "unsupported atomic rmw op");
   4267   }
   4268   aa_emit32(t->mc, aa_order_release(order)
   4269                        ? aa_stlxr(sz, status, next_reg, base)
   4270                        : aa_stxr(sz, status, next_reg, base));
   4271   aa_emit32(t->mc, aa64_cbnz_imm(0, status, 0));
   4272   mc_emit_label_ref(t->mc, retry, R_AARCH64_CONDBR19, 4, 0);
   4273   aa_saved_tmp_restore(a, status);
   4274   if (order == KIT_CG_MO_SEQ_CST)
   4275     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4276 }
   4277 
   4278 static void aa_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok,
   4279                           NativeAddr addr, NativeLoc expected,
   4280                           NativeLoc desired, MemAccess mem,
   4281                           KitCgMemOrder success, KitCgMemOrder failure) {
   4282   u32 base = AA_TMP0;
   4283   u32 status = AA_TMP1;
   4284   u32 sz = size_idx(mem.size ? mem.size : loc_size32(t, prior));
   4285   u32 sf = sz == 3u;
   4286   int acquire = aa_order_acquire(success) || aa_order_acquire(failure);
   4287   int release = aa_order_release(success);
   4288   MCLabel retry = mc_label_new(t->mc);
   4289   MCLabel fail = mc_label_new(t->mc);
   4290   MCLabel done = mc_label_new(t->mc);
   4291   if (success == KIT_CG_MO_SEQ_CST || failure == KIT_CG_MO_SEQ_CST)
   4292     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4293   aa_atomic_addr_reg(t, addr, base);
   4294   mc_label_place(t->mc, retry);
   4295   aa_emit32(t->mc, acquire ? aa_ldaxr(sz, loc_reg(prior), base)
   4296                            : aa_ldxr(sz, loc_reg(prior), base));
   4297   aa_emit32(t->mc, aa_subs_reg(sf, AA64_ZR, loc_reg(prior), loc_reg(expected)));
   4298   aa_emit32(t->mc, aa64_brcond_pack((AA64BrCond){.cond = cmp_cond(CMP_NE)}));
   4299   mc_emit_label_ref(t->mc, fail, R_AARCH64_CONDBR19, 4, 0);
   4300   aa_emit32(t->mc, release ? aa_stlxr(sz, status, loc_reg(desired), base)
   4301                            : aa_stxr(sz, status, loc_reg(desired), base));
   4302   aa_emit32(t->mc, aa64_cbnz_imm(0, status, 0));
   4303   mc_emit_label_ref(t->mc, retry, R_AARCH64_CONDBR19, 4, 0);
   4304   aa_emit_load_imm(t->mc, loc_is_64(t, ok), loc_reg(ok), 1);
   4305   aa_jump(t, done);
   4306   mc_label_place(t->mc, fail);
   4307   aa_emit32(t->mc, aa64_clrex(AA64_BARRIER_OPT_SY));
   4308   aa_emit_load_imm(t->mc, loc_is_64(t, ok), loc_reg(ok), 0);
   4309   mc_label_place(t->mc, done);
   4310   if (success == KIT_CG_MO_SEQ_CST || failure == KIT_CG_MO_SEQ_CST)
   4311     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4312 }
   4313 
   4314 static void aa_fence(NativeTarget* t, KitCgMemOrder order) {
   4315   if (order != KIT_CG_MO_RELAXED)
   4316     aa_emit32(t->mc, aa64_dmb(AA64_BARRIER_OPT_ISH));
   4317 }
   4318 
   4319 /* Map a KitCgBarrierScope (passed as an immediate arg to DMB/DSB) onto an
   4320  * AArch64 barrier domain option. Defaults to full-system (SY) when the scope
   4321  * is absent or unrecognized. */
   4322 static u32 aa_barrier_opt(const NativeLoc* args, u32 narg) {
   4323   if (narg < 1u || args[0].kind != NATIVE_LOC_IMM) return AA64_BARRIER_OPT_SY;
   4324   switch ((KitCgBarrierScope)args[0].v.imm) {
   4325     case KIT_CG_BARRIER_FULL:
   4326       return AA64_BARRIER_OPT_SY;
   4327     case KIT_CG_BARRIER_INNER:
   4328       return AA64_BARRIER_OPT_ISH;
   4329     case KIT_CG_BARRIER_INNER_STORE:
   4330       return AA64_BARRIER_OPT_ISHST;
   4331     case KIT_CG_BARRIER_OUTER:
   4332       return AA64_BARRIER_OPT_OSH;
   4333     case KIT_CG_BARRIER_OUTER_STORE:
   4334       return AA64_BARRIER_OPT_OSHST;
   4335     case KIT_CG_BARRIER_NON_SHARE:
   4336       return AA64_BARRIER_OPT_NSH;
   4337   }
   4338   return AA64_BARRIER_OPT_SY;
   4339 }
   4340 
   4341 static void aa_intrinsic(NativeTarget* t, IntrinKind kind,
   4342                          const NativeLoc* dsts, u32 ndst, const NativeLoc* args,
   4343                          u32 narg) {
   4344   AggregateAccess access;
   4345   NativeAddr dst_addr;
   4346   NativeAddr src_addr;
   4347   memset(&access, 0, sizeof access);
   4348   memset(&dst_addr, 0, sizeof dst_addr);
   4349   memset(&src_addr, 0, sizeof src_addr);
   4350   switch (kind) {
   4351     case INTRIN_NONE:
   4352       if (ndst == 1u && narg == 3u && native_loc_is_fp(dsts[0])) {
   4353         u32 d = type_size32(t, dsts[0].type) == 8u;
   4354         aa_emit32(t->mc, aa_fp_bin(0x000800u, d, loc_reg(dsts[0]),
   4355                                    loc_reg(args[0]), loc_reg(args[1])));
   4356         aa_emit32(t->mc, aa_fp_bin(0x002800u, d, loc_reg(dsts[0]),
   4357                                    loc_reg(dsts[0]), loc_reg(args[2])));
   4358         return;
   4359       }
   4360       break;
   4361     case INTRIN_CLZ:
   4362       if (ndst == 1u && narg == 1u) {
   4363         aa_emit32(t->mc, aa_clz(loc_is_64(t, args[0]), loc_reg(dsts[0]),
   4364                                 loc_reg(args[0])));
   4365         return;
   4366       }
   4367       break;
   4368     case INTRIN_CTZ:
   4369       if (ndst == 1u && narg == 1u) {
   4370         u32 sf = loc_is_64(t, args[0]);
   4371         aa_emit32(t->mc, aa_rbit(sf, loc_reg(dsts[0]), loc_reg(args[0])));
   4372         aa_emit32(t->mc, aa_clz(sf, loc_reg(dsts[0]), loc_reg(dsts[0])));
   4373         return;
   4374       }
   4375       break;
   4376     case INTRIN_POPCOUNT:
   4377       if (ndst == 1u && narg == 1u) {
   4378         u32 sf = loc_is_64(t, args[0]);
   4379         u32 rd = loc_reg(dsts[0]);
   4380         u32 rn = loc_reg(args[0]);
   4381         MCLabel loop = mc_label_new(t->mc);
   4382         MCLabel done = mc_label_new(t->mc);
   4383         aa_emit_load_imm(t->mc, sf, rd, 0);
   4384         aa_emit32(t->mc, aa64_mov_reg(sf, AA_TMP0, rn));
   4385         mc_label_place(t->mc, loop);
   4386         aa_emit32(t->mc, aa64_cbz(sf, AA_TMP0, 0));
   4387         mc_emit_label_ref(t->mc, done, R_AARCH64_CONDBR19, 4, 0);
   4388         aa_emit_load_imm(t->mc, sf, AA_TMP1, 1);
   4389         aa_emit32(t->mc, aa64_and(sf, AA_TMP1, AA_TMP0, AA_TMP1));
   4390         aa_emit32(t->mc, aa64_add(sf, rd, rd, AA_TMP1));
   4391         aa_emit_load_imm(t->mc, sf, AA_TMP1, 1);
   4392         aa_emit32(t->mc, aa64_lsrv(sf, AA_TMP0, AA_TMP0, AA_TMP1));
   4393         aa_jump(t, loop);
   4394         mc_label_place(t->mc, done);
   4395         return;
   4396       }
   4397       break;
   4398     case INTRIN_BSWAP:
   4399       if (ndst == 1u && narg == 1u) {
   4400         u32 width = abi_cg_sizeof(t->c->abi, dsts[0].type);
   4401         switch (width) {
   4402           case 2: {
   4403             u32 sf = 0;
   4404             aa_emit32(t->mc, aa_rev(sf, loc_reg(dsts[0]), loc_reg(args[0])));
   4405             aa_emit_load_imm(t->mc, 0, AA_TMP0, 16);
   4406             aa_emit32(t->mc, aa64_lsrv(0, loc_reg(dsts[0]), loc_reg(dsts[0]),
   4407                                        AA_TMP0));
   4408             return;
   4409           }
   4410           case 4: {
   4411             u32 sf = 0;
   4412             aa_emit32(t->mc, aa_rev(sf, loc_reg(dsts[0]), loc_reg(args[0])));
   4413             return;
   4414           }
   4415           case 8: {
   4416             u32 sf = 1;
   4417             aa_emit32(t->mc, aa_rev(sf, loc_reg(dsts[0]), loc_reg(args[0])));
   4418             return;
   4419           }
   4420           default:
   4421             break;
   4422         }
   4423       }
   4424       break;
   4425     case INTRIN_SADD_OVERFLOW:
   4426     case INTRIN_UADD_OVERFLOW:
   4427     case INTRIN_SSUB_OVERFLOW:
   4428     case INTRIN_USUB_OVERFLOW:
   4429       if (ndst == 2u && narg == 2u) {
   4430         u32 sf = loc_is_64(t, dsts[0]);
   4431         u32 rd = loc_reg(dsts[0]);
   4432         if (kind == INTRIN_SADD_OVERFLOW || kind == INTRIN_UADD_OVERFLOW)
   4433           aa_emit32(t->mc,
   4434                     aa64_addsubsr_pack((AA64AddSubSR){.sf = sf,
   4435                                                       .op = 0,
   4436                                                       .S = 1,
   4437                                                       .Rm = loc_reg(args[1]),
   4438                                                       .Rn = loc_reg(args[0]),
   4439                                                       .Rd = rd}));
   4440         else
   4441           aa_emit32(t->mc,
   4442                     aa64_addsubsr_pack((AA64AddSubSR){.sf = sf,
   4443                                                       .op = 1,
   4444                                                       .S = 1,
   4445                                                       .Rm = loc_reg(args[1]),
   4446                                                       .Rn = loc_reg(args[0]),
   4447                                                       .Rd = rd}));
   4448         aa_emit32(t->mc,
   4449                   aa_cset(loc_is_64(t, dsts[1]), loc_reg(dsts[1]),
   4450                           (kind == INTRIN_SADD_OVERFLOW ||
   4451                            kind == INTRIN_SSUB_OVERFLOW)
   4452                               ? 0x6u
   4453                               : (kind == INTRIN_UADD_OVERFLOW ? 0x2u : 0x3u)));
   4454         return;
   4455       }
   4456       break;
   4457     case INTRIN_SMUL_OVERFLOW:
   4458     case INTRIN_UMUL_OVERFLOW:
   4459       if (ndst == 2u && narg == 2u) {
   4460         u32 sf = loc_is_64(t, dsts[0]);
   4461         if (sf) {
   4462           if (kind == INTRIN_SMUL_OVERFLOW) {
   4463             aa_emit32(t->mc,
   4464                       aa_smulh(AA_TMP0, loc_reg(args[0]), loc_reg(args[1])));
   4465             aa_emit32(t->mc, aa64_mul(1, loc_reg(dsts[0]), loc_reg(args[0]),
   4466                                       loc_reg(args[1])));
   4467             aa_emit32(t->mc, aa_sbfm(1, AA_TMP1, loc_reg(dsts[0]), 63, 63));
   4468             aa_emit32(t->mc, aa_subs_reg(1, AA64_ZR, AA_TMP0, AA_TMP1));
   4469             aa_emit32(t->mc, aa_cset(0, loc_reg(dsts[1]), cmp_cond(CMP_NE)));
   4470           } else {
   4471             aa_emit32(t->mc,
   4472                       aa_umulh(AA_TMP0, loc_reg(args[0]), loc_reg(args[1])));
   4473             aa_emit32(t->mc, aa64_mul(1, loc_reg(dsts[0]), loc_reg(args[0]),
   4474                                       loc_reg(args[1])));
   4475             aa_emit32(t->mc, aa_subs_reg(1, AA64_ZR, AA_TMP0, AA64_ZR));
   4476             aa_emit32(t->mc, aa_cset(0, loc_reg(dsts[1]), cmp_cond(CMP_NE)));
   4477           }
   4478         } else if (kind == INTRIN_SMUL_OVERFLOW) {
   4479           aa_emit32(t->mc, aa_smaddl(AA_TMP0, loc_reg(args[0]),
   4480                                      loc_reg(args[1]), AA64_ZR));
   4481           aa_emit32(t->mc, aa64_mov_reg(0, loc_reg(dsts[0]), AA_TMP0));
   4482           aa_emit32(t->mc, aa_sbfm(1, AA_TMP1, loc_reg(dsts[0]), 0, 31));
   4483           aa_emit32(t->mc, aa_subs_reg(1, AA64_ZR, AA_TMP0, AA_TMP1));
   4484           aa_emit32(t->mc, aa_cset(0, loc_reg(dsts[1]), cmp_cond(CMP_NE)));
   4485         } else {
   4486           aa_emit32(t->mc, aa_umaddl(AA_TMP0, loc_reg(args[0]),
   4487                                      loc_reg(args[1]), AA64_ZR));
   4488           aa_emit32(t->mc, aa64_mov_reg(0, loc_reg(dsts[0]), AA_TMP0));
   4489           aa_emit_load_imm(t->mc, 1, AA_TMP1, 32);
   4490           aa_emit32(t->mc, aa64_lsrv(1, AA_TMP1, AA_TMP0, AA_TMP1));
   4491           aa_emit32(t->mc, aa_subs_reg(1, AA64_ZR, AA_TMP1, AA64_ZR));
   4492           aa_emit32(t->mc, aa_cset(0, loc_reg(dsts[1]), cmp_cond(CMP_NE)));
   4493         }
   4494         return;
   4495       }
   4496       break;
   4497     case INTRIN_SMUL_HIGH:
   4498     case INTRIN_UMUL_HIGH:
   4499       if (ndst == 1u && narg == 2u) {
   4500         u32 sf = loc_is_64(t, dsts[0]);
   4501         u32 rd = loc_reg(dsts[0]);
   4502         u32 rn = loc_reg(args[0]);
   4503         u32 rm = loc_reg(args[1]);
   4504         if (sf) {
   4505           aa_emit32(t->mc, kind == INTRIN_SMUL_HIGH
   4506                                ? aa_smulh(rd, rn, rm)
   4507                                : aa_umulh(rd, rn, rm));
   4508         } else {
   4509           aa_emit32(t->mc, kind == INTRIN_SMUL_HIGH
   4510                                ? aa_smaddl(AA_TMP0, rn, rm, AA64_ZR)
   4511                                : aa_umaddl(AA_TMP0, rn, rm, AA64_ZR));
   4512           if (kind == INTRIN_SMUL_HIGH)
   4513             aa_asr_imm(t, 1, rd, AA_TMP0, 32);
   4514           else
   4515             aa_lsr_imm(t, 1, rd, AA_TMP0, 32);
   4516         }
   4517         return;
   4518       }
   4519       break;
   4520     case INTRIN_MEMMOVE: {
   4521       MCLabel forward = mc_label_new(t->mc);
   4522       MCLabel done = mc_label_new(t->mc);
   4523       if (narg != 3u || args[0].kind != NATIVE_LOC_REG ||
   4524           args[1].kind != NATIVE_LOC_REG || args[2].kind != NATIVE_LOC_IMM)
   4525         aa_panic(aa_of(t), "unsupported memory intrinsic operands");
   4526       if (args[2].v.imm < 0 || args[2].v.imm > 0xffffffffll)
   4527         aa_panic(aa_of(t), "unsupported memory intrinsic size");
   4528       access.size = (u32)args[2].v.imm;
   4529       access.align = 1u;
   4530       dst_addr.base_kind = NATIVE_ADDR_BASE_REG;
   4531       dst_addr.base.reg = args[0].v.reg;
   4532       src_addr.base_kind = NATIVE_ADDR_BASE_REG;
   4533       src_addr.base.reg = args[1].v.reg;
   4534       aa_emit32(t->mc, aa_subs_reg(1, AA64_ZR, args[0].v.reg, args[1].v.reg));
   4535       aa_emit32(t->mc,
   4536                 aa64_brcond_pack((AA64BrCond){.cond = cmp_cond(CMP_LT_U)}));
   4537       mc_emit_label_ref(t->mc, forward, R_AARCH64_CONDBR19, 4, 0);
   4538       aa_copy_bytes_dir(t, dst_addr, src_addr, access, 1);
   4539       aa_jump(t, done);
   4540       mc_label_place(t->mc, forward);
   4541       aa_copy_bytes_dir(t, dst_addr, src_addr, access, 0);
   4542       mc_label_place(t->mc, done);
   4543       return;
   4544     }
   4545     case INTRIN_EXPECT:
   4546     case INTRIN_ASSUME_ALIGNED:
   4547       if (ndst == 1u && narg >= 1u) {
   4548         if (args[0].kind == NATIVE_LOC_IMM)
   4549           aa_load_imm_native(t, dsts[0], args[0].v.imm);
   4550         else
   4551           aa_move(t, dsts[0], args[0]);
   4552       }
   4553       return;
   4554     case INTRIN_PREFETCH:
   4555       if (narg >= 1u && args[0].kind == NATIVE_LOC_REG) {
   4556         /* GCC locality 0..3 maps to streaming L1, then L3/L2/L1 keep. PST
   4557          * differs from PLD by bit 3 of prfop; both are baseline A64 hints. */
   4558         static const u8 op[4] = {1u, 4u, 2u, 0u};
   4559         u32 rw = 0u;
   4560         u32 locality = 3u;
   4561         if (narg >= 2u && args[1].kind == NATIVE_LOC_IMM)
   4562           rw = (u32)args[1].v.imm;
   4563         if (narg >= 3u && args[2].kind == NATIVE_LOC_IMM)
   4564           locality = (u32)args[2].v.imm;
   4565         if (locality > 3u) locality = 3u;
   4566         aa_emit32(t->mc, aa_prfm(op[locality] | (rw ? 8u : 0u),
   4567                                 loc_reg(args[0])));
   4568       }
   4569       return;
   4570     case INTRIN_TRAP:
   4571       aa_trap(t);
   4572       return;
   4573     case INTRIN_SYSCALL:
   4574       if (ndst == 1u && narg >= 1u && narg <= 7u) {
   4575         static const u32 syscall_regs[7] = {AA_X8, 0u, 1u, 2u, 3u, 4u, 5u};
   4576         AAArgMove moves[7];
   4577         for (u32 i = 0; i < narg; ++i) {
   4578           AAArgMove* m = &moves[i];
   4579           memset(m, 0, sizeof *m);
   4580           m->dst =
   4581               native_loc_reg(dsts[0].type, NATIVE_REG_INT, syscall_regs[i]);
   4582           m->src = args[i];
   4583           m->size = t->c->target.ptr_size;
   4584         }
   4585         aa_emit_reg_arg_moves(t, moves, narg);
   4586         aa_emit32(t->mc, aa64_svc(0));
   4587         aa_move(t, dsts[0], native_loc_reg(dsts[0].type, NATIVE_REG_INT, 0));
   4588       }
   4589       return;
   4590     case INTRIN_READCYCLECOUNTER:
   4591       /* MRS Xd, CNTVCT_EL0 (op0=3 op1=3 CRn=14 CRm=0 op2=2): the EL0-readable
   4592        * virtual counter. Prefer it over PMCCNTR_EL0, which traps at EL0 unless
   4593        * the PMU is explicitly enabled for userspace. */
   4594       if (ndst == 1u)
   4595         aa_emit32(t->mc, aa64_sysreg_move(1, 3, 3, 14, 0, 2, loc_reg(dsts[0])));
   4596       return;
   4597     case INTRIN_CPU_NOP:
   4598       aa_emit32(t->mc, aa64_hint(AA64_HINT_OP_NOP));
   4599       return;
   4600     case INTRIN_CPU_YIELD:
   4601       aa_emit32(t->mc, aa64_hint(AA64_HINT_OP_YIELD));
   4602       return;
   4603     case INTRIN_WFI:
   4604       aa_emit32(t->mc, aa64_hint(AA64_HINT_OP_WFI));
   4605       return;
   4606     case INTRIN_WFE:
   4607       aa_emit32(t->mc, aa64_hint(AA64_HINT_OP_WFE));
   4608       return;
   4609     case INTRIN_SEV:
   4610       aa_emit32(t->mc, aa64_hint(AA64_HINT_OP_SEV));
   4611       return;
   4612     case INTRIN_ISB:
   4613       aa_emit32(t->mc, aa64_isb(AA64_BARRIER_OPT_SY));
   4614       return;
   4615     case INTRIN_DMB:
   4616       aa_emit32(t->mc, aa64_dmb(aa_barrier_opt(args, narg)));
   4617       return;
   4618     case INTRIN_DSB:
   4619       aa_emit32(t->mc, aa64_dsb(aa_barrier_opt(args, narg)));
   4620       return;
   4621     case INTRIN_IRQ_SAVE:
   4622       /* Read the interrupt-mask state, then mask D,A,I,F. */
   4623       if (ndst == 1u) {
   4624         aa_emit32(t->mc, aa64_mrs_daif(loc_reg(dsts[0])));
   4625         aa_emit32(t->mc, aa64_msr_daifset(AA64_DAIF_ALL));
   4626       }
   4627       return;
   4628     case INTRIN_IRQ_RESTORE:
   4629       if (narg == 1u) aa_emit32(t->mc, aa64_msr_daif(loc_reg(args[0])));
   4630       return;
   4631     case INTRIN_IRQ_DISABLE:
   4632       aa_emit32(t->mc, aa64_msr_daifset(AA64_DAIF_ALL));
   4633       return;
   4634     case INTRIN_IRQ_ENABLE:
   4635       aa_emit32(t->mc, aa64_msr_daifclr(AA64_DAIF_ALL));
   4636       return;
   4637     case INTRIN_FRAME_ADDRESS:
   4638     case INTRIN_RETURN_ADDRESS:
   4639       /* Walk the AAPCS64 frame-record chain. Every kit prologue stores
   4640        * {x29, x30} and anchors x29 at the record: [x29] = caller's x29,
   4641        * [x29 + 8] = saved x30 (this frame's return address). The level is a
   4642        * compile-time constant, so the walk unrolls to `level` dependent loads.
   4643        */
   4644       if (ndst == 1u) {
   4645         u32 level = (narg >= 1u && args[0].kind == NATIVE_LOC_IMM)
   4646                         ? (u32)args[0].v.imm
   4647                         : 0u;
   4648         u32 rd = loc_reg(dsts[0]);
   4649         aa_emit32(t->mc, aa64_mov_reg(1, rd, AA_FP));
   4650         for (u32 i = 0; i < level; ++i)
   4651           aa_emit32(t->mc, aa64_ldr64_uimm12(rd, rd, 0)); /* rd = *(rd) */
   4652         if (kind == INTRIN_RETURN_ADDRESS)
   4653           aa_emit32(t->mc, aa64_ldr64_uimm12(rd, rd, 1)); /* rd = *(rd + 8) */
   4654       }
   4655       return;
   4656     default:
   4657       aa_panic(aa_of(t), "unsupported compiler intrinsic");
   4658   }
   4659 }
   4660 
   4661 static void aa_trap(NativeTarget* t) { aa_emit32(t->mc, aa64_brk(0)); }
   4662 
   4663 /* file_scope_asm + finalize are shared (cg/native_asm.h). */
   4664 
   4665 static int aa_machine_op_clobbers(NativeTarget* t, const NativeMachineOp* op,
   4666                                   u32 mask[NATIVE_REG_CLASS_COUNT]) {
   4667   mask[0] = mask[1] = mask[2] = 0;
   4668   switch ((NativeMachineOpKind)op->kind) {
   4669     case NATIVE_MOP_TLS_ADDR:
   4670       /* ELF Local-Exec materializes the address using only the destination
   4671        * register. Mach-O TLV calls its resolver through private x16/x17/LR;
   4672        * only x0 can hold an optimizer-owned value at this boundary. */
   4673       if (!obj_format_tls_via_descriptor(t->c)) return 0;
   4674       mask[NATIVE_REG_INT] = 1u << 0;
   4675       return 1;
   4676     case NATIVE_MOP_INTRINSIC:
   4677       if ((IntrinKind)op->intrin != INTRIN_SYSCALL) return 0;
   4678       mask[NATIVE_REG_INT] = (1u << 0) | (1u << 1) | (1u << 2) | (1u << 3) |
   4679                              (1u << 4) | (1u << 5) | (1u << AA_X8);
   4680       return 1;
   4681     case NATIVE_MOP_BINOP:
   4682     case NATIVE_MOP_BITFIELD_LOAD:
   4683     case NATIVE_MOP_BITFIELD_STORE:
   4684     case NATIVE_MOP_VA_START:
   4685     case NATIVE_MOP_VA_ARG:
   4686     case NATIVE_MOP_ATOMIC_CAS:
   4687     case NATIVE_MOP_ATOMIC_RMW:
   4688       return 0;
   4689     case NATIVE_MOP_COUNT:
   4690       break;
   4691   }
   4692   aa_panic(aa_of(t), "invalid machine-effect operation");
   4693   return 0;
   4694 }
   4695 
   4696 static void aa_set_loc(NativeTarget* t, SrcLoc loc) {
   4697   AANativeTarget* a = aa_of(t);
   4698   a->loc = loc;
   4699   if (t->mc) mc_set_loc(t->mc, loc);
   4700 }
   4701 
   4702 static void aa_bind_native_param(NativeTarget* t, const CGParamDesc* p,
   4703                                  NativeLoc dst);
   4704 
   4705 /* ABI argument/return registers (x0..x7) come FIRST so the -O0 value cache
   4706  * prefers them: an argument expression then computes straight into its ABI arg
   4707  * register and the per-call parallel-copy (native_arg_shuffle) collapses to
   4708  * no-ops — the tcc get_reg(0..N) trick (mirrors RV's NDT a0..a7 bank and
   4709  * x64's leading rsi/rdi). They are caller-saved, so the deferred prologue stays
   4710  * callee-save-free. Incoming params are spilled to frame homes at entry before
   4711  * any body op (aa_bind_native_param), so caching in x0..x7 cannot clobber a
   4712  * live parameter. Next come the remaining caller-saved temps x8/x12..x15;
   4713  * callee-saved x19..x28 are appended and only chosen under register pressure,
   4714  * after which the prologue saves/restores them. x9/x10/x11 are the explicit O1
   4715  * operand-temp bank (also the O0 scratch policy) and stay out of this pool. */
   4716 static const Reg aa_ndt_int_allocable[] = {
   4717     0u,  1u,  2u,  3u,  4u,  5u,  6u,  7u,  8u,  12u, 13u, 14u,
   4718     15u, 19u, 20u, 21u, 22u, 23u, 24u, 25u, 26u, 27u, 28u};
   4719 /* Three O1 operand temporaries, not two: a 3-operand op (for example
   4720  * `binop dst, a, b` or `store [base+index], value`) can require three distinct
   4721  * materializations in one instruction scope. Location MIR leaves spilled
   4722  * operands in their frame homes until this point; the scope leases x9/x10/x11
   4723  * without inventing persistent MIR register values. All three are caller-saved
   4724  * and reserved out of the allocable set above. The O0 direct target happens to
   4725  * use the same bank through its independent `scratch` policy. */
   4726 static const Reg aa_int_scratch[] = {9u, 10u, 11u};
   4727 static const Reg aa_int_asm_temps[] = {AA_TMP0, AA_TMP1};
   4728 static const Reg aa_direct_asm_int[] = {0u, 1u, 2u,  3u,  4u,  5u,  6u,
   4729                                         7u, 8u, 11u, 12u, 13u, 14u, 15u};
   4730 /* ABI fp argument/return registers (v0..v7) come FIRST (same rationale as the
   4731  * int pool: fp args land in place, the shuffle is a no-op). Then the caller-
   4732  * saved fp temps v18/v19, v22..v31 — the rest of the -O0 value cache under
   4733  * ndt_caller_saved_only. v16 is a backend-private/arg-shuffle cycle temporary;
   4734  * v20/v21 are the O1 operand-temp bank and the O0 per-op scratch policy, so all
   4735  * three stay out of the pool. Callee-saved v8..v15 (AAPCS64) are appended
   4736  * like the int set; the caller-saved-only NDT never selects them. */
   4737 static const Reg aa_ndt_fp_allocable[] = {
   4738     0u,  1u,  2u,  3u,  4u,  5u,  6u, 7u, 18u, 19u, 22u, 23u, 24u, 25u,
   4739     26u, 27u, 28u, 29u, 30u, 31u, 8u, 9u, 10u, 11u, 12u, 13u, 14u, 15u};
   4740 static const Reg aa_fp_scratch[] = {20u, 21u};
   4741 static const Reg aa_direct_asm_fp[] = {
   4742     0u,  1u,  2u,  3u,  4u,  5u,  6u,  7u,  16u, 17u, 18u,
   4743     19u, 22u, 23u, 24u, 25u, 26u, 27u, 28u, 29u, 30u, 31u};
   4744 
   4745 #define AA_PHYS_INT_ALLOC(r)                                \
   4746   {.reg = (r),                                              \
   4747    .cls = NATIVE_REG_INT,                                   \
   4748    .abi_index = 0xffu,                                      \
   4749    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED, \
   4750    .spill_cost = 1u,                                        \
   4751    .copy_cost = 1u}
   4752 #define AA_PHYS_INT_CALLER(r)        \
   4753   {.reg = (r),                       \
   4754    .cls = NATIVE_REG_INT,            \
   4755    .abi_index = 0xffu,               \
   4756    .flags = NATIVE_REG_CALLER_SAVED, \
   4757    .spill_cost = 1u,                 \
   4758    .copy_cost = 1u}
   4759 #define AA_PHYS_INT_ARG(r)                             \
   4760   {.reg = (r),                                         \
   4761    .cls = NATIVE_REG_INT,                              \
   4762    .abi_index = (r),                                   \
   4763    .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \
   4764             ((r) < 2u ? NATIVE_REG_RET : 0),           \
   4765    .spill_cost = 1u,                                   \
   4766    .copy_cost = 1u}
   4767 #define AA_PHYS_INT_CALLEE(r)                               \
   4768   {.reg = (r),                                              \
   4769    .cls = NATIVE_REG_INT,                                   \
   4770    .abi_index = 0xffu,                                      \
   4771    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \
   4772    .spill_cost = 4u,                                        \
   4773    .copy_cost = 1u}
   4774 #define AA_PHYS_INT_RESERVED_ROLE(r, role) \
   4775   {.reg = (r),                              \
   4776    .cls = NATIVE_REG_INT,                   \
   4777    .abi_index = 0xffu,                      \
   4778    .flags = NATIVE_REG_RESERVED | (role),   \
   4779    .spill_cost = 0u,                        \
   4780    .copy_cost = 0u}
   4781 #define AA_PHYS_INT_RESERVED(r) \
   4782   AA_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_NONE)
   4783 #define AA_PHYS_INT_RESERVED_CALLER(r) \
   4784   AA_PHYS_INT_RESERVED_ROLE((r), NATIVE_REG_CALLER_SAVED)
   4785 
   4786 static const NativePhysRegInfo aa_int_phys[] = {
   4787     AA_PHYS_INT_ARG(0u),       AA_PHYS_INT_ARG(1u),
   4788     AA_PHYS_INT_ARG(2u),       AA_PHYS_INT_ARG(3u),
   4789     AA_PHYS_INT_ARG(4u),       AA_PHYS_INT_ARG(5u),
   4790     AA_PHYS_INT_ARG(6u),       AA_PHYS_INT_ARG(7u),
   4791     AA_PHYS_INT_ALLOC(8u),     AA_PHYS_INT_RESERVED_CALLER(9u),
   4792     AA_PHYS_INT_RESERVED_CALLER(10u), AA_PHYS_INT_RESERVED_CALLER(11u),
   4793     AA_PHYS_INT_ALLOC(12u),    AA_PHYS_INT_ALLOC(13u),
   4794     AA_PHYS_INT_ALLOC(14u),    AA_PHYS_INT_ALLOC(15u),
   4795     AA_PHYS_INT_RESERVED_CALLER(16u), AA_PHYS_INT_RESERVED_CALLER(17u),
   4796     AA_PHYS_INT_RESERVED_CALLER(18u), AA_PHYS_INT_CALLEE(19u),
   4797     AA_PHYS_INT_CALLEE(20u),   AA_PHYS_INT_CALLEE(21u),
   4798     AA_PHYS_INT_CALLEE(22u),   AA_PHYS_INT_CALLEE(23u),
   4799     AA_PHYS_INT_CALLEE(24u),   AA_PHYS_INT_CALLEE(25u),
   4800     AA_PHYS_INT_CALLEE(26u),   AA_PHYS_INT_CALLEE(27u),
   4801     AA_PHYS_INT_CALLEE(28u),   AA_PHYS_INT_RESERVED(29u),
   4802     AA_PHYS_INT_RESERVED(30u), AA_PHYS_INT_RESERVED(31u),
   4803 };
   4804 
   4805 #define AA_PHYS_FP_ALLOC(r)                                 \
   4806   {.reg = (r),                                              \
   4807    .cls = NATIVE_REG_FP,                                    \
   4808    .abi_index = 0xffu,                                      \
   4809    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLER_SAVED, \
   4810    .spill_cost = 1u,                                        \
   4811    .copy_cost = 1u}
   4812 #define AA_PHYS_FP_CALLER(r)         \
   4813   {.reg = (r),                       \
   4814    .cls = NATIVE_REG_FP,             \
   4815    .abi_index = 0xffu,               \
   4816    .flags = NATIVE_REG_CALLER_SAVED, \
   4817    .spill_cost = 1u,                 \
   4818    .copy_cost = 1u}
   4819 #define AA_PHYS_FP_ARG(r)                              \
   4820   {.reg = (r),                                         \
   4821    .cls = NATIVE_REG_FP,                               \
   4822    .abi_index = (r),                                   \
   4823    .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \
   4824             ((r) < 4u ? NATIVE_REG_RET : 0),           \
   4825    .spill_cost = 1u,                                   \
   4826    .copy_cost = 1u}
   4827 #define AA_PHYS_FP_CALLEE(r)                                \
   4828   {.reg = (r),                                              \
   4829    .cls = NATIVE_REG_FP,                                    \
   4830    .abi_index = 0xffu,                                      \
   4831    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \
   4832    .spill_cost = 4u,                                        \
   4833    .copy_cost = 1u}
   4834 #define AA_PHYS_FP_RESERVED(r)                   \
   4835   {.reg = (r),                                   \
   4836    .cls = NATIVE_REG_FP,                         \
   4837    .abi_index = 0xffu,                           \
   4838    .flags = NATIVE_REG_RESERVED |                \
   4839             NATIVE_REG_CALLER_SAVED,             \
   4840    .spill_cost = 0u,                             \
   4841    .copy_cost = 0u}
   4842 
   4843 static const NativePhysRegInfo aa_fp_phys[] = {
   4844     AA_PHYS_FP_ARG(0u),       AA_PHYS_FP_ARG(1u),     AA_PHYS_FP_ARG(2u),
   4845     AA_PHYS_FP_ARG(3u),       AA_PHYS_FP_ARG(4u),     AA_PHYS_FP_ARG(5u),
   4846     AA_PHYS_FP_ARG(6u),       AA_PHYS_FP_ARG(7u),     AA_PHYS_FP_CALLEE(8u),
   4847     AA_PHYS_FP_CALLEE(9u),    AA_PHYS_FP_CALLEE(10u), AA_PHYS_FP_CALLEE(11u),
   4848     AA_PHYS_FP_CALLEE(12u),   AA_PHYS_FP_CALLEE(13u), AA_PHYS_FP_CALLEE(14u),
   4849     AA_PHYS_FP_CALLEE(15u),   AA_PHYS_FP_CALLER(16u), AA_PHYS_FP_CALLER(17u),
   4850     AA_PHYS_FP_ALLOC(18u),    AA_PHYS_FP_ALLOC(19u),  AA_PHYS_FP_RESERVED(20u),
   4851     AA_PHYS_FP_RESERVED(21u), AA_PHYS_FP_CALLER(22u), AA_PHYS_FP_CALLER(23u),
   4852     AA_PHYS_FP_CALLER(24u),   AA_PHYS_FP_CALLER(25u), AA_PHYS_FP_CALLER(26u),
   4853     AA_PHYS_FP_CALLER(27u),   AA_PHYS_FP_CALLER(28u), AA_PHYS_FP_CALLER(29u),
   4854     AA_PHYS_FP_CALLER(30u),   AA_PHYS_FP_CALLER(31u),
   4855 };
   4856 
   4857 static const NativeAllocClassInfo aa_classes[] = {
   4858     {.cls = NATIVE_REG_INT,
   4859      .ndt_allocable = aa_ndt_int_allocable,
   4860      .ndt_allocable_count =
   4861          sizeof aa_ndt_int_allocable / sizeof aa_ndt_int_allocable[0],
   4862      .scratch = aa_int_scratch,
   4863      .nscratch = sizeof aa_int_scratch / sizeof aa_int_scratch[0],
   4864      .emit_temps = aa_int_scratch,
   4865      .nemit_temps = sizeof aa_int_scratch / sizeof aa_int_scratch[0],
   4866      .asm_temps = aa_int_asm_temps,
   4867      .nasm_temps = sizeof aa_int_asm_temps / sizeof aa_int_asm_temps[0],
   4868      .direct_asm_allocable = aa_direct_asm_int,
   4869      .ndirect_asm_allocable =
   4870          sizeof aa_direct_asm_int / sizeof aa_direct_asm_int[0],
   4871      .emit_cache_mask = (1u << 9u) | (1u << 10u) | (1u << 11u),
   4872      .phys = aa_int_phys,
   4873      .nphys = sizeof aa_int_phys / sizeof aa_int_phys[0]},
   4874     {.cls = NATIVE_REG_FP,
   4875      .ndt_allocable = aa_ndt_fp_allocable,
   4876      .ndt_allocable_count =
   4877          sizeof aa_ndt_fp_allocable / sizeof aa_ndt_fp_allocable[0],
   4878      .scratch = aa_fp_scratch,
   4879      .nscratch = sizeof aa_fp_scratch / sizeof aa_fp_scratch[0],
   4880      .emit_temps = aa_fp_scratch,
   4881      .nemit_temps = sizeof aa_fp_scratch / sizeof aa_fp_scratch[0],
   4882      .direct_asm_allocable = aa_direct_asm_fp,
   4883      .ndirect_asm_allocable =
   4884          sizeof aa_direct_asm_fp / sizeof aa_direct_asm_fp[0],
   4885      .emit_cache_mask = (1u << 20u) | (1u << 21u),
   4886      .phys = aa_fp_phys,
   4887      .nphys = sizeof aa_fp_phys / sizeof aa_fp_phys[0]},
   4888 };
   4889 
   4890 /* Resolve a register name ("x8", "v3", ...) to its (class, Reg). Powers the
   4891  * optimizer's inline-asm clobber masks and explicit hard-register operands
   4892  * ("{x8}" from a GNU local register variable). x0..x30 are DWARF 0..30; the
   4893  * SIMD/FP bank v0..v31 is DWARF 64..95. Returns non-zero for a non-register
   4894  * name (cc/memory/unknown), which the caller skips. */
   4895 static int aa_resolve_name(const NativeRegInfo* ri, Slice name, Reg* out,
   4896                            NativeAllocClass* cls_out) {
   4897   char buf[16];
   4898   uint32_t dwarf;
   4899   (void)ri;
   4900   if (!name.s || !name.len || name.len >= sizeof buf) return 1;
   4901   memcpy(buf, name.s, name.len);
   4902   buf[name.len] = '\0';
   4903   if (aa64_register_index(buf, &dwarf) != 0) return 1;
   4904   if (dwarf <= 30u) {
   4905     *cls_out = NATIVE_REG_INT;
   4906     *out = (Reg)dwarf;
   4907     return 0;
   4908   }
   4909   if (dwarf >= 64u && dwarf <= 95u) {
   4910     *cls_out = NATIVE_REG_FP;
   4911     *out = (Reg)(dwarf - 64u);
   4912     return 0;
   4913   }
   4914   return 1;
   4915 }
   4916 
   4917 static int aa_asm_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls,
   4918                                  Reg reg) {
   4919   (void)ri;
   4920   if (cls == NATIVE_REG_INT) {
   4921     if (reg <= 8u) return 1;
   4922     if (reg >= 12u && reg <= 15u) return 1;
   4923     if (reg >= 19u && reg <= 28u) return 1;
   4924     return 0;
   4925   }
   4926   if (cls == NATIVE_REG_FP) {
   4927     if (reg <= 19u) return 1;
   4928     if (reg >= 22u && reg <= 31u) return 1;
   4929   }
   4930   return 0;
   4931 }
   4932 
   4933 static int aa_asm_constraint_reg(const NativeRegInfo* ri, const char* body,
   4934                                  NativeAllocClass* cls_out, Reg* fixed_out,
   4935                                  u32* allowed_mask_out) {
   4936   (void)ri;
   4937   if (!body || !body[0] || body[1]) return 0;
   4938   if (fixed_out) *fixed_out = REG_NONE;
   4939   if (allowed_mask_out) *allowed_mask_out = 0;
   4940   switch (body[0]) {
   4941     case 'r':
   4942       if (cls_out) *cls_out = NATIVE_REG_INT;
   4943       return 1;
   4944     case 'w':
   4945       if (cls_out) *cls_out = NATIVE_REG_FP;
   4946       return 1;
   4947     case 'x':
   4948       if (cls_out) *cls_out = NATIVE_REG_FP;
   4949       if (allowed_mask_out) *allowed_mask_out = 0x0000ffffu; /* v0..v15 */
   4950       return 1;
   4951     case 'y':
   4952       if (cls_out) *cls_out = NATIVE_REG_FP;
   4953       if (allowed_mask_out) *allowed_mask_out = 0x000000ffu; /* v0..v7 */
   4954       return 1;
   4955     default:
   4956       return 0;
   4957   }
   4958 }
   4959 
   4960 static const NativeRegInfo aa_reg_info = {
   4961     .classes = aa_classes,
   4962     .nclasses = sizeof aa_classes / sizeof aa_classes[0],
   4963     /* x8..x15 give NDT a rich caller-saved scratch pool, so the -O0 path never
   4964      * needs a callee-saved register — enabling the tcc-style deferred-`sub`
   4965      * prologue (see aa_func_begin / AA_NDT_SUB_WORDS). */
   4966     .ndt_caller_saved_only = 1u,
   4967     /* x0 is clobbered only by calls (div/mul take explicit operands), so a
   4968      * scalar call result can stay cached in it — see L4 Phase 1 in nd_call. */
   4969     .ndt_result_reg_stable = 1u,
   4970     /* aa_emit_mem emits ldrsb/ldrsh (opc=10, X form) for an MF_SEXT_LOAD narrow
   4971      * integer load, filling the whole register — so the -O0 NDT may drop the
   4972      * following CV_SEXT (Lever 4). */
   4973     .load_sext = 1u,
   4974     .resolve_name = aa_resolve_name,
   4975     .asm_operand_reg_ok = aa_asm_operand_reg_ok,
   4976     .asm_constraint_reg = aa_asm_constraint_reg,
   4977 };
   4978 
   4979 static void aa_va_start_native(NativeTarget* t, NativeLoc ap_ptr);
   4980 static void aa_va_arg_native(NativeTarget* t, NativeLoc dst, NativeLoc ap_ptr,
   4981                              KitCgTypeId type);
   4982 static void aa_va_end_native(NativeTarget* t, NativeLoc ap_ptr);
   4983 static void aa_va_copy_native(NativeTarget* t, NativeLoc dst_ap_ptr,
   4984                               NativeLoc src_ap_ptr);
   4985 static void aa_asm_block_native(NativeTarget* t, const char* tmpl,
   4986                                 const AsmConstraint* outs, u32 nout,
   4987                                 NativeLoc* out_locs, const AsmConstraint* ins,
   4988                                 u32 nin, const NativeLoc* in_locs,
   4989                                 const Sym* clobbers, u32 nclob);
   4990 
   4991 NativeTarget* aa64_native_target_new(Compiler* c, ObjBuilder* obj,
   4992                                      MCEmitter* mc) {
   4993   AANativeTarget* a = arena_znew(c->tu, AANativeTarget);
   4994   NativeTarget* t;
   4995   if (!a) return NULL;
   4996   t = &a->base;
   4997   t->c = c;
   4998   t->obj = obj;
   4999   t->mc = mc;
   5000   native_frame_init(&a->frame, c);
   5001   t->regs = &aa_reg_info;
   5002   t->class_for_type = aa_class_for_type;
   5003   t->imm_legal = aa_imm_legal;
   5004   t->addr_legal = aa_addr_legal;
   5005   t->can_fold_shift_into_alu = aa_can_fold_shift_into_alu;
   5006   t->can_fold_extend_into_addr = aa_can_fold_extend_into_addr;
   5007   t->machine_op_clobbers = aa_machine_op_clobbers;
   5008   t->func_begin = aa_func_begin;
   5009   t->func_begin_known_frame = aa_func_begin_known_frame;
   5010   t->note_frame_state = aa_note_frame_state;
   5011   t->reserve_callee_saves = aa_reserve_callee_saves;
   5012   t->signature_stack_bytes = aa_signature_stack_bytes;
   5013   t->call_stack_bytes = aa_call_stack_bytes;
   5014   t->has_store_zero_reg = 1;
   5015   t->store_zero_reg = 31u; /* wzr/xzr in the Rt position of a store */
   5016   t->func_end = aa_func_end;
   5017   t->frame_slot = aa_frame_slot;
   5018   t->release_frame_slot = aa_release_frame_slot;
   5019   t->frame_slot_debug_loc = aa_frame_slot_debug_loc;
   5020   t->bind_param = aa_bind_native_param;
   5021   t->label_new = aa_label_new;
   5022   t->label_place = aa_label_place;
   5023   t->jump = aa_jump;
   5024   t->cmp_branch = aa_cmp_branch;
   5025   t->indirect_branch = aa_indirect_branch;
   5026   t->load_label_addr = aa_load_label_addr;
   5027   t->move = aa_move;
   5028   t->load_imm = aa_load_imm_native;
   5029   t->load_const = aa_load_const;
   5030   t->load_addr = aa_load_addr;
   5031   t->load = aa_load_native;
   5032   t->store = aa_store_native;
   5033   t->tls_addr_of = aa_tls_addr_of;
   5034   t->copy_bytes = aa_copy_bytes;
   5035   t->set_bytes = aa_set_bytes;
   5036   t->bitfield_load = aa_bitfield_load;
   5037   t->bitfield_store = aa_bitfield_store;
   5038   t->binop = aa_binop;
   5039   t->unop = aa_unop;
   5040   t->cmp = aa_cmp;
   5041   t->convert = aa_convert;
   5042   t->binop_rr = aa_binop_rr;
   5043   t->move_rr = aa_move_rr;
   5044   t->cmp_rr = aa_cmp_rr;
   5045   t->convert_rr = aa_convert_rr;
   5046   t->alloca_ = aa_alloca;
   5047   t->spill = aa_spill;
   5048   t->reload = aa_reload;
   5049   t->marshal_call = aa_marshal_call;
   5050   t->emit_call = aa_emit_call;
   5051   t->marshal_ret = aa_marshal_ret;
   5052   t->ret = aa_ret;
   5053   t->atomic_load = aa_atomic_load;
   5054   t->atomic_store = aa_atomic_store;
   5055   t->atomic_rmw = aa_atomic_rmw;
   5056   t->atomic_cas = aa_atomic_cas;
   5057   t->fence = aa_fence;
   5058   t->va_start_ = aa_va_start_native;
   5059   t->va_arg_ = aa_va_arg_native;
   5060   t->va_end_ = aa_va_end_native;
   5061   t->va_copy_ = aa_va_copy_native;
   5062   t->intrinsic = aa_intrinsic;
   5063   t->asm_block = aa_asm_block_native;
   5064   t->file_scope_asm = native_file_scope_asm;
   5065   t->trap = aa_trap;
   5066   t->set_loc = aa_set_loc;
   5067   t->finalize = native_finalize;
   5068   return t;
   5069 }
   5070 
   5071 /* Place the incoming parameter into `dst`: a hard register (the common
   5072  * register-allocated scalar case -> a single arg-reg move, or a stack load
   5073  * straight into the register), a frame slot (address-taken / aggregate /
   5074  * spilled), or nowhere (unused). Incoming arg registers are never allocable,
   5075  * so a register dst never aliases an incoming arg register. */
   5076 static void aa_bind_native_param(NativeTarget* t, const CGParamDesc* p,
   5077                                  NativeLoc dst) {
   5078   AANativeTarget* a = aa_of(t);
   5079   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type);
   5080   const ABIArgInfo* ai =
   5081       p->index < abi->nparams ? &abi->params[p->index] : NULL;
   5082   int to_reg = dst.kind == NATIVE_LOC_REG;
   5083   if (!ai || ai->kind == ABI_ARG_IGNORE) return;
   5084   if (ai->kind == ABI_ARG_INDIRECT) {
   5085     NativeAddr d_addr, from;
   5086     AggregateAccess access;
   5087     NativeLoc src =
   5088         native_loc_reg(p->type, NATIVE_REG_INT,
   5089                        a->next_param_int < 8u ? a->next_param_int++ : AA_TMP0);
   5090     if (src.v.reg == AA_TMP0) {
   5091       NativeAddr saddr;
   5092       memset(&saddr, 0, sizeof saddr);
   5093       saddr.base_kind = NATIVE_ADDR_BASE_REG;
   5094       saddr.base.reg = AA_FP;
   5095       saddr.offset = aa_fp_off_in_arg(a, a->next_param_stack);
   5096       aa_emit_mem(a, 1, src, saddr, aa_mem_for_type(t, p->type, 8));
   5097       a->next_param_stack += 8u;
   5098     }
   5099     if (dst.kind != NATIVE_LOC_FRAME)
   5100       aa_panic(a, "indirect parameter requires a frame destination");
   5101     memset(&d_addr, 0, sizeof d_addr);
   5102     d_addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5103     d_addr.base.frame = dst.v.frame;
   5104     d_addr.base_type = p->type;
   5105     memset(&from, 0, sizeof from);
   5106     from.base_kind = NATIVE_ADDR_BASE_REG;
   5107     from.base.reg = src.v.reg;
   5108     from.base_type = p->type;
   5109     memset(&access, 0, sizeof access);
   5110     access.type = p->type;
   5111     access.size = p->size ? p->size : (u32)cg_type_size(t->c, p->type);
   5112     access.align = p->align ? p->align : type_align32(t, p->type);
   5113     aa_copy_bytes(t, d_addr, from, access);
   5114     return;
   5115   }
   5116   for (u32 i = 0; i < ai->nparts; ++i) {
   5117     const ABIArgPart* part = &ai->parts[i];
   5118     NativeAllocClass cls =
   5119         part->cls == ABI_CLASS_FP ? NATIVE_REG_FP : NATIVE_REG_INT;
   5120     int reg_dst = to_reg && (NativeAllocClass)dst.cls == cls;
   5121     NativeLoc src;
   5122     if (cls == NATIVE_REG_FP && a->next_param_fp < 8u) {
   5123       src = native_loc_reg(p->type, cls, a->next_param_fp++);
   5124     } else if (cls == NATIVE_REG_INT && a->next_param_int < 8u) {
   5125       src = native_loc_reg(p->type, cls, a->next_param_int++);
   5126     } else {
   5127       /* Stack-passed part: load straight into the dst register when possible,
   5128        * otherwise use a backend-private transfer register for the frame store. */
   5129       Reg tmp =
   5130           reg_dst ? (Reg)dst.v.reg : (cls == NATIVE_REG_FP ? 16u : AA_TMP0);
   5131       NativeAddr saddr;
   5132       src = native_loc_reg(p->type, cls, tmp);
   5133       a->next_param_stack =
   5134           align_up_u32(a->next_param_stack, aa_part_stack_align(abi, part));
   5135       memset(&saddr, 0, sizeof saddr);
   5136       saddr.base_kind = NATIVE_ADDR_BASE_REG;
   5137       saddr.base.reg = AA_FP;
   5138       saddr.base_type = p->type;
   5139       saddr.offset = aa_fp_off_in_arg(a, a->next_param_stack);
   5140       aa_emit_mem(a, 1, src, saddr, aa_mem_for_type(t, p->type, part->size));
   5141       a->next_param_stack += aa_part_stack_size(abi, part);
   5142     }
   5143     if (dst.kind == NATIVE_LOC_NONE) {
   5144       /* Unused parameter: only the ABI cursor advances. */
   5145     } else if (to_reg) {
   5146       NativeLoc d = native_loc_reg(dst.type ? dst.type : p->type,
   5147                                    (NativeAllocClass)dst.cls, (Reg)dst.v.reg);
   5148       if (!(src.kind == NATIVE_LOC_REG && src.v.reg == d.v.reg &&
   5149             (NativeAllocClass)src.cls == (NativeAllocClass)d.cls))
   5150         aa_move(t, d, src);
   5151     } else {
   5152       aa_store_part(
   5153           t, native_loc_stack(p->type, dst.v.frame, (i32)part->src_offset), src,
   5154           0, part->size);
   5155     }
   5156   }
   5157   a->incoming_stack_size = align_up_u32(a->next_param_stack, 16u);
   5158 }
   5159 
   5160 static void aa_bind_param(NativeDirectTarget* d, const CGParamDesc* p,
   5161                           CGLocal local, NativeDirectLocal* l) {
   5162   NativeLoc dst;
   5163   (void)local;
   5164   memset(&dst, 0, sizeof dst);
   5165   dst.kind = NATIVE_LOC_FRAME;
   5166   dst.type = p->type;
   5167   dst.v.frame = l->home;
   5168   aa_bind_native_param(d->native, p, dst);
   5169 }
   5170 
   5171 static const char* aa_no_tail(NativeDirectTarget* d, const CGCallDesc* call) {
   5172   NativeCallDesc nd;
   5173   u32 stack;
   5174   native_direct_project_tail_call_desc(d, call, &nd);
   5175   stack = aa_call_stack_size(d->native, &nd);
   5176   if (stack > aa_of(d->native)->incoming_stack_size)
   5177     return "aarch64 tail call: stack argument area too small";
   5178   return NULL;
   5179 }
   5180 
   5181 static NativeAddr aa_direct_addr(NativeDirectTarget* d, Operand op) {
   5182   NativeAddr addr;
   5183   memset(&addr, 0, sizeof addr);
   5184   switch ((OpKind)op.kind) {
   5185     case OPK_LOCAL:
   5186       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5187       addr.base.frame = d->locals[op.v.local - 1u].home;
   5188       addr.base_type = op.type;
   5189       return addr;
   5190     case OPK_INDIRECT:
   5191       addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
   5192       addr.base.frame = d->locals[op.v.ind.base - 1u].home;
   5193       addr.cls = d->locals[op.v.ind.base - 1u].cls;
   5194       addr.base_type = d->locals[op.v.ind.base - 1u].type;
   5195       addr.offset = op.v.ind.ofs;
   5196       return addr;
   5197     default:
   5198       compiler_panic(d->base.c, d->loc,
   5199                      "aarch64 native target: operand is not addressable");
   5200   }
   5201 }
   5202 
   5203 static NativeAddr aa_direct_materialize_addr(NativeDirectTarget* d,
   5204                                              Operand op) {
   5205   NativeAddr addr = aa_direct_addr(d, op);
   5206   if (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
   5207     NativeLoc base = native_loc_reg(addr.base_type, NATIVE_REG_INT, AA_TMP1);
   5208     NativeAddr load;
   5209     memset(&load, 0, sizeof load);
   5210     load.base_kind = NATIVE_ADDR_BASE_FRAME;
   5211     load.base.frame = addr.base.frame;
   5212     load.base_type = addr.base_type;
   5213     aa_emit_mem(aa_of(d->native), 1, base, load,
   5214                 aa_mem_for_type(d->native, addr.base_type, 8));
   5215     addr.base_kind = NATIVE_ADDR_BASE_REG;
   5216     addr.base.reg = AA_TMP1;
   5217   }
   5218   return addr;
   5219 }
   5220 
   5221 static NativeAddr aa_direct_pointer_addr(NativeDirectTarget* d, Operand op) {
   5222   NativeAddr addr;
   5223   memset(&addr, 0, sizeof addr);
   5224   if (op.kind == OPK_LOCAL) {
   5225     NativeLoc base = native_loc_reg(op.type, NATIVE_REG_INT, AA_TMP1);
   5226     NativeAddr load;
   5227     memset(&load, 0, sizeof load);
   5228     load.base_kind = NATIVE_ADDR_BASE_FRAME;
   5229     load.base.frame = d->locals[op.v.local - 1u].home;
   5230     load.base_type = op.type;
   5231     aa_emit_mem(aa_of(d->native), 1, base, load,
   5232                 aa_mem_for_type(d->native, op.type, 8));
   5233     addr.base_kind = NATIVE_ADDR_BASE_REG;
   5234     addr.base.reg = AA_TMP1;
   5235     addr.base_type = op.type;
   5236     return addr;
   5237   }
   5238   return aa_direct_materialize_addr(d, op);
   5239 }
   5240 
   5241 static NativeAddr aa_reg_addr(KitCgTypeId type, u32 reg, i32 offset) {
   5242   NativeAddr addr;
   5243   memset(&addr, 0, sizeof addr);
   5244   addr.base_kind = NATIVE_ADDR_BASE_REG;
   5245   addr.base.reg = reg;
   5246   addr.base_type = type;
   5247   addr.offset = offset;
   5248   return addr;
   5249 }
   5250 
   5251 static void aa_load_ap_addr(NativeDirectTarget* d, Operand ap_addr,
   5252                             u32 dst_reg) {
   5253   NativeLoc dst =
   5254       native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, dst_reg);
   5255   NativeAddr ap = aa_direct_pointer_addr(d, ap_addr);
   5256   d->native->load_addr(d->native, dst, ap);
   5257 }
   5258 
   5259 /* The va cores use caller-provided operand registers plus backend-private
   5260  * x16/x17 and v16. Direct wrappers choose x9/x10 from the O0 scratch policy;
   5261  * the optimized emitter supplies an instruction-scoped operand location. None
   5262  * can alias a persistently allocated live value. */
   5263 static u32 aa_va_base_reg(AANativeTarget* a, NativeAddr ap) {
   5264   if (ap.base_kind != NATIVE_ADDR_BASE_REG)
   5265     compiler_panic(a->base.c, a->func ? a->func->loc : (SrcLoc){0, 0, 0},
   5266                    "aarch64 native target: va_list pointer not in register");
   5267   return ap.base.reg;
   5268 }
   5269 
   5270 /* va_list layout is queried from the ABI; the optimizer/direct callers pass the
   5271  * va_list pointer opaquely. `ap` addresses the va_list object itself. */
   5272 static void aa_va_start_core(AANativeTarget* a, NativeAddr ap) {
   5273   NativeTarget* t = &a->base;
   5274   const ABIFuncInfo* abi =
   5275       a->func ? abi_cg_func_info(t->c->abi, a->func->fn_type) : NULL;
   5276   ABIVaListInfo vai = abi_va_list_layout(t->c->abi);
   5277   NativeLoc ptr =
   5278       native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, AA_TMP0);
   5279   if (vai.kind == ABI_VA_LIST_POINTER) {
   5280     if (a->top_home_bytes) {
   5281       /* Windows: `va_list = &<first vararg>` inside the contiguous
   5282        * [GP home area | incoming stack args] block. Named args consume the
   5283        * leading slots; next_param_int (FP params remapped to GP included) plus
   5284        * next_param_stack locate the first unnamed slot. Home slot
   5285        * gp_reg_count coincides with incoming-arg byte 0, so a single formula
   5286        * spans both regions. */
   5287       i32 off =
   5288           aa_fp_off_home_slot(a->next_param_int) + (i32)a->next_param_stack;
   5289       aa_emit_add_imm(a, AA_TMP0, AA_FP, off);
   5290       aa_emit_mem(a, 0, ptr, ap, aa_mem_for_type(t, ptr.type, 8));
   5291       return;
   5292     }
   5293     /* `va_list = &<first vararg>`. Variadic stack args follow the fixed
   5294      * incoming params in the same caller window. Apple ARM64 compact fixed
   5295      * stack args may leave this cursor at +4, while the first variadic slot
   5296      * starts at the next 8-byte boundary. */
   5297     u32 stack = aa_vararg_stack_start(abi, a->next_param_stack);
   5298     aa_emit_add_imm(a, AA_TMP0, AA_FP, aa_fp_off_in_arg(a, stack));
   5299     aa_emit_mem(a, 0, ptr, ap, aa_mem_for_type(t, ptr.type, 8));
   5300     return;
   5301   }
   5302   if (vai.kind == ABI_VA_LIST_AAPCS64) {
   5303     KitCgTypeId i32_ty = builtin_id(KIT_CG_BUILTIN_I32);
   5304     NativeLoc i32tmp = native_loc_reg(i32_ty, NATIVE_REG_INT, AA_TMP1);
   5305     MemAccess ptr_mem = aa_mem_for_type(t, ptr.type, 8);
   5306     MemAccess i32_mem = aa_mem_for_type(t, i32_ty, 4);
   5307     AANativeSlot* gr = aa_slot(a, a->va_gr_slot);
   5308     AANativeSlot* vr = aa_slot(a, a->va_vr_slot);
   5309     u32 base = aa_va_base_reg(a, ap);
   5310     u32 used_gr = a->next_param_int < vai.gp_reg_count ? a->next_param_int
   5311                                                        : vai.gp_reg_count;
   5312     u32 used_vr = a->next_param_fp < vai.fp_reg_count ? a->next_param_fp
   5313                                                       : vai.fp_reg_count;
   5314     /* __stack points at the incoming stack args, which sit above the saved
   5315      * fp/lr pair — the same address bind_param uses (aa_fp_off_in_arg), not the
   5316      * raw next_param_stack cursor. */
   5317     aa_emit_add_imm(a, AA_TMP0, AA_FP,
   5318                     aa_fp_off_in_arg(a, a->next_param_stack));
   5319     aa_emit_mem(a, 0, ptr, aa_reg_addr(ptr.type, base, (i32)vai.stack_offset),
   5320                 ptr_mem);
   5321     aa_emit_add_imm(a, AA_TMP0, AA_FP,
   5322                     aa_fp_off_slot(a, gr->off) +
   5323                         (i32)(vai.gp_reg_count * vai.gp_slot_size));
   5324     aa_emit_mem(a, 0, ptr, aa_reg_addr(ptr.type, base, (i32)vai.gr_top_offset),
   5325                 ptr_mem);
   5326     aa_emit_add_imm(a, AA_TMP0, AA_FP,
   5327                     aa_fp_off_slot(a, vr->off) +
   5328                         (i32)(vai.fp_reg_count * vai.fp_slot_size));
   5329     aa_emit_mem(a, 0, ptr, aa_reg_addr(ptr.type, base, (i32)vai.vr_top_offset),
   5330                 ptr_mem);
   5331     aa_emit_load_imm(t->mc, 0, AA_TMP1,
   5332                      -(i32)((vai.gp_reg_count - used_gr) * vai.gp_slot_size));
   5333     aa_emit_mem(a, 0, i32tmp,
   5334                 aa_reg_addr(i32_ty, base, (i32)vai.gr_offs_offset), i32_mem);
   5335     aa_emit_load_imm(t->mc, 0, AA_TMP1,
   5336                      -(i32)((vai.fp_reg_count - used_vr) * vai.fp_slot_size));
   5337     aa_emit_mem(a, 0, i32tmp,
   5338                 aa_reg_addr(i32_ty, base, (i32)vai.vr_offs_offset), i32_mem);
   5339     return;
   5340   }
   5341   compiler_panic(t->c, a->func ? a->func->loc : (SrcLoc){0, 0, 0},
   5342                  "aarch64 native target: unsupported va_list layout");
   5343 }
   5344 
   5345 static void aa_va_arg_core(AANativeTarget* a, NativeLoc dst, NativeAddr ap,
   5346                            KitCgTypeId type) {
   5347   NativeTarget* t = &a->base;
   5348   ABIVaListInfo vai = abi_va_list_layout(t->c->abi);
   5349   NativeLoc cur =
   5350       native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, AA_TMP0);
   5351   /* The fetched value is written directly into caller-provided `dst`, which is
   5352    * distinct from the va_list base. Only TMP0/TMP1 are backend-private. */
   5353   NativeLoc val = dst;
   5354   NativeAddr src;
   5355   MemAccess ptr_mem = aa_mem_for_type(t, cur.type, 8);
   5356   MemAccess val_mem = aa_mem_for_type(t, type, type_size32(t, type));
   5357   if (dst.kind != NATIVE_LOC_REG)
   5358     compiler_panic(t->c, a->func ? a->func->loc : (SrcLoc){0, 0, 0},
   5359                    "aarch64 native target: va_arg destination must be a "
   5360                    "register");
   5361   if (vai.kind == ABI_VA_LIST_POINTER) {
   5362     aa_emit_mem(a, 1, cur, ap, ptr_mem);
   5363     src = aa_reg_addr(type, AA_TMP0, 0);
   5364     {
   5365       const ABIFuncInfo* abi =
   5366           a->func ? abi_cg_func_info(t->c->abi, a->func->fn_type) : NULL;
   5367       ABIArgPart part;
   5368       memset(&part, 0, sizeof part);
   5369       part.cls = cg_type_is_float(t->c, type) ? ABI_CLASS_FP : ABI_CLASS_INT;
   5370       part.size = type_size32(t, type);
   5371       part.align = type_align32(t, type);
   5372       aa_emit_add_imm(a, AA_TMP1, AA_TMP0,
   5373                       (i32)aa_part_vararg_stack_size(abi, &part));
   5374     }
   5375     aa_emit_mem(a, 0, native_loc_reg(cur.type, NATIVE_REG_INT, AA_TMP1), ap,
   5376                 ptr_mem);
   5377     aa_emit_mem(a, 1, val, src, val_mem);
   5378     return;
   5379   }
   5380   if (vai.kind == ABI_VA_LIST_AAPCS64) {
   5381     KitCgTypeId i32_ty = builtin_id(KIT_CG_BUILTIN_I32);
   5382     NativeLoc off = native_loc_reg(i32_ty, NATIVE_REG_INT, AA_TMP1);
   5383     MemAccess i32_mem = aa_mem_for_type(t, i32_ty, 4);
   5384     int is_fp = cg_type_is_float(t->c, type);
   5385     u32 base = aa_va_base_reg(a, ap);
   5386     u32 offs_field = is_fp ? vai.vr_offs_offset : vai.gr_offs_offset;
   5387     u32 top_field = is_fp ? vai.vr_top_offset : vai.gr_top_offset;
   5388     u32 slot_size = is_fp ? vai.fp_slot_size : vai.gp_slot_size;
   5389     MCLabel stack_label = mc_label_new(t->mc);
   5390     MCLabel done_label = mc_label_new(t->mc);
   5391     aa_emit_mem(a, 1, off, aa_reg_addr(i32_ty, base, (i32)offs_field), i32_mem);
   5392     aa_emit32(t->mc, aa64_subs_imm12(0, AA64_ZR, AA_TMP1, 0, 0));
   5393     aa_emit32(t->mc,
   5394               aa64_brcond_pack((AA64BrCond){.cond = cmp_cond(CMP_GE_S)}));
   5395     mc_emit_label_ref(t->mc, stack_label, R_AARCH64_CONDBR19, 4, 0);
   5396     aa_emit_mem(a, 1, cur, aa_reg_addr(cur.type, base, (i32)top_field),
   5397                 ptr_mem);
   5398     aa_emit32(t->mc, aa_sbfm(1, AA_TMP1, AA_TMP1, 0, 31));
   5399     aa_emit32(t->mc, aa64_add(1, AA_TMP0, AA_TMP0, AA_TMP1));
   5400     aa_emit_mem(a, 1, val, aa_reg_addr(type, AA_TMP0, 0), val_mem);
   5401     aa_emit_add_imm(a, AA_TMP1, AA_TMP1, (i32)slot_size);
   5402     aa_emit_mem(a, 0, off, aa_reg_addr(i32_ty, base, (i32)offs_field), i32_mem);
   5403     aa_emit32(t->mc, aa64_b(0));
   5404     mc_emit_label_ref(t->mc, done_label, R_AARCH64_JUMP26, 4, 0);
   5405     mc_label_place(t->mc, stack_label);
   5406     aa_emit_mem(a, 1, cur, aa_reg_addr(cur.type, base, (i32)vai.stack_offset),
   5407                 ptr_mem);
   5408     aa_emit_mem(a, 1, val, aa_reg_addr(type, AA_TMP0, 0), val_mem);
   5409     aa_emit_add_imm(a, AA_TMP0, AA_TMP0, 8);
   5410     aa_emit_mem(a, 0, cur, aa_reg_addr(cur.type, base, (i32)vai.stack_offset),
   5411                 ptr_mem);
   5412     mc_label_place(t->mc, done_label);
   5413     return;
   5414   }
   5415   compiler_panic(t->c, a->func ? a->func->loc : (SrcLoc){0, 0, 0},
   5416                  "aarch64 native target: unsupported va_list layout");
   5417 }
   5418 
   5419 static void aa_va_copy_core(AANativeTarget* a, NativeAddr dst_ap,
   5420                             NativeAddr src_ap) {
   5421   NativeTarget* t = &a->base;
   5422   ABIVaListInfo vai = abi_va_list_layout(t->c->abi);
   5423   NativeLoc tmp =
   5424       native_loc_reg(builtin_id(KIT_CG_BUILTIN_I64), NATIVE_REG_INT, AA_TMP0);
   5425   MemAccess mem = aa_mem_for_type(t, tmp.type, 8);
   5426   if (vai.kind == ABI_VA_LIST_POINTER) {
   5427     aa_emit_mem(a, 1, tmp, src_ap, mem);
   5428     aa_emit_mem(a, 0, tmp, dst_ap, mem);
   5429     return;
   5430   }
   5431   if (vai.kind == ABI_VA_LIST_AAPCS64) {
   5432     u32 sb = aa_va_base_reg(a, src_ap);
   5433     u32 db = aa_va_base_reg(a, dst_ap);
   5434     for (u32 off = 0; off < vai.type.size; off += 8u) {
   5435       aa_emit_mem(a, 1, tmp, aa_reg_addr(tmp.type, sb, (i32)off), mem);
   5436       aa_emit_mem(a, 0, tmp, aa_reg_addr(tmp.type, db, (i32)off), mem);
   5437     }
   5438     return;
   5439   }
   5440   compiler_panic(t->c, a->func ? a->func->loc : (SrcLoc){0, 0, 0},
   5441                  "aarch64 native target: unsupported va_list layout");
   5442 }
   5443 
   5444 /* ---- Direct-path (NativeDirectTarget) wrappers: convert semantic operands to
   5445  * NativeAddr/NativeLoc, then call the shared cores above. ---- */
   5446 
   5447 /* The cores reserve x16/x17 (TMP0/TMP1) as backend-private temporaries and
   5448  * require the va_list base register(s) to be distinct. aa_direct_pointer_addr
   5449  * returns the pointer in TMP1, so the direct wrappers first relocate it into
   5450  * x9/x10 before calling the cores. */
   5451 static NativeAddr aa_direct_va_base(NativeDirectTarget* d, Operand ap_addr,
   5452                                     u32 reg) {
   5453   aa_load_ap_addr(d, ap_addr, reg);
   5454   return aa_reg_addr(builtin_id(KIT_CG_BUILTIN_I64), reg, 0);
   5455 }
   5456 
   5457 static void aa_va_start_(NativeDirectTarget* d, Operand ap_addr) {
   5458   aa_va_start_core(aa_of(d->native), aa_direct_va_base(d, ap_addr, 10u));
   5459 }
   5460 
   5461 static void aa_va_arg_(NativeDirectTarget* d, Operand dst_op, Operand ap_addr,
   5462                        KitCgTypeId type) {
   5463   AANativeTarget* a = aa_of(d->native);
   5464   int is_fp = cg_type_is_float(d->base.c, type);
   5465   NativeLoc res = native_loc_reg(type, is_fp ? NATIVE_REG_FP : NATIVE_REG_INT,
   5466                                  is_fp ? 16u : 9u);
   5467   MemAccess val_mem =
   5468       aa_mem_for_type(d->native, type, type_size32(d->native, type));
   5469   NativeAddr dst;
   5470   aa_va_arg_core(a, res, aa_direct_va_base(d, ap_addr, 10u), type);
   5471   dst = aa_direct_materialize_addr(d, dst_op);
   5472   aa_emit_mem(a, 0, res, dst, val_mem);
   5473 }
   5474 
   5475 static void aa_va_end_(NativeDirectTarget* d, Operand ap_addr) {
   5476   (void)d;
   5477   (void)ap_addr;
   5478 }
   5479 
   5480 static void aa_va_copy_(NativeDirectTarget* d, Operand dst_ap_addr,
   5481                         Operand src_ap_addr) {
   5482   AANativeTarget* a = aa_of(d->native);
   5483   NativeAddr src = aa_direct_va_base(d, src_ap_addr, 9u);
   5484   NativeAddr dst = aa_direct_va_base(d, dst_ap_addr, 10u);
   5485   aa_va_copy_core(a, dst, src);
   5486 }
   5487 
   5488 /* ---- NativeTarget (optimizer) hooks: the optimizer passes the va_list
   5489  * pointer as a materialized register; layout is resolved inside the cores. ----
   5490  */
   5491 
   5492 static NativeAddr aa_va_addr_from_ptr(NativeLoc ap_ptr) {
   5493   NativeAddr addr;
   5494   memset(&addr, 0, sizeof addr);
   5495   addr.base_kind = NATIVE_ADDR_BASE_REG;
   5496   addr.cls = NATIVE_REG_INT;
   5497   addr.base.reg = ap_ptr.v.reg;
   5498   addr.base_type = ap_ptr.type;
   5499   return addr;
   5500 }
   5501 
   5502 static void aa_va_start_native(NativeTarget* t, NativeLoc ap_ptr) {
   5503   aa_va_start_core(aa_of(t), aa_va_addr_from_ptr(ap_ptr));
   5504 }
   5505 
   5506 static void aa_va_arg_native(NativeTarget* t, NativeLoc dst, NativeLoc ap_ptr,
   5507                              KitCgTypeId type) {
   5508   aa_va_arg_core(aa_of(t), dst, aa_va_addr_from_ptr(ap_ptr), type);
   5509 }
   5510 
   5511 static void aa_va_end_native(NativeTarget* t, NativeLoc ap_ptr) {
   5512   (void)t;
   5513   (void)ap_ptr;
   5514 }
   5515 
   5516 static void aa_va_copy_native(NativeTarget* t, NativeLoc dst_ap_ptr,
   5517                               NativeLoc src_ap_ptr) {
   5518   aa_va_copy_core(aa_of(t), aa_va_addr_from_ptr(dst_ap_ptr),
   5519                   aa_va_addr_from_ptr(src_ap_ptr));
   5520 }
   5521 
   5522 /* constraint_body / constraint_early / match_index are shared
   5523  * (cg/native_asm.h). */
   5524 
   5525 _Noreturn static void aa_asm_panic_at(Compiler* c, SrcLoc loc,
   5526                                       const char* msg) {
   5527   compiler_panic(c, loc, "aarch64 inline asm: %s", msg);
   5528 }
   5529 
   5530 _Noreturn static void aa_asm_panic(NativeDirectTarget* d, const char* msg) {
   5531   aa_asm_panic_at(d->base.c, d->loc, msg);
   5532 }
   5533 
   5534 AA_UNUSED_FN static void aa_asm_bound_reg(Operand* out, KitCgTypeId type,
   5535                                           NativeAllocClass cls, Reg reg) {
   5536   memset(out, 0, sizeof *out);
   5537   out->kind = AA64_INLINE_OPK_REG;
   5538   out->pad[0] =
   5539       (cls == NATIVE_REG_FP) ? AA64_INLINE_OPCLS_FP : AA64_INLINE_OPCLS_INT;
   5540   out->type = type;
   5541   out->v.local = (CGLocal)reg;
   5542 }
   5543 
   5544 AA_UNUSED_FN static void aa_asm_bound_mem(Operand* out, KitCgTypeId type,
   5545                                           Reg base) {
   5546   memset(out, 0, sizeof *out);
   5547   out->kind = OPK_INDIRECT;
   5548   out->type = type;
   5549   out->v.ind.base = (CGLocal)base;
   5550   out->v.ind.index = CG_LOCAL_NONE;
   5551 }
   5552 
   5553 static int aa_asm_parse_reg_clobber(Compiler* c, SrcLoc loc, Sym name,
   5554                                     NativeAllocClass* cls_out, Reg* reg_out) {
   5555   Slice s = pool_slice(c->global, name);
   5556   char buf[16];
   5557   uint32_t dwarf;
   5558   if (!s.s || !s.len) return 0;
   5559   if (s.len == 2 && s.s[0] == 'c' && s.s[1] == 'c') return 0;
   5560   if (s.len == 6 && memcmp(s.s, "memory", 6) == 0) return 0;
   5561   if (s.len >= sizeof buf) aa_asm_panic_at(c, loc, "clobber name is too long");
   5562   memcpy(buf, s.s, s.len);
   5563   buf[s.len] = '\0';
   5564   if (aa64_register_index(buf, &dwarf) != 0)
   5565     aa_asm_panic_at(c, loc, "unknown clobber register");
   5566   if (dwarf <= 30u) {
   5567     *cls_out = NATIVE_REG_INT;
   5568     *reg_out = (Reg)dwarf;
   5569     return 1;
   5570   }
   5571   if (dwarf >= 64u && dwarf <= 95u) {
   5572     *cls_out = NATIVE_REG_FP;
   5573     *reg_out = (Reg)(dwarf - 64u);
   5574     return 1;
   5575   }
   5576   aa_asm_panic_at(c, loc, "unsupported clobber register");
   5577   return 0;
   5578 }
   5579 
   5580 static void aa_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers,
   5581                                  u32 nclob, u32* int_mask, u32* fp_mask) {
   5582   *int_mask = 0;
   5583   *fp_mask = 0;
   5584   for (u32 i = 0; i < nclob; ++i) {
   5585     NativeAllocClass cls;
   5586     Reg reg;
   5587     if (!aa_asm_parse_reg_clobber(c, loc, clobbers[i], &cls, &reg)) continue;
   5588     if (cls == NATIVE_REG_INT)
   5589       *int_mask |= 1u << reg;
   5590     else if (cls == NATIVE_REG_FP)
   5591       *fp_mask |= 1u << reg;
   5592   }
   5593 }
   5594 
   5595 /* Pin resolution + panic is the shared native_asm_bind_direct_operands path. */
   5596 
   5597 AA_UNUSED_FN static void aa_direct_load_operand_to_reg(NativeDirectTarget* d,
   5598                                                        Operand op,
   5599                                                        NativeLoc dst) {
   5600   NativeAddr addr;
   5601   memset(&addr, 0, sizeof addr);
   5602   switch ((OpKind)op.kind) {
   5603     case OPK_IMM:
   5604       if ((NativeAllocClass)dst.cls != NATIVE_REG_INT)
   5605         aa_asm_panic(d, "floating-point immediate asm input is unsupported");
   5606       d->native->load_imm(d->native, dst, op.v.imm);
   5607       return;
   5608     case OPK_LOCAL:
   5609       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5610       addr.base.frame = d->locals[op.v.local - 1u].home;
   5611       addr.base_type = op.type;
   5612       aa_emit_mem(aa_of(d->native), 1, dst, addr,
   5613                   aa_mem_for_type(d->native, op.type, 0));
   5614       return;
   5615     case OPK_GLOBAL:
   5616       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
   5617       addr.base.global.sym = op.v.global.sym;
   5618       addr.base.global.addend = op.v.global.addend;
   5619       addr.base_type = op.type;
   5620       d->native->load_addr(d->native, dst, addr);
   5621       return;
   5622     case OPK_INDIRECT:
   5623       addr = aa_direct_materialize_addr(d, op);
   5624       aa_emit_mem(aa_of(d->native), 1, dst, addr,
   5625                   aa_mem_for_type(d->native, op.type, 0));
   5626       return;
   5627   }
   5628   aa_asm_panic(d, "unsupported asm input operand");
   5629 }
   5630 
   5631 AA_UNUSED_FN static void aa_direct_load_address_to_reg(NativeDirectTarget* d,
   5632                                                        Operand op,
   5633                                                        NativeLoc dst) {
   5634   NativeAddr addr = aa_direct_addr(d, op);
   5635   d->native->load_addr(d->native, dst, addr);
   5636 }
   5637 
   5638 AA_UNUSED_FN static void aa_direct_store_reg_to_operand(NativeDirectTarget* d,
   5639                                                         Operand op,
   5640                                                         NativeLoc src) {
   5641   NativeAddr addr;
   5642   memset(&addr, 0, sizeof addr);
   5643   if (op.kind == OPK_LOCAL) {
   5644     addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5645     addr.base.frame = d->locals[op.v.local - 1u].home;
   5646     addr.base_type = op.type;
   5647   } else {
   5648     addr = aa_direct_materialize_addr(d, op);
   5649   }
   5650   aa_emit_mem(aa_of(d->native), 0, src, addr,
   5651               aa_mem_for_type(d->native, op.type, 0));
   5652 }
   5653 
   5654 typedef struct AAAsmSavedClobber {
   5655   NativeFrameSlot slot;
   5656   NativeAllocClass cls;
   5657   Reg reg;
   5658   KitCgTypeId type;
   5659 } AAAsmSavedClobber;
   5660 
   5661 static void aa_asm_save_one(AANativeTarget* a, AAAsmSavedClobber* s) {
   5662   NativeFrameSlotDesc desc;
   5663   NativeAddr addr;
   5664   NativeLoc reg;
   5665   memset(&desc, 0, sizeof desc);
   5666   desc.type = s->type;
   5667   desc.size = 8;
   5668   desc.align = 8;
   5669   desc.kind = NATIVE_FRAME_SLOT_SAVE;
   5670   s->slot = a->base.frame_slot(&a->base, &desc);
   5671   memset(&addr, 0, sizeof addr);
   5672   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5673   addr.base.frame = s->slot;
   5674   addr.base_type = s->type;
   5675   reg = native_loc_reg(s->type, s->cls, s->reg);
   5676   aa_emit_mem(a, 0, reg, addr, aa_mem_for_type(&a->base, s->type, 8));
   5677 }
   5678 
   5679 AA_UNUSED_FN static void aa_asm_restore_one(AANativeTarget* a,
   5680                                             const AAAsmSavedClobber* s) {
   5681   NativeAddr addr;
   5682   NativeLoc reg = native_loc_reg(s->type, s->cls, s->reg);
   5683   memset(&addr, 0, sizeof addr);
   5684   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5685   addr.base.frame = s->slot;
   5686   addr.base_type = s->type;
   5687   aa_emit_mem(a, 1, reg, addr, aa_mem_for_type(&a->base, s->type, 8));
   5688 }
   5689 
   5690 AA_UNUSED_FN static AAAsmSavedClobber* aa_asm_save_callee_clobbers(
   5691     AANativeTarget* a, u32 int_mask, u32 fp_mask, u32* nsaved_out) {
   5692   AAAsmSavedClobber* saved =
   5693       arena_zarray(a->base.c->tu, AAAsmSavedClobber, 20u);
   5694   u32 n = 0;
   5695   KitCgTypeId i64 = builtin_id(KIT_CG_BUILTIN_I64);
   5696   KitCgTypeId f64 = builtin_id(KIT_CG_BUILTIN_F64);
   5697   for (Reg r = 19u; r <= 28u; ++r) {
   5698     if ((int_mask & (1u << r)) == 0) continue;
   5699     saved[n].cls = NATIVE_REG_INT;
   5700     saved[n].reg = r;
   5701     saved[n].type = i64;
   5702     aa_asm_save_one(a, &saved[n++]);
   5703   }
   5704   for (Reg r = 8u; r <= 15u; ++r) {
   5705     if ((fp_mask & (1u << r)) == 0) continue;
   5706     saved[n].cls = NATIVE_REG_FP;
   5707     saved[n].reg = r;
   5708     saved[n].type = f64;
   5709     aa_asm_save_one(a, &saved[n++]);
   5710   }
   5711   *nsaved_out = n;
   5712   return saved;
   5713 }
   5714 
   5715 /* Hook adapters that bridge the arch-typed save/restore + assembler entry to
   5716  * the shared NativeAsmDirectHooks signatures (which speak NativeDirectTarget +
   5717  * an opaque save record). */
   5718 static void* aa_asm_hook_save_callee_clobbers(NativeDirectTarget* d,
   5719                                               u32 int_mask, u32 fp_mask,
   5720                                               u32* nsaved_out) {
   5721   return aa_asm_save_callee_clobbers(aa_of(d->native), int_mask, fp_mask,
   5722                                      nsaved_out);
   5723 }
   5724 static void aa_asm_hook_restore_one(NativeDirectTarget* d, void* saved,
   5725                                     u32 idx) {
   5726   aa_asm_restore_one(aa_of(d->native), &((AAAsmSavedClobber*)saved)[idx]);
   5727 }
   5728 static void aa_asm_hook_run_template(NativeDirectTarget* d, const char* tmpl,
   5729                                      const AsmConstraint* outs, u32 nout,
   5730                                      Operand* bound_outs,
   5731                                      const AsmConstraint* ins, u32 nin,
   5732                                      Operand* bound_ins, const Sym* clobbers,
   5733                                      u32 nclob) {
   5734   AA64Asm* a = aa64_asm_open(d->base.c);
   5735   aa64_inline_bind(a, outs, nout, bound_outs, ins, nin, bound_ins, clobbers,
   5736                    nclob);
   5737   aa64_asm_run_template(a, d->native->mc, tmpl);
   5738   aa64_asm_close(a);
   5739 }
   5740 
   5741 static void aa_direct_asm_block(NativeDirectTarget* d, const char* tmpl,
   5742                                 const AsmConstraint* outs, u32 nout,
   5743                                 Operand* out_ops, const AsmConstraint* ins,
   5744                                 u32 nin, const Operand* in_ops,
   5745                                 const Sym* clobbers, u32 nclob,
   5746                                 u32 clobber_abi_sets) {
   5747   static const NativeAsmDirectHooks hooks = {
   5748       .opk_reg = AA64_INLINE_OPK_REG,
   5749       .opcls_fp = AA64_INLINE_OPCLS_FP,
   5750       .panic = aa_asm_panic,
   5751       .bound_reg = aa_asm_bound_reg,
   5752       .bound_mem = aa_asm_bound_mem,
   5753       .clobber_masks = aa_asm_clobber_masks,
   5754       .save_callee_clobbers = aa_asm_hook_save_callee_clobbers,
   5755       .restore_one = aa_asm_hook_restore_one,
   5756       .load_operand_to_reg = aa_direct_load_operand_to_reg,
   5757       .load_address_to_reg = aa_direct_load_address_to_reg,
   5758       .store_reg_to_operand = aa_direct_store_reg_to_operand,
   5759       .run_template = aa_asm_hook_run_template,
   5760   };
   5761   native_asm_bind_direct_operands(d, tmpl, outs, nout, out_ops, ins, nin,
   5762                                   in_ops, clobbers, nclob, clobber_abi_sets,
   5763                                   &hooks);
   5764 }
   5765 
   5766 /* ---- NativeTarget (optimizer) asm hook ----
   5767  *
   5768  * The optimized emitter owns register placement, staging, and output
   5769  * writeback. This hook binds its concrete locations and only materializes
   5770  * memory-constraint bases in backend-private registers. */
   5771 
   5772 static NativeAddr aa_asm_loc_to_addr(AANativeTarget* a, SrcLoc loc,
   5773                                      NativeLoc src) {
   5774   NativeAddr addr;
   5775   memset(&addr, 0, sizeof addr);
   5776   addr.base_type = src.type;
   5777   switch ((NativeLocKind)src.kind) {
   5778     case NATIVE_LOC_FRAME:
   5779       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   5780       addr.base.frame = src.v.frame;
   5781       return addr;
   5782     case NATIVE_LOC_ADDR:
   5783       return src.v.addr;
   5784     case NATIVE_LOC_GLOBAL:
   5785       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
   5786       addr.base.global.sym = src.v.global.sym;
   5787       addr.base.global.addend = src.v.global.addend;
   5788       return addr;
   5789     case NATIVE_LOC_REG:
   5790       addr.base_kind = NATIVE_ADDR_BASE_REG;
   5791       addr.cls = NATIVE_REG_INT;
   5792       addr.base.reg = src.v.reg;
   5793       return addr;
   5794     default:
   5795       aa_asm_panic_at(a->base.c, loc, "unsupported memory asm operand");
   5796   }
   5797 }
   5798 
   5799 /* Resolve a memory-constraint operand to a single base register with zero
   5800  * offset, folding any frame/global/offset into a backend-private register. At
   5801  * most x16/x17 are used across one asm block. */
   5802 static Reg aa_asm_native_mem_base(AANativeTarget* a, SrcLoc loc, NativeLoc src,
   5803                                   u32* ntmp) {
   5804   NativeAddr addr = aa_asm_loc_to_addr(a, loc, src);
   5805   u32 base;
   5806   i32 off;
   5807   Reg dst;
   5808   if (addr.index_kind != NATIVE_ADDR_INDEX_NONE)
   5809     aa_asm_panic_at(a->base.c, loc, "indexed memory asm operand unsupported");
   5810   aa_addr_base(a, addr, 0u, &base, &off);
   5811   if (off == 0) return (Reg)base;
   5812   if (*ntmp >= 2u)
   5813     aa_asm_panic_at(a->base.c, loc, "too many memory asm operands");
   5814   dst = (*ntmp == 0u) ? AA_TMP0 : AA_TMP1;
   5815   (*ntmp)++;
   5816   aa_emit_add_imm(a, dst, base, off);
   5817   return dst;
   5818 }
   5819 
   5820 static void aa_asm_native_panic(NativeTarget* t, SrcLoc loc, const char* msg) {
   5821   aa_asm_panic_at(t->c, loc, msg);
   5822 }
   5823 
   5824 static Reg aa_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc,
   5825                                        NativeLoc src, u32* ntmp) {
   5826   return aa_asm_native_mem_base(aa_of(t), loc, src, ntmp);
   5827 }
   5828 
   5829 static void aa_asm_native_run_template_hook(
   5830     NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout,
   5831     Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins,
   5832     const Sym* clobbers, u32 nclob) {
   5833   AA64Asm* asmh = aa64_asm_open(t->c);
   5834   aa64_inline_bind(asmh, outs, nout, bound_outs, ins, nin, bound_ins, clobbers,
   5835                    nclob);
   5836   aa64_asm_run_template(asmh, t->mc, tmpl);
   5837   aa64_asm_close(asmh);
   5838 }
   5839 
   5840 static void aa_asm_block_native(NativeTarget* t, const char* tmpl,
   5841                                 const AsmConstraint* outs, u32 nout,
   5842                                 NativeLoc* out_locs, const AsmConstraint* ins,
   5843                                 u32 nin, const NativeLoc* in_locs,
   5844                                 const Sym* clobbers, u32 nclob) {
   5845   AANativeTarget* a = aa_of(t);
   5846   SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0};
   5847   static const NativeAsmNativeHooks hooks = {
   5848       .panic = aa_asm_native_panic,
   5849       .bound_reg = aa_asm_bound_reg,
   5850       .bound_mem = aa_asm_bound_mem,
   5851       .mem_base = aa_asm_native_mem_base_hook,
   5852       .run_template = aa_asm_native_run_template_hook,
   5853   };
   5854   native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin,
   5855                                   in_locs, clobbers, nclob, &hooks);
   5856 }
   5857 
   5858 static const NativeOps aa_direct_ops = {
   5859     .bind_param = aa_bind_param,
   5860     .tail_call_unrealizable_reason = aa_no_tail,
   5861     .va_start_ = aa_va_start_,
   5862     .va_arg_ = aa_va_arg_,
   5863     .va_end_ = aa_va_end_,
   5864     .va_copy_ = aa_va_copy_,
   5865     .asm_block = aa_direct_asm_block,
   5866 };
   5867 
   5868 const NativeOps* aa64_native_direct_ops(void) { return &aa_direct_ops; }