kit

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

native.c (171311B)


      1 /* ARM32 (ARMv7-M Thumb-2, soft-float) NativeTarget backend.
      2  *
      3  * Built on the shared NativeDirectTarget/MCEmitter/NativeFrame substrate. This
      4  * is the Phase-1 "walking skeleton" backend: it lowers the smoke set (return,
      5  * ALU, compare + if/else, and a <=4-int-arg call) correctly; the op-group
      6  * expansion (i64 lanes, bitfields, atomics, varargs, inline asm, the disasm
      7  * tables, code-density tiers) is Phase 2.
      8  *
      9  * Frame model (single-pass, FP-anchored):
     10  *   prologue:  PUSH {r7, lr}     ; save the frame pointer + return address
     11  *              MOV  r7, sp       ; r7 = frame anchor (just below the saved pair)
     12  *              SUB  sp, sp, #N    ; reserve locals + outgoing args (patched at end)
     13  *   slots:     addressed [r7, #-off]  (off fixed at allocation, anchor fixed)
     14  *   epilogue:  MOV  sp, r7        ; drop the frame (size-independent)
     15  *              POP  {r7, pc}      ; restore r7 + return (Thumb bit from stacked lr)
     16  *
     17  * The NDT value-cache pool is the caller-saved arg registers r0..r3 (Lever 1:
     18  * args materialize directly into their ABI registers); r12 (IP) is its O0
     19  * scratch and a backend-private hook temporary; r7 is the frame pointer.
     20  * Callee-saved r4..r11 are reserved from the -O0 pool (the optimizer allocates
     21  * over them at -O1). */
     22 #include <string.h>
     23 
     24 #include "abi/abi.h"
     25 #include "arch/arm32/arm32.h"
     26 #include "arch/arm32/asm.h"
     27 #include "arch/arm32/isa.h"
     28 #include "arch/arm32/regs.h"
     29 #include "arch/arm32/variant.h"
     30 #include "arch/mc.h"
     31 #include "arch/native_target.h"
     32 #include "cg/cgir.h"
     33 #include "cg/native_argmove.h"
     34 #include "cg/native_asm.h"
     35 #include "cg/native_direct_target.h"
     36 #include "cg/native_frame.h"
     37 #include "cg/type.h"
     38 #include "core/bytes.h"
     39 #include "core/core.h"
     40 #include "core/pool.h"
     41 #include "obj/obj.h"
     42 
     43 extern void debug_emit_row(Debug*, ObjSecId text_section, u32 offset, SrcLoc);
     44 extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs,
     45                                 u32 end_ofs);
     46 
     47 #define ARM_FP 7u  /* frame pointer (Thumb convention) */
     48 #define ARM_SCRATCH 12u /* IP: NDT operand-materialization / cycle-break scratch */
     49 /* Backend emit-internal temp for multi-step sequences (div/mod). lr is RESERVED
     50  * (never holds an NDT operand) and dead in the body — its live value is saved on
     51  * the stack by the prologue and restored via `pop {..,pc}`, so clobbering the lr
     52  * register between calls is free and cannot collide with an NDT operand in IP. */
     53 #define ARM_TMP 14u
     54 #define ARM_MAX_REG_ARG_MOVES 16u
     55 /* Worst-case T32 instruction slots the single-pass deferred-`SUB sp` region must
     56  * hold: an IP-staged large frame is MOVW + MOVT + SUB sp,sp,ip = 3 words. A
     57  * small frame patches the first slot with one SUBW and NOP-fills the rest. */
     58 #define ARM_NDT_SUB_WORDS 3u
     59 
     60 /* Incoming-arg window geometry (r7-relative). The prologue saves {r7, lr} (8
     61  * bytes), so r7 sits 8 bytes below the caller's outgoing-arg block: incoming
     62  * stack args begin at [r7 + 8]. A variadic callee additionally spills the GP
     63  * argument registers r0..r3 (16 bytes) into a save area pushed ABOVE the saved
     64  * pair, making them contiguous-below the named stack args; that shifts the
     65  * stack-arg window down by 16 and locates the variadic register-save area at
     66  * [r7 + 8]. */
     67 #define ARM_SAVED_PAIR_BYTES 8u
     68 #define ARM_VA_GP_SAVE_BYTES 16u /* r0..r3 spilled for variadics */
     69 
     70 /* ============================ state ============================ */
     71 
     72 typedef struct Arm32NativeTarget {
     73   NativeTarget base; /* MUST be first member (arm_of is a reinterpret cast) */
     74   const Arm32Variant* variant;
     75   SrcLoc loc;
     76   const CGFuncDesc* func;
     77   NativeFrame frame;
     78   u32 next_param_int;  /* incoming param cursor: r0..r3 */
     79   u32 next_param_stack;
     80   u8 has_sret;
     81   u8 is_variadic;
     82   NativeFrameSlot sret_ptr_slot;
     83   /* Lazily-allocated 4-byte home for an indirect call target when the call has
     84    * outgoing stack args. At -O0 the two backend staging registers (IP for the
     85    * value, LR for a large-offset address) may both be consumed, so the callee
     86    * cannot remain in either register across staging: it is spilled here and
     87    * reloaded just before BLX. NATIVE_FRAME_SLOT_NONE until first needed. */
     88   NativeFrameSlot callee_spill_slot;
     89   u32 func_start;
     90   u32 prologue_sub_pos; /* file offset of the deferred SUB sp (patched at end) */
     91   MCLabel epilogue_label;
     92   /* Known-frame (-O1) state. On the known-frame path the optimizer hands us the
     93    * exact frame up front, so the prologue is emitted final (no deferred SUB to
     94    * patch in arm_func_end). saved_reglist is the PUSH/POP register set (callee-
     95    * saves + r7 + lr); saved_block_bytes is its size in bytes, which incoming
     96    * stack args sit above (so bind_param adds it instead of the bare 8). */
     97   u8 known_frame;
     98   u8 slim_prologue;   /* leaf, no frame: emit BX lr, no PUSH/anchor */
     99   u16 saved_reglist;  /* registers PUSHed by the prologue (POP restores via pc) */
    100   u32 saved_block_bytes;
    101   /* Bytes of incoming stack-argument window this function owns (its own params
    102    * beyond r0..r3, 8-byte rounded). A tail/sibling call is realizable only if the
    103    * callee's outgoing stack args fit here — its outgoing stack args reuse this
    104    * window in place. */
    105   u32 incoming_stack_size;
    106   /* alloca: each dynamic allocation moves sp down and returns sp + max_outgoing
    107    * (the block sits just above the call outgoing-arg area at the bottom of the
    108    * frame). max_outgoing is only final at func_end, so the `ADD dst, sp, #off`
    109    * is emitted with a placeholder and patched here. */
    110   u32 alloca_patch_pos[16];
    111   u8 alloca_patch_rd[16];
    112   u8 n_alloca_patch;
    113 } Arm32NativeTarget;
    114 
    115 static Arm32NativeTarget* arm_of(NativeTarget* t) {
    116   return (Arm32NativeTarget*)t;
    117 }
    118 
    119 static _Noreturn void arm_panic(Arm32NativeTarget* a, const char* msg) {
    120   compiler_panic(a->base.c, a->loc, "arm32 native target: %s", msg);
    121 }
    122 
    123 static u32 loc_reg(NativeLoc loc) { return loc.v.reg & 0xfu; }
    124 
    125 /* Forward declarations for helpers referenced before their definitions (the
    126  * aggregate copy is shared by INDIRECT param binding, INDIRECT returns, and the
    127  * wide va_arg path). */
    128 static void arm_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src,
    129                            AggregateAccess acc);
    130 
    131 /* ============================ emit ============================ */
    132 
    133 static void arm_emit_t16(MCEmitter* mc, u16 hw) {
    134   u8 b[2];
    135   u32 ofs = obj_pos(mc->obj, mc->section_id);
    136   b[0] = (u8)(hw & 0xffu);
    137   b[1] = (u8)((hw >> 8) & 0xffu);
    138   mc_emit_bytes(mc, b, sizeof b);
    139   if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
    140 }
    141 
    142 /* A 32-bit Thumb-2 instruction (passed as (hw1<<16)|hw2) is two LE half-words,
    143  * hw1 first in memory. NOT a single LE word — never route through mc_emit32. */
    144 static void arm_emit_t32(MCEmitter* mc, u32 instr) {
    145   u8 b[4];
    146   u32 ofs = obj_pos(mc->obj, mc->section_id);
    147   u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu;
    148   b[0] = (u8)(hw1 & 0xffu);
    149   b[1] = (u8)((hw1 >> 8) & 0xffu);
    150   b[2] = (u8)(hw2 & 0xffu);
    151   b[3] = (u8)((hw2 >> 8) & 0xffu);
    152   mc_emit_bytes(mc, b, sizeof b);
    153   if (mc->debug) debug_emit_row(mc->debug, mc->section_id, ofs, mc->loc);
    154 }
    155 
    156 static void arm_patch_t32(Arm32NativeTarget* a, u32 pos, u32 instr) {
    157   u8 b[4];
    158   u32 hw1 = (instr >> 16) & 0xffffu, hw2 = instr & 0xffffu;
    159   b[0] = (u8)(hw1 & 0xffu);
    160   b[1] = (u8)((hw1 >> 8) & 0xffu);
    161   b[2] = (u8)(hw2 & 0xffu);
    162   b[3] = (u8)((hw2 >> 8) & 0xffu);
    163   obj_patch(a->base.obj, a->base.mc->section_id, pos, b, 4);
    164 }
    165 
    166 /* Emit PUSH/POP, picking the 16-bit Thumb-1 form (STMDB sp!/LDMIA sp!) when the
    167  * register list fits it (no flags). The 16-bit PUSH allows {r0..r7, lr}; POP
    168  * allows {r0..r7, pc}; any other high register (r8..r12, or lr in a POP / pc in
    169  * a PUSH) forces the 32-bit .W form. The common frame pairs {r7,lr}/{r7,pc} and
    170  * a low-reg callee-save set narrow; mixed high callee-saves stay .W. */
    171 static void arm_emit_push(MCEmitter* mc, u32 reglist) {
    172   u32 low = reglist & 0xffu, hi = reglist & ~0xffu;
    173   if ((hi & ~(1u << 14u)) == 0u) /* only r0..r7 and optionally lr */
    174     arm_emit_t16(mc, arm_push16(low, (hi >> 14) & 1u));
    175   else
    176     arm_emit_t32(mc, arm_push_w(reglist));
    177 }
    178 static void arm_emit_pop(MCEmitter* mc, u32 reglist) {
    179   u32 low = reglist & 0xffu, hi = reglist & ~0xffu;
    180   if ((hi & ~(1u << 15u)) == 0u) /* only r0..r7 and optionally pc */
    181     arm_emit_t16(mc, arm_pop16(low, (hi >> 15) & 1u));
    182   else
    183     arm_emit_t32(mc, arm_pop_w(reglist));
    184 }
    185 
    186 /* ============================ register tables ============================ */
    187 
    188 #define ARM_PHYS_ARG(r, idx)                           \
    189   {.reg = (r),                                         \
    190    .cls = NATIVE_REG_INT,                              \
    191    .abi_index = (idx),                                 \
    192    .flags = NATIVE_REG_CALLER_SAVED | NATIVE_REG_ARG | \
    193             ((idx) < 2u ? NATIVE_REG_RET : 0),         \
    194    .spill_cost = 1u,                                   \
    195    .copy_cost = 1u}
    196 #define ARM_PHYS_CALLEE(r)                                  \
    197   {.reg = (r),                                              \
    198    .cls = NATIVE_REG_INT,                                   \
    199    .abi_index = 0xffu,                                      \
    200    .flags = NATIVE_REG_ALLOCABLE | NATIVE_REG_CALLEE_SAVED, \
    201    .spill_cost = 4u,                                        \
    202    .copy_cost = 1u}
    203 #define ARM_PHYS_RESERVED_ROLE(r, role) \
    204   {.reg = (r),                             \
    205    .cls = NATIVE_REG_INT,                  \
    206    .abi_index = 0xffu,                     \
    207    .flags = NATIVE_REG_RESERVED | (role),  \
    208    .spill_cost = 0u,                       \
    209    .copy_cost = 0u}
    210 #define ARM_PHYS_RESERVED(r) \
    211   ARM_PHYS_RESERVED_ROLE((r), NATIVE_REG_NONE)
    212 
    213 /* NDT (-O0) value-cache pool: the caller-saved arg registers r0..r3, fronted so
    214  * a producer materializes a call's args directly into their ABI registers
    215  * (Lever 1 via api_pack_call_args_in_order). The -O0 cache flushes at every
    216  * call/branch/return, so caller-saved suffices and the prologue needs no
    217  * callee-save spills. */
    218 static const Reg arm_ndt_int_allocable[] = {0u, 1u, 2u, 3u};
    219 static const Reg arm_int_scratch[] = {ARM_SCRATCH};
    220 /* O1 operand temporaries are the caller-saved low registers. They are excluded
    221  * from persistent allocation and every Thumb2 operand hook accepts them. IP
    222  * and LR remain backend-private: many hooks use those two internally, so
    223  * exposing either as an operand location would couple independent lifetimes. */
    224 static const Reg arm_int_emit_temps[] = {0u, 1u, 2u, 3u};
    225 static const Reg arm_int_asm_temps[] = {ARM_TMP};
    226 static const Reg arm_direct_asm_int[] = {0u, 1u, 2u, 3u, 4u, 5u,
    227                                          6u, 8u, 9u, 10u, 11u};
    228 
    229 static const NativePhysRegInfo arm_int_phys[] = {
    230     ARM_PHYS_ARG(0u, 0u),  ARM_PHYS_ARG(1u, 1u),
    231     ARM_PHYS_ARG(2u, 2u),  ARM_PHYS_ARG(3u, 3u),
    232     ARM_PHYS_CALLEE(4u),   ARM_PHYS_CALLEE(5u),
    233     ARM_PHYS_CALLEE(6u),
    234     ARM_PHYS_RESERVED_ROLE(7u, NATIVE_REG_CALLEE_SAVED), /* fp */
    235     ARM_PHYS_CALLEE(8u),   ARM_PHYS_CALLEE(9u),
    236     ARM_PHYS_CALLEE(10u),  ARM_PHYS_CALLEE(11u),
    237     ARM_PHYS_RESERVED_ROLE(12u, NATIVE_REG_CALLER_SAVED), /* ip/temp */
    238     ARM_PHYS_RESERVED(13u),                        /* sp */
    239     ARM_PHYS_RESERVED(14u),                        /* lr */
    240     ARM_PHYS_RESERVED(15u),                        /* pc */
    241 };
    242 
    243 static int arm_resolve_name(const NativeRegInfo* ri, Slice name, Reg* out,
    244                             NativeAllocClass* cls_out) {
    245   char buf[16];
    246   uint32_t dwarf;
    247   (void)ri;
    248   if (!name.s || !name.len || name.len >= sizeof buf) return 1;
    249   memcpy(buf, name.s, name.len);
    250   buf[name.len] = '\0';
    251   if (arm32_register_index(buf, &dwarf) != 0) return 1;
    252   if (dwarf <= 15u) {
    253     *cls_out = NATIVE_REG_INT;
    254     *out = (Reg)dwarf;
    255     return 0;
    256   }
    257   return 1;
    258 }
    259 
    260 static int arm_asm_operand_reg_ok(const NativeRegInfo* ri, NativeAllocClass cls,
    261                                   Reg reg) {
    262   (void)ri;
    263   if (cls != NATIVE_REG_INT) return 0;
    264   if (reg <= 3u) return 1;            /* r0..r3 */
    265   if (reg >= 4u && reg <= 11u && reg != 7u) return 1; /* r4..r11 except fp */
    266   return 0;
    267 }
    268 
    269 static int arm_asm_constraint_reg(const NativeRegInfo* ri, const char* body,
    270                                   NativeAllocClass* cls_out, Reg* fixed_out,
    271                                   u32* allowed_mask_out) {
    272   (void)ri;
    273   if (!body || !body[0]) return 0;
    274   if (fixed_out) *fixed_out = REG_NONE;
    275   if (allowed_mask_out) *allowed_mask_out = 0;
    276   if ((body[0] == 'r' || body[0] == 'l') && body[1] == '\0') {
    277     if (cls_out) *cls_out = NATIVE_REG_INT;
    278     if (body[0] == 'l' && allowed_mask_out)
    279       *allowed_mask_out = 0x000000ffu; /* r0..r7, filtered by reg_ok. */
    280     return 1;
    281   }
    282   return 0;
    283 }
    284 
    285 static const NativeAllocClassInfo arm_classes[] = {
    286     {.cls = NATIVE_REG_INT,
    287      .ndt_allocable = arm_ndt_int_allocable,
    288      .ndt_allocable_count =
    289          sizeof arm_ndt_int_allocable / sizeof arm_ndt_int_allocable[0],
    290      .scratch = arm_int_scratch,
    291      .nscratch = sizeof arm_int_scratch / sizeof arm_int_scratch[0],
    292      .emit_temps = arm_int_emit_temps,
    293      .nemit_temps = sizeof arm_int_emit_temps / sizeof arm_int_emit_temps[0],
    294      .asm_temps = arm_int_asm_temps,
    295      .nasm_temps = sizeof arm_int_asm_temps / sizeof arm_int_asm_temps[0],
    296      .direct_asm_allocable = arm_direct_asm_int,
    297      .ndirect_asm_allocable =
    298          sizeof arm_direct_asm_int / sizeof arm_direct_asm_int[0],
    299      .emit_cache_mask = 0x0000000fu, /* r0..r3; hooks use private ip/lr */
    300      .phys = arm_int_phys,
    301      .nphys = sizeof arm_int_phys / sizeof arm_int_phys[0]},
    302 };
    303 
    304 static const NativeRegInfo arm_reg_info = {
    305     .classes = arm_classes,
    306     .nclasses = sizeof arm_classes / sizeof arm_classes[0],
    307     .ndt_caller_saved_only = 1u,
    308     .ndt_result_reg_stable = 1u,
    309     .resolve_name = arm_resolve_name,
    310     .asm_operand_reg_ok = arm_asm_operand_reg_ok,
    311     .asm_constraint_reg = arm_asm_constraint_reg,
    312 };
    313 
    314 /* ============================ legality ============================ */
    315 
    316 /* Resolve `rd = ra <ADD|SUB> imm` into a concrete non-negative (is_sub, mag)
    317  * immediate. Adding `imm` equals subtracting `-imm` and vice versa (modulo
    318  * 2^32), so a small negative constant folds into the opposite operation: SUB #3
    319  * for `+ -3`, ADD #3 for `- -3`. Returns 1 (and fills is_sub/mag) when the
    320  * magnitude is a Thumb modified-immediate OR fits the 12-bit ADDW/SUBW field;
    321  * 0 when the constant must be materialized into a register first. The emit path
    322  * (arm_emit_alu_imm) and the legality predicate (arm_imm_legal) BOTH route
    323  * through this, so a value can never be accepted as legal yet mis-encoded —
    324  * the bug where `SUB #-3` legalized via the -imm trick but emitted SUBW with a
    325  * 12-bit-truncated 0xFFFFFFFD. */
    326 static int arm_resolve_addsub_imm(BinOp op, i64 imm, int* is_sub, u32* mag) {
    327   /* k = the value actually added to ra (subtracting imm adds -imm). */
    328   u32 k = (u32)(op == BO_ISUB ? (u64)0 - (u64)imm : (u64)imm);
    329   u32 nk = (u32)((u64)0 - (u64)k); /* the magnitude of the SUB alternative */
    330   u32 enc;
    331   if (thumb_expand_imm_encode(k, &enc) || k <= 0xfffu) {
    332     *is_sub = 0;
    333     *mag = k;
    334     return 1;
    335   }
    336   if (thumb_expand_imm_encode(nk, &enc) || nk <= 0xfffu) {
    337     *is_sub = 1;
    338     *mag = nk;
    339     return 1;
    340   }
    341   return 0;
    342 }
    343 
    344 static int arm_imm_legal(NativeTarget* t, NativeImmUse use, u32 op,
    345                          KitCgTypeId type, i64 imm) {
    346   u32 enc;
    347   (void)t;
    348   (void)type;
    349   switch (use) {
    350     case NATIVE_IMM_MOVE:
    351       return 1; /* MOVW/MOVT materializes any 32-bit value */
    352     case NATIVE_IMM_CMP:
    353       return thumb_expand_imm_encode((u32)imm, &enc);
    354     case NATIVE_IMM_BINOP:
    355       switch ((BinOp)op) {
    356         case BO_IADD:
    357         case BO_ISUB: {
    358           int is_sub;
    359           u32 mag;
    360           return arm_resolve_addsub_imm((BinOp)op, imm, &is_sub, &mag);
    361         }
    362         case BO_AND:
    363         case BO_OR:
    364         case BO_XOR:
    365           return thumb_expand_imm_encode((u32)imm, &enc);
    366         case BO_SHL:
    367         case BO_SHR_S:
    368         case BO_SHR_U:
    369           return imm >= 0 && imm < 32;
    370         default:
    371           return 0;
    372       }
    373     case NATIVE_IMM_ADDR_OFFSET:
    374       return imm >= -255 && imm <= 4095;
    375   }
    376   return 0;
    377 }
    378 
    379 static int arm_addr_legal(NativeTarget* t, const NativeAddr* addr,
    380                           MemAccess mem) {
    381   (void)t;
    382   (void)mem;
    383   if (!addr) return 0;
    384   /* Base + 12-bit positive or 8-bit signed offset, no scaled index in v1. */
    385   if (addr->base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) return 0;
    386   if (addr->index_kind != NATIVE_ADDR_INDEX_NONE) return 0;
    387   return addr->offset >= -255 && addr->offset <= 4095;
    388 }
    389 
    390 /* Capability hook for the arch-neutral L7 shift-into-ALU fold
    391  * (src/opt/pass_combine.c). arm32 has the 32-bit shifted-register forms wired up
    392  * (arm_binop's register path emits ADD.W/SUB.W/AND.W/ORR.W/EOR.W rd,rn,rm,LSL #k
    393  * via the arm_*_reg_lsl encoders), but the capability is left OFF here.
    394  *
    395  * WHY OFF: opting in surfaces a latent miscompile in the shared combine pass for
    396  * the `base + (i << k)` address pattern when k exceeds the address index-scale
    397  * range (k>=4, e.g. a 16-byte struct stride). The order is: addr-synth's index
    398  * fold (pass_combine.c rule (b)) only absorbs a shift of 1..3 into the EA's
    399  * log2_scale; for k=4 it declines, leaving the `i<<4` as an explicit IADD that
    400  * computes the element address. L7 then stamps a shift rider on that IADD's rhs.
    401  * On a later combine iteration, addr-synth rule (a) (reg+reg) folds that IADD
    402  * into the indirect's index but copies only rhs.v.reg, DROPPING rhs.shift — so
    403  * the `*16` silently becomes `*1` and every element aliases element 0
    404  * (test/parse/cases/opt_03_addr_index_alias_store regresses 42 -> 1 under qemu).
    405  * aa64 avoids this because its addr_legal accepts the scaled-index EA, so
    406  * addr-synth claims the address shift before L7 ever sees it; arm32's addr_legal
    407  * rejects every indexed address (no scaled-index load/store in v1), so the
    408  * shift stays in the ALU lane and the rider-drop bites. Re-enable once the
    409  * shared pass preserves rhs.shift across the rule-(a) index fold; the encoders +
    410  * arm_binop branch + decode round-trip test are already in place. */
    411 static int arm_can_fold_shift_into_alu(NativeTarget* t) {
    412   (void)t;
    413   return 0;
    414 }
    415 
    416 /* ============================ 16-bit narrow helpers ============================
    417  * The 16-bit Thumb-1 data-processing forms used by the density pass always set
    418  * the condition flags (there is no S=0 narrow DP form outside an IT block). kit
    419  * keeps flags live only across CMP→IT (and comparisons always re-emit their
    420  * CMP), so binop/move/shift/ldst results are FLAG-DEAD — narrowing them to a
    421  * flag-setting 16-bit encoding is observationally identical. The MOV.W sites
    422  * that sit between a CMP and an IT-predicated consumer (arm_cmp, arm_unop's
    423  * UO_NOT, arm_emit_setcc) are NOT routed through here and stay 32-bit. */
    424 static inline int arm_low_reg(u32 r) { return r <= 7u; }
    425 /* True when an unsigned immediate fits an n-bit field. */
    426 static inline int arm_uimm_fits(u32 v, u32 nbits) { return v < (1u << nbits); }
    427 
    428 /* ============================ memory ============================ */
    429 
    430 static void arm_emit_load_u32(Arm32NativeTarget* a, u32 rd, u32 v);
    431 static u32 arm_pick_scratch(Arm32NativeTarget* a, u32 rt, u32 pinned);
    432 
    433 /* Add a signed compile-time constant to rd in place (rd += k), using a reserved
    434  * scratch only for a magnitude exceeding the inline immediate forms. */
    435 static void arm_emit_add_const(Arm32NativeTarget* a, u32 rd, i64 k) {
    436   MCEmitter* mc = a->base.mc;
    437   u32 enc, mag;
    438   if (k == 0) return;
    439   mag = (u32)(k < 0 ? -k : k);
    440   /* 16-bit `ADD/SUB sp,sp,#imm7*4` (no flags) for the SP-adjust forms (e.g. the
    441    * variadic GP-save reclaim). Requires rd==sp, a non-zero multiple of 4, and
    442    * imm7 in range. */
    443   if (rd == 13u && (mag & 3u) == 0u && (mag >> 2) <= 0x7fu) {
    444     arm_emit_t16(mc, k < 0 ? arm_sub_sp_sp_imm16(mag >> 2)
    445                            : arm_add_sp_sp_imm16(mag >> 2));
    446     return;
    447   }
    448   if (mag <= 0xfffu) {
    449     arm_emit_t32(mc, k < 0 ? arm_sub_imm12(rd, rd, mag)
    450                            : arm_add_imm12(rd, rd, mag));
    451   } else if (thumb_expand_imm_encode(mag, &enc)) {
    452     arm_emit_t32(mc, arm_dp_imm(k < 0 ? 13u : 8u, 0u, rd, rd, enc));
    453   } else {
    454     u32 s = arm_pick_scratch(a, rd, 0u);
    455     arm_emit_load_u32(a, s, mag);
    456     arm_emit_t32(mc, k < 0 ? arm_sub_reg(rd, rd, s) : arm_add_reg(rd, rd, s));
    457   }
    458 }
    459 
    460 /* Materialize the runtime address of `sym` (+ addend) into register `rd`.
    461  *
    462  * arm32's house strategy is MOVW/MOVT absolute (Cortex-M has no literal pools
    463  * and the bare-metal lane is non-PIC):
    464  *     MOVW rd, #:lower16:sym    ; R_ARM_THM_MOVW_ABS_NC -> S[15:0]
    465  *     MOVT rd, #:upper16:sym    ; R_ARM_THM_MOVT_ABS    -> S[31:16]
    466  * A constant byte `addend` is applied as a SEPARATE add, NOT folded into the
    467  * reloc: ARM uses REL relocations (the addend rides in the relocated field), and
    468  * the MOVW/MOVT pair splits a 32-bit value across two 16-bit fields whose
    469  * per-half (S+A) computation is carry-lossy across the 16-bit boundary. Keeping
    470  * the reloc addend 0 and adding the offset here is both always-correct and the
    471  * contract elf_arm_reloc_field_addend documents (and ld.lld-compatible). */
    472 static void arm_emit_global_addr(Arm32NativeTarget* a, u32 rd, ObjSymId sym,
    473                                  i64 addend) {
    474   MCEmitter* mc = a->base.mc;
    475   u32 pos = mc_pos(mc);
    476   arm_emit_t32(mc, arm_movw(rd, 0u));
    477   mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_MOVW_ABS_NC, sym, 0, 1, 0);
    478   pos = mc_pos(mc);
    479   arm_emit_t32(mc, arm_movt(rd, 0u));
    480   mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_MOVT_ABS, sym, 0, 1, 0);
    481   arm_emit_add_const(a, rd, addend);
    482 }
    483 
    484 /* The unified memory-access address layer (ARM32.md §1) lives here. Every
    485  * load/store/copy resolves a NativeAddr to a legal (base register, in-range
    486  * displacement) through ONE function, arm_resolve_mem, with ONE temporary-
    487  * reservation invariant asserted in one place.
    488  *
    489  * Backend budget: IP (r12, ARM_SCRATCH) and LR (r14, ARM_TMP) are reserved for
    490  * hook-internal staging; they are deliberately not O1 operand locations. r7
    491  * (FP) holds the frame anchor and is never temporary storage. A single access
    492  * needs at most one address-staging register, distinct from transfer register
    493  * `rt`. The picker
    494  * draws it from {IP,LR} minus {rt} minus a caller `pinned` mask (held bases the
    495  * access must preserve) and panics if none is free, so the invariant is explicit
    496  * rather than relying on incidental liveness. */
    497 static void arm_emit_base_off(Arm32NativeTarget* a, u32 rd, u32 base, i32 off);
    498 static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr);
    499 
    500 /* True if every byte of a `span`-byte access at signed displacement `off` fits
    501  * the Thumb-2 single-granule immediate forms (T4 negative -255..0, T3 positive
    502  * 0..4095): off >= -255 and off+span <= 4096 implies every granule offset in
    503  * [off, off+span-1] lands in [-255, 4095]. */
    504 static int arm_span_inline(i32 off, u32 span) {
    505   return off >= -255 && off + (i32)span <= 4096;
    506 }
    507 
    508 /* Pick a reserved staging register from {LR, IP} that is neither the transfer
    509  * register `rt` (0xff = none) nor in `pinned` (held base registers the access
    510  * must preserve). LR (ARM_TMP) is preferred because IP (ARM_SCRATCH) is the NDT's
    511  * operand-materialization scratch and can hold a live value across the access
    512  * (e.g. a store whose value the NDT homed in IP); LR is only ever a transient
    513  * backend temp within a single emit, so staging an address there is free. The
    514  * single point that enforces the scratch invariant. */
    515 static u32 arm_pick_scratch(Arm32NativeTarget* a, u32 rt, u32 pinned) {
    516   u32 avoid = (rt < 16u ? (1u << rt) : 0u) | pinned;
    517   if (!(avoid & (1u << ARM_TMP))) return ARM_TMP;
    518   if (!(avoid & (1u << ARM_SCRATCH))) return ARM_SCRATCH;
    519   arm_panic(a, "memory access: no free address-staging scratch");
    520 }
    521 
    522 typedef struct ArmMemAddr {
    523   u32 base; /* base register to address through */
    524   i32 off;  /* signed displacement; access [base, #off (+lane/granule)] */
    525 } ArmMemAddr;
    526 
    527 /* Materialize the unsigned 32-bit constant `v` into `rd` (MOVW + optional MOVT).
    528  * Used to stage large frame/memory offsets and SUB-sp amounts that exceed the
    529  * inline immediate forms. */
    530 static void arm_emit_load_u32(Arm32NativeTarget* a, u32 rd, u32 v) {
    531   MCEmitter* mc = a->base.mc;
    532   u32 enc;
    533   if (thumb_expand_imm_encode(v, &enc)) {
    534     arm_emit_t32(mc, arm_mov_imm(rd, enc));
    535     return;
    536   }
    537   if (thumb_expand_imm_encode(~v, &enc)) {
    538     arm_emit_t32(mc, arm_mvn_imm(rd, enc));
    539     return;
    540   }
    541   arm_emit_t32(mc, arm_movw(rd, v & 0xffffu));
    542   if ((v >> 16) != 0u) arm_emit_t32(mc, arm_movt(rd, (v >> 16) & 0xffffu));
    543 }
    544 
    545 /* Resolve `addr` for a `span`-byte access whose transfer register is `rt`
    546  * (0xff = none; the wide path pins both lane registers via `pinned` instead),
    547  * returning a legal (base, off). A FRAME/REG base already in range is used
    548  * directly; otherwise the full effective address is materialized once into a
    549  * picked scratch (off := 0). A GLOBAL base is always materialized via MOVW/MOVT,
    550  * folding addr.offset into the reloc addend so off := 0 — this is the single
    551  * point that guarantees a store's global address never lands in the value
    552  * register (the i64-store-to-global miscompile). */
    553 static ArmMemAddr arm_resolve_mem(Arm32NativeTarget* a, const NativeAddr* addr,
    554                                   u32 rt, u32 span, u32 pinned) {
    555   ArmMemAddr r;
    556   /* Scaled-index address (base + offset + index<<scale): materialize the whole
    557    * effective address once into a reserved scratch and return (scratch, 0).
    558    * arm32 has no LDRD/STRD indexed form, so a wide (8-byte) indexed access must
    559    * share ONE computed base across both lanes; doing it here keeps every base
    560    * kind (frame / reg / global) uniform and is the §1 single-base guarantee for
    561    * the high lane. The scratch aliases neither the transfer register(s) — rt and
    562    * `pinned` — nor the index register. */
    563   if (addr->index_kind == NATIVE_ADDR_INDEX_REG) {
    564     MCEmitter* mc = a->base.mc;
    565     u32 idx = addr->index.reg & 0xfu;
    566     u32 s = arm_pick_scratch(a, rt, pinned | (1u << idx));
    567     switch (addr->base_kind) {
    568       case NATIVE_ADDR_BASE_FRAME: {
    569         NativeFrameSlotEntry* e =
    570             native_frame_slot_at(&a->frame, addr->base.frame);
    571         arm_emit_base_off(a, s, ARM_FP, -(i32)e->off + addr->offset);
    572         break;
    573       }
    574       case NATIVE_ADDR_BASE_REG:
    575         arm_emit_base_off(a, s, addr->base.reg & 0xfu, addr->offset);
    576         break;
    577       case NATIVE_ADDR_BASE_GLOBAL:
    578         arm_emit_global_addr(a, s, addr->base.global.sym,
    579                              addr->base.global.addend + addr->offset);
    580         break;
    581       default:
    582         arm_panic(a, "unsupported indexed addressing mode");
    583     }
    584     arm_emit_t32(mc, arm_add_reg_lsl(s, s, idx, addr->log2_scale));
    585     r.base = s;
    586     r.off = 0;
    587     return r;
    588   }
    589   switch (addr->base_kind) {
    590     case NATIVE_ADDR_BASE_FRAME: {
    591       NativeFrameSlotEntry* s =
    592           native_frame_slot_at(&a->frame, addr->base.frame);
    593       i32 off = -(i32)s->off + addr->offset;
    594       if (arm_span_inline(off, span)) {
    595         r.base = ARM_FP;
    596         r.off = off;
    597         return r;
    598       }
    599       r.base = arm_pick_scratch(a, rt, pinned | (1u << ARM_FP));
    600       arm_emit_base_off(a, r.base, ARM_FP, off);
    601       r.off = 0;
    602       return r;
    603     }
    604     case NATIVE_ADDR_BASE_REG: {
    605       u32 base = addr->base.reg & 0xfu;
    606       if (arm_span_inline(addr->offset, span)) {
    607         r.base = base;
    608         r.off = addr->offset;
    609         return r;
    610       }
    611       r.base = arm_pick_scratch(a, rt, pinned | (1u << base));
    612       arm_emit_base_off(a, r.base, base, addr->offset);
    613       r.off = 0;
    614       return r;
    615     }
    616     case NATIVE_ADDR_BASE_GLOBAL:
    617       r.base = arm_pick_scratch(a, rt, pinned);
    618       arm_emit_global_addr(a, r.base, addr->base.global.sym,
    619                            addr->base.global.addend + addr->offset);
    620       r.off = 0;
    621       return r;
    622     default:
    623       arm_panic(a, "unsupported addressing mode");
    624   }
    625 }
    626 
    627 /* Emit one in-range granule access: LDR/STR (+B/H) of `rt` at [base, #off],
    628  * dispatching width. The caller (via arm_resolve_mem) guarantees `off` is in the
    629  * T3/T4 immediate range for this width. Loads zero-extend; signed narrowing is a
    630  * separate convert. */
    631 static void arm_emit_mem_one(Arm32NativeTarget* a, int is_load, u32 rt, u32 base,
    632                              i32 off, u32 size) {
    633   MCEmitter* mc = a->base.mc;
    634   u32 t3hw1, t4hw1;
    635   /* 16-bit narrow forms (no flags, biggest -O0 win since loads/stores dominate):
    636    *   T2 [sp, #imm8*4]: word only, rt low reg, base == sp (NOT r7 — the frame is
    637    *     r7-anchored, so this applies only to genuine sp-based slots).
    638    *   T1 [rn, #imm5*scale]: rt + rn both low regs (r7/ARM_FP qualifies), positive
    639    *     offset that is an exact multiple of the access size and fits 5 bits.
    640    * Signed loads (LDRSB/LDRSH) have no 16-bit immediate-offset form, so the
    641    * width dispatch below only narrows the unsigned word/byte/half loads + stores;
    642    * the resolver never asks this function for a signed-narrowing load (that is a
    643    * separate convert). */
    644   if (size == 4u && base == ARM_SP && arm_low_reg(rt) && off >= 0 &&
    645       (off & 3) == 0 && arm_uimm_fits((u32)off >> 2, 8u)) {
    646     arm_emit_t16(mc, arm_ldst_sp_16(is_load ? 0x9800u : 0x9000u, rt,
    647                                     (u32)off >> 2));
    648     return;
    649   }
    650   if (arm_low_reg(rt) && arm_low_reg(base) && off >= 0) {
    651     u32 uoff = (u32)off;
    652     if (size == 4u && (uoff & 3) == 0 && arm_uimm_fits(uoff >> 2, 5u)) {
    653       arm_emit_t16(mc, arm_ldst_i5_16(is_load ? 0x6800u : 0x6000u, rt, base,
    654                                       uoff >> 2));
    655       return;
    656     }
    657     if (size == 2u && (uoff & 1) == 0 && arm_uimm_fits(uoff >> 1, 5u)) {
    658       arm_emit_t16(mc, arm_ldst_i5_16(is_load ? 0x8800u : 0x8000u, rt, base,
    659                                       uoff >> 1));
    660       return;
    661     }
    662     if (size == 1u && arm_uimm_fits(uoff, 5u)) {
    663       arm_emit_t16(mc, arm_ldst_i5_16(is_load ? 0x7800u : 0x7000u, rt, base,
    664                                       uoff));
    665       return;
    666     }
    667   }
    668   if (size == 1u) {
    669     t3hw1 = is_load ? 0xf890u : 0xf880u;
    670     t4hw1 = is_load ? 0xf810u : 0xf800u;
    671   } else if (size == 2u) {
    672     t3hw1 = is_load ? 0xf8b0u : 0xf8a0u;
    673     t4hw1 = is_load ? 0xf830u : 0xf820u;
    674   } else {
    675     t3hw1 = is_load ? 0xf8d0u : 0xf8c0u;
    676     t4hw1 = is_load ? 0xf850u : 0xf840u;
    677   }
    678   if (off >= 0 && off <= 4095) {
    679     arm_emit_t32(mc, arm_t32(t3hw1 | base, (rt << 12) | (u32)off));
    680   } else if (off < 0 && off >= -255) {
    681     arm_emit_t32(mc, arm_ldst_t4(t4hw1, rt, base, (u32)(-off), 0u));
    682   } else {
    683     arm_panic(a, "arm_emit_mem_one: offset out of range (resolver bug)");
    684   }
    685 }
    686 
    687 /* Emit a load (is_load=1) or store of `reg` at `addr`, dispatching width from
    688  * mem.size. A wide (8-byte i64/double) access splits into two 4-byte lanes over
    689  * the register pair rt:rt+1 sharing ONE resolved base — the high lane's address
    690  * is thus computed correctly for frame/reg/global uniformly. */
    691 static void arm_emit_mem(Arm32NativeTarget* a, int is_load, NativeLoc reg,
    692                          NativeAddr addr, MemAccess mem) {
    693   u32 rt = loc_reg(reg);
    694   u32 size = mem.size ? mem.size : native_type_size(&a->base, reg.type);
    695   if (size == 0u) arm_panic(a, "zero-width memory access");
    696   if (size > 8u) arm_panic(a, "memory access wider than 8 bytes");
    697   if (size == 8u) {
    698     u32 hi = (rt + 1u) & 0xfu;
    699     ArmMemAddr r =
    700         arm_resolve_mem(a, &addr, 0xffu, 8u, (1u << rt) | (1u << hi));
    701     arm_emit_mem_one(a, is_load, rt, r.base, r.off, 4u);
    702     arm_emit_mem_one(a, is_load, hi, r.base, r.off + 4, 4u);
    703     return;
    704   }
    705   if (size != 1u && size != 2u && size != 4u) {
    706     NativePartChunkIter it = native_part_chunks(size, 4u);
    707     KitCgTypeId lane_type = builtin_id(KIT_CG_BUILTIN_I32);
    708     u32 carrier_mask = 1u << rt;
    709     u32 addr_inputs = 0u;
    710     u32 chunk_off, chunk_size;
    711     u32 tmp = REG_NONE;
    712     ArmMemAddr r;
    713     if (size > 4u) carrier_mask |= 1u << ((rt + 1u) & 0xfu);
    714     if (addr.base_kind == NATIVE_ADDR_BASE_REG)
    715       addr_inputs |= 1u << (addr.base.reg & 0xfu);
    716     if (addr.index_kind == NATIVE_ADDR_INDEX_REG)
    717       addr_inputs |= 1u << (addr.index.reg & 0xfu);
    718     if (is_load && (carrier_mask & addr_inputs)) {
    719       /* A single LDR may use the destination as its base (`ldr r0, [r0]`).
    720        * An exact part is a sequence, so doing that would destroy the address
    721        * before its later chunks. Retain the complete effective address in a
    722        * private register first; avoid every address input because load_addr
    723        * still needs them while it constructs the indexed form. */
    724       u32 addr_reg = arm_pick_scratch(a, 0xffu, carrier_mask | addr_inputs);
    725       NativeLoc addr_loc =
    726           native_loc_reg(lane_type, NATIVE_REG_INT, (Reg)addr_reg);
    727       arm_load_addr(&a->base, addr_loc, addr);
    728       r.base = addr_reg;
    729       r.off = 0;
    730     } else {
    731       /* Resolve once for the full span. Besides avoiding repeated global/far
    732        * address formation, this gives every chunk one stable base and makes
    733        * scratch ownership explicit: a later pack temporary must avoid r.base. */
    734       r = arm_resolve_mem(a, &addr, 0xffu, size, carrier_mask);
    735     }
    736     /* ABIArgPart.size is exact even when the carrier is a 32-bit register.
    737      * Split 3/5/6/7-byte parts into bounded 4/2/1-byte accesses and pack each
    738      * chunk into the appropriate low/high carrier lane. This boundary is also
    739      * used by generic call-result moves, so no caller can silently widen a
    740      * semantic part into an adjacent frame object. */
    741     while (native_part_chunk_next(&it, &chunk_off, &chunk_size)) {
    742       u32 lane_reg = (rt + chunk_off / 4u) & 0xfu;
    743       u32 lane_shift = (chunk_off & 3u) * 8u;
    744       u32 transfer_reg = lane_reg;
    745       if (lane_shift) {
    746         if (tmp == REG_NONE)
    747           tmp = arm_pick_scratch(a, 0xffu,
    748                                  carrier_mask | (1u << (r.base & 0xfu)));
    749         transfer_reg = tmp;
    750         if (!is_load)
    751           arm_emit_t32(a->base.mc,
    752                        arm_shift_imm(1u, tmp, lane_reg, lane_shift));
    753       }
    754       arm_emit_mem_one(a, is_load, transfer_reg, r.base,
    755                        r.off + (i32)chunk_off, chunk_size);
    756       if (is_load && lane_shift)
    757         arm_emit_t32(a->base.mc,
    758                      arm_orr_reg_lsl(lane_reg, lane_reg, tmp, lane_shift));
    759     }
    760     return;
    761   }
    762   {
    763     ArmMemAddr r = arm_resolve_mem(a, &addr, rt, size, 0u);
    764     arm_emit_mem_one(a, is_load, rt, r.base, r.off, size);
    765   }
    766 }
    767 
    768 /* ============================ moves / immediates ============================ */
    769 
    770 static void arm_load_imm(NativeTarget* t, NativeLoc dst, i64 imm) {
    771   MCEmitter* mc = t->mc;
    772   u32 rd = loc_reg(dst);
    773   u32 v = (u32)imm;
    774   u32 enc;
    775   /* MOVS rd,#imm8 (low reg, 0..255) — flag-dead 16-bit immediate move. This is
    776    * the general NDT load_imm hook, never the CMP→IT flag-preserving MOV.W (those
    777    * sites call arm_mov_imm directly), so setting flags here is observationally
    778    * inert. */
    779   if (arm_low_reg(rd) && v <= 0xffu) {
    780     arm_emit_t16(mc, arm_movs_imm8(rd, v));
    781     return;
    782   }
    783   if (thumb_expand_imm_encode(v, &enc)) {
    784     arm_emit_t32(mc, arm_mov_imm(rd, enc));
    785     return;
    786   }
    787   if (thumb_expand_imm_encode(~v, &enc)) {
    788     arm_emit_t32(mc, arm_mvn_imm(rd, enc));
    789     return;
    790   }
    791   arm_emit_t32(mc, arm_movw(rd, v & 0xffffu));
    792   if ((v >> 16) != 0u) arm_emit_t32(mc, arm_movt(rd, (v >> 16) & 0xffffu));
    793 }
    794 
    795 static void arm_move(NativeTarget* t, NativeLoc dst, NativeLoc src) {
    796   u32 rd = loc_reg(dst), rs = loc_reg(src);
    797   if (rd == rs) return;
    798   arm_emit_t16(t->mc, arm_mov_hi(rd, rs));
    799 }
    800 
    801 static void arm_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
    802                      MemAccess mem) {
    803   arm_emit_mem(arm_of(t), 1, dst, addr, mem);
    804 }
    805 static void arm_store(NativeTarget* t, NativeAddr addr, NativeLoc src,
    806                       MemAccess mem) {
    807   arm_emit_mem(arm_of(t), 0, src, addr, mem);
    808 }
    809 static void arm_spill(NativeTarget* t, NativeLoc src, NativeFrameSlot slot,
    810                       MemAccess mem) {
    811   NativeAddr addr;
    812   memset(&addr, 0, sizeof addr);
    813   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    814   addr.base.frame = slot;
    815   addr.base_type = src.type;
    816   arm_emit_mem(arm_of(t), 0, src, addr, mem);
    817 }
    818 static void arm_reload(NativeTarget* t, NativeLoc dst, NativeFrameSlot slot,
    819                        MemAccess mem) {
    820   NativeAddr addr;
    821   memset(&addr, 0, sizeof addr);
    822   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    823   addr.base.frame = slot;
    824   addr.base_type = dst.type;
    825   arm_emit_mem(arm_of(t), 1, dst, addr, mem);
    826 }
    827 
    828 /* Materialize a full address into `rd`: base + signed byte offset. */
    829 static void arm_emit_base_off(Arm32NativeTarget* a, u32 rd, u32 base, i32 off) {
    830   MCEmitter* mc = a->base.mc;
    831   u32 enc;
    832   if (off == 0) {
    833     if (rd != base) arm_emit_t16(mc, arm_mov_hi(rd, base));
    834   } else if (off > 0 && off <= 0xfff) {
    835     arm_emit_t32(mc, arm_add_imm12(rd, base, (u32)off));
    836   } else if (off < 0 && thumb_expand_imm_encode((u32)(-off), &enc)) {
    837     arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, base, enc)); /* SUB rd, base, #-off */
    838   } else {
    839     /* Large frame/array offset: stage |off| in a temp then add/sub into rd. The
    840      * temp must not alias `base` (read after write). Use rd itself when rd !=
    841      * base (the resolver guarantees a staged base reg differs from its source);
    842      * only when rd aliases base (load_addr's address-of-an-indexed-REG case)
    843      * fall back to the reserved ARM_TMP, which is dead in that context. */
    844     u32 tmp = (rd != base) ? rd : arm_pick_scratch(a, 0xffu, 1u << base);
    845     arm_emit_load_u32(a, tmp, (u32)(off < 0 ? -off : off));
    846     arm_emit_t32(mc, off < 0 ? arm_sub_reg(rd, base, tmp)
    847                              : arm_add_reg(rd, base, tmp));
    848   }
    849 }
    850 
    851 /* Materialize the address of a NativeAddr into the destination register.
    852  *
    853  * The NDT routes a base+index (or any addr the addr_legal hook rejects)
    854  * through here and then treats `rd` as a plain BASE_REG, so this MUST fold any
    855  * scaled index: rd = base + offset + (index << log2_scale). A GLOBAL base's
    856  * byte offset folds into the MOVW/MOVT reloc addend; FRAME/REG bases add the
    857  * offset explicitly. `rd` is a fresh NDT scratch, distinct from the pinned
    858  * index register, so no aliasing post-add is needed. */
    859 static void arm_load_addr(NativeTarget* t, NativeLoc dst, NativeAddr addr) {
    860   Arm32NativeTarget* a = arm_of(t);
    861   MCEmitter* mc = t->mc;
    862   u32 rd = loc_reg(dst);
    863   if (addr.base_kind == NATIVE_ADDR_BASE_GLOBAL) {
    864     arm_emit_global_addr(a, rd, addr.base.global.sym,
    865                          addr.base.global.addend + addr.offset);
    866   } else if (addr.base_kind == NATIVE_ADDR_BASE_FRAME) {
    867     NativeFrameSlotEntry* s = native_frame_slot_at(&a->frame, addr.base.frame);
    868     arm_emit_base_off(a, rd, ARM_FP, -(i32)s->off + addr.offset);
    869   } else if (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
    870     NativeAddr load;
    871     NativeLoc value;
    872     MemAccess mem;
    873     if (!addr.base_type)
    874       arm_panic(a, "frame-value base has no exact type");
    875     memset(&load, 0, sizeof load);
    876     load.base_kind = NATIVE_ADDR_BASE_FRAME;
    877     load.base.frame = addr.base.frame;
    878     load.base_type = addr.base_type;
    879     memset(&value, 0, sizeof value);
    880     value.kind = NATIVE_LOC_REG;
    881     value.cls = NATIVE_REG_INT;
    882     value.type = addr.base_type;
    883     value.v.reg = rd;
    884     memset(&mem, 0, sizeof mem);
    885     mem.type = value.type;
    886     mem = native_mem_for_type(t, value.type, 0);
    887     arm_emit_mem(a, 1, value, load, mem);
    888     arm_emit_base_off(a, rd, rd, addr.offset);
    889   } else if (addr.base_kind == NATIVE_ADDR_BASE_REG) {
    890     arm_emit_base_off(a, rd, addr.base.reg & 0xfu, addr.offset);
    891   } else {
    892     arm_panic(a, "unsupported addressing mode in load_addr");
    893   }
    894   if (addr.index_kind == NATIVE_ADDR_INDEX_REG) {
    895     u32 idx = addr.index.reg & 0xfu;
    896     arm_emit_t32(mc, arm_add_reg_lsl(rd, rd, idx, addr.log2_scale));
    897   } else if (addr.index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) {
    898     NativeAddr load;
    899     NativeLoc value;
    900     MemAccess mem;
    901     u32 idx = arm_pick_scratch(a, rd, 1u << rd);
    902     if (!addr.index_type)
    903       arm_panic(a, "frame-value index has no exact type");
    904     memset(&load, 0, sizeof load);
    905     load.base_kind = NATIVE_ADDR_BASE_FRAME;
    906     load.base.frame = addr.index.frame;
    907     load.base_type = addr.index_type;
    908     memset(&value, 0, sizeof value);
    909     value.kind = NATIVE_LOC_REG;
    910     value.cls = NATIVE_REG_INT;
    911     value.type = addr.index_type;
    912     value.v.reg = idx;
    913     memset(&mem, 0, sizeof mem);
    914     mem.type = value.type;
    915     mem = native_mem_for_type(t, value.type, 0);
    916     arm_emit_mem(a, 1, value, load, mem);
    917     arm_emit_t32(mc, arm_add_reg_lsl(rd, rd, idx, addr.log2_scale));
    918   } else if (addr.index_kind != NATIVE_ADDR_INDEX_NONE) {
    919     arm_panic(a, "unsupported address index kind in load_addr");
    920   }
    921 }
    922 
    923 /* ============================ ALU ============================ */
    924 
    925 /* Emit `rd = ra (+|-) mag` in the densest legal Thumb-2 encoding: the 16-bit
    926  * ADDS/SUBS imm3/imm8 (flag-dead) when the magnitude is small and both registers
    927  * are low, else the 32-bit modified-immediate form, else the 12-bit ADDW/SUBW
    928  * raw form. `mag` is guaranteed by arm_resolve_addsub_imm to fit a modified
    929  * immediate or the 12-bit field, so the raw fallback never truncates. */
    930 static void arm_emit_alu_imm(MCEmitter* mc, int is_sub, u32 rd, u32 ra,
    931                              u32 mag) {
    932   u32 enc;
    933   if (!is_sub) {
    934     if (arm_low_reg(rd) && arm_low_reg(ra) && arm_uimm_fits(mag, 3u))
    935       arm_emit_t16(mc, arm_adds_imm3(rd, ra, mag));
    936     else if (rd == ra && arm_low_reg(rd) && arm_uimm_fits(mag, 8u))
    937       arm_emit_t16(mc, arm_adds_imm8(rd, mag));
    938     else if (thumb_expand_imm_encode(mag, &enc))
    939       arm_emit_t32(mc, arm_dp_imm(8u, 0u, rd, ra, enc));
    940     else
    941       arm_emit_t32(mc, arm_add_imm12(rd, ra, mag & 0xfffu));
    942   } else {
    943     if (arm_low_reg(rd) && arm_low_reg(ra) && arm_uimm_fits(mag, 3u))
    944       arm_emit_t16(mc, arm_subs_imm3(rd, ra, mag));
    945     else if (rd == ra && arm_low_reg(rd) && arm_uimm_fits(mag, 8u))
    946       arm_emit_t16(mc, arm_subs_imm8(rd, mag));
    947     else if (thumb_expand_imm_encode(mag, &enc))
    948       arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, ra, enc));
    949     else
    950       arm_emit_t32(mc, arm_sub_imm12(rd, ra, mag & 0xfffu));
    951   }
    952 }
    953 
    954 static void arm_binop(NativeTarget* t, BinOp op, NativeLoc dst, NativeLoc a_loc,
    955                       NativeLoc b) {
    956   Arm32NativeTarget* a = arm_of(t);
    957   MCEmitter* mc = t->mc;
    958   u32 rd = loc_reg(dst), ra = loc_reg(a_loc);
    959   int b_imm = b.kind == NATIVE_LOC_IMM;
    960   u32 rb = b_imm ? 0u : loc_reg(b);
    961   u32 enc;
    962   /* L7 shifted-register fold (O1-only; the rider is set by pass_combine when the
    963    * target advertised can_fold_shift_into_alu). `b` is a register pre-shifted
    964    * left by `b.shift` (1..4): emit the one-instruction 32-bit shifted form
    965    * `<op>.W rd,ra,rb,LSL #k` instead of a separate LSL + op. Only the five DP
    966    * ops binop_takes_shifted_rhs admits ever carry a rider. The 16-bit forms have
    967    * no shifted operand, so this forces a .W encoding — still a net -1 insn.
    968    * NOTE: arm_can_fold_shift_into_alu currently returns 0 (see its comment for
    969    * the shared-pass rider-drop miscompile), so b.shift is always 0 here today —
    970    * this branch is the ready-to-go emit path for when the fold is re-enabled. */
    971   if (!b_imm && b.shift) {
    972     u32 k = b.shift;
    973     switch (op) {
    974       case BO_IADD: arm_emit_t32(mc, arm_add_reg_lsl(rd, ra, rb, k)); return;
    975       case BO_ISUB: arm_emit_t32(mc, arm_sub_reg_lsl(rd, ra, rb, k)); return;
    976       case BO_AND:  arm_emit_t32(mc, arm_and_reg_lsl(rd, ra, rb, k)); return;
    977       case BO_OR:   arm_emit_t32(mc, arm_orr_reg_lsl(rd, ra, rb, k)); return;
    978       case BO_XOR:  arm_emit_t32(mc, arm_eor_reg_lsl(rd, ra, rb, k)); return;
    979       default:      arm_panic(a, "shift rider on unsupported binop");
    980     }
    981   }
    982   switch (op) {
    983     case BO_IADD:
    984       if (b_imm) {
    985         /* Fold the constant into ADD/SUB (a negative addend becomes SUB) via the
    986          * shared resolver; arm_emit_alu_imm picks the densest legal encoding. */
    987         int is_sub;
    988         u32 mag;
    989         if (!arm_resolve_addsub_imm(BO_IADD, b.v.imm, &is_sub, &mag))
    990           arm_panic(a, "add immediate not encodable");
    991         arm_emit_alu_imm(mc, is_sub, rd, ra, mag);
    992       } else if (arm_low_reg(rd) && arm_low_reg(ra) && arm_low_reg(rb)) {
    993         arm_emit_t16(mc, arm_adds_reg16(rd, ra, rb)); /* flag-dead */
    994       } else {
    995         arm_emit_t32(mc, arm_add_reg(rd, ra, rb));
    996       }
    997       return;
    998     case BO_ISUB:
    999       if (b_imm) {
   1000         int is_sub;
   1001         u32 mag;
   1002         if (!arm_resolve_addsub_imm(BO_ISUB, b.v.imm, &is_sub, &mag))
   1003           arm_panic(a, "sub immediate not encodable");
   1004         arm_emit_alu_imm(mc, is_sub, rd, ra, mag);
   1005       } else if (arm_low_reg(rd) && arm_low_reg(ra) && arm_low_reg(rb)) {
   1006         arm_emit_t16(mc, arm_subs_reg16(rd, ra, rb)); /* flag-dead */
   1007       } else {
   1008         arm_emit_t32(mc, arm_sub_reg(rd, ra, rb));
   1009       }
   1010       return;
   1011     case BO_IMUL:
   1012       arm_emit_t32(mc, arm_mul(rd, ra, rb));
   1013       return;
   1014     case BO_SDIV:
   1015       arm_emit_t32(mc, arm_sdiv(rd, ra, rb));
   1016       return;
   1017     case BO_UDIV:
   1018       arm_emit_t32(mc, arm_udiv(rd, ra, rb));
   1019       return;
   1020     case BO_SREM:
   1021     case BO_UREM: {
   1022       /* rd = ra - (ra/rb)*rb via SDIV/UDIV into a quotient temp, then MLS. The
   1023        * temp must survive ra/rb (read by MLS), so it must differ from ra and rb;
   1024        * it may be rd when rd aliases neither operand (the common case — no extra
   1025        * register). Otherwise pick a reserved scratch (LR, else IP): when rd does
   1026        * alias an operand, rd is an allocated register (≠ IP/LR), so at most one of
   1027        * {IP,LR} can be an operand and the other is always free. */
   1028       u32 q = (rd != ra && rd != rb)
   1029                   ? rd
   1030                   : ((ra != ARM_TMP && rb != ARM_TMP) ? ARM_TMP : ARM_SCRATCH);
   1031       arm_emit_t32(mc, op == BO_SREM ? arm_sdiv(q, ra, rb) : arm_udiv(q, ra, rb));
   1032       arm_emit_t32(mc, arm_mls(rd, q, rb, ra));
   1033       return;
   1034     }
   1035     case BO_AND:
   1036       if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
   1037         arm_emit_t32(mc, arm_dp_imm(0u, 0u, rd, ra, enc));
   1038       else
   1039         arm_emit_t32(mc, arm_and_reg(rd, ra, rb));
   1040       return;
   1041     case BO_OR:
   1042       if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
   1043         arm_emit_t32(mc, arm_dp_imm(2u, 0u, rd, ra, enc));
   1044       else
   1045         arm_emit_t32(mc, arm_orr_reg(rd, ra, rb));
   1046       return;
   1047     case BO_XOR:
   1048       if (b_imm && thumb_expand_imm_encode((u32)b.v.imm, &enc))
   1049         arm_emit_t32(mc, arm_dp_imm(4u, 0u, rd, ra, enc));
   1050       else
   1051         arm_emit_t32(mc, arm_eor_reg(rd, ra, rb));
   1052       return;
   1053     case BO_SHL: {
   1054       /* LSLS rd,rm,#imm5 (low regs, flag-dead). LSL #0 is a plain move in both
   1055        * widths, so narrowing the whole 0..31 range is safe for LSL. */
   1056       u32 sh = (u32)b.v.imm & 31u;
   1057       if (b_imm && arm_low_reg(rd) && arm_low_reg(ra))
   1058         arm_emit_t16(mc, arm_lsls_imm16(rd, ra, sh));
   1059       else if (b_imm)
   1060         arm_emit_t32(mc, arm_shift_imm(0u, rd, ra, sh));
   1061       else
   1062         arm_emit_t32(mc, arm_shift_reg(0u, rd, ra, rb));
   1063       return;
   1064     }
   1065     case BO_SHR_U: {
   1066       /* LSRS: the 16-bit form encodes #32 as imm5=0, so only narrow #1..#31;
   1067        * #0 (no shift) keeps the .W form whose imm5=0 means #0 (a move). */
   1068       u32 sh = (u32)b.v.imm & 31u;
   1069       if (b_imm && sh != 0u && arm_low_reg(rd) && arm_low_reg(ra))
   1070         arm_emit_t16(mc, arm_lsrs_imm16(rd, ra, sh));
   1071       else if (b_imm)
   1072         arm_emit_t32(mc, arm_shift_imm(1u, rd, ra, sh));
   1073       else
   1074         arm_emit_t32(mc, arm_shift_reg(1u, rd, ra, rb));
   1075       return;
   1076     }
   1077     case BO_SHR_S: {
   1078       u32 sh = (u32)b.v.imm & 31u; /* ASRS: same #32-as-0 caveat as LSRS. */
   1079       if (b_imm && sh != 0u && arm_low_reg(rd) && arm_low_reg(ra))
   1080         arm_emit_t16(mc, arm_asrs_imm16(rd, ra, sh));
   1081       else if (b_imm)
   1082         arm_emit_t32(mc, arm_shift_imm(2u, rd, ra, sh));
   1083       else
   1084         arm_emit_t32(mc, arm_shift_reg(2u, rd, ra, rb));
   1085       return;
   1086     }
   1087     default:
   1088       arm_panic(a, "binop not lowered (FP / wide8 is Phase 2)");
   1089   }
   1090 }
   1091 
   1092 static void arm_unop(NativeTarget* t, UnOp op, NativeLoc dst, NativeLoc src) {
   1093   Arm32NativeTarget* a = arm_of(t);
   1094   MCEmitter* mc = t->mc;
   1095   u32 rd = loc_reg(dst), rs = loc_reg(src);
   1096   u32 enc;
   1097   switch (op) {
   1098     case UO_NEG:
   1099       thumb_expand_imm_encode(0u, &enc);
   1100       arm_emit_t32(mc, arm_dp_imm(14u, 0u, rd, rs, enc)); /* RSB rd, rs, #0 */
   1101       return;
   1102     case UO_BNOT:
   1103       arm_emit_t32(mc, arm_mvn_reg(rd, rs));
   1104       return;
   1105     case UO_NOT: {
   1106       /* Logical not -> (rs == 0) materialized as 0/1: CMP rs,#0; MOV rd,#0;
   1107        * IT EQ; MOV<eq> rd,#1. CMP precedes the MOV (rd may alias rs). */
   1108       u32 enc0, enc1;
   1109       thumb_expand_imm_encode(0u, &enc0);
   1110       thumb_expand_imm_encode(1u, &enc1);
   1111       arm_emit_t32(mc, arm_cmp_imm(rs, enc0));
   1112       arm_emit_t32(mc, arm_mov_imm(rd, enc0));
   1113       arm_emit_t16(mc, (u16)(0xbf08u | (ARM_CC_EQ << 4))); /* IT EQ */
   1114       arm_emit_t32(mc, arm_mov_imm(rd, enc1));
   1115       return;
   1116     }
   1117     default:
   1118       arm_panic(a, "unop not lowered (FNEG is a soft-float follow-on)");
   1119   }
   1120 }
   1121 
   1122 /* Map a CmpOp to its ARM condition code (taken-when-true). */
   1123 static u32 arm_cond_for(Arm32NativeTarget* a, CmpOp op) {
   1124   switch (op) {
   1125     case CMP_EQ:   return ARM_CC_EQ;
   1126     case CMP_NE:   return ARM_CC_NE;
   1127     case CMP_LT_S: return ARM_CC_LT;
   1128     case CMP_LE_S: return ARM_CC_LE;
   1129     case CMP_GT_S: return ARM_CC_GT;
   1130     case CMP_GE_S: return ARM_CC_GE;
   1131     case CMP_LT_U: return ARM_CC_CC;
   1132     case CMP_LE_U: return ARM_CC_LS;
   1133     case CMP_GT_U: return ARM_CC_HI;
   1134     case CMP_GE_U: return ARM_CC_CS;
   1135     default:
   1136       arm_panic(a, "FP compare not lowered (Phase 2)");
   1137   }
   1138 }
   1139 
   1140 static void arm_emit_cmp_operands(Arm32NativeTarget* a, NativeLoc x,
   1141                                   NativeLoc y) {
   1142   MCEmitter* mc = a->base.mc;
   1143   u32 enc;
   1144   if (y.kind == NATIVE_LOC_IMM && thumb_expand_imm_encode((u32)y.v.imm, &enc))
   1145     arm_emit_t32(mc, arm_cmp_imm(loc_reg(x), enc));
   1146   else
   1147     arm_emit_t32(mc, arm_cmp_reg(loc_reg(x), loc_reg(y)));
   1148 }
   1149 
   1150 /* Materialize the boolean (a OP b) into dst: CMP; MOV dst,#0; IT cc; MOV dst,#1.
   1151  * The CMP must precede the `MOV dst,#0`: dst can ALIAS a compare operand (e.g. a
   1152  * soft-float compare leaves its libcall result in r0 and the bool destination is
   1153  * also r0), and a leading `MOV dst,#0` would clobber that operand before CMP
   1154  * reads it. MOV.W #imm (T2, S=0) does not set flags, so emitting it after CMP
   1155  * leaves the compare flags intact for the IT-predicated `MOV dst,#1`. */
   1156 static void arm_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc a_loc,
   1157                     NativeLoc b) {
   1158   Arm32NativeTarget* a = arm_of(t);
   1159   MCEmitter* mc = t->mc;
   1160   u32 rd = loc_reg(dst);
   1161   u32 cc = arm_cond_for(a, op);
   1162   u32 enc0, enc1;
   1163   thumb_expand_imm_encode(0u, &enc0);
   1164   thumb_expand_imm_encode(1u, &enc1);
   1165   arm_emit_cmp_operands(a, a_loc, b);          /* CMP a, b (sets flags) */
   1166   arm_emit_t32(mc, arm_mov_imm(rd, enc0));    /* dst = 0 (MOV.W, no flag set) */
   1167   arm_emit_t16(mc, (u16)(0xbf08u | (cc << 4))); /* IT cc (1 insn, mask 0b1000) */
   1168   arm_emit_t32(mc, arm_mov_imm(rd, enc1));    /* MOV<cc> dst, #1 */
   1169 }
   1170 
   1171 static void arm_convert(NativeTarget* t, ConvKind op, NativeLoc dst,
   1172                         NativeLoc src) {
   1173   Arm32NativeTarget* a = arm_of(t);
   1174   MCEmitter* mc = t->mc;
   1175   u32 rd = loc_reg(dst), rs = loc_reg(src);
   1176   u32 dsz = native_type_size(t, dst.type);
   1177   u32 ssz = native_type_size(t, src.type);
   1178   switch (op) {
   1179     case CV_SEXT:
   1180       if (ssz == 1u) arm_emit_t32(mc, arm_sxtb(rd, rs));
   1181       else if (ssz == 2u) arm_emit_t32(mc, arm_sxth(rd, rs));
   1182       else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
   1183       return;
   1184     case CV_ZEXT:
   1185       if (ssz == 1u) arm_emit_t32(mc, arm_uxtb(rd, rs));
   1186       else if (ssz == 2u) arm_emit_t32(mc, arm_uxth(rd, rs));
   1187       else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
   1188       return;
   1189     case CV_TRUNC:
   1190       if (dsz == 1u) arm_emit_t32(mc, arm_uxtb(rd, rs));
   1191       else if (dsz == 2u) arm_emit_t32(mc, arm_uxth(rd, rs));
   1192       else if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
   1193       return;
   1194     case CV_BITCAST:
   1195       if (rd != rs) arm_emit_t16(mc, arm_mov_hi(rd, rs));
   1196       return;
   1197     default:
   1198       arm_panic(a, "convert not lowered (FP conversions are Phase 2)");
   1199   }
   1200 }
   1201 
   1202 /* ============================ _rr narrow hooks ============================ */
   1203 
   1204 static void arm_binop_rr(NativeTarget* t, BinOp op, NativeRegLoc dst,
   1205                          NativeRegLoc a, NativeRegLoc b) {
   1206   arm_binop(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
   1207             native_loc_from_reg(b));
   1208 }
   1209 static void arm_move_rr(NativeTarget* t, NativeRegLoc dst, NativeRegLoc src) {
   1210   arm_move(t, native_loc_from_reg(dst), native_loc_from_reg(src));
   1211 }
   1212 static void arm_cmp_rr(NativeTarget* t, CmpOp op, NativeRegLoc dst,
   1213                        NativeRegLoc a, NativeRegLoc b) {
   1214   arm_cmp(t, op, native_loc_from_reg(dst), native_loc_from_reg(a),
   1215           native_loc_from_reg(b));
   1216 }
   1217 static void arm_convert_rr(NativeTarget* t, ConvKind op, NativeRegLoc dst,
   1218                            NativeRegLoc src) {
   1219   arm_convert(t, op, native_loc_from_reg(dst), native_loc_from_reg(src));
   1220 }
   1221 
   1222 /* ============================ control flow ============================ */
   1223 
   1224 static MCLabel arm_label_new(NativeTarget* t) { return mc_label_new(t->mc); }
   1225 static void arm_label_place(NativeTarget* t, MCLabel l) {
   1226   mc_label_place(t->mc, l);
   1227 }
   1228 static void arm_jump(NativeTarget* t, MCLabel l) {
   1229   arm_emit_t32(t->mc, arm_b_w());
   1230   mc_emit_label_ref(t->mc, l, R_ARM_THM_JUMP24, 4, 0);
   1231 }
   1232 static void arm_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc a_loc,
   1233                            NativeLoc b, MCLabel target) {
   1234   Arm32NativeTarget* a = arm_of(t);
   1235   u32 cc = arm_cond_for(a, op);
   1236   arm_emit_cmp_operands(a, a_loc, b);
   1237   arm_emit_t32(t->mc, arm_b_cond_w(cc));
   1238   mc_emit_label_ref(t->mc, target, R_ARM_THM_JUMP19, 4, 0);
   1239 }
   1240 
   1241 /* ============================ frame lifecycle ============================ */
   1242 
   1243 static u32 arm_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
   1244                                      int* variadic, u32* nparams);
   1245 
   1246 static NativeFrameSlot arm_frame_slot(NativeTarget* t,
   1247                                       const NativeFrameSlotDesc* d) {
   1248   return native_frame_slot_alloc(&arm_of(t)->frame, d);
   1249 }
   1250 static void arm_release_frame_slot(NativeTarget* t, NativeFrameSlot slot) {
   1251   native_frame_release_slot(&arm_of(t)->frame, slot);
   1252 }
   1253 static int arm_frame_slot_debug_loc(NativeTarget* t, NativeFrameSlot slot,
   1254                                     CGDebugLoc* out) {
   1255   Arm32NativeTarget* a = arm_of(t);
   1256   NativeFrameSlotEntry* s;
   1257   if (!out) return 0;
   1258   memset(out, 0, sizeof *out);
   1259   if (slot == NATIVE_FRAME_SLOT_NONE || slot > a->frame.nslots) return 0;
   1260   s = native_frame_slot_at(&a->frame, slot);
   1261   out->kind = CG_DEBUG_LOC_FRAME;
   1262   out->v.frame_ofs = -(i32)s->off; /* r7-relative */
   1263   return 1;
   1264 }
   1265 
   1266 static u32 arm_frame_size(const Arm32NativeTarget* a) {
   1267   u32 f = align_up_u32(a->frame.cum_off + a->frame.max_outgoing, 8u);
   1268   /* AAPCS: sp must be 8-byte aligned at public interfaces (calls). The total
   1269    * adjustment from entry sp is saved_block_bytes + (variadic ? 16 : 0) + f; the
   1270    * 16-byte GP-save area is 8-aligned, so only an odd-word saved-register block
   1271    * (e.g. {r4,r7,lr} = 12) breaks alignment — pad the frame by a word then. */
   1272   if (a->saved_block_bytes & 4u) f += 4u;
   1273   return f;
   1274 }
   1275 
   1276 /* r7-relative offset where the incoming stack-argument window begins. The saved
   1277  * {r7, lr} pair is 8 bytes; a variadic callee also spilled r0..r3 (16 bytes) in
   1278  * a save area above the pair, pushing the named stack args 16 bytes higher. The
   1279  * spilled r0..r3 occupy [r7 + ARM_SAVED_PAIR_BYTES, r7 + arg_window_base). */
   1280 static u32 arm_arg_window_base(const Arm32NativeTarget* a) {
   1281   /* saved_block_bytes covers the saved-register block ({r7,lr} = 8 at -O0, plus
   1282    * any callee-saves the -O1 known-frame prologue PUSHes); a variadic callee
   1283    * also spilled r0..r3 (16 bytes) just above it. Both push the named incoming
   1284    * stack args higher, so the window base accounts for them together. */
   1285   return a->saved_block_bytes + (a->is_variadic ? ARM_VA_GP_SAVE_BYTES : 0u);
   1286 }
   1287 
   1288 static void arm_reserve_callee_saves(NativeTarget* t, const u32* used_by_class,
   1289                                      u32 nclasses) {
   1290   native_frame_set_callee_saves(&arm_of(t)->frame, used_by_class, nclasses,
   1291                                 NULL, 0, 0);
   1292 }
   1293 
   1294 /* AAPCS callee-saved integer registers this backend lets the allocator use:
   1295  * r4..r11 except r7 (the frame pointer, preserved by the saved-pair head, not as
   1296  * an ordinary callee-save). Matches arm_int_phys's ARM_PHYS_CALLEE set. */
   1297 static int arm_reg_is_callee_int(Reg r) {
   1298   return r >= 4u && r <= 11u && r != ARM_FP;
   1299 }
   1300 
   1301 static void arm_asm_clobber_masks(Compiler* c, SrcLoc loc,
   1302                                   const Sym* clobbers, u32 nclob,
   1303                                   u32* int_mask, u32* fp_mask);
   1304 
   1305 /* Build the callee-saved set the prologue must preserve, from the allocator's
   1306  * per-class masks plus opaque inline-asm clobbers. Only the integer class
   1307  * exists in this soft-float backend. r7 is structural frame state and is
   1308  * excluded here (and rejected when named explicitly by the clobber parser). */
   1309 static int arm_asm_reg_is_callee_saved(NativeTarget* t, NativeAllocClass cls,
   1310                                        Reg r) {
   1311   (void)t;
   1312   return cls == NATIVE_REG_INT && arm_reg_is_callee_int(r);
   1313 }
   1314 
   1315 static u32 arm_known_callee_saves(NativeTarget* t,
   1316                                   const NativeKnownFrameDesc* frame) {
   1317   Arm32NativeTarget* a = arm_of(t);
   1318   u32 masks[NATIVE_REG_CLASS_COUNT] = {0, 0, 0};
   1319   SrcLoc loc = a->func ? a->func->loc : (SrcLoc){0, 0, 0};
   1320   native_asm_known_callee_saves(t, loc, frame, masks,
   1321                                 NATIVE_REG_CLASS_COUNT,
   1322                                 arm_asm_clobber_masks,
   1323                                 arm_asm_reg_is_callee_saved);
   1324   return masks[NATIVE_REG_INT];
   1325 }
   1326 
   1327 /* True (and sets *imm7) when `n` fits the 16-bit `SUB sp,sp,#imm7*4` form:
   1328  * a non-zero multiple of 4 with imm7 in 0..127 (n <= 508). The frame size is a
   1329  * multiple of 8, so the multiple-of-4 requirement is always met. */
   1330 static int arm_sp_adjust_narrow(u32 n, u32* imm7) {
   1331   if (n == 0 || (n & 3u) != 0u || (n >> 2) > 0x7fu) return 0;
   1332   *imm7 = n >> 2;
   1333   return 1;
   1334 }
   1335 
   1336 /* Build the `SUB sp, sp, #n` instruction sequence (n a multiple of 8) into
   1337  * words[], returning the count (<= ARM_NDT_SUB_WORDS). A small frame fits the
   1338  * 16-bit `SUB sp,sp,#imm7*4`, packed (SUB16 in hw1, NOP16 in hw2) into one T32
   1339  * slot so the single-pass deferred patch still overwrites exactly one reserved
   1340  * word; medium amounts use one SUBW/SUB.W modimm; large amounts (n > 4095, big
   1341  * frames) stage n into IP via MOVW(+MOVT) and `SUB sp, sp, ip`. IP is the emit
   1342  * scratch, dead at the prologue. Shared by the inline known-frame emit and the
   1343  * single-pass patch. */
   1344 static u32 arm_build_sub_sp(u32 n, u32* words) {
   1345   u32 enc, imm7, wi = 0;
   1346   if (n == 0) return 0;
   1347   if (arm_sp_adjust_narrow(n, &imm7)) {
   1348     /* hw1 = SUB sp (16-bit), hw2 = NOP (16-bit). arm_patch_t32 writes hw1 first
   1349      * in memory, so SUB executes then NOP. */
   1350     words[wi++] = arm_t32(arm_sub_sp_sp_imm16(imm7), arm_nop16());
   1351     return wi;
   1352   }
   1353   if (n <= 0xfffu) {
   1354     words[wi++] = arm_sub_imm12(13u, 13u, n);
   1355     return wi;
   1356   }
   1357   if (thumb_expand_imm_encode(n, &enc)) {
   1358     words[wi++] = arm_dp_imm(13u, 0u, 13u, 13u, enc); /* SUB.W sp,sp,#modimm */
   1359     return wi;
   1360   }
   1361   words[wi++] = arm_movw(ARM_SCRATCH, n & 0xffffu);
   1362   if ((n >> 16) != 0u) words[wi++] = arm_movt(ARM_SCRATCH, (n >> 16) & 0xffffu);
   1363   words[wi++] = arm_sub_reg(13u, 13u, ARM_SCRATCH);
   1364   return wi;
   1365 }
   1366 
   1367 /* Emit `SUB sp, sp, #n` inline (known-frame prologue, where the size is final).
   1368  * The inline path is not slot-constrained, so a small frame emits a bare 16-bit
   1369  * SUB (no NOP padding); larger frames reuse arm_build_sub_sp's word list. */
   1370 static void arm_emit_sub_sp(Arm32NativeTarget* a, u32 n) {
   1371   u32 words[ARM_NDT_SUB_WORDS];
   1372   u32 imm7, nwords, i;
   1373   if (arm_sp_adjust_narrow(n, &imm7)) {
   1374     arm_emit_t16(a->base.mc, arm_sub_sp_sp_imm16(imm7));
   1375     return;
   1376   }
   1377   nwords = arm_build_sub_sp(n, words);
   1378   for (i = 0; i < nwords; ++i) arm_emit_t32(a->base.mc, words[i]);
   1379 }
   1380 
   1381 /* sret: reserve a hidden frame slot for the incoming destination pointer (r0)
   1382  * and advance the param cursor past it. Shared by the single-pass and
   1383  * known-frame paths (slot creation must happen at the same point in both). */
   1384 static void arm_reserve_sret_slot(Arm32NativeTarget* a) {
   1385   NativeTarget* t = &a->base;
   1386   if (a->has_sret) {
   1387     NativeFrameSlotDesc sd;
   1388     memset(&sd, 0, sizeof sd);
   1389     sd.type = builtin_id(KIT_CG_BUILTIN_I32);
   1390     sd.size = 4;
   1391     sd.align = 4;
   1392     sd.kind = NATIVE_FRAME_SLOT_SAVE;
   1393     a->sret_ptr_slot = arm_frame_slot(t, &sd);
   1394     a->next_param_int = 1; /* r0 consumed by the sret pointer */
   1395   }
   1396 }
   1397 
   1398 /* Lazily reserve the per-function indirect-callee spill home (4 bytes, r7-
   1399  * anchored). Allocated on first need during body codegen — at -O0 the frame is
   1400  * deferred (settled in arm_func_end), so a body-time slot just grows cum_off;
   1401  * one slot is reused across every indirect call in the function. */
   1402 static NativeFrameSlot arm_callee_spill_slot(Arm32NativeTarget* a) {
   1403   if (a->callee_spill_slot == NATIVE_FRAME_SLOT_NONE) {
   1404     NativeFrameSlotDesc sd;
   1405     memset(&sd, 0, sizeof sd);
   1406     sd.type = builtin_id(KIT_CG_BUILTIN_I32);
   1407     sd.size = 4;
   1408     sd.align = 4;
   1409     sd.kind = NATIVE_FRAME_SLOT_SAVE;
   1410     a->callee_spill_slot = arm_frame_slot(&a->base, &sd);
   1411   }
   1412   return a->callee_spill_slot;
   1413 }
   1414 
   1415 /* Spill the incoming sret pointer (r0) to its hidden home. The home is
   1416  * r7-anchored, so this runs correctly after the frame is set up. */
   1417 static void arm_emit_sret_store(Arm32NativeTarget* a) {
   1418   NativeTarget* t = &a->base;
   1419   if (a->has_sret && a->sret_ptr_slot != NATIVE_FRAME_SLOT_NONE) {
   1420     KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   1421     NativeAddr addr;
   1422     memset(&addr, 0, sizeof addr);
   1423     addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   1424     addr.base.frame = a->sret_ptr_slot;
   1425     addr.base_type = i32t;
   1426     arm_emit_mem(a, 0, native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0), addr,
   1427                  native_mem_for_type(t, i32t, 4));
   1428   }
   1429 }
   1430 
   1431 static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) {
   1432   Arm32NativeTarget* a = arm_of(t);
   1433   MCEmitter* mc = t->mc;
   1434   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
   1435   a->func = fd;
   1436   a->loc = fd->loc;
   1437   native_frame_reset(&a->frame);
   1438   a->next_param_int = 0;
   1439   a->next_param_stack = 0;
   1440   a->has_sret = (abi && abi->has_sret) ? 1u : 0u;
   1441   a->is_variadic = (abi && abi->variadic) ? 1u : 0u;
   1442   a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE;
   1443   a->callee_spill_slot = NATIVE_FRAME_SLOT_NONE;
   1444   a->known_frame = 0;
   1445   a->slim_prologue = 0;
   1446   a->saved_reglist = (u16)((1u << ARM_FP) | (1u << 14u)); /* {r7, lr} */
   1447   a->saved_block_bytes = 8u;                              /* two words */
   1448   a->n_alloca_patch = 0;
   1449   a->incoming_stack_size = arm_signature_stack_bytes(t, fd->fn_type, NULL, NULL);
   1450 
   1451   mc_set_section(mc, fd->text_section_id);
   1452   mc_emit_align(mc, 4, 0);
   1453   a->func_start = mc_pos(mc);
   1454   mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start);
   1455   mc_cfi_startproc(mc);
   1456   a->epilogue_label = mc_label_new(mc);
   1457 
   1458   /* Prologue: save fp+lr, set the frame anchor, reserve the (deferred) frame.
   1459    * The deferred `SUB sp` is patched in arm_func_end once cum_off/max_outgoing
   1460    * are final. Reserve ARM_NDT_SUB_WORDS T32 slots so a large frame (> 4095) can
   1461    * be patched as an IP-staged MOVW/MOVT/SUB sequence; a small frame patches the
   1462    * first slot with SUBW and fills the rest with NOPs.
   1463    * A variadic callee first spills the GP arg registers r0..r3 into a save area
   1464    * just above the saved pair (so they are contiguous-below the named incoming
   1465    * stack args at [r7 + 8 + 16]); va_start/va_arg walk forward across both. */
   1466   if (a->is_variadic)
   1467     arm_emit_push(mc, 0xfu);                        /* PUSH {r0-r3} (16-bit) */
   1468   arm_emit_push(mc, (1u << ARM_FP) | (1u << 14u)); /* PUSH {r7, lr} (16-bit) */
   1469   arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u));                   /* MOV r7, sp */
   1470   a->prologue_sub_pos = mc_pos(mc);
   1471   for (u32 i = 0; i < ARM_NDT_SUB_WORDS; ++i)
   1472     arm_emit_t32(mc, arm_nop32()); /* placeholder, patched in arm_func_end */
   1473 
   1474   /* sret: spill the incoming destination pointer (r0) to a hidden home. */
   1475   arm_reserve_sret_slot(a);
   1476   arm_emit_sret_store(a);
   1477 }
   1478 
   1479 /* Optimizer (-O1) entry point: the optimizer has run register allocation and
   1480  * supplies the exact frame up front, so the prologue is emitted final the moment
   1481  * it is built — no deferred `SUB sp, #0` placeholder, no arm_func_end patch
   1482  * (arm_func_end skips patching when known_frame). The callee-saved registers the
   1483  * allocator used are folded directly into the PUSH/POP register list rather than
   1484  * given their own frame slots: they ride above the frame anchor r7 in the
   1485  * STMDB-saved block, which keeps slot offsets ([r7, #-off]) byte-identical to
   1486  * the single-pass layout and lets one LDMIA-via-pc restore-and-return.
   1487  *
   1488  * Slot creation order matches the single-pass path: static slots first, then the
   1489  * sret entry-save slot. (Callee-saves take no slots here, so order vs. them is
   1490  * moot.) */
   1491 static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd,
   1492                                        const NativeKnownFrameDesc* kf,
   1493                                        NativeFrameSlot* out_slots) {
   1494   Arm32NativeTarget* a = arm_of(t);
   1495   MCEmitter* mc = t->mc;
   1496   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
   1497   u32 cs_mask;
   1498   u32 reglist, nsaved, frame, i;
   1499 
   1500   a->func = fd;
   1501   a->loc = fd->loc;
   1502   cs_mask = arm_known_callee_saves(t, kf);
   1503   native_frame_reset(&a->frame);
   1504   a->next_param_int = 0;
   1505   a->next_param_stack = 0;
   1506   a->has_sret = (abi && abi->has_sret) ? 1u : 0u;
   1507   a->is_variadic = (abi && abi->variadic) ? 1u : 0u;
   1508   a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE;
   1509   a->callee_spill_slot = NATIVE_FRAME_SLOT_NONE;
   1510   a->known_frame = 1;
   1511   a->slim_prologue = 0;
   1512   a->n_alloca_patch = 0;
   1513   a->incoming_stack_size = arm_signature_stack_bytes(t, fd->fn_type, NULL, NULL);
   1514   a->frame.known_frame = 1;
   1515   a->frame.has_alloca = kf ? kf->has_alloca : 0u;
   1516 
   1517   mc_set_section(mc, fd->text_section_id);
   1518   mc_emit_align(mc, 4, 0);
   1519   a->func_start = mc_pos(mc);
   1520   mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start);
   1521   mc_cfi_startproc(mc);
   1522   a->epilogue_label = mc_label_new(mc);
   1523 
   1524   /* Record the callee-save set (for parity / debug) and build the body's slots
   1525    * and outgoing-area reservation, then settle the exact frame. */
   1526   if (cs_mask) {
   1527     u32 used[NATIVE_REG_CLASS_COUNT] = {0, 0, 0};
   1528     used[NATIVE_REG_INT] = cs_mask;
   1529     arm_reserve_callee_saves(t, used, NATIVE_REG_CLASS_COUNT);
   1530   }
   1531   if (kf) {
   1532     for (i = 0; i < kf->nslots; ++i) {
   1533       NativeFrameSlot slot = arm_frame_slot(t, &kf->slots[i]);
   1534       if (out_slots) out_slots[i] = slot;
   1535     }
   1536     arm_reserve_sret_slot(a);
   1537     native_frame_note_outgoing(&a->frame, kf->max_outgoing);
   1538   } else {
   1539     arm_reserve_sret_slot(a);
   1540   }
   1541 
   1542   /* The PUSH/POP register list: callee-saves (low to high) + r7 + lr. STMDB
   1543    * stores in ascending register order regardless of the bit order, so r7 lands
   1544    * in the block correctly and `MOV r7, sp` anchors just below the whole block.
   1545    * The popped lr is restored into pc, returning. */
   1546   reglist = cs_mask | (1u << ARM_FP) | (1u << 14u);
   1547   nsaved = 0;
   1548   for (i = 0; i < 16u; ++i)
   1549     if (reglist & (1u << i)) nsaved++;
   1550   a->saved_reglist = (u16)reglist;
   1551   a->saved_block_bytes = nsaved * 4u;
   1552 
   1553   /* Leaf minimal-frame tier (the register-starved analog of the aa64/rv64
   1554    * frameless leaf): a leaf with no callee-saves, no body slots, no outgoing
   1555    * args, no sret/variadic and only register params needs no frame record, no r7
   1556    * anchor and no SUB sp. It cannot, however, return through a bare `BX lr`:
   1557    * arm32 is register-starved and uses LR (ARM_TMP) pervasively as a
   1558    * backend-private temporary — global-address staging in arm_resolve_mem,
   1559    * `__builtin_*_overflow` sequences, popcount, bitfield insert, and the
   1560    * byte-copy granule engine. A leaf body may hit any of these (e.g.
   1561    * `static int g; return g;` stages &g into LR,
   1562    * after which a bare `BX lr` would branch to &g). So the tier still saves and
   1563    * restores the return address with a single-register `PUSH {lr}` / `POP {pc}`,
   1564    * leaving the body free to clobber lr; it just skips the r7 frame record and
   1565    * the stack reservation. (aa64/rv64 keep their return-address register out of
   1566    * the backend temporary bank, so their frameless leaves skip even this push; arm32
   1567    * cannot.) Inline asm cannot occur here (asm_block is not installed). */
   1568   a->slim_prologue = kf && kf->is_leaf && !kf->reads_frame && cs_mask == 0 &&
   1569                      !a->frame.has_alloca && a->frame.cum_off == 0 &&
   1570                      a->frame.max_outgoing == 0 && !a->has_sret &&
   1571                      !a->is_variadic &&
   1572                      arm_signature_stack_bytes(t, fd->fn_type, NULL, NULL) == 0;
   1573   if (a->slim_prologue) {
   1574     a->saved_reglist = (u16)(1u << 14u); /* {lr} */
   1575     a->saved_block_bytes = 4u;
   1576     arm_emit_push(mc, 1u << 14u); /* PUSH {lr} (16-bit, 0xb500) */
   1577     native_frame_set_final(&a->frame);
   1578     return;
   1579   }
   1580 
   1581   /* Emit the final prologue as a fp-at-pair split push: the callee-saves ride
   1582    * ABOVE a tight {r7, lr} frame-record pair so r7 anchors exactly that pair —
   1583    * [r7]=caller's r7, [r7+4]=return address — which @frame_address/@return_address
   1584    * and any frame-pointer backtrace walk. (A single combined PUSH would leave r7
   1585    * pointing at the lowest callee-save, with the return address an arbitrary
   1586    * distance above, breaking the chain whenever r8..r11 sit between r7 and lr.)
   1587    * Push order, high address to low: variadic GP-save (r0..r3) first, then the
   1588    * callee-saves, then the pair; the total block size is unchanged, so
   1589    * saved_block_bytes and every [r7, #-off] body slot keep their offsets, and
   1590    * va_start still reaches the GP-save at r7 + saved_block_bytes.
   1591    * A variadic callee's r0..r3 save area sits just above the saved block (below
   1592    * the named incoming stack args); va_start/va_arg walk forward across both, and
   1593    * the teardown reclaims it (arm_emit_frame_restore). */
   1594   if (a->is_variadic) arm_emit_push(mc, 0xfu); /* PUSH {r0-r3} */
   1595   if (cs_mask) arm_emit_push(mc, cs_mask);     /* PUSH {callee-saves} */
   1596   arm_emit_push(mc, (1u << ARM_FP) | (1u << 14u)); /* PUSH {r7, lr} */
   1597   arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u));                  /* MOV r7, sp */
   1598   frame = arm_frame_size(a);
   1599   arm_emit_sub_sp(a, frame);
   1600 
   1601   arm_emit_sret_store(a);
   1602   native_frame_set_final(&a->frame);
   1603 }
   1604 
   1605 /* Emit the frame teardown, mirroring the fp-at-pair split prologue: r7 anchors
   1606  * the {r7, lr} pair at the bottom of the saved block, the callee-saves sit above
   1607  * it, and (for a variadic callee) the r0..r3 GP-save above those.
   1608  *
   1609  * The common case — no callee-saves, no GP-save, returning — peels the pair with
   1610  * a single `POP {r7, pc}` (the saved lr lands straight in pc). Otherwise restore
   1611  * the pair with `POP {r7, lr}`, then peel the callee-saves and reclaim the
   1612  * GP-save, leaving lr = the caller's return address — popped into pc with a final
   1613  * `BX lr` when returning, or kept for a tail-call branch when not. */
   1614 static void arm_emit_frame_restore(Arm32NativeTarget* a, int to_pc) {
   1615   MCEmitter* mc = a->base.mc;
   1616   u32 va = a->is_variadic ? ARM_VA_GP_SAVE_BYTES : 0u;
   1617   u32 cs_mask = a->saved_reglist & ~((1u << ARM_FP) | (1u << 14u));
   1618   arm_emit_t16(mc, arm_mov_hi(13u, ARM_FP)); /* MOV sp, r7 (sp -> {r7,lr} pair) */
   1619   if (to_pc && va == 0u && cs_mask == 0u) {
   1620     arm_emit_pop(mc, (1u << ARM_FP) | (1u << 15u)); /* POP {r7, pc} (16-bit) */
   1621     return;
   1622   }
   1623   arm_emit_pop(mc, (1u << ARM_FP) | (1u << 14u)); /* POP {r7, lr} (16-bit) */
   1624   if (cs_mask) arm_emit_pop(mc, cs_mask);         /* POP {callee-saves} */
   1625   if (va) arm_emit_add_const(a, 13u, (i64)va);              /* reclaim r0..r3 save */
   1626   if (to_pc) arm_emit_t16(mc, arm_bx(14u));                 /* BX lr (return) */
   1627 }
   1628 
   1629 static void arm_func_end(NativeTarget* t) {
   1630   Arm32NativeTarget* a = arm_of(t);
   1631   MCEmitter* mc = t->mc;
   1632   u32 frame;
   1633   /* Place the epilogue and emit the teardown. */
   1634   mc_label_place(mc, a->epilogue_label);
   1635   if (a->slim_prologue)
   1636     /* Minimal leaf: only lr was saved (PUSH {lr}); pop it straight into pc. This
   1637      * restores the return address the body may have clobbered using LR as a
   1638      * backend-private temporary — see the func_begin slim_prologue note. */
   1639     arm_emit_pop(mc, 1u << 15u); /* POP {pc} (16-bit, 0xbd00) */
   1640   else
   1641     arm_emit_frame_restore(a, /*to_pc=*/1);
   1642 
   1643   /* Single-pass path: settle the frame and patch the deferred `SUB sp` region.
   1644    * The known-frame path emitted its prologue final in
   1645    * arm_func_begin_known_frame (frame already settled there), so it never
   1646    * patches. The reserved region is ARM_NDT_SUB_WORDS NOP.W slots; patch the
   1647    * leading slots with the built SUB sequence (one SUBW for a small frame, an
   1648    * IP-staged MOVW/MOVT/SUB for a large one) and leave the trailing NOP.W slots,
   1649    * which execute harmlessly. */
   1650   if (!a->known_frame) {
   1651     u32 words[ARM_NDT_SUB_WORDS];
   1652     u32 nwords, i;
   1653     native_frame_set_final(&a->frame);
   1654     frame = arm_frame_size(a);
   1655     nwords = arm_build_sub_sp(frame, words);
   1656     for (i = 0; i < nwords; ++i)
   1657       arm_patch_t32(a, a->prologue_sub_pos + i * 4u, words[i]);
   1658   }
   1659 
   1660   /* Patch each alloca's `ADD dst, sp, #max_outgoing` now that the outgoing-arg
   1661    * area size is final (the alloca block sits just above it, so a later call's
   1662    * sp-relative outgoing stores never clobber it). */
   1663   {
   1664     u32 i, mo = a->frame.max_outgoing;
   1665     if (a->n_alloca_patch && mo > 0xfffu)
   1666       arm_panic(a, "alloca with a large outgoing-arg area (needs IP staging)");
   1667     for (i = 0; i < a->n_alloca_patch; ++i)
   1668       arm_patch_t32(a, a->alloca_patch_pos[i],
   1669                     arm_add_imm12(a->alloca_patch_rd[i], 13u, mo));
   1670   }
   1671 
   1672   /* Publish the function symbol. ARM marks Thumb STT_FUNC symbols with the
   1673    * low bit set (the Thumb bit), so &fn and indirect BLX reach Thumb state;
   1674    * direct-branch relocs mask it off (S & ~1). */
   1675   {
   1676     u32 end = mc_pos(mc);
   1677     ObjSecId sec = a->func->text_section_id;
   1678     obj_symbol_define(t->obj, a->func->sym, sec, (u64)a->func_start | 1u,
   1679                       (u64)(end - a->func_start));
   1680     if (a->func->atomize)
   1681       obj_atom_define(t->obj, sec, a->func_start, end - a->func_start,
   1682                       a->func->sym, 0);
   1683     if (mc->debug) debug_func_pc_range(mc->debug, sec, a->func_start, end);
   1684   }
   1685 
   1686   mc_cfi_endproc(mc);
   1687   mc_end_function(mc);
   1688   a->func = NULL;
   1689 }
   1690 
   1691 /* ============================ param binding ============================ */
   1692 
   1693 #define ARM32_GPR_BYTES 4u
   1694 
   1695 /* Lane scalar type for an ABI part: an 8-byte i64/double DIRECT argument is two
   1696  * 4-byte INT lanes, and an aggregate uses word/sub-word lanes. The NDT sizes
   1697  * every load/store/move it emits for return/call marshalling or a param bind
   1698  * from the lane location's TYPE (nd_type_mem -> cg_type_size), so each lane must
   1699  * carry a type whose natural size equals the lane's byte width — NOT the full
   1700  * 8-byte scalar type (which would drive an 8-byte access the 32-bit backend has
   1701  * no single-instruction form for). Mirrors riscv's rv_part_scalar_type. */
   1702 static KitCgTypeId arm_part_scalar_type(const ABIArgPart* part) {
   1703   switch (part->size) {
   1704     case 1u: return builtin_id(KIT_CG_BUILTIN_I8);
   1705     case 2u: return builtin_id(KIT_CG_BUILTIN_I16);
   1706     case 8u: return builtin_id(KIT_CG_BUILTIN_I64);
   1707     default: return builtin_id(KIT_CG_BUILTIN_I32);
   1708   }
   1709 }
   1710 
   1711 /* AAPCS even/8-align rule: an argument whose natural alignment is 8 bytes
   1712  * rounds NCRN up to an even core register (r0:r1 or r2:r3) and 8-byte-aligns its
   1713  * stack slot. This covers both an i64/double scalar pair AND an 8-byte-aligned
   1714  * aggregate (e.g. struct{double;long;}). The ABI classifier surfaces that
   1715  * alignment on the first part (scalar pair lanes carry align=8; an aggregate's
   1716  * parts[0].align is the record's natural alignment; the synthesized variadic
   1717  * form below mirrors this), so parts[0].align is the single discriminator —
   1718  * uniform for named and variadic args and independent of part count.
   1719  *
   1720  * This predicate is ONLY the even/8-align rule. Whether an argument may split
   1721  * across the reg/stack boundary is not a separate flag: round-to-even makes an
   1722  * 8-byte scalar pair fit-or-fully-spill (its 2 parts never straddle), while a
   1723  * larger aggregate straddles per AAPCS C.5 — the naive per-part walks below
   1724  * realize both with no special case. */
   1725 static int arm_arg_needs_even(const ABIArgInfo* ai) {
   1726   return ai->kind == ABI_ARG_DIRECT && ai->nparts > 0u && ai->parts &&
   1727          ai->parts[0].align >= 2u * ARM32_GPR_BYTES;
   1728 }
   1729 
   1730 static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p,
   1731                                   NativeLoc dst) {
   1732   Arm32NativeTarget* a = arm_of(t);
   1733   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type);
   1734   const ABIArgInfo* ai = p->index < abi->nparams ? &abi->params[p->index] : NULL;
   1735   int to_reg = dst.kind == NATIVE_LOC_REG;
   1736   int even = ai && arm_arg_needs_even(ai);
   1737   u32 i;
   1738   if (!ai || ai->kind == ABI_ARG_IGNORE) return;
   1739   if (ai->kind == ABI_ARG_INDIRECT) {
   1740     /* An ABI-classified indirect argument is a pointer to caller-owned storage.
   1741      * Copy it into the parameter's frame home so the body owns a private copy
   1742      * (matching the by-value semantics). */
   1743     KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   1744     NativeLoc src = native_loc_reg(i32t, NATIVE_REG_INT,
   1745                                    a->next_param_int < 4u
   1746                                        ? (Reg)a->next_param_int
   1747                                        : ARM_SCRATCH);
   1748     NativeAddr d_addr, from;
   1749     AggregateAccess access;
   1750     if (a->next_param_int < 4u) {
   1751       a->next_param_int++;
   1752     } else {
   1753       NativeAddr sa;
   1754       memset(&sa, 0, sizeof sa);
   1755       sa.base_kind = NATIVE_ADDR_BASE_REG;
   1756       sa.base.reg = ARM_FP;
   1757       sa.base_type = i32t;
   1758       sa.offset = (i32)(arm_arg_window_base(a) + a->next_param_stack);
   1759       arm_emit_mem(a, 1, src, sa, native_mem_for_type(t, i32t, 4));
   1760       a->next_param_stack += 4u;
   1761     }
   1762     if (dst.kind != NATIVE_LOC_FRAME)
   1763       arm_panic(a, "indirect parameter requires a frame destination");
   1764     memset(&d_addr, 0, sizeof d_addr);
   1765     d_addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   1766     d_addr.base.frame = dst.v.frame;
   1767     d_addr.base_type = p->type;
   1768     memset(&from, 0, sizeof from);
   1769     from.base_kind = NATIVE_ADDR_BASE_REG;
   1770     from.base.reg = loc_reg(src);
   1771     from.base_type = p->type;
   1772     memset(&access, 0, sizeof access);
   1773     access.type = p->type;
   1774     access.size = p->size ? p->size : (u32)cg_type_size(t->c, p->type);
   1775     access.align = p->align ? p->align : native_type_align(t, p->type);
   1776     arm_copy_bytes(t, d_addr, from, access);
   1777     return;
   1778   }
   1779   /* AAPCS: round NCRN up to even before an 8-byte-aligned argument. The parts
   1780    * then fill core registers r0..r3; any that do not fit spill to the (8-byte-
   1781    * aligned for part 0) incoming stack window. An 8-byte scalar pair fully fits
   1782    * or fully spills (round-to-even guarantees it); a larger aggregate straddles
   1783    * the reg/stack boundary (AAPCS C.5). */
   1784   if (even) a->next_param_int = align_up_u32(a->next_param_int, 2u);
   1785   for (i = 0; i < ai->nparts; ++i) {
   1786     const ABIArgPart* part = &ai->parts[i];
   1787     KitCgTypeId lty = arm_part_scalar_type(part);
   1788     NativeLoc src;
   1789     if (a->next_param_int < 4u) {
   1790       src = native_loc_reg(lty, NATIVE_REG_INT, (Reg)(a->next_param_int++));
   1791     } else {
   1792       /* Incoming stack arg: above the saved register block = [r7 + N + k],
   1793        * where N = saved_block_bytes (8 for the bare {r7,lr} -O0 prologue; larger
   1794        * when the -O1 known-frame prologue also PUSHes callee-saves). The first
   1795        * stack part of an 8-byte-aligned arg lands on an 8-byte boundary. */
   1796       NativeAddr sa;
   1797       if (i == 0 && even)
   1798         a->next_param_stack =
   1799             align_up_u32(a->next_param_stack, 2u * ARM32_GPR_BYTES);
   1800       src = native_loc_reg(lty, NATIVE_REG_INT, ARM_SCRATCH);
   1801       memset(&sa, 0, sizeof sa);
   1802       sa.base_kind = NATIVE_ADDR_BASE_REG;
   1803       sa.base.reg = ARM_FP;
   1804       sa.base_type = lty;
   1805       sa.offset = (i32)(arm_arg_window_base(a) + a->next_param_stack);
   1806       arm_emit_mem(a, 1, src, sa, native_mem_for_type(t, lty, part->size));
   1807       a->next_param_stack += 4u;
   1808     }
   1809     if (dst.kind == NATIVE_LOC_NONE) {
   1810       /* unused parameter; cursor already advanced */
   1811     } else if (to_reg) {
   1812       NativeLoc d = native_loc_reg(dst.type ? dst.type : p->type,
   1813                                    (NativeAllocClass)dst.cls, (Reg)dst.v.reg);
   1814       if (!(src.kind == NATIVE_LOC_REG && loc_reg(src) == loc_reg(d)))
   1815         arm_move(t, d, src);
   1816     } else {
   1817       NativeAddr home;
   1818       memset(&home, 0, sizeof home);
   1819       home.base_kind = NATIVE_ADDR_BASE_FRAME;
   1820       home.base.frame = dst.v.frame;
   1821       home.base_type = lty;
   1822       home.offset = (i32)part->src_offset;
   1823       arm_emit_mem(a, 0, src, home, native_mem_for_type(t, lty, part->size));
   1824     }
   1825   }
   1826 }
   1827 
   1828 /* ============================ calls / returns ============================ */
   1829 
   1830 /* Advance the AAPCS argument-placement cursor across ONE argument `ai`, updating
   1831  * the core-register count *next_int (r0..r3) and the outgoing-stack byte count
   1832  * *stack. The single authority for the pair rule, shared by the signature
   1833  * stack-size accounting so it cannot drift from emission:
   1834  *   - an 8-byte aligned pair rounds NCRN up to even and, if it does not fully
   1835  *     fit in r0..r3, passes ENTIRELY on the 8-byte-aligned stack (no straddle);
   1836  *   - every other part takes the next core register, else a 4-byte stack slot. */
   1837 static void arm_arg_advance(const ABIArgInfo* ai, u32* next_int, u32* stack) {
   1838   int even = arm_arg_needs_even(ai);
   1839   u32 p;
   1840   if (ai->kind == ABI_ARG_IGNORE) return;
   1841   if (ai->kind == ABI_ARG_INDIRECT) {
   1842     if (*next_int < 4u) (*next_int)++;
   1843     else *stack += 4u;
   1844     return;
   1845   }
   1846   if (even) *next_int = align_up_u32(*next_int, 2u);
   1847   for (p = 0; p < ai->nparts; ++p) {
   1848     if (p == 0 && even && *next_int >= 4u)
   1849       *stack = align_up_u32(*stack, 2u * ARM32_GPR_BYTES);
   1850     if (*next_int < 4u) (*next_int)++;
   1851     else *stack += 4u;
   1852   }
   1853 }
   1854 
   1855 /* Resolve the ABI classification of call argument `i`. Named params come from
   1856  * the callee's ABIFuncInfo; unnamed (variadic) args are synthesized here. A
   1857  * scalar wider than one GPR (8-byte i64 / soft double) becomes two 4-byte INT
   1858  * parts (AAPCS32 passes them in an even/odd register pair or 8-byte-aligned on
   1859  * the stack — the even/8-byte alignment is applied by the caller below). */
   1860 static const ABIArgInfo* arm_param_abi(NativeTarget* t, const ABIFuncInfo* abi,
   1861                                        const NativeCallDesc* desc, u32 i,
   1862                                        ABIArgInfo* scratch) {
   1863   u32 sz, align;
   1864   if (abi && i < abi->nparams) return &abi->params[i];
   1865   sz = native_type_size(t, desc->args[i].type);
   1866   align = native_type_align(t, desc->args[i].type);
   1867   memset(scratch, 0, sizeof *scratch);
   1868   scratch->kind = ABI_ARG_DIRECT;
   1869   if (sz > 4u) {
   1870     u32 nparts = (sz + 3u) / 4u, p;
   1871     ABIArgPart* parts = arena_zarray(t->c->tu, ABIArgPart, nparts);
   1872     for (p = 0; p < nparts; ++p) {
   1873       u32 off = p * 4u;
   1874       parts[p].cls = ABI_CLASS_INT;
   1875       parts[p].loc = ABI_LOC_REG;
   1876       parts[p].size = (sz - off) < 4u ? (sz - off) : 4u;
   1877       parts[p].align = 4u;
   1878       parts[p].src_offset = off;
   1879     }
   1880     /* Mirror the named classifier: parts[0].align carries the argument's natural
   1881      * alignment so the AAPCS even/8-align walk treats a synthesized variadic
   1882      * i64/double (or 8-aligned aggregate) the same as a named one. */
   1883     parts[0].align = align;
   1884     scratch->nparts = nparts;
   1885     scratch->parts = parts;
   1886     return scratch;
   1887   }
   1888   scratch->nparts = 1;
   1889   {
   1890     ABIArgPart* part = arena_zarray(t->c->tu, ABIArgPart, 1);
   1891     part->cls = ABI_CLASS_INT;
   1892     part->loc = ABI_LOC_REG;
   1893     part->size = sz;
   1894     part->align = align;
   1895     scratch->parts = part;
   1896   }
   1897   return scratch;
   1898 }
   1899 
   1900 /* Outgoing stack-argument bytes for a call: int parts beyond r0..r3. */
   1901 static u32 arm_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) {
   1902   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   1903   u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
   1904   u32 stack = 0, i, p;
   1905   ABIArgInfo scratch;
   1906   if (!abi) return 0;
   1907   for (i = 0; i < desc->nargs; ++i) {
   1908     const ABIArgInfo* ai = arm_param_abi(t, abi, desc, i, &scratch);
   1909     int even = arm_arg_needs_even(ai);
   1910     if (ai->kind == ABI_ARG_IGNORE) continue;
   1911     if (ai->kind == ABI_ARG_INDIRECT) {
   1912       if (next_int < 4u) next_int++;
   1913       else {
   1914         stack = align_up_u32(stack, 4u);
   1915         stack += 4u;
   1916       }
   1917       continue;
   1918     }
   1919     if (even) next_int = align_up_u32(next_int, 2u); /* even-pair */
   1920     for (p = 0; p < ai->nparts; ++p) {
   1921       if (p == 0 && even && next_int >= 4u) stack = align_up_u32(stack, 8u);
   1922       if (next_int < 4u) next_int++;
   1923       else stack += 4u;
   1924     }
   1925   }
   1926   return align_up_u32(stack, 8u);
   1927 }
   1928 
   1929 static u32 arm_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type,
   1930                                      int* variadic, u32* nparams) {
   1931   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fn_type);
   1932   u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
   1933   u32 stack = 0, i;
   1934   if (variadic) *variadic = abi && abi->variadic;
   1935   if (nparams) *nparams = abi ? abi->nparams : 0u;
   1936   if (!abi) return 0;
   1937   for (i = 0; i < abi->nparams; ++i)
   1938     arm_arg_advance(&abi->params[i], &next_int, &stack);
   1939   return align_up_u32(stack, 8u);
   1940 }
   1941 
   1942 static u32 arm_call_stack_bytes(NativeTarget* t, const NativeCallDesc* desc) {
   1943   return arm_call_stack_size(t, desc);
   1944 }
   1945 
   1946 static void arm_load_part(NativeTarget* t, NativeLoc dst, NativeLoc src,
   1947                           u32 src_offset, u32 size) {
   1948   NativeAddr addr;
   1949   /* Load `size` bytes at src_offset of `src` into register `dst`. */
   1950   if (src.kind == NATIVE_LOC_REG) {
   1951     arm_move(t, dst, src);
   1952     return;
   1953   }
   1954   if (src.kind == NATIVE_LOC_IMM) {
   1955     i64 part;
   1956     if (!native_loc_imm_part(src, src_offset, size, &part))
   1957       arm_panic(arm_of(t), "invalid immediate argument part");
   1958     arm_load_imm(t, dst, part);
   1959     return;
   1960   }
   1961   if (native_loc_addr_role(src) == NATIVE_LOC_ADDR_ROLE_VALUE) {
   1962     if (src_offset != 0u || size != t->c->target.ptr_size ||
   1963         !native_loc_address_value(src, &addr))
   1964       arm_panic(arm_of(t), "split or invalid address-value argument");
   1965     arm_load_addr(t, dst, addr);
   1966     return;
   1967   }
   1968   {
   1969     MemAccess mem;
   1970     if (!native_loc_storage_addr(src, (i32)src_offset, &addr))
   1971       arm_panic(arm_of(t), "unsupported arg source location");
   1972     addr.base_type = dst.type;
   1973     memset(&mem, 0, sizeof mem);
   1974     mem.type = dst.type;
   1975     mem.size = size;
   1976     arm_emit_mem(arm_of(t), 1, dst, addr, mem);
   1977   }
   1978 }
   1979 
   1980 /* Store an outgoing stack argument. A normal call writes the sp-anchored
   1981  * outgoing-arg area ([sp, #stack_off]). A tail/sibling call instead writes the
   1982  * caller's incoming-arg window ([r7 + arg_window_base + stack_off]) — the exact
   1983  * address the tail-callee will read after the epilogue restores sp to the
   1984  * caller's entry sp and branches. */
   1985 static void arm_store_outgoing(NativeTarget* t, u32 stack_off, NativeLoc src,
   1986                                u32 size, int tail) {
   1987   Arm32NativeTarget* a = arm_of(t);
   1988   NativeAddr addr;
   1989   MemAccess mem;
   1990   memset(&addr, 0, sizeof addr);
   1991   addr.base_kind = NATIVE_ADDR_BASE_REG;
   1992   addr.base.reg = tail ? ARM_FP : 13u;
   1993   addr.offset = (i32)(tail ? arm_arg_window_base(a) + stack_off : stack_off);
   1994   addr.base_type = src.type;
   1995   memset(&mem, 0, sizeof mem);
   1996   mem.type = src.type;
   1997   mem.size = size;
   1998   arm_emit_mem(a, 0, src, addr, mem);
   1999 }
   2000 
   2001 /* Materialize the address of a frame/stack-resident NativeLoc into `dst` (used
   2002  * for arguments that the ABI explicitly classifies as indirect). */
   2003 static void arm_addr_of_loc(NativeTarget* t, NativeLoc dst, NativeLoc src) {
   2004   NativeAddr addr;
   2005   if (!native_loc_storage_addr(src, 0, &addr))
   2006     arm_panic(arm_of(t), "address-of non-memory location");
   2007   addr.base_type = dst.type;
   2008   arm_load_addr(t, dst, addr);
   2009 }
   2010 
   2011 static void arm_emit_one_arg_move(NativeTarget* t, const NativeArgMove* m) {
   2012   if (m->is_addr)
   2013     arm_addr_of_loc(t, m->dst, m->src);
   2014   else
   2015     arm_load_part(t, m->dst, m->src, m->src_offset, m->size);
   2016 }
   2017 
   2018 static void arm_emit_reg_arg_moves(NativeTarget* t, NativeArgMove* moves,
   2019                                    u32 n) {
   2020   NativeArgShuffle s;
   2021   if (n > ARM_MAX_REG_ARG_MOVES) arm_panic(arm_of(t), "too many register args");
   2022   memset(&s, 0, sizeof s);
   2023   s.t = t;
   2024   s.emit_one = arm_emit_one_arg_move;
   2025   s.reg_move = arm_move;
   2026   s.scratch[NATIVE_REG_INT] = ARM_SCRATCH;
   2027   s.scratch_class_mask = 1u << NATIVE_REG_INT;
   2028   native_arg_shuffle(&s, moves, n);
   2029 }
   2030 
   2031 static void arm_marshal_call(NativeTarget* t, const NativeCallDesc* desc,
   2032                              NativeCallPhase* plan) {
   2033   Arm32NativeTarget* a = arm_of(t);
   2034   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type);
   2035   NativeCallPhaseRet* rets;
   2036   u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults)
   2037                       ? abi->ret.nparts
   2038                       : ((!abi && desc->nresults) ? 1u : 0u);
   2039   int tail = (desc->flags & CG_CALL_TAIL) != 0;
   2040   int tail_callee_saved = 0;
   2041   memset(plan, 0, sizeof *plan);
   2042   rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPhaseRet, nrets_cap) : NULL;
   2043   plan->callee = desc->callee;
   2044   plan->rets = rets;
   2045   plan->flags = desc->flags;
   2046   plan->has_sret = abi && abi->has_sret;
   2047   plan->is_variadic = abi && abi->variadic;
   2048   plan->stack_arg_size = arm_call_stack_size(t, desc);
   2049   /* A tail call's outgoing stack args reuse the incoming-arg window in place, so
   2050    * they do not enlarge this frame's outgoing-arg area. */
   2051   if (!tail && plan->stack_arg_size > a->frame.max_outgoing)
   2052     a->frame.max_outgoing = plan->stack_arg_size;
   2053 
   2054   /* Outgoing stack args stage through IP normally; a tail call holds the callee
   2055    * in IP, so it stages through LR instead (LR is restored by the epilogue
   2056    * afterward; the in-window store address needs no scratch). */
   2057   u32 arg_stage = tail ? ARM_TMP : ARM_SCRATCH;
   2058 
   2059   /* Stage an indirect callee into a register that survives the arg-register
   2060    * setup, the outgoing stack-arg staging, AND (for a tail call) the epilogue's
   2061    * POP of r0..r11. A tail call holds it in IP (never popped) and stages args via
   2062    * LR; a normal call stages args via IP, so an IP- or arg-register-resident
   2063    * callee moves to LR (BLX lr is valid). A callee already in a preserved
   2064    * register (r4..r11, or the chosen staging reg) needs no move.
   2065    *
   2066    * The exception is the -O0 (single-pass) call with outgoing stack args: staging
   2067    * those args may consume BOTH backend temporaries (IP for the value, LR for a
   2068    * large-offset store address), so the callee cannot survive in LR. Spill it
   2069    * to its frame home now — before any arg move clobbers its register — and
   2070    * reload just before BLX (arm_emit_call). The known-frame (-O1) path keeps
   2071    * the register form: its optimizer-allocated callee already survives, and
   2072    * its frame is final (no body-time slot allocation). */
   2073   if (plan->callee.kind == NATIVE_LOC_REG &&
   2074       (NativeAllocClass)plan->callee.cls == NATIVE_REG_INT) {
   2075     u32 cr = plan->callee.v.reg & 0xfu;
   2076     if (!tail && !a->known_frame && plan->stack_arg_size > 0u) {
   2077       NativeFrameSlot slot = arm_callee_spill_slot(a);
   2078       NativeAddr home;
   2079       memset(&home, 0, sizeof home);
   2080       home.base_kind = NATIVE_ADDR_BASE_FRAME;
   2081       home.base.frame = slot;
   2082       home.base_type = plan->callee.type;
   2083       arm_emit_mem(a, 0, plan->callee, home,
   2084                    native_mem_for_type(t, plan->callee.type, 4));
   2085       plan->callee = native_loc_stack(plan->callee.type, slot, 0);
   2086     } else {
   2087       u32 dst = tail ? ARM_SCRATCH : ARM_TMP;
   2088       int needs = tail ? 1 : (cr <= 3u || cr == arg_stage);
   2089       if (needs && cr != dst) {
   2090         NativeLoc scratch =
   2091             native_loc_reg(plan->callee.type, NATIVE_REG_INT, dst);
   2092         arm_move(t, scratch, plan->callee);
   2093         plan->callee = scratch;
   2094       }
   2095     }
   2096   }
   2097 
   2098   /* An indirect sibling call lives in IP across argument marshalling because
   2099    * r0-r11 are restored by the tail epilogue and LR is the stack-part carrier.
   2100    * A three-byte part needs both private registers: LR carries the part while
   2101    * IP packs its final byte (and a far outgoing address can need IP as its
   2102    * resolved base). Preserve the callee with one balanced temporary stack save
   2103    * for the entire no-call marshalling interval, making both private registers
   2104    * available to the exact-width boundary. Tail stack arguments are addressed
   2105    * through FP into the incoming window, never through the temporarily shifted
   2106    * SP; CFA is FP-anchored. There are no compiler-time exits between this PUSH
   2107    * and its matching POP below. */
   2108   if (tail && plan->callee.kind == NATIVE_LOC_REG &&
   2109       loc_reg(plan->callee) == ARM_SCRATCH) {
   2110     arm_emit_push(t->mc, 1u << ARM_SCRATCH);
   2111     tail_callee_saved = 1;
   2112   }
   2113   {
   2114     u32 next_int = (abi && abi->has_sret) ? 1u : 0u;
   2115     u32 stack = 0, nmoves = 0, i, p;
   2116     NativeArgMove moves[ARM_MAX_REG_ARG_MOVES];
   2117     KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   2118     ABIArgInfo scratch;
   2119     for (i = 0; i < desc->nargs; ++i) {
   2120       const ABIArgInfo* ai = arm_param_abi(t, abi, desc, i, &scratch);
   2121       int even = arm_arg_needs_even(ai);
   2122       if (ai->kind == ABI_ARG_IGNORE) continue;
   2123       if (ai->kind == ABI_ARG_INDIRECT) {
   2124         /* Pass an ABI-classified indirect argument as a pointer to the caller's
   2125          * copy (the callee makes its own copy in bind_param). The pointer rides
   2126          * a core reg when one is free, else the outgoing stack. */
   2127         if (next_int < 4u) {
   2128           NativeArgMove* m = &moves[nmoves++];
   2129           m->dst = native_loc_reg(i32t, NATIVE_REG_INT, (Reg)(next_int++));
   2130           m->src = desc->args[i];
   2131           m->src_offset = 0;
   2132           m->size = 4u;
   2133           m->is_addr = 1;
   2134         } else {
   2135           NativeLoc ptr = native_loc_reg(i32t, NATIVE_REG_INT, arg_stage);
   2136           arm_addr_of_loc(t, ptr, desc->args[i]);
   2137           stack = align_up_u32(stack, 4u);
   2138           arm_store_outgoing(t, stack, ptr, 4u, tail);
   2139           stack += 4u;
   2140         }
   2141         continue;
   2142       }
   2143       /* AAPCS32: round NCRN up to even for an 8-byte-aligned arg, then fill core
   2144        * registers r0..r3; parts that do not fit spill to the (8-byte-aligned for
   2145        * part 0) outgoing stack. An 8-byte scalar pair fits-or-fully-spills;
   2146        * a larger aggregate straddles the reg/stack boundary (AAPCS C.5). */
   2147       if (even) next_int = align_up_u32(next_int, 2u);
   2148       for (p = 0; p < ai->nparts; ++p) {
   2149         const ABIArgPart* part = &ai->parts[p];
   2150         KitCgTypeId lty = arm_part_scalar_type(part);
   2151         if (next_int < 4u) {
   2152           NativeArgMove* m = &moves[nmoves++];
   2153           m->dst = native_loc_reg(lty, NATIVE_REG_INT, (Reg)(next_int++));
   2154           m->src = desc->args[i];
   2155           m->src_offset = part->src_offset;
   2156           m->size = part->size;
   2157           m->is_addr = 0;
   2158         } else {
   2159           if (p == 0 && even) stack = align_up_u32(stack, 8u);
   2160           else stack = align_up_u32(stack, 4u);
   2161           if (desc->args[i].kind == NATIVE_LOC_REG) {
   2162             /* Source already in a register: store it straight to the outgoing
   2163              * slot. Routing it through backend-private IP is unnecessary and
   2164              * would add an independent lifetime while other call operands are
   2165              * still leased. Keeping the source location intact also preserves
   2166              * the instruction-scoped ownership contract. */
   2167             arm_store_outgoing(t, stack, desc->args[i], part->size, tail);
   2168           } else {
   2169             NativeLoc tmp = native_loc_reg(lty, NATIVE_REG_INT, arg_stage);
   2170             arm_load_part(t, tmp, desc->args[i], part->src_offset, part->size);
   2171             arm_store_outgoing(t, stack, tmp, part->size, tail);
   2172           }
   2173           stack += 4u;
   2174         }
   2175       }
   2176     }
   2177     arm_emit_reg_arg_moves(t, moves, nmoves);
   2178     if (abi && abi->has_sret) {
   2179       /* sret destination pointer in r0. A tail call forwards OUR own incoming
   2180        * sret pointer (spilled at entry) so the tail-callee writes the result
   2181        * straight into our caller's buffer; a normal call passes &result. */
   2182       NativeLoc r0 = native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0);
   2183       if (tail && a->has_sret) {
   2184         NativeLoc saved = native_loc_stack(i32t, a->sret_ptr_slot, 0);
   2185         arm_load_part(t, r0, saved, 0, 4u);
   2186       } else if (desc->nresults) {
   2187         arm_load_addr(t, r0, (NativeAddr){.base_kind = NATIVE_ADDR_BASE_FRAME,
   2188                                           .base.frame = desc->results[0].v.frame,
   2189                                           .base_type = i32t});
   2190       }
   2191     }
   2192   }
   2193   if (tail_callee_saved) arm_emit_pop(t->mc, 1u << ARM_SCRATCH);
   2194   if (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) {
   2195     u32 nr = 0, ni = 0, p;
   2196     for (p = 0; p < abi->ret.nparts; ++p) {
   2197       const ABIArgPart* part = &abi->ret.parts[p];
   2198       /* Per-lane type so each NDT-emitted result move is sized to the lane (4),
   2199        * not the full 8-byte scalar (which the 32-bit backend cannot single-step
   2200        * load/store). The ret pair is always r0:r1 (no even-rounding needed). */
   2201       KitCgTypeId pty = arm_part_scalar_type(part);
   2202       rets[nr].src = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++));
   2203       rets[nr].dst = desc->results[0];
   2204       if (rets[nr].dst.kind == NATIVE_LOC_FRAME)
   2205         rets[nr].dst = native_loc_stack(pty, desc->results[0].v.frame,
   2206                                         (i32)part->src_offset);
   2207       rets[nr].mem = native_mem_for_type(t, pty, part->size);
   2208       nr++;
   2209     }
   2210     plan->nrets = nr;
   2211   } else if (!abi && desc->nresults) {
   2212     rets[0].src = native_loc_reg(desc->results[0].type, NATIVE_REG_INT, ARM_R0);
   2213     rets[0].dst = desc->results[0];
   2214     rets[0].mem = native_mem_for_type(t, desc->results[0].type, 0);
   2215     plan->nrets = 1;
   2216   }
   2217 }
   2218 
   2219 static void arm_emit_call(NativeTarget* t, const NativeCallPhase* plan) {
   2220   Arm32NativeTarget* a = arm_of(t);
   2221   MCEmitter* mc = t->mc;
   2222   if (plan->flags & CG_CALL_TAIL) {
   2223     /* Sibling call: the outgoing args are already placed (register args live;
   2224      * stack args written into the incoming-arg window). Tear the frame down with
   2225      * lr restored to OUR caller's return address, then BRANCH to the callee — it
   2226      * runs on our caller's frame and returns straight to our caller. The arm32
   2227      * epilogue is frame-size-independent (MOV sp,r7), so no patch is needed even
   2228      * on the single-pass path. The callee is direct (B.W) or held in IP (BX). */
   2229     if (plan->callee.kind != NATIVE_LOC_GLOBAL &&
   2230         plan->callee.kind != NATIVE_LOC_REG)
   2231       arm_panic(a, "unsupported tail target");
   2232     arm_emit_frame_restore(a, /*to_pc=*/0);
   2233     if (plan->callee.kind == NATIVE_LOC_GLOBAL) {
   2234       u32 pos = mc_pos(mc);
   2235       arm_emit_t32(mc, arm_b_w());
   2236       mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_JUMP24,
   2237                        plan->callee.v.global.sym, plan->callee.v.global.addend, 0,
   2238                        0);
   2239     } else {
   2240       arm_emit_t16(mc, arm_bx(loc_reg(plan->callee)));
   2241     }
   2242     return;
   2243   }
   2244   if (plan->callee.kind == NATIVE_LOC_GLOBAL) {
   2245     u32 pos = mc_pos(mc);
   2246     arm_emit_t32(mc, arm_bl());
   2247     mc_emit_reloc_at(mc, mc->section_id, pos, R_ARM_THM_CALL,
   2248                      plan->callee.v.global.sym, plan->callee.v.global.addend, 0,
   2249                      0);
   2250     return;
   2251   }
   2252   if (plan->callee.kind == NATIVE_LOC_REG) {
   2253     arm_emit_t16(mc, arm_blx_reg(loc_reg(plan->callee)));
   2254     return;
   2255   }
   2256   if (plan->callee.kind == NATIVE_LOC_STACK) {
   2257     /* -O0 indirect call with stack args: the callee was spilled to a frame home
   2258      * (arm_marshal_call) to free the backend staging registers. Arg staging is
   2259      * done now, so IP is free; reload the target and BLX it. */
   2260     NativeLoc tmp = native_loc_reg(plan->callee.type, NATIVE_REG_INT, ARM_SCRATCH);
   2261     NativeAddr home;
   2262     memset(&home, 0, sizeof home);
   2263     home.base_kind = NATIVE_ADDR_BASE_FRAME;
   2264     home.base.frame = plan->callee.v.stack.slot;
   2265     home.base_type = plan->callee.type;
   2266     arm_emit_mem(a, 1, tmp, home, native_mem_for_type(t, plan->callee.type, 4));
   2267     arm_emit_t16(mc, arm_blx_reg(ARM_SCRATCH));
   2268     return;
   2269   }
   2270   arm_panic(a, "unsupported call target");
   2271 }
   2272 
   2273 static void arm_marshal_ret(NativeTarget* t, const CGFuncDesc* fd,
   2274                             const NativeLoc* value,
   2275                             NativeCallPhaseRet** out_rets, u32* out_nrets) {
   2276   Arm32NativeTarget* a = arm_of(t);
   2277   const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type);
   2278   NativeCallPhaseRet* rets = NULL;
   2279   u32 nr = 0;
   2280   if (value) rets = arena_zarray(t->c->tu, NativeCallPhaseRet, 4);
   2281   if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) {
   2282     /* Large aggregate return: copy the value into the caller-provided
   2283      * destination via the hidden sret pointer spilled at entry. The body's
   2284      * return slot is *value; load the saved pointer into ip, then copy. No
   2285      * register return parts. */
   2286     KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   2287     NativeLoc dstp = native_loc_reg(i32t, NATIVE_REG_INT, ARM_SCRATCH);
   2288     NativeLoc saved = native_loc_stack(i32t, a->sret_ptr_slot, 0);
   2289     NativeAddr dst_addr, src_addr;
   2290     AggregateAccess access;
   2291     arm_load_part(t, dstp, saved, 0, 4u);
   2292     memset(&dst_addr, 0, sizeof dst_addr);
   2293     dst_addr.base_kind = NATIVE_ADDR_BASE_REG;
   2294     dst_addr.base.reg = ARM_SCRATCH;
   2295     dst_addr.base_type = value->type;
   2296     memset(&src_addr, 0, sizeof src_addr);
   2297     if (value->kind == NATIVE_LOC_FRAME) {
   2298       src_addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   2299       src_addr.base.frame = value->v.frame;
   2300     } else if (value->kind == NATIVE_LOC_STACK) {
   2301       src_addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   2302       src_addr.base.frame = value->v.stack.slot;
   2303       src_addr.offset = value->v.stack.offset;
   2304     } else {
   2305       arm_panic(a, "indirect return value must be in memory");
   2306     }
   2307     src_addr.base_type = value->type;
   2308     memset(&access, 0, sizeof access);
   2309     access.type = value->type;
   2310     access.size = (u32)cg_type_size(t->c, value->type);
   2311     access.align = native_type_align(t, value->type);
   2312     arm_copy_bytes(t, dst_addr, src_addr, access);
   2313     *out_rets = NULL;
   2314     *out_nrets = 0;
   2315     return;
   2316   }
   2317   if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) {
   2318     u32 ni = 0, p;
   2319     int exact_parts = 0;
   2320     for (p = 0; p < abi->ret.nparts; ++p) {
   2321       const ABIArgPart* part = &abi->ret.parts[p];
   2322       if (part->size != 1u && part->size != 2u && part->size != 4u &&
   2323           part->size != 8u) {
   2324         exact_parts = 1;
   2325         break;
   2326       }
   2327     }
   2328 
   2329     /* A return-plan register destination makes the generic write phase
   2330      * materialize its source using the scalar carrier type. For a three-byte
   2331      * AAPCS aggregate tail that carrier is I32, so the exact mem.size=3 on the
   2332      * final move arrives too late: materialization has already read four bytes.
   2333      * Future 5/6/7-byte parts have the same mismatch. Once any such part is
   2334      * present, fill every return register here in ABI order through the shared
   2335      * exact-width argument boundary. arm_load_part uses only backend-private
   2336      * temporaries, preserving return registers populated by earlier parts. */
   2337     if (exact_parts) {
   2338       for (p = 0; p < abi->ret.nparts; ++p) {
   2339         const ABIArgPart* part = &abi->ret.parts[p];
   2340         KitCgTypeId pty = arm_part_scalar_type(part);
   2341         NativeLoc dst =
   2342             native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++));
   2343         arm_load_part(t, dst, *value, part->src_offset, part->size);
   2344       }
   2345       *out_rets = NULL;
   2346       *out_nrets = 0;
   2347       return;
   2348     }
   2349 
   2350     for (p = 0; p < abi->ret.nparts; ++p) {
   2351       const ABIArgPart* part = &abi->ret.parts[p];
   2352       /* Per-lane type: an i64/double return is two i32 lanes in r0:r1; sizing
   2353        * each move from the lane type keeps the 32-bit backend off the 8-byte
   2354        * memory path. (See arm_part_scalar_type.) */
   2355       KitCgTypeId pty = arm_part_scalar_type(part);
   2356       rets[nr].src = *value;
   2357       if (rets[nr].src.kind == NATIVE_LOC_FRAME)
   2358         rets[nr].src =
   2359             native_loc_stack(pty, value->v.frame, (i32)part->src_offset);
   2360       rets[nr].dst = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++));
   2361       rets[nr].mem = native_mem_for_type(t, pty, part->size);
   2362       nr++;
   2363     }
   2364   } else if (value) {
   2365     rets[0].src = *value;
   2366     rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, ARM_R0);
   2367     rets[0].mem = native_mem_for_type(t, value->type, 0);
   2368     nr = 1;
   2369   }
   2370   *out_rets = rets;
   2371   *out_nrets = nr;
   2372 }
   2373 
   2374 static void arm_ret(NativeTarget* t) {
   2375   Arm32NativeTarget* a = arm_of(t);
   2376   arm_jump(t, a->epilogue_label);
   2377 }
   2378 
   2379 /* ============================ misc / stubs ============================ */
   2380 
   2381 static void arm_trap(NativeTarget* t) { arm_emit_t16(t->mc, arm_bkpt(0u)); }
   2382 static void arm_set_loc(NativeTarget* t, SrcLoc loc) {
   2383   arm_of(t)->loc = loc;
   2384   mc_set_loc(t->mc, loc);
   2385 }
   2386 
   2387 #define ARM_UNIMPL(name) arm_panic(arm_of(t), name " not implemented in Phase 1")
   2388 
   2389 /* Assemble up to 4 little-endian bytes of `c` starting at byte `off` into a u32. */
   2390 static u32 arm_const_lane(ConstBytes c, u32 off) {
   2391   u32 v = 0, i;
   2392   for (i = 0; i < 4u && off + i < c.size; ++i)
   2393     v |= (u32)c.bytes[off + i] << (i * 8u);
   2394   return v;
   2395 }
   2396 
   2397 static void arm_load_const(NativeTarget* t, NativeLoc dst, ConstBytes c) {
   2398   Arm32NativeTarget* a = arm_of(t);
   2399   /* Scalar constant <=4 bytes: reinterpret the ABI bytes (little-endian on
   2400    * arm32) as a u32 and materialize via arm_load_imm (MOV.W/MVN.W/MOVW+MOVT). */
   2401   if (c.size <= 4u) {
   2402     arm_load_imm(t, dst, (i64)(i32)arm_const_lane(c, 0u));
   2403     return;
   2404   }
   2405   /* 8-byte (i64/double) backstop. At -O0 the NDT lowers wide8 constants into two
   2406    * 32-bit lanes before they reach the backend (nd_load_const guards it), so
   2407    * this is only exercised by paths that hand the full 8-byte constant straight
   2408    * to a destination — materialize the low lane then the high lane, little-end
   2409    * first. A memory destination (frame/stack home) carries both lanes 4 bytes
   2410    * apart; a single 32-bit register can only hold the low lane (the high lane
   2411    * has no home in one NativeLoc), which matches a 32-bit reinterpret. */
   2412   if (c.size > 8u) arm_panic(a, "load_const wider than 8 bytes");
   2413   if (dst.kind == NATIVE_LOC_REG) {
   2414     arm_load_imm(t, dst, (i64)(i32)arm_const_lane(c, 0u));
   2415     return;
   2416   }
   2417   {
   2418     KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   2419     NativeLoc lane = native_loc_reg(i32t, NATIVE_REG_INT, ARM_TMP);
   2420     NativeAddr home;
   2421     MemAccess mem = native_mem_for_type(t, i32t, 4u);
   2422     u32 off;
   2423     memset(&home, 0, sizeof home);
   2424     home.base_type = i32t;
   2425     if (dst.kind == NATIVE_LOC_FRAME) {
   2426       home.base_kind = NATIVE_ADDR_BASE_FRAME;
   2427       home.base.frame = dst.v.frame;
   2428     } else if (dst.kind == NATIVE_LOC_STACK) {
   2429       home.base_kind = NATIVE_ADDR_BASE_FRAME;
   2430       home.base.frame = dst.v.stack.slot;
   2431       home.offset = dst.v.stack.offset;
   2432     } else {
   2433       arm_panic(a, "8-byte load_const destination not lowered");
   2434     }
   2435     for (off = 0; off < c.size; off += 4u) {
   2436       NativeAddr d = home;
   2437       d.offset += (i32)off;
   2438       arm_load_imm(t, lane, (i64)(i32)arm_const_lane(c, off));
   2439       arm_emit_mem(a, 0, lane, d, mem);
   2440     }
   2441   }
   2442 }
   2443 static void arm_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) {
   2444   /* `&&label` address-take (computed goto / jump-table base): materialize the
   2445    * label's address via MOVW/MOVT ABS against its per-block local symbol — the
   2446    * same form arm_emit_global_addr uses for a global, so an encoding-divergent
   2447    * assembler recomputes the split immediate. (Setting the Thumb bit for a
   2448    * BX/BLX through this address is the indirect-branch consumer's job.) */
   2449   Arm32NativeTarget* a = arm_of(t);
   2450   ObjSymId sym = mc_label_symbol(t->mc, l);
   2451   arm_emit_global_addr(a, loc_reg(dst), sym, 0);
   2452 }
   2453 static void arm_indirect_branch(NativeTarget* t, NativeLoc addr,
   2454                                 const MCLabel* targets, u32 n) {
   2455   /* The cg layer materialized the dense switch's target code address into
   2456    * `addr` (a load of table[idx] from the .Lkit_jt rodata table). The table
   2457    * entries are R_ARM_ABS32 relocations against per-block local label symbols,
   2458    * which carry NO Thumb bit (only STT_FUNC symbols get it in arm_func_end), so
   2459    * the loaded address is even. BX to an even address would switch to ARM
   2460    * state (UNPREDICTABLE on M-profile); force the Thumb bit with ORR #1 first.
   2461    * `addr` is released by the caller right after this, so clobbering it is free.
   2462    */
   2463   MCEmitter* mc = t->mc;
   2464   u32 ra = loc_reg(addr);
   2465   u32 enc;
   2466   (void)targets;
   2467   (void)n;
   2468   thumb_expand_imm_encode(1u, &enc);
   2469   arm_emit_t32(mc, arm_dp_imm(2u, 0u, ra, ra, enc)); /* ORR ra, ra, #1 */
   2470   arm_emit_t16(mc, arm_bx(ra));                       /* BX ra */
   2471 }
   2472 static void arm_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym,
   2473                             i64 addend) {
   2474   /* The M-profile (Cortex-M) has no CP15 thread-ID register and the bare-metal
   2475    * arm32 lane is single-threaded, so the local-exec model degenerates: each
   2476    * `__thread` object has exactly one instance, addressed absolutely like a
   2477    * regular static. Materialize its address with the same MOVW/MOVT-absolute
   2478    * sequence as a global; the freestanding image places the TLS image as part
   2479    * of its data, so &tlsvar is a link-time constant. (A true variant-I TLS path
   2480    * with a software thread pointer is a follow-on for a multi-threaded lane.) */
   2481   arm_emit_global_addr(arm_of(t), loc_reg(dst), sym, addend);
   2482 }
   2483 /* Aggregate / wide byte copy (struct/array, INDIRECT param/return copies, the
   2484  * wide va_arg copy, and memmove's loops). Each side is resolved ONCE to a stable
   2485  * (base register, displacement): a FRAME/REG side in range for the whole span is
   2486  * addressed directly; a GLOBAL or out-of-range side has its effective address
   2487  * materialized into a held scratch. A 4|2|1 granule ladder then transfers each
   2488  * chunk through a register that aliases neither base.
   2489  *
   2490  * Register budget, allocated explicitly up front: two reserved scratch (IP, LR);
   2491  * one callee-save borrowed (pushed/popped) only for the rare case where BOTH
   2492  * sides must be materialized AND the two bases plus the transfer exceed {IP,LR}.
   2493  * The borrow is taken before any frame-relative materialization, so r7-relative
   2494  * offsets (and already-held REG bases) are unaffected by the SP change. */
   2495 
   2496 /* The natural (base register, displacement) of a FRAME/REG addr, no range check. */
   2497 static void arm_addr_natural(Arm32NativeTarget* a, const NativeAddr* addr,
   2498                              u32* base, i32* off) {
   2499   if (addr->base_kind == NATIVE_ADDR_BASE_FRAME) {
   2500     NativeFrameSlotEntry* s = native_frame_slot_at(&a->frame, addr->base.frame);
   2501     *base = ARM_FP;
   2502     *off = -(i32)s->off + addr->offset;
   2503   } else {
   2504     *base = addr->base.reg & 0xfu;
   2505     *off = addr->offset;
   2506   }
   2507 }
   2508 
   2509 /* Whether `addr` can be addressed directly for a `span`-byte access (a real base
   2510  * register + in-range displacement for the whole span). GLOBAL is never direct.
   2511  * On success sets base+off to the natural (base register, displacement). */
   2512 static int arm_addr_direct(Arm32NativeTarget* a, const NativeAddr* addr,
   2513                            u32 span, u32* base, i32* off) {
   2514   if (addr->base_kind != NATIVE_ADDR_BASE_FRAME &&
   2515       addr->base_kind != NATIVE_ADDR_BASE_REG)
   2516     return 0;
   2517   arm_addr_natural(a, addr, base, off);
   2518   return arm_span_inline(*off, span);
   2519 }
   2520 
   2521 /* Take a transfer/held-base register for copy_bytes: a free reserved scratch
   2522  * (IP then LR) not already in `*used`, else a borrowed callee-save (r4..r6 not in
   2523  * `*used`, pushed; recorded in `*borrowed`, which starts 0xff and is set once). */
   2524 static u32 arm_copy_take_reg(Arm32NativeTarget* a, u32* used, u32* borrowed) {
   2525   u32 r;
   2526   if (!(*used & (1u << ARM_SCRATCH))) r = ARM_SCRATCH;
   2527   else if (!(*used & (1u << ARM_TMP))) r = ARM_TMP;
   2528   else {
   2529     r = (*used & (1u << 4)) ? ((*used & (1u << 5)) ? 6u : 5u) : 4u;
   2530     *borrowed = r;
   2531     arm_emit_push(a->base.mc, 1u << r);
   2532   }
   2533   *used |= 1u << r;
   2534   return r;
   2535 }
   2536 
   2537 static void arm_copy_bytes(NativeTarget* t, NativeAddr dst, NativeAddr src,
   2538                            AggregateAccess acc) {
   2539   Arm32NativeTarget* a = arm_of(t);
   2540   u32 size = acc.size, off;
   2541   u32 sbase, dbase, xfer, used = 0u, borrowed = 0xffu;
   2542   i32 soff = 0, doff = 0;
   2543   int sdir, ddir;
   2544   if (!size) return;
   2545   if (size > 4096u) arm_panic(a, "aggregate copy > 4096 bytes (needs memcpy)");
   2546 
   2547   sdir = arm_addr_direct(a, &src, size, &sbase, &soff);
   2548   ddir = arm_addr_direct(a, &dst, size, &dbase, &doff);
   2549   if (sdir) used |= 1u << sbase;
   2550   if (ddir) used |= 1u << dbase;
   2551 
   2552   /* Allocate held scratch for the non-direct sides, then the transfer reg. */
   2553   if (!sdir) sbase = arm_copy_take_reg(a, &used, &borrowed);
   2554   if (!ddir) dbase = arm_copy_take_reg(a, &used, &borrowed);
   2555   xfer = arm_copy_take_reg(a, &used, &borrowed);
   2556 
   2557   /* Materialize the non-direct sides (after any borrow-push so a frame-relative
   2558    * address still reads the unchanged r7). */
   2559   if (!sdir) {
   2560     if (src.base_kind == NATIVE_ADDR_BASE_GLOBAL)
   2561       arm_emit_global_addr(a, sbase, src.base.global.sym,
   2562                            src.base.global.addend + src.offset);
   2563     else {
   2564       u32 nb;
   2565       i32 no;
   2566       arm_addr_natural(a, &src, &nb, &no);
   2567       arm_emit_base_off(a, sbase, nb, no);
   2568     }
   2569     soff = 0;
   2570   }
   2571   if (!ddir) {
   2572     if (dst.base_kind == NATIVE_ADDR_BASE_GLOBAL)
   2573       arm_emit_global_addr(a, dbase, dst.base.global.sym,
   2574                            dst.base.global.addend + dst.offset);
   2575     else {
   2576       u32 nb;
   2577       i32 no;
   2578       arm_addr_natural(a, &dst, &nb, &no);
   2579       arm_emit_base_off(a, dbase, nb, no);
   2580     }
   2581     doff = 0;
   2582   }
   2583 
   2584   /* Granule ladder: word, then trailing halfword/byte. Each chunk stays in range
   2585    * (the resolver guaranteed the span fits, and a materialized base has off 0). */
   2586   for (off = 0; off < size;) {
   2587     u32 rem = size - off;
   2588     u32 sz = rem >= 4u ? 4u : rem >= 2u ? 2u : 1u;
   2589     arm_emit_mem_one(a, 1, xfer, sbase, soff + (i32)off, sz);
   2590     arm_emit_mem_one(a, 0, xfer, dbase, doff + (i32)off, sz);
   2591     off += sz;
   2592   }
   2593   if (borrowed != 0xffu) arm_emit_pop(a->base.mc, 1u << borrowed);
   2594 }
   2595 
   2596 /* Copy `size` bytes between two register-based addresses with the same 4|2|1
   2597  * granule ladder as copy_bytes, but in a chosen direction: forward (low→high)
   2598  * or backward (high→low). memmove picks the overlap-safe direction at runtime.
   2599  * Transfers through `xfer`, which must alias neither base. */
   2600 static void arm_copy_granules_dir(Arm32NativeTarget* a, u32 dbase, u32 sbase,
   2601                                   u32 size, u32 xfer, int backward) {
   2602   if (backward) {
   2603     u32 off = size;
   2604     while (off > 0u) {
   2605       u32 sz = off >= 4u ? 4u : off >= 2u ? 2u : 1u;
   2606       off -= sz;
   2607       arm_emit_mem_one(a, 1, xfer, sbase, (i32)off, sz);
   2608       arm_emit_mem_one(a, 0, xfer, dbase, (i32)off, sz);
   2609     }
   2610   } else {
   2611     u32 off = 0;
   2612     while (off < size) {
   2613       u32 rem = size - off;
   2614       u32 sz = rem >= 4u ? 4u : rem >= 2u ? 2u : 1u;
   2615       arm_emit_mem_one(a, 1, xfer, sbase, (i32)off, sz);
   2616       arm_emit_mem_one(a, 0, xfer, dbase, (i32)off, sz);
   2617       off += sz;
   2618     }
   2619   }
   2620 }
   2621 
   2622 /* set_bytes: store the materialized fill byte `v` across acc.size bytes. The
   2623  * common case is struct/array zero-init (v == 0); any fill byte is correct. One
   2624  * STRB per byte — correctness-first; word-splat widening is deferred (it would
   2625  * need a second scratch for the 0x01010101 multiplier or a shifted-ORR encoder,
   2626  * and the dst base may itself live in ip). */
   2627 static void arm_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc v,
   2628                           AggregateAccess acc) {
   2629   Arm32NativeTarget* a = arm_of(t);
   2630   MemAccess mem = acc.mem;
   2631   mem.size = 1u;
   2632   mem.align = 1u;
   2633   for (u32 off = 0; off < acc.size; ++off) {
   2634     NativeAddr d = dst;
   2635     d.offset += (i32)off;
   2636     arm_emit_mem(a, 0, v, d, mem); /* STRB v, [dst+off] */
   2637   }
   2638 }
   2639 /* C bit-fields. The storage unit {bf.storage.type, bf.storage.size} lives at
   2640  * bf.storage_offset from `addr`; the field is `bf.bit_width` bits at bit
   2641  * bf.bit_offset within it. Load = load the unit (zero-extended) then UBFX/SBFX;
   2642  * store = load the unit, BFI the value in, store the unit back. Sub-word storage
   2643  * units (1/2 bytes) are loaded/stored at their natural width, so the BFI/UBFX
   2644  * geometry (offset+width <= storage_bits) stays in range. */
   2645 static void arm_bitfield_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
   2646                               BitFieldAccess bf) {
   2647   Arm32NativeTarget* a = arm_of(t);
   2648   MCEmitter* mc = t->mc;
   2649   u32 rd = loc_reg(dst);
   2650   u32 width = bf.bit_width ? bf.bit_width : 1u;
   2651   NativeAddr saddr = addr;
   2652   NativeLoc unit = dst;
   2653   saddr.offset += (i32)bf.storage_offset;
   2654   unit.type = bf.storage.type ? bf.storage.type : dst.type;
   2655   arm_emit_mem(a, 1, unit, saddr, bf.storage); /* LDR(B/H) rd, [addr+soff] */
   2656   if (bf.signed_)
   2657     arm_emit_t32(mc, arm_sbfx(rd, rd, bf.bit_offset, width));
   2658   else
   2659     arm_emit_t32(mc, arm_ubfx(rd, rd, bf.bit_offset, width));
   2660 }
   2661 static void arm_bitfield_store(NativeTarget* t, NativeAddr addr, NativeLoc v,
   2662                                BitFieldAccess bf) {
   2663   Arm32NativeTarget* a = arm_of(t);
   2664   MCEmitter* mc = t->mc;
   2665   u32 rv = loc_reg(v);
   2666   u32 width = bf.bit_width ? bf.bit_width : 1u;
   2667   KitCgTypeId unit_ty = bf.storage.type ? bf.storage.type : v.type;
   2668   NativeLoc word = native_loc_reg(unit_ty, NATIVE_REG_INT, ARM_TMP);
   2669   NativeAddr saddr = addr;
   2670   saddr.offset += (i32)bf.storage_offset;
   2671   arm_emit_mem(a, 1, word, saddr, bf.storage);              /* LDR tmp, [addr] */
   2672   arm_emit_t32(mc, arm_bfi(ARM_TMP, rv, bf.bit_offset, width)); /* insert v */
   2673   arm_emit_mem(a, 0, word, saddr, bf.storage);              /* STR tmp, [addr] */
   2674 }
   2675 static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size,
   2676                        u32 align) {
   2677   Arm32NativeTarget* a = arm_of(t);
   2678   MCEmitter* mc = t->mc;
   2679   u32 rsz = loc_reg(size), rd = loc_reg(dst);
   2680   u32 enc7;
   2681   (void)align; /* AAPCS keeps sp 8-byte aligned; round the request up to 8. */
   2682   thumb_expand_imm_encode(7u, &enc7);
   2683   /* ip = (size + 7) & ~7; sp -= ip. */
   2684   arm_emit_t32(mc, arm_add_imm12(ARM_SCRATCH, rsz, 7u));
   2685   arm_emit_t32(mc, arm_dp_imm(1u, 0u, ARM_SCRATCH, ARM_SCRATCH, enc7)); /* BIC */
   2686   arm_emit_t32(mc, arm_sub_reg(13u, 13u, ARM_SCRATCH));
   2687   /* dst = sp + max_outgoing (the block sits above the outgoing-arg area). The
   2688    * offset is finalized in arm_func_end; emit a placeholder ADDW to patch. */
   2689   if (a->n_alloca_patch >= 16u) arm_panic(a, "too many alloca sites");
   2690   a->alloca_patch_rd[a->n_alloca_patch] = (u8)rd;
   2691   a->alloca_patch_pos[a->n_alloca_patch++] = mc_pos(mc);
   2692   arm_emit_t32(mc, arm_add_imm12(rd, 13u, 0u));
   2693   a->frame.has_alloca = 1;
   2694 }
   2695 /* ============================ atomics ============================ */
   2696 /* ARMv7-M atomics over LDREX/STREX (word/halfword/byte) + DMB. The cg layer
   2697  * routes 8-byte _Atomic to the spinlock libcall (atomic_lock_free_max=4), so
   2698  * only <=4-byte ops reach here.
   2699  *
   2700  * Register budget: LR (ARM_TMP) holds the access address. On the shared O0
   2701  * path, a value operand may occupy IP (ARM_SCRATCH); on O1, operands arrive in
   2702  * allocated r4..r11 or instruction-scoped r0..r3 locations and IP/LR remain
   2703  * backend-private. The core must therefore keep IP unavailable for its own
   2704  * extra state so it is safe on both paths. STREX additionally needs its status,
   2705  * value and base registers mutually distinct.
   2706  *
   2707  * The rmw/cas loops therefore borrow callee-saved registers for the private
   2708  * temporaries (new value + STREX status). At -O1 the optimizer freely allocates
   2709  * the operands (prior/expected/desired/ok, or rmw dst/val) and the address base
   2710  * across the whole callee-saved set, so a fixed r4/r5 would alias an operand. The
   2711  * borrow is therefore OPERAND-AWARE (arm_atomic_borrow): it picks free callee-
   2712  * saved registers clear of the live operand+base set, guaranteeing base + all
   2713    * operands + the STREX temporaries are mutually distinct. The picked
   2714    * registers are push/pop-balanced so the
   2715  * caller's callee-saved values are preserved (free at -O0, safe at -O1). */
   2716 
   2717 /* Pick `n` distinct callee-saved temporaries (r4,r5,r6,r8,r9,r10,r11 — fp/r7 is
   2718  * reserved) clear of the `used` register set, for the atomic LDREX/STREX private
   2719  * temporaries. Fills regs[0..n) and returns their push/pop mask in *mask. The
   2720  * pool has 7 registers; the largest live set (rmw: dst+val+base = 3, cas:
   2721  * 4 operands + base = 5) always leaves enough free for n (2 / 1 respectively). */
   2722 static void arm_atomic_borrow(Arm32NativeTarget* a, u32 used, u32 n, u32* regs,
   2723                               u32* mask) {
   2724   static const u32 pool[] = {4u, 5u, 6u, 8u, 9u, 10u, 11u};
   2725   u32 got = 0u, m = 0u, i;
   2726   for (i = 0; i < sizeof pool / sizeof pool[0] && got < n; ++i) {
   2727     if (used & (1u << pool[i])) continue;
   2728     regs[got++] = pool[i];
   2729     m |= 1u << pool[i];
   2730   }
   2731   if (got < n) arm_panic(a, "atomic: no free scratch register");
   2732   *mask = m;
   2733 }
   2734 
   2735 static int arm_order_acquire(KitCgMemOrder o) {
   2736   return o == KIT_CG_MO_CONSUME || o == KIT_CG_MO_ACQUIRE ||
   2737          o == KIT_CG_MO_ACQ_REL || o == KIT_CG_MO_SEQ_CST;
   2738 }
   2739 static int arm_order_release(KitCgMemOrder o) {
   2740   return o == KIT_CG_MO_RELEASE || o == KIT_CG_MO_ACQ_REL ||
   2741          o == KIT_CG_MO_SEQ_CST;
   2742 }
   2743 
   2744 /* Materialize the access address into lr (ARM_TMP) and return it. lr is never an
   2745  * NDT operand and is dead in the body, so it never collides with the value
   2746  * operands (which the value-cache keeps in r0..r3 / ip). The incoming NativeAddr
   2747  * may be a frame slot or a register; load_addr lands the pointer in lr, which
   2748  * the LDREX/STREX sequence then uses as the base. */
   2749 static u32 arm_atomic_addr_reg(Arm32NativeTarget* a, NativeAddr addr) {
   2750   /* Use the pointer's own register when the address already is one (the -O1
   2751    * case: location MIR keeps the allocated address component explicit).
   2752    * Otherwise (a FRAME/GLOBAL address, e.g. -O0) materialize into
   2753    * backend-private LR, which holds no operand there. */
   2754   if (addr.base_kind == NATIVE_ADDR_BASE_REG && addr.offset == 0 &&
   2755       addr.index_kind == NATIVE_ADDR_INDEX_NONE)
   2756     return addr.base.reg & 0xfu;
   2757   arm_load_addr(&a->base,
   2758                 native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32), NATIVE_REG_INT,
   2759                                ARM_TMP),
   2760                 addr);
   2761   return ARM_TMP;
   2762 }
   2763 
   2764 /* LDREX-family load for the access width (word / halfword / byte). */
   2765 static void arm_emit_ldrex(Arm32NativeTarget* a, u32 sz, u32 rt, u32 base) {
   2766   MCEmitter* mc = a->base.mc;
   2767   if (sz >= 4u) arm_emit_t32(mc, arm_ldrex(rt, base, 0u));
   2768   else if (sz == 2u) arm_emit_t32(mc, arm_ldrexh(rt, base));
   2769   else arm_emit_t32(mc, arm_ldrexb(rt, base));
   2770 }
   2771 /* STREX-family store; writes the 0/1 success status into `rd`. */
   2772 static void arm_emit_strex(Arm32NativeTarget* a, u32 sz, u32 rd, u32 rt,
   2773                            u32 base) {
   2774   MCEmitter* mc = a->base.mc;
   2775   if (sz >= 4u) arm_emit_t32(mc, arm_strex(rd, rt, base, 0u));
   2776   else if (sz == 2u) arm_emit_t32(mc, arm_strexh(rd, rt, base));
   2777   else arm_emit_t32(mc, arm_strexb(rd, rt, base));
   2778 }
   2779 
   2780 static void arm_atomic_load(NativeTarget* t, NativeLoc dst, NativeAddr addr,
   2781                             MemAccess mem, KitCgMemOrder order) {
   2782   Arm32NativeTarget* a = arm_of(t);
   2783   MCEmitter* mc = t->mc;
   2784   u32 sz = mem.size ? mem.size : native_type_size(t, dst.type);
   2785   u32 base = arm_atomic_addr_reg(a, addr);
   2786   NativeAddr m;
   2787   if (sz > 4u) arm_panic(a, "8-byte atomic load not lowered (spinlock libcall)");
   2788   memset(&m, 0, sizeof m);
   2789   m.base_kind = NATIVE_ADDR_BASE_REG;
   2790   m.base.reg = base;
   2791   m.base_type = dst.type;
   2792   arm_emit_mem(a, 1, dst, m, mem); /* plain LDR/LDRH/LDRB is atomic for <=4B */
   2793   if (arm_order_acquire(order)) arm_emit_t32(mc, arm_dmb(0xfu));
   2794 }
   2795 
   2796 static void arm_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc v,
   2797                              MemAccess mem, KitCgMemOrder order) {
   2798   Arm32NativeTarget* a = arm_of(t);
   2799   MCEmitter* mc = t->mc;
   2800   u32 sz = mem.size ? mem.size : native_type_size(t, v.type);
   2801   u32 base = arm_atomic_addr_reg(a, addr);
   2802   NativeAddr m;
   2803   if (sz > 4u) arm_panic(a, "8-byte atomic store not lowered (spinlock libcall)");
   2804   if (arm_order_release(order)) arm_emit_t32(mc, arm_dmb(0xfu));
   2805   memset(&m, 0, sizeof m);
   2806   m.base_kind = NATIVE_ADDR_BASE_REG;
   2807   m.base.reg = base;
   2808   m.base_type = v.type;
   2809   arm_emit_mem(a, 0, v, m, mem); /* plain STR/STRH/STRB is atomic for <=4B */
   2810   if (order == KIT_CG_MO_SEQ_CST) arm_emit_t32(mc, arm_dmb(0xfu));
   2811 }
   2812 /* The ARMv7-M barrier option for a full-system barrier ("sy"). 64-bit clz/ctz/
   2813  * bswap on a 32-bit target are already routed to __*di2 libcalls by cg, so the
   2814  * backend only ever sees the 16/32-bit forms here. Each lowered intrinsic must
   2815  * stay in lockstep with arm32_supports_intrinsic (arch.c). */
   2816 #define ARM_BARRIER_SY 0xfu
   2817 
   2818 static void arm_atomic_rmw(NativeTarget* t, KitCgAtomicOp op, NativeLoc dst,
   2819                            NativeAddr addr, NativeLoc val, MemAccess mem,
   2820                            KitCgMemOrder order) {
   2821   Arm32NativeTarget* a = arm_of(t);
   2822   MCEmitter* mc = t->mc;
   2823   u32 sz = mem.size ? mem.size : native_type_size(t, dst.type);
   2824   u32 rd = loc_reg(dst);
   2825   u32 rv = loc_reg(val);
   2826   u32 base, newv, status, temps[2], pushmask;
   2827   MCLabel retry = mc_label_new(mc);
   2828   if (sz > 4u) arm_panic(a, "8-byte atomic rmw not lowered (spinlock libcall)");
   2829   /* Borrow a new-value temp + a STREX-status temp clear of {addr base, rd, rv}
   2830    * across a balanced push/pop, so the sequence touches no live operand. ip is
   2831    * left alone (an operand may live there) and the operand-aware pick avoids the
   2832    * base/operand registers the optimizer chose at -O1. */
   2833   base = arm_atomic_addr_reg(a, addr); /* lr (-O0) or the pointer's own reg */
   2834   arm_atomic_borrow(a, (1u << rd) | (1u << rv) | (1u << base), 2u, temps,
   2835                     &pushmask);
   2836   newv = temps[0];
   2837   status = temps[1];
   2838   arm_emit_push(mc, pushmask);
   2839   if (arm_order_release(order)) arm_emit_t32(mc, arm_dmb(0xfu));
   2840   /* Retry loop: rd = *base (LDREX); r4 = rd OP val; STREX r5,r4,[base]; retry
   2841    * while r5 != 0. */
   2842   mc_label_place(mc, retry);
   2843   arm_emit_ldrex(a, sz, rd, base);
   2844   switch (op) {
   2845     case KIT_CG_ATOMIC_XCHG:
   2846       arm_emit_t16(mc, arm_mov_hi(newv, rv));
   2847       break;
   2848     case KIT_CG_ATOMIC_ADD:
   2849       arm_emit_t32(mc, arm_add_reg(newv, rd, rv));
   2850       break;
   2851     case KIT_CG_ATOMIC_SUB:
   2852       arm_emit_t32(mc, arm_sub_reg(newv, rd, rv));
   2853       break;
   2854     case KIT_CG_ATOMIC_AND:
   2855       arm_emit_t32(mc, arm_and_reg(newv, rd, rv));
   2856       break;
   2857     case KIT_CG_ATOMIC_OR:
   2858       arm_emit_t32(mc, arm_orr_reg(newv, rd, rv));
   2859       break;
   2860     case KIT_CG_ATOMIC_XOR:
   2861       arm_emit_t32(mc, arm_eor_reg(newv, rd, rv));
   2862       break;
   2863     case KIT_CG_ATOMIC_NAND:
   2864       arm_emit_t32(mc, arm_and_reg(newv, rd, rv));
   2865       arm_emit_t32(mc, arm_mvn_reg(newv, newv));
   2866       break;
   2867     default:
   2868       arm_panic(a, "unsupported atomic rmw op");
   2869   }
   2870   arm_emit_strex(a, sz, status, newv, base); /* STREX r5, r4, [lr] */
   2871   arm_emit_t32(mc, arm_cmp_imm(status, 0u)); /* CMP r5, #0 */
   2872   arm_emit_t32(mc, arm_b_cond_w(ARM_CC_NE)); /* BNE retry */
   2873   mc_emit_label_ref(mc, retry, R_ARM_THM_JUMP19, 4, 0);
   2874   if (arm_order_acquire(order)) arm_emit_t32(mc, arm_dmb(0xfu));
   2875   arm_emit_pop(mc, pushmask);
   2876 }
   2877 
   2878 static void arm_atomic_cas(NativeTarget* t, NativeLoc prior, NativeLoc ok,
   2879                            NativeAddr addr, NativeLoc expected,
   2880                            NativeLoc desired, MemAccess mem,
   2881                            KitCgMemOrder success, KitCgMemOrder failure) {
   2882   Arm32NativeTarget* a = arm_of(t);
   2883   MCEmitter* mc = t->mc;
   2884   u32 sz = mem.size ? mem.size : native_type_size(t, prior.type);
   2885   u32 rprior = loc_reg(prior);
   2886   u32 rexp = loc_reg(expected);
   2887   u32 rdes = loc_reg(desired);
   2888   u32 rok = loc_reg(ok);
   2889   u32 base, status, statusv[1], pushmask;
   2890   u32 enc0, enc1;
   2891   MCLabel retry = mc_label_new(mc);
   2892   MCLabel fail = mc_label_new(mc);
   2893   MCLabel done = mc_label_new(mc);
   2894   (void)failure;
   2895   if (sz > 4u) arm_panic(a, "8-byte atomic cas not lowered (spinlock libcall)");
   2896   thumb_expand_imm_encode(0u, &enc0);
   2897   thumb_expand_imm_encode(1u, &enc1);
   2898   /* STREX status temp clear of {addr base, prior, expected, desired, ok} —
   2899    * never ip (an operand may live there), and operand-aware so the optimizer's
   2900    * -O1 register choices for the five live values never alias it. */
   2901   base = arm_atomic_addr_reg(a, addr); /* lr (-O0) or the pointer's own reg */
   2902   arm_atomic_borrow(a,
   2903                     (1u << rprior) | (1u << rexp) | (1u << rdes) | (1u << rok) |
   2904                         (1u << base),
   2905                     1u, statusv, &pushmask);
   2906   status = statusv[0];
   2907   arm_emit_push(mc, pushmask);
   2908   if (arm_order_release(success)) arm_emit_t32(mc, arm_dmb(0xfu));
   2909   /* retry: prior = *base (LDREX); if prior != expected goto fail; STREX
   2910    * r4,desired,[base]; retry on failure; ok = 1; goto done. fail: clear the
   2911    * monitor (CLREX) and ok = 0. */
   2912   mc_label_place(mc, retry);
   2913   arm_emit_ldrex(a, sz, rprior, base);
   2914   arm_emit_t32(mc, arm_cmp_reg(rprior, rexp));
   2915   arm_emit_t32(mc, arm_b_cond_w(ARM_CC_NE)); /* BNE fail */
   2916   mc_emit_label_ref(mc, fail, R_ARM_THM_JUMP19, 4, 0);
   2917   arm_emit_strex(a, sz, status, rdes, base);
   2918   arm_emit_t32(mc, arm_cmp_imm(status, 0u));
   2919   arm_emit_t32(mc, arm_b_cond_w(ARM_CC_NE)); /* BNE retry */
   2920   mc_emit_label_ref(mc, retry, R_ARM_THM_JUMP19, 4, 0);
   2921   arm_emit_t32(mc, arm_mov_imm(rok, enc1)); /* ok = 1 */
   2922   arm_emit_t32(mc, arm_b_w());              /* B done */
   2923   mc_emit_label_ref(mc, done, R_ARM_THM_JUMP24, 4, 0);
   2924   mc_label_place(mc, fail);
   2925   arm_emit_t32(mc, arm_clrex());            /* drop the exclusive reservation */
   2926   arm_emit_t32(mc, arm_mov_imm(rok, enc0)); /* ok = 0 */
   2927   mc_label_place(mc, done);
   2928   if (arm_order_acquire(success)) arm_emit_t32(mc, arm_dmb(0xfu));
   2929   arm_emit_pop(mc, pushmask);
   2930 }
   2931 
   2932 static void arm_fence(NativeTarget* t, KitCgMemOrder order) {
   2933   if (order == KIT_CG_MO_RELAXED) return;
   2934   arm_emit_t32(t->mc, arm_dmb(0xfu));
   2935 }
   2936 
   2937 /* ============================ varargs ============================ */
   2938 /* AAPCS32 va_list is a plain 4-byte pointer to the next argument slot. The
   2939  * prologue spilled the unconsumed GP arg registers r0..r3 into a save area just
   2940  * above the saved {r7, lr} pair (at [r7 + ARM_SAVED_PAIR_BYTES]); the named
   2941  * incoming stack args follow contiguously at [r7 + arg_window_base], so a
   2942  * uniform 4-byte stride (8-byte-aligned for i64/double) walks both regions.
   2943  * `ap` is a NativeAddr addressing the va_list object itself. */
   2944 
   2945 static void arm_va_start_core(Arm32NativeTarget* a, NativeAddr ap) {
   2946   NativeTarget* t = &a->base;
   2947   MCEmitter* mc = t->mc;
   2948   KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   2949   NativeLoc ptr = native_loc_reg(i32t, NATIVE_REG_INT, ARM_TMP);
   2950   ABIVaListInfo vai = abi_va_list_layout(t->c->abi);
   2951   if (vai.kind != ABI_VA_LIST_POINTER) arm_panic(a, "unsupported va_list layout");
   2952   if (!a->is_variadic) arm_panic(a, "va_start: function not variadic");
   2953   /* *ap = r7 + saved_block_bytes + next_param_int*4: the GP save area (r0..r3)
   2954    * sits just above the saved register block, so its base is saved_block_bytes
   2955    * (8 for the bare {r7,lr} single-pass prologue; larger when the -O1 known-frame
   2956    * prologue also PUSHes callee-saves), and next_param_int*4 skips the GP slots
   2957    * the fixed params already consumed. lr (ARM_TMP) is the staging temp. */
   2958   arm_emit_t32(mc, arm_add_imm12(ARM_TMP, ARM_FP,
   2959                                  a->saved_block_bytes + a->next_param_int * 4u));
   2960   arm_emit_mem(a, 0, ptr, ap, native_mem_for_type(t, i32t, 4));
   2961 }
   2962 
   2963 /* Whether a va_arg value is too wide to move through one core register (an
   2964  * 8-byte i64 / soft-double): it occupies two GP slots and is copied straight
   2965  * from the save area into its destination memory. */
   2966 static int arm_va_arg_is_wide(NativeTarget* t, KitCgTypeId type) {
   2967   return native_type_size(t, type) > 4u;
   2968 }
   2969 
   2970 /* Wide / aggregate va_arg: read the cursor, 8-byte-align it (i64/double slots
   2971  * are 8-byte aligned on the stack per AAPCS32), advance past the whole span,
   2972  * then byte-copy from the (aligned) cursor into the destination memory. ip holds
   2973  * the cursor across the copy; arm_copy_bytes uses lr internally. */
   2974 static void arm_va_arg_wide(Arm32NativeTarget* a, NativeAddr dst, NativeAddr ap,
   2975                             u32 sz) {
   2976   NativeTarget* t = &a->base;
   2977   MCEmitter* mc = t->mc;
   2978   KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   2979   u32 span = align_up_u32(sz, 4u);
   2980   u32 enc7;
   2981   NativeLoc cur = native_loc_reg(i32t, NATIVE_REG_INT, ARM_SCRATCH);
   2982   NativeLoc nxt = native_loc_reg(i32t, NATIVE_REG_INT, ARM_TMP);
   2983   NativeAddr src;
   2984   AggregateAccess acc;
   2985   /* cur = *ap. */
   2986   arm_emit_mem(a, 1, cur, ap, native_mem_for_type(t, i32t, 4));
   2987   /* 8-byte align the cursor: cur = (cur + 7) & ~7. ~7 (0xFFFFFFF8) is not a
   2988    * Thumb modified immediate, so clear the low 3 bits with BIC #7 (op4=1; #7 IS
   2989    * encodable). */
   2990   arm_emit_t32(mc, arm_add_imm12(ARM_SCRATCH, ARM_SCRATCH, 7u));
   2991   thumb_expand_imm_encode(7u, &enc7);
   2992   arm_emit_t32(mc, arm_dp_imm(1u, 0u, ARM_SCRATCH, ARM_SCRATCH, enc7)); /* BIC */
   2993   /* *ap = cur + span. */
   2994   arm_emit_t32(mc, arm_add_imm12(ARM_TMP, ARM_SCRATCH, span));
   2995   arm_emit_mem(a, 0, nxt, ap, native_mem_for_type(t, i32t, 4));
   2996   /* Copy sz bytes from [cur] to the destination. */
   2997   memset(&src, 0, sizeof src);
   2998   src.base_kind = NATIVE_ADDR_BASE_REG;
   2999   src.base.reg = ARM_SCRATCH;
   3000   src.base_type = i32t;
   3001   memset(&acc, 0, sizeof acc);
   3002   acc.type = i32t;
   3003   acc.size = sz;
   3004   acc.align = 4u;
   3005   arm_copy_bytes(t, dst, src, acc);
   3006 }
   3007 
   3008 static void arm_va_arg_core(Arm32NativeTarget* a, NativeLoc dst, NativeAddr ap,
   3009                             KitCgTypeId type) {
   3010   NativeTarget* t = &a->base;
   3011   MCEmitter* mc = t->mc;
   3012   KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   3013   u32 sz = native_type_size(t, type);
   3014   /* The cursor must survive the value load (dst = [cursor]) and the +4 update,
   3015    * so it cannot alias `dst`. O1 destinations come from the scoped r0..r3 bank;
   3016    * the O0 direct path may use IP, so choose LR only for that case and IP
   3017    * otherwise. */
   3018   u32 cur_reg = (dst.kind == NATIVE_LOC_REG && loc_reg(dst) == ARM_SCRATCH)
   3019                     ? ARM_TMP
   3020                     : ARM_SCRATCH;
   3021   NativeLoc cur = native_loc_reg(i32t, NATIVE_REG_INT, cur_reg);
   3022   NativeAddr from;
   3023   ABIVaListInfo vai = abi_va_list_layout(t->c->abi);
   3024   if (vai.kind != ABI_VA_LIST_POINTER) arm_panic(a, "unsupported va_list layout");
   3025   if (dst.kind != NATIVE_LOC_REG) arm_panic(a, "va_arg destination must be reg");
   3026   /* cur = *ap; load value from [cur]; *ap = cur + 4 (one GP-slot stride). */
   3027   arm_emit_mem(a, 1, cur, ap, native_mem_for_type(t, i32t, 4));
   3028   memset(&from, 0, sizeof from);
   3029   from.base_kind = NATIVE_ADDR_BASE_REG;
   3030   from.base.reg = cur_reg;
   3031   from.base_type = type;
   3032   arm_emit_mem(a, 1, dst, from, native_mem_for_type(t, type, sz));
   3033   arm_emit_t32(mc, arm_add_imm12(cur_reg, cur_reg, 4u));
   3034   arm_emit_mem(a, 0, cur, ap, native_mem_for_type(t, i32t, 4));
   3035 }
   3036 
   3037 static void arm_va_copy_core(Arm32NativeTarget* a, NativeAddr dst_ap,
   3038                              NativeAddr src_ap) {
   3039   NativeTarget* t = &a->base;
   3040   KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   3041   NativeLoc tmp = native_loc_reg(i32t, NATIVE_REG_INT, ARM_SCRATCH);
   3042   arm_emit_mem(a, 1, tmp, src_ap, native_mem_for_type(t, i32t, 4));
   3043   arm_emit_mem(a, 0, tmp, dst_ap, native_mem_for_type(t, i32t, 4));
   3044 }
   3045 
   3046 /* ---- NativeTarget (optimizer) hooks: ap is a materialized register ---- */
   3047 static NativeAddr arm_va_addr_from_ptr(NativeLoc ap_ptr) {
   3048   NativeAddr addr;
   3049   memset(&addr, 0, sizeof addr);
   3050   addr.base_kind = NATIVE_ADDR_BASE_REG;
   3051   addr.cls = NATIVE_REG_INT;
   3052   addr.base.reg = ap_ptr.v.reg;
   3053   addr.base_type = ap_ptr.type;
   3054   return addr;
   3055 }
   3056 /* The destination memory of a value-location: a wide (i64/double) va_arg result
   3057  * is memory-resident, so the optimizer hands its storage (a frame/stack slot, or
   3058  * an explicit address), NOT a pointer in a register. Convert it to a NativeAddr
   3059  * the byte-copy can write through. (Mirrors RISC-V's storage-address
   3060  * resolver.) */
   3061 static NativeAddr arm_loc_addr(Arm32NativeTarget* a, NativeLoc loc) {
   3062   NativeAddr addr;
   3063   memset(&addr, 0, sizeof addr);
   3064   switch ((NativeLocKind)loc.kind) {
   3065     case NATIVE_LOC_FRAME:
   3066       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3067       addr.base.frame = loc.v.frame;
   3068       addr.base_type = loc.type;
   3069       return addr;
   3070     case NATIVE_LOC_STACK:
   3071       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3072       addr.base.frame = loc.v.stack.slot;
   3073       addr.base_type = loc.type;
   3074       addr.offset = loc.v.stack.offset;
   3075       return addr;
   3076     case NATIVE_LOC_ADDR:
   3077       return loc.v.addr;
   3078     default:
   3079       arm_panic(a, "va_arg destination is not addressable");
   3080   }
   3081 }
   3082 static void arm_va_start_native(NativeTarget* t, NativeLoc ap_ptr) {
   3083   arm_va_start_core(arm_of(t), arm_va_addr_from_ptr(ap_ptr));
   3084 }
   3085 static void arm_va_arg_native(NativeTarget* t, NativeLoc dst, NativeLoc ap_ptr,
   3086                               KitCgTypeId type) {
   3087   Arm32NativeTarget* a = arm_of(t);
   3088   if (arm_va_arg_is_wide(t, type)) {
   3089     arm_va_arg_wide(a, arm_loc_addr(a, dst), arm_va_addr_from_ptr(ap_ptr),
   3090                     native_type_size(t, type));
   3091     return;
   3092   }
   3093   arm_va_arg_core(a, dst, arm_va_addr_from_ptr(ap_ptr), type);
   3094 }
   3095 static void arm_va_end_native(NativeTarget* t, NativeLoc ap_ptr) {
   3096   (void)t;
   3097   (void)ap_ptr;
   3098 }
   3099 static void arm_va_copy_native(NativeTarget* t, NativeLoc dst_ap_ptr,
   3100                                NativeLoc src_ap_ptr) {
   3101   arm_va_copy_core(arm_of(t), arm_va_addr_from_ptr(dst_ap_ptr),
   3102                    arm_va_addr_from_ptr(src_ap_ptr));
   3103 }
   3104 
   3105 /* Materialize the boolean of ARM condition `cc` into rd: MOV rd,#0; IT cc;
   3106  * MOV<cc> rd,#1. The flag state must already be set by a preceding CMP; the
   3107  * leading MOV.W (no S) does not disturb it. */
   3108 static void arm_emit_setcc(MCEmitter* mc, u32 rd, u32 cc) {
   3109   u32 enc0, enc1;
   3110   thumb_expand_imm_encode(0u, &enc0);
   3111   thumb_expand_imm_encode(1u, &enc1);
   3112   arm_emit_t32(mc, arm_mov_imm(rd, enc0));
   3113   arm_emit_t16(mc, (u16)(0xbf08u | (cc << 4))); /* IT cc (mask 0b1000) */
   3114   arm_emit_t32(mc, arm_mov_imm(rd, enc1));
   3115 }
   3116 
   3117 /* The 32-bit `__builtin_*_overflow` family. dsts = [value, overflow]; args =
   3118  * [a, b]. i64 overflow is lowered to lane ops by the cg layer (arith.c), so the
   3119  * backend only sees the 32-bit forms. LR/IP are backend-private temporaries on
   3120  * O1; on O0, IP's operand value has already been consumed before these internal
   3121  * sequences use it. */
   3122 /* nd_intrinsic forwards an immediate argument as NATIVE_LOC_IMM (not in a
   3123  * register). A value-consuming intrinsic needs it in a register: materialize it
   3124  * into `into` (an output register, free to reuse as an input) and return the reg
   3125  * loc; pass a register arg through unchanged. Mirrors riscv's intrinsic path. */
   3126 static NativeLoc arm_intrin_arg(Arm32NativeTarget* a, NativeLoc arg, u32 into) {
   3127   NativeLoc r;
   3128   if (arg.kind != NATIVE_LOC_IMM) return arg;
   3129   r = native_loc_reg(arg.type ? arg.type : builtin_id(KIT_CG_BUILTIN_I32),
   3130                       NATIVE_REG_INT, into);
   3131   arm_load_imm(&a->base, r, arg.v.imm);
   3132   return r;
   3133 }
   3134 
   3135 static void arm_overflow(Arm32NativeTarget* a, IntrinKind kind,
   3136                          const NativeLoc* dsts, const NativeLoc* args) {
   3137   MCEmitter* mc = a->base.mc;
   3138   u32 ra = loc_reg(args[0]), rb = loc_reg(args[1]);
   3139   u32 rd = loc_reg(dsts[0]), rovf = loc_reg(dsts[1]);
   3140   u32 enc0;
   3141   switch (kind) {
   3142     case INTRIN_SADD_OVERFLOW:
   3143     case INTRIN_SSUB_OVERFLOW:
   3144       /* r = a +/- b; signed ovf is the sign bit of ((a^r)&(b^r)) [ADD] or
   3145        * ((a^b)&(a^r)) [SUB]. */
   3146       if (kind == INTRIN_SADD_OVERFLOW)
   3147         arm_emit_t32(mc, arm_add_reg(ARM_TMP, ra, rb));
   3148       else
   3149         arm_emit_t32(mc, arm_sub_reg(ARM_TMP, ra, rb));
   3150       arm_emit_t32(mc, arm_eor_reg(ARM_SCRATCH, ra, ARM_TMP)); /* a ^ r */
   3151       if (kind == INTRIN_SADD_OVERFLOW)
   3152         arm_emit_t32(mc, arm_eor_reg(rovf, rb, ARM_TMP)); /* b ^ r */
   3153       else
   3154         arm_emit_t32(mc, arm_eor_reg(rovf, ra, rb)); /* a ^ b */
   3155       arm_emit_t32(mc, arm_and_reg(rovf, rovf, ARM_SCRATCH));
   3156       arm_emit_t32(mc, arm_shift_imm(1u, rovf, rovf, 31u)); /* LSR #31 -> 0/1 */
   3157       arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
   3158       return;
   3159     case INTRIN_UADD_OVERFLOW:
   3160       arm_emit_t32(mc, arm_add_reg(ARM_TMP, ra, rb)); /* r = a + b */
   3161       arm_emit_t32(mc, arm_cmp_reg(ARM_TMP, ra));     /* CMP r, a */
   3162       arm_emit_setcc(mc, rovf, ARM_CC_CC);            /* ovf = (r < a) unsigned */
   3163       arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
   3164       return;
   3165     case INTRIN_USUB_OVERFLOW:
   3166       arm_emit_t32(mc, arm_cmp_reg(ra, rb));          /* CMP a, b */
   3167       arm_emit_t32(mc, arm_sub_reg(ARM_TMP, ra, rb)); /* r = a - b (no flags) */
   3168       arm_emit_setcc(mc, rovf, ARM_CC_CC);            /* ovf = (a < b) unsigned */
   3169       arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
   3170       return;
   3171     case INTRIN_UMUL_OVERFLOW:
   3172       /* UMULL lr:ip = a*b; ovf = (high != 0); value = low. */
   3173       arm_emit_t32(mc, arm_umull(ARM_TMP, ARM_SCRATCH, ra, rb));
   3174       thumb_expand_imm_encode(0u, &enc0);
   3175       arm_emit_t32(mc, arm_cmp_imm(ARM_SCRATCH, enc0));
   3176       arm_emit_setcc(mc, rovf, ARM_CC_NE);
   3177       arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
   3178       return;
   3179     case INTRIN_SMUL_OVERFLOW:
   3180       /* SMULL lr:ip = a*b; ovf = (high != (low >>s 31)); value = low. */
   3181       arm_emit_t32(mc, arm_smull(ARM_TMP, ARM_SCRATCH, ra, rb));
   3182       arm_emit_t32(mc, arm_shift_imm(2u, rovf, ARM_TMP, 31u)); /* rovf = low ASR 31 */
   3183       arm_emit_t32(mc, arm_cmp_reg(ARM_SCRATCH, rovf));
   3184       arm_emit_setcc(mc, rovf, ARM_CC_NE);
   3185       arm_emit_t16(mc, arm_mov_hi(rd, ARM_TMP));
   3186       return;
   3187     default:
   3188       arm_panic(a, "arm_overflow: not an overflow intrinsic");
   3189   }
   3190 }
   3191 
   3192 static void arm_intrinsic(NativeTarget* t, IntrinKind kind,
   3193                           const NativeLoc* dsts, u32 ndst, const NativeLoc* args,
   3194                           u32 narg) {
   3195   Arm32NativeTarget* a = arm_of(t);
   3196   MCEmitter* mc = t->mc;
   3197   switch (kind) {
   3198     case INTRIN_TRAP:
   3199       arm_trap(t);
   3200       return;
   3201     case INTRIN_MEMMOVE: {
   3202       /* Overlap-safe move: copy forward when dst < src (unsigned), else backward,
   3203        * so each granule's load precedes any store that could clobber it. dst/src
   3204        * are pointer registers, size a constant; the transfer reg aliases neither
   3205        * base. Shares the granule ladder with copy_bytes. */
   3206       MCLabel forward = arm_label_new(t), done = arm_label_new(t);
   3207       u32 dst, src, size, xfer;
   3208       if (narg != 3u || args[0].kind != NATIVE_LOC_REG ||
   3209           args[1].kind != NATIVE_LOC_REG || args[2].kind != NATIVE_LOC_IMM)
   3210         arm_panic(a, "memmove: expected (reg dst, reg src, imm size)");
   3211       if (args[2].v.imm < 0 || args[2].v.imm > 4096)
   3212         arm_panic(a, "memmove: size out of range (needs a runtime loop)");
   3213       dst = loc_reg(args[0]);
   3214       src = loc_reg(args[1]);
   3215       size = (u32)args[2].v.imm;
   3216       xfer = (dst != ARM_TMP && src != ARM_TMP) ? ARM_TMP : ARM_SCRATCH;
   3217       if (xfer == dst || xfer == src)
   3218         arm_panic(a, "memmove: no free transfer register");
   3219       arm_cmp_branch(t, CMP_LT_U, args[0], args[1], forward);
   3220       arm_copy_granules_dir(a, dst, src, size, xfer, /*backward=*/1);
   3221       arm_jump(t, done);
   3222       arm_label_place(t, forward);
   3223       arm_copy_granules_dir(a, dst, src, size, xfer, /*backward=*/0);
   3224       arm_label_place(t, done);
   3225       return;
   3226     }
   3227     case INTRIN_CLZ:
   3228       if (ndst == 1u && narg == 1u) {
   3229         NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
   3230         arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(a0)));
   3231         return;
   3232       }
   3233       break;
   3234     case INTRIN_CTZ:
   3235       /* RBIT reverses the bit order, so the trailing-zero count becomes a
   3236        * leading-zero count of the reversed word. */
   3237       if (ndst == 1u && narg == 1u) {
   3238         NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
   3239         arm_emit_t32(mc, arm_rbit(loc_reg(dsts[0]), loc_reg(a0)));
   3240         arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(dsts[0])));
   3241         return;
   3242       }
   3243       break;
   3244     case INTRIN_BSWAP:
   3245       if (ndst == 1u && narg == 1u) {
   3246         u32 width = native_type_size(t, dsts[0].type);
   3247         NativeLoc a0 = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
   3248         /* Use the 32-bit REV.W/REV16.W forms: the dst may be a high register
   3249          * (IP at -O0 or r4..r11 when allocated at -O1) that the 16-bit T1
   3250          * encoding cannot represent. */
   3251         if (width == 4u) {
   3252           arm_emit_t32(mc, arm_rev_w(loc_reg(dsts[0]), loc_reg(a0)));
   3253           return;
   3254         }
   3255         if (width == 2u) {
   3256           /* REV16 byte-swaps within each halfword: the low halfword of the
   3257            * input becomes the swapped 16-bit result in the low halfword. */
   3258           arm_emit_t32(mc, arm_rev16_w(loc_reg(dsts[0]), loc_reg(a0)));
   3259           return;
   3260         }
   3261       }
   3262       break;
   3263     case INTRIN_POPCOUNT:
   3264       /* 32-bit SWAR popcount (no hardware popcount on ARMv7-M). All masks are
   3265        * ThumbExpandImm-encodable, so no MOVW/MOVT is needed. lr/ip are the
   3266        * scratch. 64-bit popcount stays in the cg lane path / a follow-on. */
   3267       if (ndst == 1u && narg == 1u && native_type_size(t, dsts[0].type) <= 4u) {
   3268         u32 rd = loc_reg(dsts[0]);
   3269         u32 rs = loc_reg(arm_intrin_arg(a, args[0], rd));
   3270         u32 e55, e33, e0f, e01;
   3271         thumb_expand_imm_encode(0x55555555u, &e55);
   3272         thumb_expand_imm_encode(0x33333333u, &e33);
   3273         thumb_expand_imm_encode(0x0f0f0f0fu, &e0f);
   3274         thumb_expand_imm_encode(0x01010101u, &e01);
   3275         arm_emit_t32(mc, arm_shift_imm(1u, ARM_SCRATCH, rs, 1u));            /* ip = x>>1 */
   3276         arm_emit_t32(mc, arm_dp_imm(0u, 0u, ARM_SCRATCH, ARM_SCRATCH, e55)); /* ip &= 0x55.. */
   3277         arm_emit_t32(mc, arm_sub_reg(ARM_TMP, rs, ARM_SCRATCH));            /* lr = x - ip */
   3278         arm_emit_t32(mc, arm_dp_imm(0u, 0u, ARM_SCRATCH, ARM_TMP, e33));    /* ip = lr & 0x33.. */
   3279         arm_emit_t32(mc, arm_shift_imm(1u, ARM_TMP, ARM_TMP, 2u));          /* lr >>= 2 */
   3280         arm_emit_t32(mc, arm_dp_imm(0u, 0u, ARM_TMP, ARM_TMP, e33));        /* lr &= 0x33.. */
   3281         arm_emit_t32(mc, arm_add_reg(ARM_TMP, ARM_TMP, ARM_SCRATCH));       /* lr += ip */
   3282         arm_emit_t32(mc, arm_shift_imm(1u, ARM_SCRATCH, ARM_TMP, 4u));      /* ip = lr>>4 */
   3283         arm_emit_t32(mc, arm_add_reg(ARM_TMP, ARM_TMP, ARM_SCRATCH));       /* lr += ip */
   3284         arm_emit_t32(mc, arm_dp_imm(0u, 0u, ARM_TMP, ARM_TMP, e0f));        /* lr &= 0x0f.. */
   3285         arm_emit_t32(mc, arm_mov_imm(ARM_SCRATCH, e01));                    /* ip = 0x01010101 */
   3286         arm_emit_t32(mc, arm_mul(ARM_TMP, ARM_TMP, ARM_SCRATCH));           /* lr *= ip */
   3287         arm_emit_t32(mc, arm_shift_imm(1u, rd, ARM_TMP, 24u));              /* rd = lr>>24 */
   3288         return;
   3289       }
   3290       break;
   3291     case INTRIN_EXPECT:
   3292     case INTRIN_ASSUME_ALIGNED:
   3293       /* Branch/alignment hints: the result is the first argument, unchanged. An
   3294        * immediate value materializes straight into dst; an 8-byte value carries
   3295        * two lanes, so move each (a single arm_move would drop the high lane). */
   3296       if (ndst == 1u && narg >= 1u) {
   3297         if (args[0].kind == NATIVE_LOC_IMM) {
   3298           arm_load_imm(t, dsts[0], args[0].v.imm);
   3299         } else if (native_type_size(t, dsts[0].type) > 4u &&
   3300                    dsts[0].kind == NATIVE_LOC_REG &&
   3301                    args[0].kind == NATIVE_LOC_REG) {
   3302           u32 dlo = loc_reg(dsts[0]), slo = loc_reg(args[0]);
   3303           if (dlo != slo) arm_emit_t16(mc, arm_mov_hi(dlo, slo));
   3304           arm_emit_t16(mc, arm_mov_hi((dlo + 1u) & 0xfu, (slo + 1u) & 0xfu));
   3305         } else {
   3306           arm_move(t, dsts[0], args[0]);
   3307         }
   3308         return;
   3309       }
   3310       break;
   3311     case INTRIN_PREFETCH:
   3312       return; /* no-op: M-profile has no PLD */
   3313     case INTRIN_CPU_NOP:
   3314       arm_emit_t16(mc, arm_nop16());
   3315       return;
   3316     case INTRIN_CPU_YIELD:
   3317       arm_emit_t16(mc, arm_yield16());
   3318       return;
   3319     case INTRIN_WFI:
   3320       arm_emit_t16(mc, arm_wfi16());
   3321       return;
   3322     case INTRIN_WFE:
   3323       arm_emit_t16(mc, arm_wfe16());
   3324       return;
   3325     case INTRIN_SEV:
   3326       arm_emit_t16(mc, arm_sev16());
   3327       return;
   3328     case INTRIN_DMB:
   3329       arm_emit_t32(mc, arm_dmb(ARM_BARRIER_SY));
   3330       return;
   3331     case INTRIN_DSB:
   3332       arm_emit_t32(mc, arm_dsb(ARM_BARRIER_SY));
   3333       return;
   3334     case INTRIN_ISB:
   3335       arm_emit_t32(mc, arm_isb(ARM_BARRIER_SY));
   3336       return;
   3337     case INTRIN_SADD_OVERFLOW:
   3338     case INTRIN_SSUB_OVERFLOW:
   3339     case INTRIN_UADD_OVERFLOW:
   3340     case INTRIN_USUB_OVERFLOW:
   3341     case INTRIN_SMUL_OVERFLOW:
   3342     case INTRIN_UMUL_OVERFLOW:
   3343       if (ndst == 2u && narg == 2u) {
   3344         /* Materialize immediate operands into the output registers (free to use
   3345          * as inputs: arm_overflow reads both operands before writing dsts). */
   3346         NativeLoc av[2];
   3347         av[0] = arm_intrin_arg(a, args[0], loc_reg(dsts[0]));
   3348         av[1] = arm_intrin_arg(a, args[1], loc_reg(dsts[1]));
   3349         arm_overflow(a, kind, dsts, av);
   3350         return;
   3351       }
   3352       break;
   3353     case INTRIN_SMUL_HIGH:
   3354     case INTRIN_UMUL_HIGH:
   3355       if (ndst == 1u && narg == 2u) {
   3356         u32 rd = loc_reg(dsts[0]);
   3357         u32 ra = loc_reg(args[0]);
   3358         u32 rb = loc_reg(args[1]);
   3359         arm_emit_t32(mc, kind == INTRIN_SMUL_HIGH
   3360                              ? arm_smull(ARM_TMP, ARM_SCRATCH, ra, rb)
   3361                              : arm_umull(ARM_TMP, ARM_SCRATCH, ra, rb));
   3362         arm_emit_t16(mc, arm_mov_hi(rd, ARM_SCRATCH));
   3363         return;
   3364       }
   3365       break;
   3366     case INTRIN_FRAME_ADDRESS:
   3367     case INTRIN_RETURN_ADDRESS:
   3368       /* kit's prologue anchors r7 at the saved pair: [r7]=caller r7,
   3369        * [r7+4]=this frame's saved lr (return address). The level is constant
   3370        * (unrolls to `level` dependent loads up the fp chain). A frame-reading
   3371        * function is kept off the frameless-leaf tier (reads_frame), so r7 is
   3372        * always valid here. */
   3373       if (ndst == 1u) {
   3374         u32 level = (narg >= 1u && args[0].kind == NATIVE_LOC_IMM)
   3375                         ? (u32)args[0].v.imm
   3376                         : 0u;
   3377         u32 rd = loc_reg(dsts[0]), i;
   3378         arm_emit_t16(mc, arm_mov_hi(rd, ARM_FP)); /* rd = r7 */
   3379         for (i = 0; i < level; ++i)
   3380           arm_emit_t32(mc, arm_ldr_imm(rd, rd, 0)); /* rd = *(rd) */
   3381         if (kind == INTRIN_RETURN_ADDRESS)
   3382           arm_emit_t32(mc, arm_ldr_imm(rd, rd, 4u)); /* rd = *(rd+4) = saved lr */
   3383         return;
   3384       }
   3385       break;
   3386     default:
   3387       break;
   3388   }
   3389   arm_panic(a, "intrinsic not lowered (FMA/syscall/setjmp/etc. are follow-ons)");
   3390 }
   3391 
   3392 /* ============================ inline asm ============================ */
   3393 
   3394 _Noreturn static void arm_asm_panic_at(Compiler* c, SrcLoc loc,
   3395                                        const char* msg) {
   3396   compiler_panic(c, loc, "arm32 inline asm: %s", msg);
   3397 }
   3398 _Noreturn static void arm_asm_panic(NativeDirectTarget* d, const char* msg) {
   3399   arm_asm_panic_at(d->base.c, d->loc, msg);
   3400 }
   3401 
   3402 static void arm_asm_bound_reg(Operand* out, KitCgTypeId type,
   3403                               NativeAllocClass cls, Reg reg) {
   3404   memset(out, 0, sizeof *out);
   3405   out->kind = ARM32_INLINE_OPK_REG;
   3406   out->pad[0] =
   3407       (cls == NATIVE_REG_FP) ? ARM32_INLINE_OPCLS_FP : ARM32_INLINE_OPCLS_INT;
   3408   out->type = type;
   3409   out->v.local = (CGLocal)reg;
   3410 }
   3411 static void arm_asm_bound_mem(Operand* out, KitCgTypeId type, Reg base) {
   3412   memset(out, 0, sizeof *out);
   3413   out->kind = OPK_INDIRECT;
   3414   out->type = type;
   3415   out->v.ind.base = (CGLocal)base;
   3416   out->v.ind.index = CG_LOCAL_NONE;
   3417 }
   3418 
   3419 /* Parse one clobber name into a core-register mask bit. r7 is the live frame
   3420  * anchor and sp/pc are structural state, so an inline block may not declare
   3421  * them clobbered; cc/memory contribute no register. */
   3422 static void arm_asm_clobber_masks(Compiler* c, SrcLoc loc, const Sym* clobbers,
   3423                                   u32 nclob, u32* int_mask, u32* fp_mask) {
   3424   *int_mask = 0;
   3425   *fp_mask = 0;
   3426   for (u32 i = 0; i < nclob; ++i) {
   3427     Slice s = pool_slice(c->global, clobbers[i]);
   3428     char buf[16];
   3429     uint32_t dwarf;
   3430     if (!s.s || !s.len || s.len >= sizeof buf) continue;
   3431     memcpy(buf, s.s, s.len);
   3432     buf[s.len] = '\0';
   3433     if (!strcmp(buf, "cc") || !strcmp(buf, "memory")) continue;
   3434     if (arm32_register_index(buf, &dwarf) != 0 || dwarf > 15u)
   3435       arm_asm_panic_at(c, loc, "unsupported clobber register");
   3436     if (dwarf == ARM_FP || dwarf == 13u || dwarf == 15u)
   3437       arm_asm_panic_at(c, loc, "structural register cannot be clobbered");
   3438     if (dwarf <= 15u)
   3439       *int_mask |= 1u << dwarf;
   3440   }
   3441 }
   3442 
   3443 /* Operand-address resolution for the direct (-O0) path (mirrors aa64). */
   3444 static NativeAddr arm_direct_addr(NativeDirectTarget* d, Operand op) {
   3445   NativeAddr addr;
   3446   memset(&addr, 0, sizeof addr);
   3447   switch ((OpKind)op.kind) {
   3448     case OPK_LOCAL:
   3449       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3450       addr.base.frame = d->locals[op.v.local - 1u].home;
   3451       addr.base_type = op.type;
   3452       return addr;
   3453     case OPK_INDIRECT:
   3454       addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
   3455       addr.base.frame = d->locals[op.v.ind.base - 1u].home;
   3456       addr.cls = d->locals[op.v.ind.base - 1u].cls;
   3457       addr.base_type = d->locals[op.v.ind.base - 1u].type;
   3458       addr.offset = op.v.ind.ofs;
   3459       return addr;
   3460     default:
   3461       arm_asm_panic(d, "operand is not addressable");
   3462   }
   3463 }
   3464 static NativeAddr arm_direct_materialize_addr(NativeDirectTarget* d,
   3465                                               Operand op) {
   3466   NativeAddr addr = arm_direct_addr(d, op);
   3467   if (addr.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
   3468     NativeLoc base = native_loc_reg(addr.base_type, NATIVE_REG_INT, ARM_SCRATCH);
   3469     NativeAddr load;
   3470     memset(&load, 0, sizeof load);
   3471     load.base_kind = NATIVE_ADDR_BASE_FRAME;
   3472     load.base.frame = addr.base.frame;
   3473     load.base_type = addr.base_type;
   3474     arm_emit_mem(arm_of(d->native), 1, base, load,
   3475                  native_mem_for_type(d->native, addr.base_type, 4));
   3476     addr.base_kind = NATIVE_ADDR_BASE_REG;
   3477     addr.base.reg = ARM_SCRATCH;
   3478   }
   3479   return addr;
   3480 }
   3481 
   3482 static void arm_direct_load_operand_to_reg(NativeDirectTarget* d, Operand op,
   3483                                            NativeLoc dst) {
   3484   NativeAddr addr;
   3485   memset(&addr, 0, sizeof addr);
   3486   switch ((OpKind)op.kind) {
   3487     case OPK_IMM:
   3488       d->native->load_imm(d->native, dst, op.v.imm);
   3489       return;
   3490     case OPK_LOCAL:
   3491       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3492       addr.base.frame = d->locals[op.v.local - 1u].home;
   3493       addr.base_type = op.type;
   3494       arm_emit_mem(arm_of(d->native), 1, dst, addr,
   3495                    native_mem_for_type(d->native, op.type, 0));
   3496       return;
   3497     case OPK_GLOBAL:
   3498       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
   3499       addr.base.global.sym = op.v.global.sym;
   3500       addr.base.global.addend = op.v.global.addend;
   3501       addr.base_type = op.type;
   3502       d->native->load_addr(d->native, dst, addr);
   3503       return;
   3504     case OPK_INDIRECT:
   3505       addr = arm_direct_materialize_addr(d, op);
   3506       arm_emit_mem(arm_of(d->native), 1, dst, addr,
   3507                    native_mem_for_type(d->native, op.type, 0));
   3508       return;
   3509   }
   3510   arm_asm_panic(d, "unsupported asm input operand");
   3511 }
   3512 static void arm_direct_load_address_to_reg(NativeDirectTarget* d, Operand op,
   3513                                            NativeLoc dst) {
   3514   /* For an "m" memory operand: a LOCAL's address is its frame home; an INDIRECT
   3515    * (*(base+ofs)) names an address held in a frame slot; load_addr materializes
   3516    * that typed FRAME_VALUE through backend-owned temporaries. */
   3517   d->native->load_addr(d->native, dst, arm_direct_materialize_addr(d, op));
   3518 }
   3519 static void arm_direct_store_reg_to_operand(NativeDirectTarget* d, Operand op,
   3520                                             NativeLoc src) {
   3521   NativeAddr addr;
   3522   memset(&addr, 0, sizeof addr);
   3523   if (op.kind == OPK_LOCAL) {
   3524     addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3525     addr.base.frame = d->locals[op.v.local - 1u].home;
   3526     addr.base_type = op.type;
   3527   } else {
   3528     addr = arm_direct_materialize_addr(d, op);
   3529   }
   3530   arm_emit_mem(arm_of(d->native), 0, src, addr,
   3531                native_mem_for_type(d->native, op.type, 0));
   3532 }
   3533 
   3534 /* Callee-saved spill/restore for asm clobbers (r4..r11, excluding r7=fp). */
   3535 typedef struct ArmAsmSavedClobber {
   3536   NativeFrameSlot slot;
   3537   Reg reg;
   3538   KitCgTypeId type;
   3539 } ArmAsmSavedClobber;
   3540 
   3541 static ArmAsmSavedClobber* arm_asm_save_callee_clobbers(Arm32NativeTarget* a,
   3542                                                         u32 int_mask,
   3543                                                         u32 fp_mask,
   3544                                                         u32* nsaved_out) {
   3545   ArmAsmSavedClobber* saved =
   3546       arena_zarray(a->base.c->tu, ArmAsmSavedClobber, 12u);
   3547   KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   3548   u32 n = 0;
   3549   (void)fp_mask;
   3550   for (Reg r = 4u; r <= 11u; ++r) {
   3551     NativeFrameSlotDesc sd;
   3552     NativeAddr addr;
   3553     if (r == ARM_FP || (int_mask & (1u << r)) == 0) continue;
   3554     memset(&sd, 0, sizeof sd);
   3555     sd.type = i32t;
   3556     sd.size = 4;
   3557     sd.align = 4;
   3558     sd.kind = NATIVE_FRAME_SLOT_SAVE;
   3559     saved[n].slot = a->base.frame_slot(&a->base, &sd);
   3560     saved[n].reg = r;
   3561     saved[n].type = i32t;
   3562     memset(&addr, 0, sizeof addr);
   3563     addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3564     addr.base.frame = saved[n].slot;
   3565     addr.base_type = i32t;
   3566     arm_emit_mem(a, 0, native_loc_reg(i32t, NATIVE_REG_INT, r), addr,
   3567                  native_mem_for_type(&a->base, i32t, 4));
   3568     n++;
   3569   }
   3570   *nsaved_out = n;
   3571   return saved;
   3572 }
   3573 static void arm_asm_restore_one_rec(Arm32NativeTarget* a,
   3574                                     const ArmAsmSavedClobber* s) {
   3575   NativeAddr addr;
   3576   memset(&addr, 0, sizeof addr);
   3577   addr.base_kind = NATIVE_ADDR_BASE_FRAME;
   3578   addr.base.frame = s->slot;
   3579   addr.base_type = s->type;
   3580   arm_emit_mem(a, 1, native_loc_reg(s->type, NATIVE_REG_INT, s->reg), addr,
   3581                native_mem_for_type(&a->base, s->type, 4));
   3582 }
   3583 static void* arm_asm_hook_save_callee_clobbers(NativeDirectTarget* d,
   3584                                                u32 int_mask, u32 fp_mask,
   3585                                                u32* nsaved_out) {
   3586   return arm_asm_save_callee_clobbers(arm_of(d->native), int_mask, fp_mask,
   3587                                       nsaved_out);
   3588 }
   3589 static void arm_asm_hook_restore_one(NativeDirectTarget* d, void* saved,
   3590                                      u32 idx) {
   3591   arm_asm_restore_one_rec(arm_of(d->native),
   3592                           &((ArmAsmSavedClobber*)saved)[idx]);
   3593 }
   3594 
   3595 static void arm_asm_hook_run_template(NativeDirectTarget* d, const char* tmpl,
   3596                                       const AsmConstraint* outs, u32 nout,
   3597                                       Operand* bound_outs,
   3598                                       const AsmConstraint* ins, u32 nin,
   3599                                       Operand* bound_ins, const Sym* clobbers,
   3600                                       u32 nclob) {
   3601   Arm32Asm* asmh = arm32_asm_open(d->base.c);
   3602   arm32_inline_bind(asmh, outs, nout, bound_outs, ins, nin, bound_ins, clobbers,
   3603                     nclob);
   3604   arm32_asm_run_template(asmh, d->native->mc, tmpl);
   3605   arm32_asm_close(asmh);
   3606 }
   3607 
   3608 static void arm_direct_asm_block(NativeDirectTarget* d, const char* tmpl,
   3609                                  const AsmConstraint* outs, u32 nout,
   3610                                  Operand* out_ops, const AsmConstraint* ins,
   3611                                  u32 nin, const Operand* in_ops,
   3612                                  const Sym* clobbers, u32 nclob,
   3613                                  u32 clobber_abi_sets) {
   3614   static const NativeAsmDirectHooks hooks = {
   3615       .opk_reg = ARM32_INLINE_OPK_REG,
   3616       .opcls_fp = ARM32_INLINE_OPCLS_FP,
   3617       .panic = arm_asm_panic,
   3618       .bound_reg = arm_asm_bound_reg,
   3619       .bound_mem = arm_asm_bound_mem,
   3620       .clobber_masks = arm_asm_clobber_masks,
   3621       .save_callee_clobbers = arm_asm_hook_save_callee_clobbers,
   3622       .restore_one = arm_asm_hook_restore_one,
   3623       .load_operand_to_reg = arm_direct_load_operand_to_reg,
   3624       .load_address_to_reg = arm_direct_load_address_to_reg,
   3625       .store_reg_to_operand = arm_direct_store_reg_to_operand,
   3626       .run_template = arm_asm_hook_run_template,
   3627   };
   3628   native_asm_bind_direct_operands(d, tmpl, outs, nout, out_ops, ins, nin,
   3629                                   in_ops, clobbers, nclob, clobber_abi_sets,
   3630                                   &hooks);
   3631 }
   3632 
   3633 /* Convert a concrete memory-constraint location to a NativeAddr. */
   3634 static NativeAddr arm_asm_in_loc_addr(NativeTarget* t, NativeLoc loc,
   3635                                       KitCgTypeId type) {
   3636   NativeAddr ma;
   3637   memset(&ma, 0, sizeof ma);
   3638   if (loc.kind == NATIVE_LOC_ADDR) {
   3639     ma = loc.v.addr;
   3640   } else if (loc.kind == NATIVE_LOC_FRAME || loc.kind == NATIVE_LOC_STACK) {
   3641     ma.base_kind = NATIVE_ADDR_BASE_FRAME;
   3642     ma.base.frame =
   3643         loc.kind == NATIVE_LOC_FRAME ? loc.v.frame : loc.v.stack.slot;
   3644     ma.offset = loc.kind == NATIVE_LOC_STACK ? loc.v.stack.offset : 0;
   3645     ma.base_type = type;
   3646   } else {
   3647     arm_asm_panic_at(t->c, arm_of(t)->loc, "unsupported memory asm input");
   3648   }
   3649   return ma;
   3650 }
   3651 
   3652 /* Reserve the next backend-private memory-base register: IP first, then LR.
   3653  * The optimized emitter never presents IP/LR as asm operands, so both are free
   3654  * here. */
   3655 static Reg arm_asm_mem_base_reg(NativeTarget* t, u32* nbase) {
   3656   Reg r;
   3657   if (*nbase == 0u)
   3658     r = (Reg)ARM_SCRATCH;
   3659   else if (*nbase == 1u)
   3660     r = (Reg)ARM_TMP;
   3661   else
   3662     arm_asm_panic_at(t->c, arm_of(t)->loc,
   3663                      "too many memory inline-asm operands");
   3664   (*nbase)++;
   3665   return r;
   3666 }
   3667 
   3668 static void arm_asm_native_panic(NativeTarget* t, SrcLoc loc,
   3669                                  const char* msg) {
   3670   arm_asm_panic_at(t->c, loc, msg);
   3671 }
   3672 
   3673 static Reg arm_asm_native_mem_base_hook(NativeTarget* t, SrcLoc loc,
   3674                                         NativeLoc src, u32* ntmp) {
   3675   Reg base = arm_asm_mem_base_reg(t, ntmp);
   3676   NativeAddr ma = arm_asm_in_loc_addr(t, src, src.type);
   3677   (void)loc;
   3678   arm_load_addr(
   3679       t, native_loc_reg(builtin_id(KIT_CG_BUILTIN_I32), NATIVE_REG_INT, base),
   3680       ma);
   3681   return base;
   3682 }
   3683 
   3684 static void arm_asm_native_run_template_hook(
   3685     NativeTarget* t, const char* tmpl, const AsmConstraint* outs, u32 nout,
   3686     Operand* bound_outs, const AsmConstraint* ins, u32 nin, Operand* bound_ins,
   3687     const Sym* clobbers, u32 nclob) {
   3688   Arm32Asm* asmh = arm32_asm_open(t->c);
   3689   arm32_inline_bind(asmh, outs, nout, bound_outs, ins, nin, bound_ins, clobbers,
   3690                     nclob);
   3691   arm32_asm_run_template(asmh, t->mc, tmpl);
   3692   arm32_asm_close(asmh);
   3693 }
   3694 
   3695 static void arm_asm_block_native(NativeTarget* t, const char* tmpl,
   3696                                  const AsmConstraint* outs, u32 nout,
   3697                                  NativeLoc* out_locs, const AsmConstraint* ins,
   3698                                  u32 nin, const NativeLoc* in_locs,
   3699                                  const Sym* clobbers, u32 nclob) {
   3700   SrcLoc loc = arm_of(t)->loc;
   3701   static const NativeAsmNativeHooks hooks = {
   3702       .panic = arm_asm_native_panic,
   3703       .bound_reg = arm_asm_bound_reg,
   3704       .bound_mem = arm_asm_bound_mem,
   3705       .mem_base = arm_asm_native_mem_base_hook,
   3706       .run_template = arm_asm_native_run_template_hook,
   3707   };
   3708   native_asm_bind_native_operands(t, loc, tmpl, outs, nout, out_locs, ins, nin,
   3709                                   in_locs, clobbers, nclob, &hooks);
   3710 }
   3711 
   3712 /* ============================ construction ============================ */
   3713 
   3714 NativeTarget* arm32_native_target_new(Compiler* c, ObjBuilder* obj,
   3715                                       MCEmitter* mc) {
   3716   Arm32NativeTarget* a = arena_znew(c->tu, Arm32NativeTarget);
   3717   NativeTarget* t;
   3718   if (!a) return NULL;
   3719   t = &a->base;
   3720   t->c = c;
   3721   t->obj = obj;
   3722   t->mc = mc;
   3723   a->variant = arm32_variant_for_kind(c->target.arch);
   3724   native_frame_init(&a->frame, c);
   3725   t->regs = &arm_reg_info;
   3726   t->class_for_type = native_class_for_type_fp_le8;
   3727   t->imm_legal = arm_imm_legal;
   3728   t->addr_legal = arm_addr_legal;
   3729   t->can_fold_shift_into_alu = arm_can_fold_shift_into_alu;
   3730   t->func_begin = arm_func_begin;
   3731   t->func_begin_known_frame = arm_func_begin_known_frame;
   3732   t->reserve_callee_saves = arm_reserve_callee_saves;
   3733   t->signature_stack_bytes = arm_signature_stack_bytes;
   3734   t->call_stack_bytes = arm_call_stack_bytes;
   3735   t->has_store_zero_reg = 0; /* ARM has no zero register */
   3736   t->func_end = arm_func_end;
   3737   t->frame_slot = arm_frame_slot;
   3738   t->release_frame_slot = arm_release_frame_slot;
   3739   t->frame_slot_debug_loc = arm_frame_slot_debug_loc;
   3740   t->bind_param = arm_bind_native_param;
   3741   t->label_new = arm_label_new;
   3742   t->label_place = arm_label_place;
   3743   t->jump = arm_jump;
   3744   t->cmp_branch = arm_cmp_branch;
   3745   t->indirect_branch = arm_indirect_branch;
   3746   t->load_label_addr = arm_load_label_addr;
   3747   t->move = arm_move;
   3748   t->load_imm = arm_load_imm;
   3749   t->load_const = arm_load_const;
   3750   t->load_addr = arm_load_addr;
   3751   t->load = arm_load;
   3752   t->store = arm_store;
   3753   t->tls_addr_of = arm_tls_addr_of;
   3754   t->copy_bytes = arm_copy_bytes;
   3755   t->set_bytes = arm_set_bytes;
   3756   t->bitfield_load = arm_bitfield_load;
   3757   t->bitfield_store = arm_bitfield_store;
   3758   t->binop = arm_binop;
   3759   t->unop = arm_unop;
   3760   t->cmp = arm_cmp;
   3761   t->convert = arm_convert;
   3762   t->binop_rr = arm_binop_rr;
   3763   t->move_rr = arm_move_rr;
   3764   t->cmp_rr = arm_cmp_rr;
   3765   t->convert_rr = arm_convert_rr;
   3766   t->alloca_ = arm_alloca;
   3767   t->spill = arm_spill;
   3768   t->reload = arm_reload;
   3769   t->marshal_call = arm_marshal_call;
   3770   t->emit_call = arm_emit_call;
   3771   t->marshal_ret = arm_marshal_ret;
   3772   t->ret = arm_ret;
   3773   t->atomic_load = arm_atomic_load;
   3774   t->atomic_store = arm_atomic_store;
   3775   t->atomic_rmw = arm_atomic_rmw;
   3776   t->atomic_cas = arm_atomic_cas;
   3777   t->fence = arm_fence;
   3778   t->va_start_ = arm_va_start_native;
   3779   t->va_arg_ = arm_va_arg_native;
   3780   t->va_end_ = arm_va_end_native;
   3781   t->va_copy_ = arm_va_copy_native;
   3782   t->intrinsic = arm_intrinsic;
   3783   t->asm_block = arm_asm_block_native;
   3784   t->file_scope_asm = native_file_scope_asm;
   3785   t->trap = arm_trap;
   3786   t->set_loc = arm_set_loc;
   3787   t->finalize = native_finalize;
   3788   return t;
   3789 }
   3790 
   3791 /* ============================ NativeOps (-O0) ============================ */
   3792 
   3793 static void arm_bind_param(NativeDirectTarget* d, const CGParamDesc* p,
   3794                            CGLocal local, NativeDirectLocal* l) {
   3795   NativeLoc dst;
   3796   (void)local;
   3797   memset(&dst, 0, sizeof dst);
   3798   dst.kind = NATIVE_LOC_FRAME;
   3799   dst.type = p->type;
   3800   dst.v.frame = l->home;
   3801   arm_bind_native_param(d->native, p, dst);
   3802 }
   3803 
   3804 /* A sibling call's outgoing stack args reuse the caller's incoming-arg window in
   3805  * place; it is realizable only if they fit. (Register-only callees always fit.) */
   3806 static const char* arm_no_tail(NativeDirectTarget* d, const CGCallDesc* call) {
   3807   NativeCallDesc nd;
   3808   native_direct_project_tail_call_desc(d, call, &nd);
   3809   if (arm_call_stack_size(d->native, &nd) > arm_of(d->native)->incoming_stack_size)
   3810     return "arm32 tail call: callee stack args exceed the caller's window";
   3811   return NULL;
   3812 }
   3813 
   3814 /* ---- Direct (-O0) varargs wrappers: resolve a va_list operand's address into
   3815  * a register, then call the shared cores. The va cores use ip (cursor/temp) and
   3816  * lr (copy scratch); the va_list base register must be distinct, so the direct
   3817  * wrappers stage it into a callee-saved scratch (r4) before calling. ---- */
   3818 static NativeAddr arm_direct_pointer_addr(NativeDirectTarget* d, Operand op,
   3819                                           Reg reg) {
   3820   NativeAddr addr;
   3821   KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32);
   3822   memset(&addr, 0, sizeof addr);
   3823   if (op.kind == OPK_LOCAL) {
   3824     /* The local HOLDS the pointer (&ap): load its home value. */
   3825     NativeLoc base = native_loc_reg(i32t, NATIVE_REG_INT, reg);
   3826     NativeAddr load;
   3827     memset(&load, 0, sizeof load);
   3828     load.base_kind = NATIVE_ADDR_BASE_FRAME;
   3829     load.base.frame = d->locals[op.v.local - 1u].home;
   3830     load.base_type = op.type;
   3831     arm_emit_mem(arm_of(d->native), 1, base, load,
   3832                  native_mem_for_type(d->native, op.type, 4));
   3833     addr.base_kind = NATIVE_ADDR_BASE_REG;
   3834     addr.base.reg = reg;
   3835     addr.base_type = i32t;
   3836     return addr;
   3837   }
   3838   /* OPK_INDIRECT names *(base+ofs): its address base+ofs IS the pointer. */
   3839   {
   3840     NativeAddr m = arm_direct_materialize_addr(d, op);
   3841     NativeLoc dst = native_loc_reg(i32t, NATIVE_REG_INT, reg);
   3842     d->native->load_addr(d->native, dst, m);
   3843     addr.base_kind = NATIVE_ADDR_BASE_REG;
   3844     addr.base.reg = reg;
   3845     addr.base_type = i32t;
   3846     return addr;
   3847   }
   3848 }
   3849 
   3850 static void arm_va_start_(NativeDirectTarget* d, Operand ap_addr) {
   3851   arm_va_start_core(arm_of(d->native), arm_direct_pointer_addr(d, ap_addr, 4u));
   3852 }
   3853 static void arm_va_arg_(NativeDirectTarget* d, Operand dst_op, Operand ap_addr,
   3854                         KitCgTypeId type) {
   3855   Arm32NativeTarget* a = arm_of(d->native);
   3856   NativeAddr ap = arm_direct_pointer_addr(d, ap_addr, 4u);
   3857   if (arm_va_arg_is_wide(d->native, type)) {
   3858     /* Wide (i64/double) va_arg copies straight from the save area into the
   3859      * destination memory. arm_va_arg_wide uses ip (cursor) + lr (copy scratch),
   3860      * so an OPK_INDIRECT destination pointer must live in a distinct reg (r5),
   3861      * not ip — materialize it there. A FRAME (OPK_LOCAL) destination needs no
   3862      * register. */
   3863     NativeAddr dst = arm_direct_addr(d, dst_op);
   3864     if (dst.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
   3865       NativeLoc base = native_loc_reg(dst.base_type, NATIVE_REG_INT, 5u);
   3866       NativeAddr load;
   3867       memset(&load, 0, sizeof load);
   3868       load.base_kind = NATIVE_ADDR_BASE_FRAME;
   3869       load.base.frame = dst.base.frame;
   3870       load.base_type = dst.base_type;
   3871       arm_emit_mem(a, 1, base, load,
   3872                    native_mem_for_type(d->native, dst.base_type, 4));
   3873       dst.base_kind = NATIVE_ADDR_BASE_REG;
   3874       dst.base.reg = 5u;
   3875     }
   3876     arm_va_arg_wide(a, dst, ap, native_type_size(d->native, type));
   3877     return;
   3878   }
   3879   {
   3880     NativeLoc res = native_loc_reg(type, NATIVE_REG_INT, 5u);
   3881     NativeAddr dst;
   3882     arm_va_arg_core(a, res, ap, type);
   3883     dst = arm_direct_materialize_addr(d, dst_op);
   3884     arm_emit_mem(a, 0, res, dst,
   3885                  native_mem_for_type(d->native, type,
   3886                                      native_type_size(d->native, type)));
   3887   }
   3888 }
   3889 static void arm_va_end_(NativeDirectTarget* d, Operand ap_addr) {
   3890   (void)d;
   3891   (void)ap_addr;
   3892 }
   3893 static void arm_va_copy_(NativeDirectTarget* d, Operand dst_op, Operand src_op) {
   3894   Arm32NativeTarget* a = arm_of(d->native);
   3895   NativeAddr src = arm_direct_pointer_addr(d, src_op, 4u);
   3896   NativeAddr dst = arm_direct_pointer_addr(d, dst_op, 5u);
   3897   arm_va_copy_core(a, dst, src);
   3898 }
   3899 
   3900 static const NativeOps arm_direct_ops = {
   3901     .bind_param = arm_bind_param,
   3902     .tail_call_unrealizable_reason = arm_no_tail,
   3903     .va_start_ = arm_va_start_,
   3904     .va_arg_ = arm_va_arg_,
   3905     .va_end_ = arm_va_end_,
   3906     .va_copy_ = arm_va_copy_,
   3907     .asm_block = arm_direct_asm_block,
   3908 };
   3909 
   3910 const NativeOps* arm32_native_direct_ops(void) { return &arm_direct_ops; }