kit

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

pass_native_emit.c (121889B)


      1 #include <string.h>
      2 
      3 #include "cg/native_asm.h"
      4 #include "cg/type.h"
      5 #include "core/metrics.h"
      6 #include "core/pool.h"
      7 #include "opt/opt_internal.h"
      8 
      9 #undef Operand
     10 #undef CGParamDesc
     11 #undef CGCallDesc
     12 #undef CGFuncDesc
     13 #undef CGLocalStorage
     14 #undef CGABIValue
     15 #undef CGABIPart
     16 #undef CGCallPlan
     17 #undef CGCallPlanMove
     18 #undef CGCallPlanRet
     19 #undef CGScopeDesc
     20 
     21 typedef struct NativeEmitTempScope NativeEmitTempScope;
     22 
     23 typedef struct NativeEmitCtx {
     24   Compiler* c;
     25   Func* f;
     26   NativeTarget* target;
     27   NativeFrameSlot* slot_map;
     28   MCLabel* labels;
     29   u8* label_placed;
     30   ObjSecId local_static_sec;
     31   ObjSymId local_static_sym;
     32   u32 local_static_base;
     33   u32 local_static_size;
     34   u8 local_static_active;
     35   /* Set by emit_block for the IR_RET that is the last inst of the last block
     36    * in emit_order. emit_ret consults it to skip the trailing
     37    * branch-to-epilogue: func_end places the epilogue label at the very next
     38    * position, so the branch would just jump to the next 4 bytes. */
     39   u8 emitting_terminal_ret;
     40   /* Instruction-scoped materialization state. NULL outside emit_inst (notably
     41    * during frame planning/parameter binding). */
     42   NativeEmitTempScope* temps;
     43   /* Physical registers live after each MIR instruction, indexed by InstId.
     44    * Used only to scavenge dead caller-saved registers after the dedicated temp
     45    * bank is exhausted. */
     46   OptHardRegSet* live_after_by_inst;
     47   u32 live_after_cap;
     48   u8 live_after_ready;
     49   /* Clean frame-value forwarding cache, indexed by physical register. A
     50    * nonzero slot means the register holds the exact value already stored in
     51    * that private FS_SPILL slot. Memory is always authoritative. */
     52   FrameSlot frame_cache_slot[OPT_REG_CLASSES][OPT_MAX_HARD_REGS];
     53   KitCgTypeId frame_cache_type[OPT_REG_CLASSES][OPT_MAX_HARD_REGS];
     54   u32 frame_cache_held[OPT_REG_CLASSES];
     55   /* Callee-saved registers selected by the pre-frame asm staging plan.  The
     56    * actual instruction-local allocator is deterministic and chooses the same
     57    * registers during emission; these masks make their ABI preservation part of
     58    * the known frame instead of allocating save slots mid-body. */
     59   u32 asm_stage_callee_used[OPT_REG_CLASSES];
     60   /* Callee-saved registers already represented in the known frame, from MIR
     61    * allocation or asm staging. A dead register in this set may safely retain a
     62    * clean spill value without creating a new ABI preservation obligation. */
     63   u32 callee_saved_used[OPT_REG_CLASSES];
     64 } NativeEmitCtx;
     65 
     66 static void frame_cache_clear(NativeEmitCtx* e);
     67 static void frame_cache_drop_reg(NativeEmitCtx* e, u32 cls, Reg reg);
     68 static void frame_cache_invalidate_masks(NativeEmitCtx* e,
     69                                          const u32* masks);
     70 static NativeLoc frame_cache_take_slot(NativeEmitCtx* e, FrameSlot slot,
     71                                        NativeLoc source,
     72                                        NativeAllocClass cls,
     73                                        KitCgTypeId type, Reg avoid_a,
     74                                        Reg avoid_b);
     75 static void compute_emit_live_after(NativeEmitCtx* e);
     76 
     77 static _Noreturn void emit_panic(NativeEmitCtx* e, SrcLoc loc,
     78                                  const char* msg) {
     79   compiler_panic(e->c, loc, "opt native emit: %s", msg);
     80 }
     81 
     82 static void emit_local_static_begin(NativeEmitCtx* e,
     83                                     const CGLocalStaticDataDesc* desc,
     84                                     SrcLoc loc) {
     85   Sym name;
     86   SecKind kind;
     87   u16 flags;
     88   u32 align;
     89   if (!desc) emit_panic(e, loc, "missing local static data descriptor");
     90   if (e->local_static_active) emit_panic(e, loc, "nested local static data");
     91   if (desc->attrs.section) {
     92     name = (Sym)desc->attrs.section;
     93     kind =
     94         (desc->attrs.flags & KIT_CG_DATADEF_READONLY) ? SEC_RODATA : SEC_DATA;
     95     flags = (desc->attrs.flags & KIT_CG_DATADEF_READONLY)
     96                 ? SF_ALLOC
     97                 : (SF_ALLOC | SF_WRITE);
     98   } else if (desc->attrs.flags & KIT_CG_DATADEF_READONLY) {
     99     name = pool_intern_slice(e->c->global, SLICE_LIT(".rodata"));
    100     kind = SEC_RODATA;
    101     flags = SF_ALLOC;
    102   } else {
    103     name = pool_intern_slice(e->c->global, SLICE_LIT(".data"));
    104     kind = SEC_DATA;
    105     flags = SF_ALLOC | SF_WRITE;
    106   }
    107   align = desc->align ? desc->align : 1u;
    108   e->local_static_sec = obj_section(e->target->obj, name, kind, flags, align);
    109   e->local_static_base =
    110       obj_align_to(e->target->obj, e->local_static_sec, align);
    111   e->local_static_size = 0;
    112   e->local_static_sym = desc->sym;
    113   e->local_static_active = 1;
    114 }
    115 
    116 static void emit_local_static_write(NativeEmitCtx* e, const u8* data, u64 len,
    117                                     SrcLoc loc) {
    118   u8 zero[64];
    119   u64 orig_len = len;
    120   if (!e->local_static_active) emit_panic(e, loc, "local static data inactive");
    121   if (!len) return;
    122   if (data) {
    123     obj_write(e->target->obj, e->local_static_sec, data, (size_t)len);
    124   } else {
    125     memset(zero, 0, sizeof zero);
    126     while (len >= sizeof zero) {
    127       obj_write(e->target->obj, e->local_static_sec, zero, sizeof zero);
    128       len -= sizeof zero;
    129     }
    130     if (len) obj_write(e->target->obj, e->local_static_sec, zero, (size_t)len);
    131   }
    132   e->local_static_size += (u32)orig_len;
    133 }
    134 
    135 static void emit_local_static_label_addr(NativeEmitCtx* e, MCLabel target,
    136                                          i64 addend, u32 width, SrcLoc loc) {
    137   u8 zero[8];
    138   u32 off;
    139   RelocKind kind;
    140   if (!e->local_static_active) emit_panic(e, loc, "local static data inactive");
    141   /* A jump-table / label-address slot is one target pointer wide: 8 bytes
    142    * (R_ABS64) on a 64-bit target, 4 bytes (R_ABS32) on rv32/ELFCLASS32. */
    143   if (width == 8u)
    144     kind = R_ABS64;
    145   else if (width == 4u)
    146     kind = R_ABS32;
    147   else {
    148     emit_panic(e, loc, "unsupported local static label width");
    149     return;
    150   }
    151   memset(zero, 0, sizeof zero);
    152   off = e->local_static_base + e->local_static_size;
    153   obj_write(e->target->obj, e->local_static_sec, zero, width);
    154   mc_emit_label_data_reloc(e->target->mc, e->local_static_sec, off,
    155                                        target, kind, width, addend);
    156   e->local_static_size += width;
    157 }
    158 
    159 static void emit_local_static_end(NativeEmitCtx* e, SrcLoc loc) {
    160   if (!e->local_static_active) emit_panic(e, loc, "local static data inactive");
    161   obj_symbol_define_live(e->target->obj, e->local_static_sym,
    162                          e->local_static_sec, e->local_static_base,
    163                          e->local_static_size);
    164   e->local_static_active = 0;
    165   e->local_static_sec = OBJ_SEC_NONE;
    166   e->local_static_sym = OBJ_SYM_NONE;
    167   e->local_static_base = 0;
    168   e->local_static_size = 0;
    169 }
    170 
    171 static u32 type_size_or(Compiler* c, KitCgTypeId type, u32 fallback) {
    172   u64 n = type ? cg_type_size(c, type) : 0u;
    173   if (!n || n > 0xffffffffull) return fallback;
    174   return (u32)n;
    175 }
    176 
    177 static u32 type_align_or(Compiler* c, KitCgTypeId type, u32 fallback) {
    178   u64 n = type ? cg_type_align(c, type) : 0u;
    179   if (!n || n > 0xffffffffull) return fallback;
    180   return (u32)n;
    181 }
    182 
    183 static MemAccess mem_for_type(Compiler* c, KitCgTypeId type) {
    184   MemAccess mem;
    185   memset(&mem, 0, sizeof mem);
    186   mem.type = type;
    187   mem.size = type_size_or(c, type, 8u);
    188   mem.align = type_align_or(c, type, mem.size >= 8u ? 8u : mem.size);
    189   return mem;
    190 }
    191 
    192 static NativeAllocClass class_for_type(NativeEmitCtx* e, KitCgTypeId type) {
    193   if (e->target->class_for_type)
    194     return e->target->class_for_type(e->target, type);
    195   return cg_type_is_float(e->c, type) ? NATIVE_REG_FP : NATIVE_REG_INT;
    196 }
    197 
    198 static NativeLoc loc_none(void) {
    199   NativeLoc loc;
    200   memset(&loc, 0, sizeof loc);
    201   return loc;
    202 }
    203 
    204 static NativeLoc loc_reg(KitCgTypeId type, NativeAllocClass cls, Reg reg) {
    205   NativeLoc loc;
    206   memset(&loc, 0, sizeof loc);
    207   loc.kind = NATIVE_LOC_REG;
    208   loc.cls = (u8)cls;
    209   loc.type = type;
    210   loc.v.reg = reg;
    211   return loc;
    212 }
    213 
    214 static NativeLoc loc_frame(KitCgTypeId type, NativeAllocClass cls,
    215                            NativeFrameSlot slot) {
    216   NativeLoc loc;
    217   memset(&loc, 0, sizeof loc);
    218   loc.kind = NATIVE_LOC_FRAME;
    219   loc.cls = (u8)cls;
    220   loc.type = type;
    221   loc.v.frame = slot;
    222   return loc;
    223 }
    224 
    225 static NativeLoc loc_frame_addr(KitCgTypeId type, NativeFrameSlot slot) {
    226   NativeLoc loc;
    227   memset(&loc, 0, sizeof loc);
    228   loc.kind = NATIVE_LOC_FRAME_ADDR;
    229   loc.cls = NATIVE_REG_INT;
    230   loc.type = type;
    231   loc.v.frame = slot;
    232   return loc;
    233 }
    234 
    235 static NativeLoc loc_imm(KitCgTypeId type, i64 imm) {
    236   NativeLoc loc;
    237   memset(&loc, 0, sizeof loc);
    238   loc.kind = NATIVE_LOC_IMM;
    239   loc.cls = NATIVE_REG_INT;
    240   loc.type = type;
    241   loc.v.imm = imm;
    242   return loc;
    243 }
    244 
    245 static NativeLoc loc_global(KitCgTypeId type, ObjSymId sym, i64 addend) {
    246   NativeLoc loc;
    247   memset(&loc, 0, sizeof loc);
    248   loc.kind = NATIVE_LOC_GLOBAL;
    249   loc.cls = NATIVE_REG_INT;
    250   loc.type = type;
    251   loc.v.global.sym = sym;
    252   loc.v.global.addend = addend;
    253   return loc;
    254 }
    255 
    256 static int loc_same_frame(NativeLoc a, NativeLoc b) {
    257   return a.kind == NATIVE_LOC_FRAME && b.kind == NATIVE_LOC_FRAME &&
    258          a.v.frame == b.v.frame;
    259 }
    260 
    261 #define NATIVE_EMIT_TEMP_CACHE 16u
    262 
    263 typedef struct NativeEmitTempCacheEntry {
    264   NativeLoc source;
    265   NativeLoc result;
    266 } NativeEmitTempCacheEntry;
    267 
    268 struct NativeEmitTempScope {
    269   NativeEmitCtx* emit;
    270   const Inst* inst;
    271   u8 allow_asm_temps;
    272   u8 effects_ready;
    273   u8 live_after_applied;
    274   u8 pad;
    275   u32 leased[OPT_REG_CLASSES];
    276   u32 unavailable[OPT_REG_CLASSES];
    277   /* Definition registers that currently hold a cached stack source consumed
    278    * by this same instruction. They may be used in place as source+destination
    279    * even though the normal avoidance set contains the destination register. */
    280   u32 cache_def_reuse[OPT_REG_CLASSES];
    281   NativeEmitTempCacheEntry cache[NATIVE_EMIT_TEMP_CACHE];
    282   u32 ncache;
    283 };
    284 
    285 typedef struct NativeEmitTempMark {
    286   u32 leased[OPT_REG_CLASSES];
    287   u32 ncache;
    288 } NativeEmitTempMark;
    289 
    290 typedef struct FrameCacheNeedCtx {
    291   NativeEmitCtx* emit;
    292   u32 regs[OPT_REG_CLASSES];
    293 } FrameCacheNeedCtx;
    294 
    295 static void frame_cache_collect_needed(const Inst* in,
    296                                        FrameCacheNeedCtx* need) {
    297   NativeEmitCtx* e = need->emit;
    298   for (u32 i = 0; in && i < in->nopnds; ++i) {
    299     const OptOperand* op = &in->opnds[i];
    300     if (opt_inst_operand_is_def(in, i) || op->kind != OPK_STACK) continue;
    301     for (u32 c = 0; c < OPT_REG_CLASSES; ++c)
    302       for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r)
    303         if ((e->frame_cache_held[c] & (1u << r)) &&
    304             e->frame_cache_slot[c][r] == op->v.frame_slot &&
    305             e->frame_cache_type[c][r] == op->type)
    306           need->regs[c] |= 1u << r;
    307   }
    308 }
    309 
    310 static int temp_loc_equal(NativeLoc a, NativeLoc b) {
    311   if (a.kind != b.kind || a.cls != b.cls || a.type != b.type) return 0;
    312   switch ((NativeLocKind)a.kind) {
    313     case NATIVE_LOC_NONE:
    314       return 1;
    315     case NATIVE_LOC_REG:
    316       return a.v.reg == b.v.reg;
    317     case NATIVE_LOC_FRAME:
    318     case NATIVE_LOC_FRAME_ADDR:
    319       return a.v.frame == b.v.frame;
    320     case NATIVE_LOC_STACK:
    321       return a.v.stack.slot == b.v.stack.slot &&
    322              a.v.stack.offset == b.v.stack.offset;
    323     case NATIVE_LOC_IMM:
    324       return a.v.imm == b.v.imm;
    325     case NATIVE_LOC_GLOBAL:
    326       return a.v.global.sym == b.v.global.sym &&
    327              a.v.global.addend == b.v.global.addend;
    328     case NATIVE_LOC_ADDR:
    329       return memcmp(&a.v.addr, &b.v.addr, sizeof a.v.addr) == 0;
    330   }
    331   return 0;
    332 }
    333 
    334 static void temp_scope_apply_effects(NativeEmitCtx* e,
    335                                      NativeEmitTempScope* scope) {
    336   OptRegEffectMasks effects;
    337   FrameCacheNeedCtx needed;
    338   u32 invalidate[OPT_REG_CLASSES];
    339   if (!e || !scope || scope->effects_ready) return;
    340   memset(&needed, 0, sizeof needed);
    341   opt_inst_reg_effect_masks(e->f, scope->inst, &effects);
    342   needed.emit = e;
    343   frame_cache_collect_needed(scope->inst, &needed);
    344   for (u32 c = 0; c < OPT_REG_CLASSES; ++c) {
    345     u32 clobbers = effects.clobbers.cls[c];
    346     /* Call clobbers happen after argument/callee staging. A call-local temp is
    347      * consumed by the parallel-copy plan before emit_call and may therefore
    348      * live in a caller-saved register; forbidding the entire ABI clobber set
    349      * makes high-pressure calls impossible precisely when every callee-saved
    350      * allocation register is live across them. Calls clear the block cache
    351      * below, so this exception cannot preserve a stale value past the call. */
    352     if (scope->inst && (IROp)scope->inst->op == IR_CALL) clobbers = 0u;
    353     scope->unavailable[c] |=
    354         effects.uses.cls[c] | effects.defs.cls[c] | clobbers;
    355     scope->cache_def_reuse[c] =
    356         needed.regs[c] & effects.defs.cls[c] &
    357         ~(effects.uses.cls[c] | effects.clobbers.cls[c]);
    358     invalidate[c] = scope->unavailable[c] & ~scope->cache_def_reuse[c];
    359   }
    360   if (scope->inst && ((IROp)scope->inst->op == IR_CALL ||
    361                       (IROp)scope->inst->op == IR_ASM_BLOCK))
    362     frame_cache_clear(e);
    363   else
    364     frame_cache_invalidate_masks(e, invalidate);
    365   scope->effects_ready = 1u;
    366 }
    367 
    368 static void temp_scope_begin(NativeEmitCtx* e, NativeEmitTempScope* scope,
    369                              const Inst* in) {
    370   const u32* machine_clobbers = NULL;
    371   for (u32 c = 0; c < OPT_REG_CLASSES; ++c) {
    372     scope->leased[c] = 0u;
    373     scope->unavailable[c] = 0u;
    374     scope->cache_def_reuse[c] = 0u;
    375   }
    376   scope->emit = e;
    377   scope->inst = in;
    378   scope->allow_asm_temps = 0u;
    379   scope->effects_ready = 0u;
    380   scope->live_after_applied = 0u;
    381   /* ncache is the sole validity boundary for the materialization entries.
    382    * Clearing the 16-entry payload on every MIR instruction only burns memory
    383    * bandwidth; each entry is fully assigned before ncache exposes it. */
    384   scope->ncache = 0u;
    385   e->temps = scope;
    386 
    387   if (e->live_after_ready) {
    388     if (in && in->id != INST_ID_NONE && in->id < e->live_after_cap)
    389       for (u32 c = 0; c < OPT_REG_CLASSES; ++c)
    390         scope->unavailable[c] |= e->live_after_by_inst[in->id].cls[c];
    391     scope->live_after_applied = 1u;
    392     temp_scope_apply_effects(e, scope);
    393     return;
    394   }
    395 
    396   /* Before the allocation-register scavenger is enabled, MIR's ownership
    397    * invariant keeps every explicit use/def out of the reserved emission-temp
    398    * bank. Only an exceptional backend-declared machine clobber can collide
    399    * with those temps, so handle that table directly and defer the full
    400    * use/def analysis alongside hard liveness. */
    401   machine_clobbers = opt_inst_machine_clobber_masks(e->f, in);
    402   if (in && ((IROp)in->op == IR_CALL || (IROp)in->op == IR_ASM_BLOCK)) {
    403     frame_cache_clear(e);
    404   } else if (machine_clobbers) {
    405     for (u32 c = 0; c < OPT_REG_CLASSES; ++c)
    406       scope->unavailable[c] |= machine_clobbers[c];
    407     frame_cache_invalidate_masks(e, machine_clobbers);
    408   }
    409 }
    410 
    411 /* Physical live-after is needed only for the allocation-register scavenger.
    412  * Most instructions (and most functions) fit entirely in the target's
    413  * dedicated emission-temp bank, so defer the full hard-liveness dataflow and
    414  * per-instruction table until a scope actually exhausts that bank. */
    415 static void temp_scope_require_live_after(NativeEmitCtx* e) {
    416   u32 live[OPT_REG_CLASSES];
    417   const Inst* in;
    418   if (!e) return;
    419   if (!e->live_after_ready) compute_emit_live_after(e);
    420   if (!e->temps) return;
    421   if (e->temps->live_after_applied) return;
    422   in = e->temps->inst;
    423   memset(live, 0, sizeof live);
    424   if (in && in->id != INST_ID_NONE && in->id < e->live_after_cap)
    425     for (u32 c = 0; c < OPT_REG_CLASSES; ++c) {
    426       live[c] = e->live_after_by_inst[in->id].cls[c];
    427       e->temps->unavailable[c] |= live[c];
    428     }
    429   e->temps->live_after_applied = 1u;
    430   temp_scope_apply_effects(e, e->temps);
    431   for (u32 c = 0; c < OPT_REG_CLASSES; ++c) {
    432     live[c] &= ~e->temps->cache_def_reuse[c];
    433   }
    434   frame_cache_invalidate_masks(e, live);
    435 }
    436 
    437 static NativeEmitTempMark temp_scope_mark(NativeEmitCtx* e) {
    438   NativeEmitTempMark mark;
    439   memset(&mark, 0, sizeof mark);
    440   if (!e->temps) return mark;
    441   memcpy(mark.leased, e->temps->leased, sizeof mark.leased);
    442   mark.ncache = e->temps->ncache;
    443   return mark;
    444 }
    445 
    446 static void temp_scope_rewind(NativeEmitCtx* e, NativeEmitTempMark mark) {
    447   if (!e->temps) return;
    448   memcpy(e->temps->leased, mark.leased, sizeof mark.leased);
    449   e->temps->ncache = mark.ncache;
    450 }
    451 
    452 /* Forget every instruction-local materialization fact whose value lived in
    453  * `reg`. This does not release the register: a caller that overwrote a cached
    454  * value in place still owns the new result through the existing lease. */
    455 static int temp_scope_drop_cached_reg(NativeEmitCtx* e,
    456                                       NativeAllocClass cls, Reg reg) {
    457   u32 c = (u32)cls;
    458   u32 out = 0;
    459   int owned = 0;
    460   if (!e->temps || c >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return 0;
    461   for (u32 i = 0; i < e->temps->ncache; ++i) {
    462     NativeEmitTempCacheEntry entry = e->temps->cache[i];
    463     if (entry.result.kind == NATIVE_LOC_REG && entry.result.cls == (u8)cls &&
    464         entry.result.v.reg == reg) {
    465       owned = 1;
    466       continue;
    467     }
    468     if (out != i) e->temps->cache[out] = entry;
    469     ++out;
    470   }
    471   if (!owned) return 0;
    472   e->temps->ncache = out;
    473   e->temps->cache_def_reuse[c] &= ~(1u << reg);
    474   return 1;
    475 }
    476 
    477 /* Relinquish a register only when this instruction acquired it as a cached
    478  * materialization. Earlier ABI-phase destinations are leased without a cache
    479  * entry and therefore remain protected. */
    480 static void temp_scope_release_cached_reg(NativeEmitCtx* e,
    481                                           NativeAllocClass cls, Reg reg) {
    482   u32 c = (u32)cls;
    483   if (!temp_scope_drop_cached_reg(e, cls, reg)) return;
    484   e->temps->leased[c] &= ~(1u << reg);
    485 }
    486 
    487 /* End one ordering phase inside a compound native operation. A call consumes
    488  * all argument/callee staging before it clobbers caller-saved registers; none
    489  * of those leases or materialization facts may be observed by return
    490  * writeback. The instruction scope (and its hard-reg avoidance set) remains
    491  * active for the post-call phase. */
    492 static void temp_scope_phase_barrier(NativeEmitCtx* e) {
    493   if (!e->temps) return;
    494   memset(e->temps->leased, 0, sizeof e->temps->leased);
    495   e->temps->ncache = 0;
    496 }
    497 
    498 /* Keep a completed register destination owned until the current compound
    499  * operation reaches its next ordering barrier.  write_loc deliberately
    500  * rewinds the temporary leases used to perform one move; without this separate
    501  * phase lease, a later move could reuse (and overwrite) an earlier ABI
    502  * argument or return register while materializing its own source. */
    503 static void temp_scope_hold_loc(NativeEmitCtx* e, NativeLoc loc) {
    504   u32 c;
    505   Reg r;
    506   if (!e->temps || loc.kind != NATIVE_LOC_REG) return;
    507   c = (u32)loc.cls;
    508   r = loc.v.reg;
    509   if (c >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS)
    510     emit_panic(e, e->temps->inst ? e->temps->inst->loc : (SrcLoc){0, 0, 0},
    511                "invalid phase-owned register");
    512   frame_cache_drop_reg(e, c, r);
    513   e->temps->leased[c] |= 1u << r;
    514 }
    515 
    516 static void temp_scope_end(NativeEmitCtx* e, NativeEmitTempScope* scope) {
    517   if (e->temps != scope)
    518     emit_panic(e, scope->inst ? scope->inst->loc : (SrcLoc){0, 0, 0},
    519                "temporary scope mismatch");
    520 #ifndef NDEBUG
    521   for (u32 c = 0; c < OPT_REG_CLASSES; ++c)
    522     if (scope->leased[c] & e->frame_cache_held[c])
    523       emit_panic(e, scope->inst ? scope->inst->loc : (SrcLoc){0, 0, 0},
    524                  "temporary lease escaped into frame cache");
    525 #endif
    526   memset(scope->leased, 0, sizeof scope->leased);
    527   scope->ncache = 0;
    528   e->temps = NULL;
    529 }
    530 
    531 static int temp_reg_available(NativeEmitCtx* e, NativeAllocClass cls, Reg r,
    532                               Reg a, Reg b) {
    533   u32 c = (u32)cls;
    534   u32 bit;
    535   if (r == a || r == b || r >= 32u || c >= OPT_REG_CLASSES) return 0;
    536   bit = 1u << r;
    537   if (!e->temps) return 1;
    538   return !(e->temps->leased[c] & bit) &&
    539          !(e->temps->unavailable[c] & bit) &&
    540          !(e->frame_cache_held[c] & bit);
    541 }
    542 
    543 static Reg temp_acquire(NativeEmitCtx* e, NativeAllocClass cls, Reg a, Reg b,
    544                         SrcLoc loc) {
    545   u32 c = (u32)cls;
    546   if (c < OPT_REG_CLASSES) {
    547     /* Prefer the target-declared O1 operand-temp bank. These registers are
    548      * excluded from allocation, but still respect explicit/implicit instruction
    549      * clobbers and other leases in this scope. */
    550     for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) {
    551       Reg r = e->f->emit_temp_regs[c][i];
    552       if (!temp_reg_available(e, cls, r, a, b)) continue;
    553       if (e->temps) e->temps->leased[c] |= 1u << r;
    554       return r;
    555     }
    556     /* A dead caller-saved allocation register is an equally local temporary:
    557      * it needs no prologue save and liveness proves its value is dead before
    558      * we overwrite it. This is the pressure escape hatch for two-temp targets
    559      * and for ARM32, whose backend-internal LR must not be leased. */
    560     if (e->temps) {
    561       temp_scope_require_live_after(e);
    562       for (u32 i = 0; i < e->f->opt_hard_reg_count[c]; ++i) {
    563         Reg r = e->f->opt_hard_regs[c][i];
    564         if (r >= 32u || !(e->f->opt_caller_saved[c] & (1u << r))) continue;
    565         if (!temp_reg_available(e, cls, r, a, b)) continue;
    566         e->temps->leased[c] |= 1u << r;
    567         metrics_count(e->c, "opt.native_emit.scavenges", 1);
    568         return r;
    569       }
    570     }
    571     /* A clean cache entry is always evictable: its frame store is already
    572      * authoritative. Prefer losing the forwarding opportunity over failing a
    573      * real instruction's temporary demand. */
    574     if (e->temps) {
    575       for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) {
    576         Reg r = e->f->emit_temp_regs[c][i];
    577         u32 bit = r < 32u ? 1u << r : 0u;
    578         if (!bit || !(e->frame_cache_held[c] & bit) || r == a || r == b ||
    579             (e->temps->leased[c] & bit) ||
    580             (e->temps->unavailable[c] & bit))
    581           continue;
    582         frame_cache_drop_reg(e, c, r);
    583         e->temps->leased[c] |= bit;
    584         return r;
    585       }
    586     }
    587   }
    588   compiler_panic(
    589       e->c, loc,
    590       "opt native emit: no instruction-local native temporary "
    591       "(class=%u op=%u inst=%u leased=0x%08x unavailable=0x%08x "
    592       "cached=0x%08x avoid=%u/%u)",
    593       (unsigned)c,
    594       e->temps && e->temps->inst ? (unsigned)e->temps->inst->op : 0u,
    595       e->temps && e->temps->inst ? (unsigned)e->temps->inst->id : 0u,
    596       e->temps && c < OPT_REG_CLASSES ? (unsigned)e->temps->leased[c] : 0u,
    597       e->temps && c < OPT_REG_CLASSES ? (unsigned)e->temps->unavailable[c]
    598                                       : 0u,
    599       c < OPT_REG_CLASSES ? (unsigned)e->frame_cache_held[c] : 0u,
    600       (unsigned)a, (unsigned)b);
    601 }
    602 
    603 static int asm_temp_reg_ok(NativeEmitCtx* e, NativeAllocClass cls, Reg r,
    604                            u32 allowed_mask) {
    605   if (r >= 32u || (allowed_mask && !(allowed_mask & (1u << r)))) return 0;
    606   return !e->target->regs || !e->target->regs->asm_operand_reg_ok ||
    607          e->target->regs->asm_operand_reg_ok(e->target->regs, cls, r);
    608 }
    609 
    610 static const NativeAllocClassInfo* emit_class_info(NativeEmitCtx* e,
    611                                                     NativeAllocClass cls) {
    612   if (!e->target->regs) return NULL;
    613   for (u32 i = 0; i < e->target->regs->nclasses; ++i)
    614     if (e->target->regs->classes[i].cls == (u8)cls)
    615       return &e->target->regs->classes[i];
    616   return NULL;
    617 }
    618 
    619 static void frame_cache_drop_reg(NativeEmitCtx* e, u32 cls, Reg reg) {
    620   if (cls >= OPT_REG_CLASSES || reg >= OPT_MAX_HARD_REGS) return;
    621   e->frame_cache_slot[cls][reg] = FRAME_SLOT_NONE;
    622   e->frame_cache_type[cls][reg] = 0;
    623   e->frame_cache_held[cls] &= ~(1u << reg);
    624 }
    625 
    626 static void frame_cache_clear(NativeEmitCtx* e) {
    627   memset(e->frame_cache_slot, 0, sizeof e->frame_cache_slot);
    628   memset(e->frame_cache_type, 0, sizeof e->frame_cache_type);
    629   memset(e->frame_cache_held, 0, sizeof e->frame_cache_held);
    630 }
    631 
    632 static void frame_cache_invalidate_masks(NativeEmitCtx* e,
    633                                          const u32* masks) {
    634   if (!masks) return;
    635   for (u32 c = 0; c < OPT_REG_CLASSES; ++c) {
    636     u32 killed = e->frame_cache_held[c] & masks[c];
    637     for (Reg r = 0; killed && r < 32u; ++r)
    638       if (killed & (1u << r)) {
    639         frame_cache_drop_reg(e, c, r);
    640         killed &= ~(1u << r);
    641       }
    642   }
    643 }
    644 
    645 static u32 frame_cache_retainable_mask(NativeEmitCtx* e,
    646                                        NativeAllocClass cls) {
    647   const NativeAllocClassInfo* ci = emit_class_info(e, cls);
    648   u32 c = (u32)cls;
    649   u32 mask;
    650   u32 preserved_allocable;
    651   if (!ci || c >= OPT_REG_CLASSES) return 0u;
    652   mask = ci->emit_cache_mask;
    653   /* Without hard live-after data, only target-reserved cache registers can be
    654    * retained across instructions. An ordinary allocation register reaching a
    655    * spill store may still carry a live MIR value; treating it as a cache entry
    656    * would let a later reload lease and overwrite that value. */
    657   if (!e->live_after_ready) return mask;
    658   preserved_allocable = native_target_caller_saved_mask(e->target, cls);
    659   preserved_allocable |= e->callee_saved_used[c];
    660   /* An allocable register is a legal persistent cache only when acquisition
    661    * proved it dead and its ABI preservation is already established:
    662    * caller-saved until the next call boundary, or a callee-save already present
    663    * in the known frame because MIR/asm uses it. The NativeTarget contract
    664    * requires ordinary hooks to preserve such registers unless the current
    665    * instruction's exhaustive machine_op_clobbers effect says otherwise;
    666    * calls/asm clear the cache at their phase boundary. */
    667   for (u32 i = 0; i < e->f->opt_hard_reg_count[c]; ++i) {
    668     Reg r = e->f->opt_hard_regs[c][i];
    669     if (r < 32u && (preserved_allocable & (1u << r))) mask |= 1u << r;
    670   }
    671   return mask;
    672 }
    673 
    674 /* Acquire a lease satisfying an inline-asm register constraint.  Unlike the
    675  * general temporary picker this may name a fixed ABI register (for example
    676  * x86 "a").  Regalloc has already kept values live across the asm out of that
    677  * register; the availability check enforces that contract at the boundary. */
    678 static Reg temp_acquire_asm(NativeEmitCtx* e, NativeAllocClass cls, Reg fixed,
    679                             u32 allowed_mask, SrcLoc loc) {
    680   u32 c = (u32)cls;
    681   if (!e->temps || c >= OPT_REG_CLASSES)
    682     emit_panic(e, loc, "asm register staging outside temporary scope");
    683   temp_scope_require_live_after(e);
    684   if (fixed != (Reg)REG_NONE) {
    685     if (!asm_temp_reg_ok(e, cls, fixed, allowed_mask) ||
    686         !temp_reg_available(e, cls, fixed, REG_NONE, REG_NONE))
    687       emit_panic(e, loc, "fixed asm staging register is unavailable");
    688     e->temps->leased[c] |= 1u << fixed;
    689     return fixed;
    690   }
    691   for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) {
    692     Reg r = e->f->emit_temp_regs[c][i];
    693     /* The O1 operand-temp bank is deliberately hidden from user
    694      * named-register requirements and allocator homes, but it is valid as an
    695      * internally substituted unrestricted asm operand. Restricted masks still
    696      * apply. */
    697     if (r >= 32u || (allowed_mask && !(allowed_mask & (1u << r))) ||
    698         !temp_reg_available(e, cls, r, REG_NONE, REG_NONE))
    699       continue;
    700     e->temps->leased[c] |= 1u << r;
    701     return r;
    702   }
    703   if (e->temps->allow_asm_temps) {
    704     const NativeAllocClassInfo* ci = emit_class_info(e, cls);
    705     for (u32 i = 0; ci && i < ci->nasm_temps; ++i) {
    706       Reg r = ci->asm_temps[i];
    707       if (r >= 32u || (allowed_mask && !(allowed_mask & (1u << r))) ||
    708           !temp_reg_available(e, cls, r, REG_NONE, REG_NONE))
    709         continue;
    710       e->temps->leased[c] |= 1u << r;
    711       return r;
    712     }
    713   }
    714   /* An asm boundary may use every target-declared asm register, not just the
    715    * allocator's general-purpose subset. This is what makes high-arity blocks
    716    * possible without assigning a fixed register to an entire PReg live range.
    717    * A setup prepass records any selected callee-saved registers in the known
    718    * frame, so choosing one here remains ABI-safe. */
    719   for (u32 i = 0; i < e->f->opt_phys_reg_count[c]; ++i) {
    720     Reg r = e->f->opt_phys_regs[c][i].reg;
    721     if (!asm_temp_reg_ok(e, cls, r, allowed_mask) ||
    722         !temp_reg_available(e, cls, r, REG_NONE, REG_NONE))
    723       continue;
    724     e->temps->leased[c] |= 1u << r;
    725     return r;
    726   }
    727   compiler_panic(e->c, loc,
    728                  "opt native emit: no register available for asm operand "
    729                  "staging (class=%u leased=0x%08x unavailable=0x%08x "
    730                  "allowed=0x%08x)",
    731                  (unsigned)c, (unsigned)e->temps->leased[c],
    732                  (unsigned)e->temps->unavailable[c],
    733                  (unsigned)allowed_mask);
    734 }
    735 
    736 /* A leased register is owned by the current emission scope and may therefore
    737  * be overwritten in place.  An ordinary OPK_REG may still be live after the
    738  * instruction even when the result itself is frame-resident, so reusing it as
    739  * the result would silently clobber an allocator-owned value. */
    740 static int temp_reg_is_leased(NativeEmitCtx* e, NativeAllocClass cls, Reg r) {
    741   u32 c = (u32)cls;
    742   return e->temps && c < OPT_REG_CLASSES && r < 32u &&
    743          (e->temps->leased[c] & (1u << r)) != 0;
    744 }
    745 
    746 static int temp_available(NativeEmitCtx* e, NativeAllocClass cls, Reg a,
    747                           Reg b) {
    748   u32 c = (u32)cls;
    749   if (c < OPT_REG_CLASSES) {
    750     for (u32 i = 0; i < e->f->emit_temp_reg_count[c]; ++i) {
    751       Reg r = e->f->emit_temp_regs[c][i];
    752       if (temp_reg_available(e, cls, r, a, b)) return 1;
    753     }
    754     if (e->temps) {
    755       temp_scope_require_live_after(e);
    756       for (u32 i = 0; i < e->f->opt_hard_reg_count[c]; ++i) {
    757         Reg r = e->f->opt_hard_regs[c][i];
    758         if (r < 32u && (e->f->opt_caller_saved[c] & (1u << r)) &&
    759             temp_reg_available(e, cls, r, a, b))
    760           return 1;
    761       }
    762     }
    763   }
    764   return 0;
    765 }
    766 
    767 static NativeLoc temp_loc_acquire(NativeEmitCtx* e, KitCgTypeId type,
    768                                   NativeAllocClass cls, Reg a, Reg b,
    769                                   SrcLoc loc) {
    770   return loc_reg(type, cls, temp_acquire(e, cls, a, b, loc));
    771 }
    772 
    773 static NativeFrameSlot map_slot(NativeEmitCtx* e, NativeFrameSlot slot,
    774                                 SrcLoc loc) {
    775   if (slot == NATIVE_FRAME_SLOT_NONE) return NATIVE_FRAME_SLOT_NONE;
    776   if (slot > e->f->nframe_slots) emit_panic(e, loc, "bad frame slot");
    777   if (!e->slot_map[slot]) emit_panic(e, loc, "unmapped frame slot");
    778   return e->slot_map[slot];
    779 }
    780 
    781 static MCLabel ensure_label(NativeEmitCtx* e, u32 block, SrcLoc loc) {
    782   if (block >= e->f->nblocks) emit_panic(e, loc, "bad block label");
    783   if (e->labels[block] == MC_LABEL_NONE)
    784     e->labels[block] = e->target->label_new(e->target);
    785   return e->labels[block];
    786 }
    787 
    788 static NativeAddr addr_from_loc(NativeEmitCtx* e, NativeLoc loc,
    789                                 SrcLoc src_loc) {
    790   NativeAddr addr;
    791   memset(&addr, 0, sizeof addr);
    792   addr.base_type = loc.type;
    793   switch ((NativeLocKind)loc.kind) {
    794     case NATIVE_LOC_FRAME:
    795       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    796       addr.base.frame = loc.v.frame;
    797       return addr;
    798     case NATIVE_LOC_FRAME_ADDR:
    799       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    800       addr.base.frame = loc.v.frame;
    801       return addr;
    802     case NATIVE_LOC_STACK:
    803       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    804       addr.base.frame = loc.v.stack.slot;
    805       addr.offset = loc.v.stack.offset;
    806       return addr;
    807     case NATIVE_LOC_GLOBAL:
    808       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
    809       addr.base.global.sym = loc.v.global.sym;
    810       addr.base.global.addend = loc.v.global.addend;
    811       return addr;
    812     case NATIVE_LOC_REG:
    813       addr.base_kind = NATIVE_ADDR_BASE_REG;
    814       addr.cls = loc.cls;
    815       addr.base.reg = loc.v.reg;
    816       return addr;
    817     case NATIVE_LOC_ADDR:
    818       return loc.v.addr;
    819     default:
    820       emit_panic(e, src_loc, "location is not addressable");
    821   }
    822 }
    823 
    824 static NativeAddr addr_from_operand(NativeEmitCtx* e, const OptOperand* op,
    825                                     SrcLoc loc) {
    826   NativeAddr addr;
    827   memset(&addr, 0, sizeof addr);
    828   if (!op) emit_panic(e, loc, "missing address operand");
    829   addr.base_type = op->type;
    830   switch ((OptOperandKind)op->kind) {
    831     case OPT_OPK_LOCAL:
    832       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    833       addr.base.frame = map_slot(e, op->v.frame_slot, loc);
    834       return addr;
    835     case OPT_OPK_FRAME_ADDR:
    836       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    837       addr.base.frame = map_slot(e, op->v.frame_slot, loc);
    838       return addr;
    839     case OPT_OPK_STACK:
    840       addr.base_type = op->type;
    841       {
    842         NativeFrameSlot mapped = map_slot(e, op->v.frame_slot, loc);
    843         NativeAllocClass cls = class_for_type(e, addr.base_type);
    844         NativeLoc held = frame_cache_take_slot(
    845             e, op->v.frame_slot, loc_frame(addr.base_type, cls, mapped), cls,
    846             addr.base_type, REG_NONE, REG_NONE);
    847         if (held.kind == NATIVE_LOC_REG) {
    848           addr.base_kind = NATIVE_ADDR_BASE_REG;
    849           addr.cls = held.cls;
    850           addr.base.reg = held.v.reg;
    851         } else {
    852           addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
    853           addr.base.frame = mapped;
    854         }
    855       }
    856       return addr;
    857     case OPT_OPK_GLOBAL:
    858       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
    859       addr.base.global.sym = op->v.global.sym;
    860       addr.base.global.addend = op->v.global.addend;
    861       return addr;
    862     case OPT_OPK_INDIRECT:
    863       addr.cls = NATIVE_REG_INT;
    864       addr.base_type = op->v.ind.base_type;
    865       if (!addr.base_type)
    866         emit_panic(e, loc, "indirect base has no location type");
    867       if (op->v.ind.base_kind == OPT_INDIRECT_FRAME) {
    868         {
    869           NativeFrameSlot mapped = map_slot(e, op->v.ind.base, loc);
    870           NativeLoc held = frame_cache_take_slot(
    871               e, op->v.ind.base,
    872               loc_frame(addr.base_type, NATIVE_REG_INT, mapped),
    873               NATIVE_REG_INT, addr.base_type, REG_NONE, REG_NONE);
    874           if (held.kind == NATIVE_LOC_REG) {
    875             addr.base_kind = NATIVE_ADDR_BASE_REG;
    876             addr.base.reg = held.v.reg;
    877           } else {
    878             addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
    879             addr.base.frame = mapped;
    880           }
    881         }
    882       } else if (op->v.ind.base_kind == OPT_INDIRECT_FRAME_ADDR) {
    883         addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    884         addr.base.frame = map_slot(e, op->v.ind.base, loc);
    885       } else {
    886         addr.base_kind = NATIVE_ADDR_BASE_REG;
    887         addr.base.reg = op->v.ind.base;
    888       }
    889       addr.index_cls = NATIVE_REG_INT;
    890       if (op->v.ind.index_kind == OPT_INDIRECT_FRAME) {
    891         addr.index_type = op->v.ind.index_type;
    892         if (!addr.index_type)
    893           emit_panic(e, loc, "indirect index has no location type");
    894         {
    895           NativeFrameSlot mapped = map_slot(e, op->v.ind.index, loc);
    896           NativeLoc held = frame_cache_take_slot(
    897               e, op->v.ind.index,
    898               loc_frame(addr.index_type, NATIVE_REG_INT, mapped),
    899               NATIVE_REG_INT, addr.index_type, REG_NONE, REG_NONE);
    900           if (held.kind == NATIVE_LOC_REG) {
    901             addr.index_kind = NATIVE_ADDR_INDEX_REG;
    902             addr.index.reg = held.v.reg;
    903           } else {
    904             addr.index_kind = NATIVE_ADDR_INDEX_FRAME_VALUE;
    905             addr.index.frame = mapped;
    906           }
    907         }
    908       } else if (op->v.ind.index_kind == OPT_INDIRECT_FRAME_ADDR) {
    909         emit_panic(e, loc, "frame address cannot be an indirect index");
    910       } else if (op->v.ind.index == (Reg)REG_NONE) {
    911         addr.index_kind = NATIVE_ADDR_INDEX_NONE;
    912       } else {
    913         addr.index_type = op->v.ind.index_type;
    914         if (!addr.index_type)
    915           emit_panic(e, loc, "indirect index has no location type");
    916         addr.index_kind = NATIVE_ADDR_INDEX_REG;
    917         addr.index.reg = op->v.ind.index;
    918       }
    919       addr.log2_scale = op->v.ind.log2_scale;
    920       /* L8 extend rider: OPT_IDX_EXT_* and NATIVE_ADDR_IDX_EXT_* share the
    921        * 0=NONE/1=SXTW/2=UXTW encoding. Only set by the addr-fold recognition
    922        * when the target advertised can_fold_extend_into_addr, so a backend
    923        * without the capability always sees NONE. */
    924       addr.index_ext = op->v.ind.index_ext;
    925       addr.offset = op->v.ind.ofs;
    926       return addr;
    927     case OPT_OPK_REG:
    928       addr.base_kind = NATIVE_ADDR_BASE_REG;
    929       addr.cls = op->cls;
    930       addr.base.reg = op->v.reg;
    931       return addr;
    932     default:
    933       emit_panic(e, loc, "operand is not addressable");
    934   }
    935 }
    936 
    937 static NativeAddr pointer_addr_from_operand(NativeEmitCtx* e,
    938                                             const OptOperand* op, SrcLoc loc,
    939                                             Reg avoid_a, Reg avoid_b) {
    940   NativeAddr addr;
    941   memset(&addr, 0, sizeof addr);
    942   if (!op) emit_panic(e, loc, "missing pointer operand");
    943   addr.base_type = op->type;
    944   switch ((OptOperandKind)op->kind) {
    945     case OPT_OPK_LOCAL: {
    946       NativeAddr frame;
    947       NativeLoc dst;
    948       NativeAllocClass cls;
    949       Reg r;
    950       /* An OPK_LOCAL in a pointer-address position is ambiguous. When the
    951        * operand's type is a pointer, the local *holds* the pointer value and
    952        * must be loaded to get the address. Otherwise the local *is* the
    953        * aggregate storage and its frame home is the address directly — loading
    954        * it would dereference the aggregate's first 8 bytes as a pointer (e.g.
    955        * an `__int128` call result copied by `agg_copy`). Mirrors the
    956        * single-pass path's nd_addr_pointer. */
    957       if (!cg_type_is_ptr(e->c, op->type)) {
    958         addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    959         addr.base.frame = map_slot(e, op->v.frame_slot, loc);
    960         return addr;
    961       }
    962       cls = class_for_type(e, op->type);
    963       r = temp_acquire(e, cls, avoid_a, avoid_b, loc);
    964       memset(&frame, 0, sizeof frame);
    965       frame.base_kind = NATIVE_ADDR_BASE_FRAME;
    966       frame.base.frame = map_slot(e, op->v.frame_slot, loc);
    967       frame.base_type = op->type;
    968       dst = loc_reg(op->type, cls, r);
    969       e->target->load(e->target, dst, frame, mem_for_type(e->c, op->type));
    970       addr.base_kind = NATIVE_ADDR_BASE_REG;
    971       addr.cls = (u8)cls;
    972       addr.base.reg = r;
    973       return addr;
    974     }
    975     case OPT_OPK_FRAME_ADDR:
    976       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    977       addr.base.frame = map_slot(e, op->v.frame_slot, loc);
    978       return addr;
    979     case OPT_OPK_STACK:
    980       addr.cls = class_for_type(e, op->type);
    981       addr.base_type = op->type;
    982       {
    983         NativeFrameSlot mapped = map_slot(e, op->v.frame_slot, loc);
    984         NativeAllocClass cls = (NativeAllocClass)addr.cls;
    985         NativeLoc held = frame_cache_take_slot(
    986             e, op->v.frame_slot, loc_frame(addr.base_type, cls, mapped), cls,
    987             addr.base_type, avoid_a, avoid_b);
    988         if (held.kind == NATIVE_LOC_REG) {
    989           addr.base_kind = NATIVE_ADDR_BASE_REG;
    990           addr.base.reg = held.v.reg;
    991         } else {
    992           addr.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
    993           addr.base.frame = mapped;
    994         }
    995       }
    996       return addr;
    997     case OPT_OPK_GLOBAL:
    998       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
    999       addr.base.global.sym = op->v.global.sym;
   1000       addr.base.global.addend = op->v.global.addend;
   1001       return addr;
   1002     case OPT_OPK_INDIRECT:
   1003       return addr_from_operand(e, op, loc);
   1004     case OPT_OPK_REG:
   1005       addr.base_kind = NATIVE_ADDR_BASE_REG;
   1006       addr.cls = op->cls;
   1007       addr.base.reg = op->v.reg;
   1008       return addr;
   1009     default:
   1010       emit_panic(e, loc, "operand is not a pointer address");
   1011   }
   1012 }
   1013 
   1014 static Reg addr_base_reg(const NativeAddr* addr) {
   1015   return addr && addr->base_kind == NATIVE_ADDR_BASE_REG ? addr->base.reg
   1016                                                          : REG_NONE;
   1017 }
   1018 
   1019 static Reg addr_index_reg(const NativeAddr* addr) {
   1020   return addr && addr->index_kind == NATIVE_ADDR_INDEX_REG ? addr->index.reg
   1021                                                            : REG_NONE;
   1022 }
   1023 
   1024 static void collapse_addr_to_reg(NativeEmitCtx* e, NativeAddr* addr,
   1025                                  SrcLoc loc) {
   1026   /* Materialize the full address into an instruction-local temporary. We
   1027    * normally must not reuse the base register as the destination: physical MIR
   1028    * may keep that value live past this memory op (e.g. a pointer stored into
   1029    * several of its own fields and then returned), so an in-place `add base,
   1030    * base, #off` would corrupt it. Avoid both base and index so load_addr can
   1031    * still read them.
   1032    *
   1033    * If no third temporary is available and the base is itself a scoped
   1034    * temporary, that base is transient and may be updated in place. load_addr
   1035    * reads the base before adding the scaled index, so dst==base is sound;
   1036    * dst==index would clobber the index before use and is never selected. If the
   1037    * base is caller-owned, temp_acquire reports exhaustion instead of silently
   1038    * changing its value. */
   1039   Reg base = addr_base_reg(addr);
   1040   Reg index = addr_index_reg(addr);
   1041   Reg r;
   1042   if (temp_available(e, NATIVE_REG_INT, base, index))
   1043     r = temp_acquire(e, NATIVE_REG_INT, base, index, loc);
   1044   else if (base != index && temp_reg_is_leased(e, NATIVE_REG_INT, base))
   1045     r = base;
   1046   else
   1047     r = temp_acquire(e, NATIVE_REG_INT, base, index, loc); /* diagnoses cleanly */
   1048   NativeLoc dst = loc_reg(addr->base_type, NATIVE_REG_INT, r);
   1049   e->target->load_addr(e->target, dst, *addr);
   1050   /* load_addr replaced any prior value fact attached to its destination. The
   1051    * result remains leased; only the old cache identity is invalid now. */
   1052   temp_scope_drop_cached_reg(e, NATIVE_REG_INT, r);
   1053   if (base != r)
   1054     temp_scope_release_cached_reg(e, NATIVE_REG_INT, base);
   1055   if (index != r)
   1056     temp_scope_release_cached_reg(e, NATIVE_REG_INT, index);
   1057   memset(addr, 0, sizeof *addr);
   1058   addr->base_kind = NATIVE_ADDR_BASE_REG;
   1059   addr->cls = NATIVE_REG_INT;
   1060   addr->base.reg = r;
   1061   addr->base_type = dst.type;
   1062 }
   1063 
   1064 /* Collapse an address the target cannot encode for this access (e.g. an
   1065  * index scale aarch64 cannot fold into a load/store) into a single base
   1066  * register via load_addr. Mirrors NativeDirectTarget's nd_addr_materialize so
   1067  * the O1 emit path legalizes the same address shapes as direct -O0 emission. */
   1068 static void legalize_addr(NativeEmitCtx* e, NativeAddr* addr, MemAccess mem,
   1069                           SrcLoc loc) {
   1070   if (e->target->addr_legal && !e->target->addr_legal(e->target, addr, mem))
   1071     collapse_addr_to_reg(e, addr, loc);
   1072 }
   1073 
   1074 static NativeLoc loc_from_operand(NativeEmitCtx* e, const OptOperand* op,
   1075                                   SrcLoc loc) {
   1076   if (!op) return loc_none();
   1077   switch ((OptOperandKind)op->kind) {
   1078     case OPT_OPK_REG:
   1079       return loc_reg(op->type, (NativeAllocClass)op->cls, op->v.reg);
   1080     case OPT_OPK_IMM:
   1081       return loc_imm(op->type, op->v.imm);
   1082     case OPT_OPK_GLOBAL:
   1083       return loc_global(op->type, op->v.global.sym, op->v.global.addend);
   1084     case OPT_OPK_LOCAL:
   1085       return loc_frame(op->type, class_for_type(e, op->type),
   1086                        map_slot(e, op->v.frame_slot, loc));
   1087     case OPT_OPK_STACK:
   1088       return loc_frame(op->type, class_for_type(e, op->type),
   1089                        map_slot(e, op->v.frame_slot, loc));
   1090     case OPT_OPK_FRAME_ADDR:
   1091       return loc_frame_addr(op->type, map_slot(e, op->v.frame_slot, loc));
   1092     case OPT_OPK_INDIRECT: {
   1093       NativeLoc out = loc_none();
   1094       out.kind = NATIVE_LOC_ADDR;
   1095       out.cls = op->cls;
   1096       out.type = op->type;
   1097       out.v.addr = addr_from_operand(e, op, loc);
   1098       return out;
   1099     }
   1100   }
   1101   emit_panic(e, loc, "bad operand kind");
   1102 }
   1103 
   1104 static NativeLoc materialize(NativeEmitCtx* e, NativeLoc src,
   1105                              NativeAllocClass cls, KitCgTypeId type,
   1106                              Reg avoid_a, Reg avoid_b, SrcLoc loc) {
   1107   NativeLoc dst;
   1108   NativeAddr addr;
   1109   MemAccess mem;
   1110   if (src.kind == NATIVE_LOC_REG) return src;
   1111   if (e->temps) {
   1112     KitCgTypeId want_type = type ? type : src.type;
   1113     for (u32 i = 0; i < e->temps->ncache; ++i) {
   1114       NativeEmitTempCacheEntry* entry = &e->temps->cache[i];
   1115       Reg cached_reg = entry->result.v.reg;
   1116       u32 cached_bit = cached_reg < 32u ? 1u << cached_reg : 0u;
   1117       int avoided = cached_reg == avoid_a || cached_reg == avoid_b;
   1118       if (entry->result.cls != (u8)cls || entry->result.type != want_type ||
   1119           (avoided &&
   1120            !(e->temps->cache_def_reuse[(u32)cls] & cached_bit)))
   1121         continue;
   1122       if (temp_loc_equal(entry->source, src)) return entry->result;
   1123     }
   1124   }
   1125   dst = temp_loc_acquire(e, type ? type : src.type, cls, avoid_a, avoid_b,
   1126                          loc);
   1127   switch ((NativeLocKind)src.kind) {
   1128     case NATIVE_LOC_IMM:
   1129       e->target->load_imm(e->target, dst, src.v.imm);
   1130       break;
   1131     case NATIVE_LOC_GLOBAL:
   1132       addr = addr_from_loc(e, src, loc);
   1133       e->target->load_addr(e->target, dst, addr);
   1134       break;
   1135     case NATIVE_LOC_FRAME_ADDR:
   1136       addr = addr_from_loc(e, src, loc);
   1137       e->target->load_addr(e->target, dst, addr);
   1138       break;
   1139     case NATIVE_LOC_FRAME:
   1140     case NATIVE_LOC_STACK:
   1141     case NATIVE_LOC_ADDR:
   1142       addr = addr_from_loc(e, src, loc);
   1143       mem = mem_for_type(e->c, dst.type);
   1144       e->target->load(e->target, dst, addr, mem);
   1145       break;
   1146     default:
   1147       emit_panic(e, loc, "cannot materialize location");
   1148   }
   1149   if (e->temps && e->temps->ncache < NATIVE_EMIT_TEMP_CACHE) {
   1150     NativeEmitTempCacheEntry* entry = &e->temps->cache[e->temps->ncache++];
   1151     entry->source = src;
   1152     entry->result = dst;
   1153   }
   1154   return dst;
   1155 }
   1156 
   1157 static int frame_cache_slot_is_exact_spill(NativeEmitCtx* e,
   1158                                            const OptOperand* op,
   1159                                            MemAccess mem) {
   1160   const IRFrameSlot* slot;
   1161   if (!op || op->kind != OPK_STACK || op->v.frame_slot == FRAME_SLOT_NONE ||
   1162       op->v.frame_slot > e->f->nframe_slots)
   1163     return 0;
   1164   slot = &e->f->frame_slots[op->v.frame_slot - 1u];
   1165   /* Spill-slot coloring may reuse one slot for different, non-overlapping type
   1166    * IDs. Exactness is the stored bit width; the cache entry separately records
   1167    * the current value's type/class and only answers an identical use. */
   1168   return slot->kind == FS_SPILL && slot->size >= mem.size &&
   1169          mem.size == type_size_or(e->c, op->type, 0u);
   1170 }
   1171 
   1172 static void frame_cache_invalidate_slot(NativeEmitCtx* e, FrameSlot slot) {
   1173   if (slot == FRAME_SLOT_NONE) return;
   1174   for (u32 c = 0; c < OPT_REG_CLASSES; ++c)
   1175     for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r)
   1176       if (e->frame_cache_slot[c][r] == slot)
   1177         frame_cache_drop_reg(e, c, r);
   1178 }
   1179 
   1180 static int frame_cache_types_compatible(NativeEmitCtx* e,
   1181                                         KitCgTypeId cached,
   1182                                         KitCgTypeId requested) {
   1183   (void)e;
   1184   return cached == requested;
   1185 }
   1186 
   1187 static NativeLoc frame_cache_take_slot(NativeEmitCtx* e, FrameSlot slot,
   1188                                        NativeLoc source,
   1189                                        NativeAllocClass cls,
   1190                                        KitCgTypeId type, Reg avoid_a,
   1191                                        Reg avoid_b) {
   1192   u32 c = (u32)cls;
   1193   if (!e->temps || slot == FRAME_SLOT_NONE || c >= OPT_REG_CLASSES)
   1194     return loc_none();
   1195   for (Reg r = 0; r < OPT_MAX_HARD_REGS; ++r) {
   1196     u32 bit = 1u << r;
   1197     NativeLoc result;
   1198     int avoided = r == avoid_a || r == avoid_b;
   1199     if (!(e->frame_cache_held[c] & bit) ||
   1200         (avoided && !(e->temps->cache_def_reuse[c] & bit)) ||
   1201         e->frame_cache_slot[c][r] != slot ||
   1202         !frame_cache_types_compatible(e, e->frame_cache_type[c][r], type) ||
   1203         ((e->temps->unavailable[c] & bit) &&
   1204          !(e->temps->cache_def_reuse[c] & bit)))
   1205       continue;
   1206     frame_cache_drop_reg(e, c, r);
   1207     e->temps->leased[c] |= bit;
   1208     result = loc_reg(type, cls, r);
   1209     if (e->temps->ncache < NATIVE_EMIT_TEMP_CACHE) {
   1210       NativeEmitTempCacheEntry* entry =
   1211           &e->temps->cache[e->temps->ncache++];
   1212       entry->source = source;
   1213       entry->result = result;
   1214     }
   1215     return result;
   1216   }
   1217   return loc_none();
   1218 }
   1219 
   1220 static NativeLoc frame_cache_take(NativeEmitCtx* e, const OptOperand* op,
   1221                                   NativeLoc source, NativeAllocClass cls,
   1222                                   KitCgTypeId type, Reg avoid_a, Reg avoid_b) {
   1223   if (!op || op->kind != OPK_STACK) return loc_none();
   1224   return frame_cache_take_slot(e, op->v.frame_slot, source, cls, type,
   1225                                avoid_a, avoid_b);
   1226 }
   1227 
   1228 static void frame_cache_note_store(NativeEmitCtx* e, const OptOperand* op,
   1229                                    NativeLoc src, MemAccess mem) {
   1230   NativeAllocClass cls;
   1231   u32 c, bit;
   1232   Reg r;
   1233   int retainable;
   1234   if (!op || op->kind != OPK_STACK) return;
   1235   frame_cache_invalidate_slot(e, op->v.frame_slot);
   1236   if (!e->temps || src.kind != NATIVE_LOC_REG ||
   1237       !frame_cache_slot_is_exact_spill(e, op, mem))
   1238     return;
   1239   cls = (NativeAllocClass)src.cls;
   1240   c = (u32)cls;
   1241   r = src.v.reg;
   1242   if (c >= OPT_REG_CLASSES || r >= OPT_MAX_HARD_REGS) return;
   1243   bit = 1u << r;
   1244   /* Cross-instruction retention follows the explicit NativeTarget preservation
   1245    * contract: declared cache-enabled emission temps plus liveness-proven dead
   1246    * O1 allocation registers
   1247    * whose caller/callee-save preservation is established, with every
   1248    * exceptional fixed clobber represented by the current instruction's
   1249    * exhaustive machine effect. */
   1250   retainable = (frame_cache_retainable_mask(e, cls) & bit) != 0;
   1251   if (!retainable) return;
   1252   frame_cache_drop_reg(e, c, r);
   1253   e->temps->leased[c] &= ~bit; /* transfer scope ownership to block cache */
   1254   e->frame_cache_slot[c][r] = op->v.frame_slot;
   1255   e->frame_cache_type[c][r] = op->type;
   1256   e->frame_cache_held[c] |= bit;
   1257 }
   1258 
   1259 static NativeLoc materialize_operand(NativeEmitCtx* e, const OptOperand* op,
   1260                                      NativeAllocClass cls, KitCgTypeId type,
   1261                                      Reg avoid_a, Reg avoid_b, SrcLoc loc) {
   1262   NativeLoc source = loc_from_operand(e, op, loc);
   1263   NativeLoc cached = frame_cache_take(e, op, source, cls,
   1264                                       type ? type : source.type, avoid_a,
   1265                                       avoid_b);
   1266   if (cached.kind != NATIVE_LOC_NONE) return cached;
   1267   return materialize(e, source, cls, type, avoid_a, avoid_b, loc);
   1268 }
   1269 
   1270 static void write_loc(NativeEmitCtx* e, NativeLoc dst, NativeLoc src,
   1271                       MemAccess mem, SrcLoc loc) {
   1272   NativeAddr addr;
   1273   NativeEmitTempMark mark = temp_scope_mark(e);
   1274   if (dst.kind == NATIVE_LOC_NONE) goto done;
   1275   if (loc_same_frame(dst, src)) goto done;
   1276   if (dst.kind == NATIVE_LOC_REG) {
   1277     if (src.kind == NATIVE_LOC_REG) {
   1278       if (dst.v.reg != src.v.reg || dst.cls != src.cls)
   1279         e->target->move(e->target, dst, src);
   1280       goto done;
   1281     }
   1282     /* An immediate goes straight into the destination register; routing it
   1283      * through an instruction-local temp and then moving would cost an extra
   1284      * instruction. */
   1285     if (src.kind == NATIVE_LOC_IMM) {
   1286       e->target->load_imm(e->target, dst, src.v.imm);
   1287       goto done;
   1288     }
   1289     /* A register destination is itself the best materialization register.
   1290      * Loading through an emitter temp first adds a move to every spill reload
   1291      * and ABI/frame copy. Native loads consume their address before defining
   1292      * the destination, so even an address based on dst is safe here. */
   1293     if (src.kind == NATIVE_LOC_GLOBAL ||
   1294         src.kind == NATIVE_LOC_FRAME_ADDR) {
   1295       addr = addr_from_loc(e, src, loc);
   1296       e->target->load_addr(e->target, dst, addr);
   1297       goto done;
   1298     }
   1299     if (src.kind == NATIVE_LOC_FRAME || src.kind == NATIVE_LOC_STACK ||
   1300         src.kind == NATIVE_LOC_ADDR) {
   1301       addr = addr_from_loc(e, src, loc);
   1302       e->target->load(e->target, dst, addr, mem);
   1303       goto done;
   1304     }
   1305     emit_panic(e, loc, "cannot write location to register");
   1306   }
   1307   addr = addr_from_loc(e, dst, loc);
   1308   if (src.kind != NATIVE_LOC_REG)
   1309     src = materialize(e, src, (NativeAllocClass)dst.cls, dst.type, REG_NONE,
   1310                       REG_NONE, loc);
   1311   e->target->store(e->target, addr, src, mem);
   1312 done:
   1313   temp_scope_rewind(e, mark);
   1314 }
   1315 
   1316 /* Execute one move in a multi-move ABI phase and retain its register
   1317  * destination. The backend call/return marshallers promise that moves are
   1318  * source-safe in the order returned; this helper supplies the complementary
   1319  * destination-lifetime guarantee for emitter-side materialization. */
   1320 static void write_phase_loc(NativeEmitCtx* e, NativeLoc dst, NativeLoc src,
   1321                             MemAccess mem, SrcLoc loc) {
   1322   write_loc(e, dst, src, mem, loc);
   1323   temp_scope_hold_loc(e, dst);
   1324 }
   1325 
   1326 static void write_operand(NativeEmitCtx* e, const OptOperand* dst,
   1327                           NativeLoc src, MemAccess mem, SrcLoc loc) {
   1328   NativeLoc real_dst = loc_from_operand(e, dst, loc);
   1329   /* Keep the register that actually reaches a spill store. Besides avoiding a
   1330    * second materialization in write_loc, this lets ownership transfer from the
   1331    * instruction lease to the clean block cache. Constants and copies into a
   1332    * spill home are common producers and otherwise lose their forwarding fact
   1333    * at this abstraction boundary. */
   1334   if (dst && dst->kind == OPK_STACK && src.kind != NATIVE_LOC_REG &&
   1335       !loc_same_frame(real_dst, src)) {
   1336     /* Keep integer zero in the target's architectural zero register when it
   1337      * can be stored directly. Materializing it into an emitter temp costs an
   1338      * otherwise redundant movz/li on every zero-initialized spill. */
   1339     if (src.kind == NATIVE_LOC_IMM && src.v.imm == 0 &&
   1340         e->target->has_store_zero_reg &&
   1341         class_for_type(e, real_dst.type) == NATIVE_REG_INT)
   1342       src = loc_reg(real_dst.type, NATIVE_REG_INT,
   1343                     e->target->store_zero_reg);
   1344     else
   1345       src = materialize(e, src, (NativeAllocClass)real_dst.cls, real_dst.type,
   1346                         REG_NONE, REG_NONE, loc);
   1347   }
   1348   write_loc(e, real_dst, src, mem, loc);
   1349   frame_cache_note_store(e, dst, src, mem);
   1350 }
   1351 
   1352 /* Place a value directly in an already-leased register.  This is the boundary
   1353  * operation used for fixed/constrained asm operands: going through materialize
   1354  * would acquire an unrelated temporary and then copy, which both wastes a
   1355  * register and can make a satisfiable high-arity asm look impossible. */
   1356 static void load_loc_into_reg(NativeEmitCtx* e, NativeLoc dst, NativeLoc src,
   1357                               SrcLoc loc) {
   1358   NativeAddr addr;
   1359   if (dst.kind != NATIVE_LOC_REG)
   1360     emit_panic(e, loc, "staging destination is not a register");
   1361   switch ((NativeLocKind)src.kind) {
   1362     case NATIVE_LOC_REG:
   1363       if (src.v.reg != dst.v.reg || src.cls != dst.cls)
   1364         e->target->move(e->target, dst, src);
   1365       return;
   1366     case NATIVE_LOC_IMM:
   1367       e->target->load_imm(e->target, dst, src.v.imm);
   1368       return;
   1369     case NATIVE_LOC_GLOBAL:
   1370     case NATIVE_LOC_FRAME_ADDR:
   1371       addr = addr_from_loc(e, src, loc);
   1372       e->target->load_addr(e->target, dst, addr);
   1373       return;
   1374     case NATIVE_LOC_FRAME:
   1375     case NATIVE_LOC_STACK:
   1376     case NATIVE_LOC_ADDR:
   1377       addr = addr_from_loc(e, src, loc);
   1378       e->target->load(e->target, dst, addr, mem_for_type(e->c, dst.type));
   1379       return;
   1380     default:
   1381       emit_panic(e, loc, "cannot stage asm operand in a register");
   1382   }
   1383 }
   1384 
   1385 static int asm_reg_loc_satisfies(NativeEmitCtx* e, NativeLoc loc,
   1386                                  NativeAllocClass cls, Reg fixed,
   1387                                  u32 allowed_mask) {
   1388   if (loc.kind != NATIVE_LOC_REG || loc.cls != (u8)cls) return 0;
   1389   if (fixed != (Reg)REG_NONE && loc.v.reg != fixed) return 0;
   1390   return asm_temp_reg_ok(e, cls, loc.v.reg, allowed_mask);
   1391 }
   1392 
   1393 static int asm_reg_requirement(NativeEmitCtx* e,
   1394                                const IRAsmRegRequirement* req,
   1395                                NativeAllocClass* cls, Reg* fixed,
   1396                                u32* allowed, SrcLoc loc) {
   1397   if (!req || !req->present) return 0;
   1398   if (req->cls >= OPT_REG_CLASSES || req->fixed_reg < -1 ||
   1399       req->fixed_reg >= (i32)OPT_MAX_HARD_REGS)
   1400     emit_panic(e, loc, "invalid pre-resolved asm register requirement");
   1401   *cls = (NativeAllocClass)req->cls;
   1402   *fixed = req->fixed_reg < 0 ? REG_NONE : (Reg)req->fixed_reg;
   1403   *allowed = req->allowed_mask;
   1404   return 1;
   1405 }
   1406 
   1407 /* Localize an asm register constraint at the instruction boundary. An
   1408  * unrestricted allocated register may already satisfy it; fixed/subset
   1409  * requirements are normally fulfilled by scoped staging. No emitter-owned
   1410  * register becomes a MIR home. */
   1411 static NativeLoc asm_stage_reg_operand(NativeEmitCtx* e,
   1412                                        const AsmConstraint* constraint,
   1413                                        const IRAsmRegRequirement* req,
   1414                                        NativeLoc real, int load_value,
   1415                                        int* staged, SrcLoc loc) {
   1416   NativeAllocClass cls;
   1417   KitCgTypeId type;
   1418   Reg fixed;
   1419   u32 allowed;
   1420   Reg r;
   1421   NativeLoc out;
   1422   if (staged) *staged = 0;
   1423   if (!asm_reg_requirement(e, req, &cls, &fixed, &allowed, loc))
   1424     return real;
   1425   type = constraint->type ? constraint->type : real.type;
   1426   if (asm_reg_loc_satisfies(e, real, cls, fixed, allowed)) return real;
   1427   r = temp_acquire_asm(e, cls, fixed, allowed, loc);
   1428   out = loc_reg(type, cls, r);
   1429   if (load_value) load_loc_into_reg(e, out, real, loc);
   1430   if (staged) *staged = 1;
   1431   return out;
   1432 }
   1433 
   1434 /* For an arithmetic / compare source operand: keep it as an immediate when it
   1435  * is a constant the target can encode for `use` (so no register is wasted
   1436  * materializing it); otherwise materialize into a register. */
   1437 static NativeLoc operand_imm_or_reg(NativeEmitCtx* e, const OptOperand* op,
   1438                                     NativeImmUse use, u32 sub, Reg avoid_a,
   1439                                     Reg avoid_b, SrcLoc loc) {
   1440   if (op->kind == OPK_IMM && e->target->imm_legal &&
   1441       e->target->imm_legal(e->target, use, sub, op->type, op->v.imm))
   1442     return loc_imm(op->type, op->v.imm);
   1443   return materialize_operand(e, op, class_for_type(e, op->type), op->type,
   1444                              avoid_a, avoid_b, loc);
   1445 }
   1446 
   1447 static Reg loc_avoid_reg(NativeLoc l) {
   1448   return l.kind == NATIVE_LOC_REG ? l.v.reg : REG_NONE;
   1449 }
   1450 
   1451 static int type_is_aggregate_or_large(NativeEmitCtx* e, KitCgTypeId type) {
   1452   /* "Large" = wider than one machine word (ptr_size): such a value cannot move
   1453    * through a single register, so IR_COPY/IR_LOAD/IR_STORE of it must go
   1454    * through copy_bytes. 8 on rv64/x64/aa64, 4 on rv32 (so an 8-byte i64/double
   1455    * is large there and is copied as two words rather than truncated into one
   1456    * register). */
   1457   return type && (cg_type_is_aggregate(e->c, type) ||
   1458                   type_size_or(e->c, type, 8u) > e->c->target.ptr_size);
   1459 }
   1460 
   1461 /* Acquire the register that receives a scalar memory load whose MIR result has
   1462  * a frame home. If the address already consumes every available value temp,
   1463  * first collapse it to one plain register. A leased integer base is then dead
   1464  * after address evaluation and may receive the loaded scalar in place: every
   1465  * NativeTarget load hook consumes a simple effective address before writing
   1466  * its destination. Never apply this to an allocator-owned base, an FP result,
   1467  * or a value wider than one machine register. */
   1468 static NativeLoc acquire_frame_load_result(NativeEmitCtx* e,
   1469                                            KitCgTypeId type,
   1470                                            NativeAddr* addr, SrcLoc loc) {
   1471   NativeAllocClass cls = class_for_type(e, type);
   1472   Reg base = addr_base_reg(addr);
   1473   Reg index = addr_index_reg(addr);
   1474   if (temp_available(e, cls, base, index))
   1475     return temp_loc_acquire(e, type, cls, base, index, loc);
   1476 
   1477   collapse_addr_to_reg(e, addr, loc);
   1478   base = addr_base_reg(addr);
   1479   if (cls == NATIVE_REG_INT &&
   1480       type_size_or(e->c, type, e->c->target.ptr_size) <=
   1481           e->c->target.ptr_size &&
   1482       addr->base_kind == NATIVE_ADDR_BASE_REG &&
   1483       addr->index_kind == NATIVE_ADDR_INDEX_NONE && addr->offset == 0 &&
   1484       temp_reg_is_leased(e, NATIVE_REG_INT, base))
   1485     return loc_reg(type, cls, base);
   1486 
   1487   return temp_loc_acquire(e, type, cls, base, addr_index_reg(addr), loc);
   1488 }
   1489 
   1490 /* Materialize an address whose MIR destination has a frame home. A normal
   1491  * free temp receives the address directly. Under full pressure, collapse the
   1492  * expression into its leased base and return that already-materialized value;
   1493  * collapse_addr_to_reg refuses to overwrite an allocator-owned base. */
   1494 static NativeLoc emit_frame_address_result(NativeEmitCtx* e,
   1495                                            KitCgTypeId type,
   1496                                            NativeAddr* addr, SrcLoc loc) {
   1497   NativeAllocClass cls = class_for_type(e, type);
   1498   Reg base = addr_base_reg(addr);
   1499   Reg index = addr_index_reg(addr);
   1500   NativeLoc dst;
   1501   if (temp_available(e, cls, base, index)) {
   1502     dst = temp_loc_acquire(e, type, cls, base, index, loc);
   1503     e->target->load_addr(e->target, dst, *addr);
   1504     return dst;
   1505   }
   1506   collapse_addr_to_reg(e, addr, loc);
   1507   base = addr_base_reg(addr);
   1508   if (cls != NATIVE_REG_INT ||
   1509       !temp_reg_is_leased(e, NATIVE_REG_INT, base))
   1510     emit_panic(e, loc, "materialized address has no scoped integer owner");
   1511   return loc_reg(type, cls, base);
   1512 }
   1513 
   1514 /* Materialize a scalar value while an effective address remains live.  If
   1515  * the address consumes the whole value-temp bank, reduce it to one scoped
   1516  * base first; collapse releases cache-owned components that are dead after
   1517  * address evaluation.  The value then explicitly avoids the surviving base
   1518  * and index, keeping the backend hook's address and data roles disjoint. */
   1519 static NativeLoc materialize_operand_away_from_addr(
   1520     NativeEmitCtx* e, const OptOperand* op, NativeAddr* addr, SrcLoc loc) {
   1521   NativeAllocClass cls = class_for_type(e, op->type);
   1522   if (!temp_available(e, cls, addr_base_reg(addr), addr_index_reg(addr)))
   1523     collapse_addr_to_reg(e, addr, loc);
   1524   return materialize_operand(e, op, cls, op->type, addr_base_reg(addr),
   1525                              addr_index_reg(addr), loc);
   1526 }
   1527 
   1528 /* Copy an aggregate / oversized value between two memory locations. dst and
   1529  * src must be addressable (frame/global/indirect/reg-as-pointer); used for
   1530  * IR_COPY/IR_LOAD/IR_STORE whose value type cannot move through one register.
   1531  */
   1532 static void emit_agg_move(NativeEmitCtx* e, NativeAddr da, NativeAddr sa,
   1533                           KitCgTypeId type, SrcLoc loc) {
   1534   AggregateAccess acc;
   1535   memset(&acc, 0, sizeof acc);
   1536   acc.type = type;
   1537   acc.size = type_size_or(e->c, type, 8u);
   1538   acc.align = type_align_or(e->c, type, 8u);
   1539   acc.mem = mem_for_type(e->c, type);
   1540   /* Legalize both addresses exactly as the scalar load/store path does: a
   1541    * copy_bytes granule ladder addresses base+offset only, so any addressing mode
   1542    * the target cannot encode — notably a scaled index, which an 8-byte i64/double
   1543    * load on a 32-bit target arrives with (it is a "large" move here, the array
   1544    * index folded into the address) — must collapse into a plain base register
   1545    * first. Without this the index is silently dropped and every element aliases
   1546    * element 0. */
   1547   legalize_addr(e, &da, acc.mem, loc);
   1548   legalize_addr(e, &sa, acc.mem, loc);
   1549   e->target->copy_bytes(e->target, da, sa, acc);
   1550 }
   1551 
   1552 static CGFuncDesc semantic_func_desc(NativeEmitCtx* e) {
   1553   OptCGFuncDesc* in = &e->f->desc;
   1554   CGFuncDesc out;
   1555   memset(&out, 0, sizeof out);
   1556   out.sym = in->sym;
   1557   out.text_section_id = in->text_section_id;
   1558   out.group_id = in->group_id;
   1559   out.fn_type = in->fn_type;
   1560   out.result_type = in->result_type;
   1561   out.nparams = in->nparams;
   1562   out.loc = in->loc;
   1563   out.flags = in->flags;
   1564   out.inline_policy = in->inline_policy;
   1565   out.atomize = in->atomize;
   1566   if (in->nparams && in->params) {
   1567     CGParamDesc* params = arena_zarray(e->f->arena, CGParamDesc, in->nparams);
   1568     for (u32 i = 0; i < in->nparams; ++i) {
   1569       params[i].index = in->params[i].index;
   1570       params[i].name = in->params[i].name;
   1571       params[i].type = in->params[i].type;
   1572       params[i].size = in->params[i].size;
   1573       params[i].align = in->params[i].align;
   1574       params[i].flags = in->params[i].flags;
   1575       params[i].loc = in->params[i].loc;
   1576     }
   1577     out.params = params;
   1578   }
   1579   return out;
   1580 }
   1581 
   1582 static CGParamDesc semantic_param_desc(const IRParam* p) {
   1583   CGParamDesc out;
   1584   memset(&out, 0, sizeof out);
   1585   out.index = p->index;
   1586   out.name = p->name;
   1587   out.type = p->type;
   1588   out.size = p->size;
   1589   out.align = p->align;
   1590   out.flags = p->flags;
   1591   out.loc = p->loc;
   1592   return out;
   1593 }
   1594 
   1595 static NativeLoc loc_for_preg(NativeEmitCtx* e, PReg preg, KitCgTypeId type,
   1596                               SrcLoc loc) {
   1597   u8 kind = opt_preg_alloc_kind(e->f, preg);
   1598   if (kind == OPT_ALLOC_HARD)
   1599     return loc_reg(type, (NativeAllocClass)opt_preg_loc_cls(e->f, preg),
   1600                    opt_preg_hard_reg(e->f, preg));
   1601   if (kind == OPT_ALLOC_SPILL)
   1602     return loc_frame(type, class_for_type(e, type),
   1603                      map_slot(e, opt_preg_spill_slot(e->f, preg), loc));
   1604   return loc_none();
   1605 }
   1606 
   1607 static void bind_params(NativeEmitCtx* e) {
   1608   for (u32 i = 0; i < e->f->nparams; ++i) {
   1609     IRParam* p = &e->f->params[i];
   1610     CGParamDesc sd = semantic_param_desc(p);
   1611     NativeLoc dst;
   1612     if (p->storage.kind == CG_LOCAL_STORAGE_REG)
   1613       dst = loc_for_preg(e, (PReg)p->storage.v.reg, p->type, p->loc);
   1614     else
   1615       dst = loc_frame(p->type, class_for_type(e, p->type),
   1616                       map_slot(e, p->storage.v.frame_slot, p->loc));
   1617     if (e->target->bind_param) e->target->bind_param(e->target, &sd, dst);
   1618   }
   1619   /* Let a backend that defers register-destination binds resolve them now (as a
   1620    * parallel copy), once every param's incoming location has been read. */
   1621   if (e->target->bind_params_end) e->target->bind_params_end(e->target);
   1622 }
   1623 
   1624 /* The parameter value is placed into its allocated location by bind_param at
   1625  * function entry; the IR_PARAM_DECL marker emits nothing. */
   1626 static void emit_param_decl(NativeEmitCtx* e, Inst* in) {
   1627   (void)e;
   1628   (void)in;
   1629 }
   1630 
   1631 static NativeFrameSlot temp_slot(NativeEmitCtx* e, KitCgTypeId type, SrcLoc loc,
   1632                                  NativeFrameSlotKind kind) {
   1633   NativeFrameSlotDesc d;
   1634   memset(&d, 0, sizeof d);
   1635   d.type = type;
   1636   d.loc = loc;
   1637   d.size = type_size_or(e->c, type, 8u);
   1638   d.align = type_align_or(e->c, type, d.size >= 8u ? 8u : d.size);
   1639   d.kind = kind;
   1640   return e->target->frame_slot(e->target, &d);
   1641 }
   1642 
   1643 static NativeLoc abi_storage_loc(NativeEmitCtx* e, const OptCGABIValue* v,
   1644                                  SrcLoc loc) {
   1645   if (!v) return loc_none();
   1646   return loc_from_operand(e, &v->storage, loc);
   1647 }
   1648 
   1649 static void emit_call(NativeEmitCtx* e, Inst* in) {
   1650   IRCallAux* aux = (IRCallAux*)in->extra.aux;
   1651   NativeCallDesc d;
   1652   NativeCallPhase plan;
   1653   NativeLoc* args = NULL;
   1654   NativeLoc* results = NULL;
   1655   NativeLoc final_result = loc_none();
   1656   NativeFrameSlot result_slot = NATIVE_FRAME_SLOT_NONE;
   1657   MemAccess result_mem;
   1658   if (!aux) return;
   1659   memset(&d, 0, sizeof d);
   1660   memset(&plan, 0, sizeof plan);
   1661   if (aux->desc.nargs)
   1662     args = arena_zarray(e->f->arena, NativeLoc, aux->desc.nargs);
   1663   for (u32 i = 0; i < aux->desc.nargs; ++i)
   1664     args[i] = abi_storage_loc(e, &aux->desc.args[i], in->loc);
   1665   if (aux->desc.ret.storage.kind) {
   1666     KitCgTypeId rty = aux->desc.ret.type;
   1667     results = arena_zarray(e->f->arena, NativeLoc, 1);
   1668     final_result = abi_storage_loc(e, &aux->desc.ret, in->loc);
   1669     /* Hand marshal_call the value's real destination directly whenever it is a
   1670      * register or a frame slot: a scalar result is a single move out of the ABI
   1671      * result register, and an aggregate / oversized result — which marshal_call or
   1672      * the callee writes in parts and so must land in memory — lands straight in
   1673      * its frame home. Routing either through a fresh temp slot (store then
   1674      * reload / copy_bytes) was a pure round trip on every call. The temp slot
   1675      * is a fallback for the rare result whose storage is neither a register nor
   1676      * a frame slot (e.g. written into a global); lowering hoists aggregates to
   1677      * a frame home (opt_lower_to_mir), so this branch is scalar-only in
   1678      * practice. */
   1679     if (final_result.kind == NATIVE_LOC_REG ||
   1680         final_result.kind == NATIVE_LOC_FRAME) {
   1681       results[0] = final_result;
   1682     } else {
   1683       result_slot = temp_slot(e, rty, in->loc, NATIVE_FRAME_SLOT_SPILL);
   1684       results[0] = loc_frame(rty, class_for_type(e, rty), result_slot);
   1685     }
   1686   }
   1687   d.fn_type = aux->desc.fn_type;
   1688   d.callee = loc_from_operand(e, &aux->desc.callee, in->loc);
   1689   d.args = args;
   1690   d.results = results;
   1691   d.nargs = aux->desc.nargs;
   1692   d.nresults = results ? 1u : 0u;
   1693   d.flags = aux->desc.flags;
   1694   d.tail_policy = aux->desc.tail_policy;
   1695   d.inline_policy = aux->desc.inline_policy;
   1696   /* Materialize a frame-resident indirect callee before marshal_call starts its
   1697    * argument phase.  Real backends marshal register arguments inside
   1698    * marshal_call; doing this afterward allowed the emitter to reuse an already
   1699    * populated ABI argument register for the callee.  The acquired lease also
   1700    * keeps generic phase argument moves from selecting the callee register. */
   1701   if (d.callee.kind != NATIVE_LOC_REG &&
   1702       d.callee.kind != NATIVE_LOC_GLOBAL)
   1703     d.callee = materialize(e, d.callee, NATIVE_REG_INT, d.callee.type,
   1704                            REG_NONE, REG_NONE, in->loc);
   1705   e->target->marshal_call(e->target, &d, &plan);
   1706   for (u32 i = 0; i < plan.nargs; ++i)
   1707     write_phase_loc(e, plan.args[i].dst, plan.args[i].src, plan.args[i].mem,
   1708                     in->loc);
   1709   e->target->emit_call(e->target, &plan);
   1710   temp_scope_phase_barrier(e);
   1711   for (u32 i = 0; i < plan.nrets; ++i)
   1712     write_phase_loc(e, plan.rets[i].dst, plan.rets[i].src, plan.rets[i].mem,
   1713                     in->loc);
   1714   if (result_slot && final_result.kind != NATIVE_LOC_NONE) {
   1715     KitCgTypeId rty = aux->desc.ret.type;
   1716     NativeLoc tmp = loc_frame(rty, class_for_type(e, rty), result_slot);
   1717     result_mem = mem_for_type(e->c, rty);
   1718     if (final_result.kind != NATIVE_LOC_REG &&
   1719         (cg_type_is_aggregate(e->c, rty) ||
   1720          type_size_or(e->c, rty, 8u) > e->c->target.ptr_size)) {
   1721       /* Aggregate / oversized result: move bytes rather than a scalar copy
   1722        * (which would exceed the single-register width). The result was either
   1723        * written in parts by marshal_call's rets, or by the callee via the sret
   1724        * pointer; either way it now lives in the temp slot. */
   1725       AggregateAccess acc;
   1726       NativeAddr da = addr_from_loc(e, final_result, in->loc);
   1727       NativeAddr sa = addr_from_loc(e, tmp, in->loc);
   1728       memset(&acc, 0, sizeof acc);
   1729       acc.type = rty;
   1730       acc.size = type_size_or(e->c, rty, 8u);
   1731       acc.align = type_align_or(e->c, rty, 8u);
   1732       acc.mem = result_mem;
   1733       e->target->copy_bytes(e->target, da, sa, acc);
   1734     } else {
   1735       write_loc(e, final_result, tmp, result_mem, in->loc);
   1736     }
   1737   }
   1738 }
   1739 
   1740 static void emit_ret(NativeEmitCtx* e, Inst* in, const CGFuncDesc* fd) {
   1741   IRRetAux* aux = (IRRetAux*)in->extra.aux;
   1742   NativeLoc value = loc_none();
   1743   const NativeLoc* values = NULL;
   1744   NativeCallPhaseRet* rets = NULL;
   1745   u32 nrets = 0;
   1746   if (aux && aux->present) {
   1747     /* Hand marshal_ret the value's location directly. For an aggregate / oversized
   1748      * result it is a memory location (marshal_ret copies to the sret pointer or
   1749      * reads parts into the return registers); for a scalar it is the value's
   1750      * register or slot, which marshal_ret moves into the return register. The old
   1751      * code spilled scalars to a fresh slot and reloaded them, a pure round
   1752      * trip on every return. */
   1753     value = abi_storage_loc(e, &aux->val, in->loc);
   1754     values = &value;
   1755   }
   1756   e->target->marshal_ret(e->target, fd, values, &rets, &nrets);
   1757   for (u32 i = 0; i < nrets; ++i)
   1758     write_phase_loc(e, rets[i].dst, rets[i].src, rets[i].mem, in->loc);
   1759   /* Skip the trailing branch-to-epilogue when this IR_RET is the very last
   1760    * inst emitted: func_end will place the epilogue label at mc_pos right
   1761    * after this, so the branch would jump to the next 4 bytes. The actual
   1762    * `ret` instruction lives in func_end's restore-frame sequence and is
   1763    * unaffected. */
   1764   if (!e->emitting_terminal_ret) e->target->ret(e->target);
   1765 }
   1766 
   1767 static void emit_inst_body(NativeEmitCtx* e, u32 block, u32 order_index,
   1768                            Inst* in, const CGFuncDesc* fd) {
   1769   NativeLoc dst, a, b, src, tmp;
   1770   NativeAddr addr, addr2;
   1771   Reg dst_reg;
   1772   (void)block;
   1773   if (e->target->set_loc) e->target->set_loc(e->target, in->loc);
   1774   switch ((IROp)in->op) {
   1775     case IR_NOP:
   1776     case IR_CONST_I:
   1777     case IR_CONST_BYTES:
   1778     case IR_PHI:
   1779     case IR_SCOPE_BEGIN:
   1780     case IR_SCOPE_END:
   1781       return;
   1782     case IR_PARAM_DECL:
   1783       emit_param_decl(e, in);
   1784       return;
   1785     case IR_LOAD_IMM:
   1786       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   1787       write_operand(e, &in->opnds[0],
   1788                     loc_imm(in->opnds[0].type, in->extra.imm),
   1789                     mem_for_type(e->c, in->opnds[0].type), in->loc);
   1790       return;
   1791     case IR_LOAD_CONST: {
   1792       NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc);
   1793       dst = real;
   1794       if (dst.kind != NATIVE_LOC_REG)
   1795         dst = temp_loc_acquire(e, in->opnds[0].type,
   1796                           class_for_type(e, in->opnds[0].type), REG_NONE,
   1797                           REG_NONE, in->loc);
   1798       e->target->load_const(e->target, dst, in->extra.cbytes);
   1799       if (real.kind != NATIVE_LOC_REG)
   1800         write_operand(e, &in->opnds[0], dst,
   1801                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   1802       return;
   1803     }
   1804     case IR_COPY:
   1805       if (type_is_aggregate_or_large(e, in->opnds[0].type)) {
   1806         emit_agg_move(e, addr_from_operand(e, &in->opnds[0], in->loc),
   1807                       addr_from_operand(e, &in->opnds[1], in->loc),
   1808                       in->opnds[0].type, in->loc);
   1809         return;
   1810       }
   1811       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   1812       src = loc_from_operand(e, &in->opnds[1], in->loc);
   1813       if (in->opnds[1].kind == OPK_STACK) {
   1814         NativeLoc cached = frame_cache_take(
   1815             e, &in->opnds[1], src,
   1816             class_for_type(e, in->opnds[1].type), in->opnds[1].type,
   1817             REG_NONE, REG_NONE);
   1818         if (cached.kind != NATIVE_LOC_NONE) src = cached;
   1819       }
   1820       write_operand(e, &in->opnds[0], src,
   1821                     mem_for_type(e->c, in->opnds[0].type), in->loc);
   1822       return;
   1823     case IR_LOAD:
   1824       if (type_is_aggregate_or_large(e, in->opnds[0].type)) {
   1825         addr = addr_from_operand(e, &in->opnds[1], in->loc);
   1826         emit_agg_move(e, addr_from_operand(e, &in->opnds[0], in->loc), addr,
   1827                       in->opnds[0].type, in->loc);
   1828         return;
   1829       }
   1830       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   1831       addr = addr_from_operand(e, &in->opnds[1], in->loc);
   1832       legalize_addr(e, &addr, in->extra.mem, in->loc);
   1833       if (dst.kind == NATIVE_LOC_REG) {
   1834         e->target->load(e->target, dst, addr, in->extra.mem);
   1835       } else {
   1836         tmp = acquire_frame_load_result(e, in->opnds[0].type, &addr, in->loc);
   1837         e->target->load(e->target, tmp, addr, in->extra.mem);
   1838         write_operand(e, &in->opnds[0], tmp, in->extra.mem, in->loc);
   1839       }
   1840       return;
   1841     case IR_STORE:
   1842       if (type_is_aggregate_or_large(e, in->opnds[1].type)) {
   1843         emit_agg_move(e, addr_from_operand(e, &in->opnds[0], in->loc),
   1844                       addr_from_operand(e, &in->opnds[1], in->loc),
   1845                       in->opnds[1].type, in->loc);
   1846         return;
   1847       }
   1848       addr = addr_from_operand(e, &in->opnds[0], in->loc);
   1849       legalize_addr(e, &addr, in->extra.mem, in->loc);
   1850       src = loc_from_operand(e, &in->opnds[1], in->loc);
   1851       /* Storing a constant 0 from the hardware zero register avoids
   1852        * materializing 0 into an operand temp first (e.g. `strb wzr, [..]`
   1853        * rather than `movz w9,0; strb w9, [..]`). */
   1854       if (src.kind == NATIVE_LOC_IMM && src.v.imm == 0 &&
   1855           e->target->has_store_zero_reg &&
   1856           class_for_type(e, in->opnds[1].type) == NATIVE_REG_INT)
   1857         src = loc_reg(in->opnds[1].type, NATIVE_REG_INT,
   1858                       e->target->store_zero_reg);
   1859       /* Source register aliases the address base/index (e.g. `*p = (T)p`).
   1860        * Collapse the address into an instruction-local temp:
   1861        * collapse_addr_to_reg selects one distinct from both base and index —
   1862        * hence distinct from `src` — so the store reads `src` and writes through
   1863        * the fresh temp with no alias. This stays entirely in registers; the
   1864        * frame is fully planned before emission, so emit never allocates a slot
   1865        * here. */
   1866       if (src.kind == NATIVE_LOC_REG && (src.v.reg == addr_base_reg(&addr) ||
   1867                                          src.v.reg == addr_index_reg(&addr)))
   1868         collapse_addr_to_reg(e, &addr, in->loc);
   1869       if (src.kind != NATIVE_LOC_REG) {
   1870         src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr,
   1871                                                  in->loc);
   1872       }
   1873       e->target->store(e->target, addr, src, in->extra.mem);
   1874       return;
   1875     case IR_ADDR_OF: {
   1876       NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc);
   1877       addr = addr_from_operand(e, &in->opnds[1], in->loc);
   1878       dst = real;
   1879       if (dst.kind != NATIVE_LOC_REG ||
   1880           (addr.index_kind == NATIVE_ADDR_INDEX_REG &&
   1881            dst.v.reg == addr.index.reg)) {
   1882         dst = emit_frame_address_result(e, in->opnds[0].type, &addr, in->loc);
   1883       } else {
   1884         e->target->load_addr(e->target, dst, addr);
   1885       }
   1886       if (!temp_loc_equal(real, dst))
   1887         write_operand(e, &in->opnds[0], dst,
   1888                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   1889       return;
   1890     }
   1891     case IR_TLS_ADDR_OF: {
   1892       IRTlsAux* aux = (IRTlsAux*)in->extra.aux;
   1893       NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc);
   1894       dst = real;
   1895       if (dst.kind != NATIVE_LOC_REG)
   1896         dst = temp_loc_acquire(e, in->opnds[0].type, NATIVE_REG_INT, REG_NONE,
   1897                           REG_NONE, in->loc);
   1898       e->target->tls_addr_of(e->target, dst, aux->sym, aux->addend);
   1899       if (real.kind != NATIVE_LOC_REG)
   1900         write_operand(e, &in->opnds[0], dst,
   1901                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   1902       return;
   1903     }
   1904     case IR_AGG_COPY: {
   1905       IRAggAux* aux = (IRAggAux*)in->extra.aux;
   1906       addr = pointer_addr_from_operand(e, &in->opnds[0], in->loc, REG_NONE,
   1907                                        REG_NONE);
   1908       addr2 = pointer_addr_from_operand(
   1909           e, &in->opnds[1], in->loc,
   1910           addr.base_kind == NATIVE_ADDR_BASE_REG ? addr.base.reg : REG_NONE,
   1911           REG_NONE);
   1912       legalize_addr(e, &addr, aux->access.mem, in->loc);
   1913       legalize_addr(e, &addr2, aux->access.mem, in->loc);
   1914       e->target->copy_bytes(e->target, addr, addr2, aux->access);
   1915       return;
   1916     }
   1917     case IR_AGG_SET: {
   1918       IRAggAux* aux = (IRAggAux*)in->extra.aux;
   1919       addr = pointer_addr_from_operand(e, &in->opnds[0], in->loc, REG_NONE,
   1920                                        REG_NONE);
   1921       legalize_addr(e, &addr, aux->access.mem, in->loc);
   1922       src = loc_from_operand(e, &in->opnds[1], in->loc);
   1923       if (src.kind != NATIVE_LOC_REG) {
   1924         src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr,
   1925                                                  in->loc);
   1926       }
   1927       e->target->set_bytes(e->target, addr, src, aux->access);
   1928       return;
   1929     }
   1930     case IR_BITFIELD_LOAD: {
   1931       IRBitFieldAux* aux = (IRBitFieldAux*)in->extra.aux;
   1932       NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc);
   1933       dst = real;
   1934       addr = addr_from_operand(e, &in->opnds[1], in->loc);
   1935       legalize_addr(e, &addr, aux->access.storage, in->loc);
   1936       if (dst.kind != NATIVE_LOC_REG)
   1937         dst = acquire_frame_load_result(e, in->opnds[0].type, &addr,
   1938                                         in->loc);
   1939       e->target->bitfield_load(e->target, dst, addr, aux->access);
   1940       if (real.kind != NATIVE_LOC_REG)
   1941         write_operand(e, &in->opnds[0], dst,
   1942                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   1943       return;
   1944     }
   1945     case IR_BITFIELD_STORE: {
   1946       IRBitFieldAux* aux = (IRBitFieldAux*)in->extra.aux;
   1947       addr = addr_from_operand(e, &in->opnds[0], in->loc);
   1948       legalize_addr(e, &addr, aux->access.storage, in->loc);
   1949       src = loc_from_operand(e, &in->opnds[1], in->loc);
   1950       /* The field store is a read-modify-write that keeps the address live
   1951        * across the value placement, so the value must not be materialized into
   1952        * the address's base/index register. Mirror IR_STORE/IR_AGG_SET: when no
   1953        * other operand temp is free, collapse the address to one temp first,
   1954        * then materialize the value avoiding the address regs. Without this, a
   1955        * spilled pointer reloaded into the first temp and a value reusing that
   1956        * temp collide — the value `mov`s over the address, nulling it (SIGSEGV
   1957        * in optimized SQLite). */
   1958       if (src.kind != NATIVE_LOC_REG) {
   1959         src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr,
   1960                                                  in->loc);
   1961       }
   1962       e->target->bitfield_store(e->target, addr, src, aux->access);
   1963       return;
   1964     }
   1965     case IR_BINOP:
   1966       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   1967       dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE;
   1968       b = loc_from_operand(e, &in->opnds[2], in->loc);
   1969       a = materialize_operand(e, &in->opnds[1],
   1970                               class_for_type(e, in->opnds[1].type),
   1971                               in->opnds[1].type, dst_reg, loc_avoid_reg(b),
   1972                               in->loc);
   1973       b = operand_imm_or_reg(e, &in->opnds[2], NATIVE_IMM_BINOP,
   1974                              (u32)in->extra.imm, a.v.reg, dst_reg, in->loc);
   1975       /* L7 shift rider: a single-use SHL folded into this binop's second
   1976        * source. The rider only survives onto a register `b` (a shifted-reg
   1977        * ALU operand); an immediate `b` never carries one. The recognition
   1978        * pass only stamps it for targets advertising can_fold_shift_into_alu,
   1979        * so a backend ignoring NativeLoc.shift never receives a nonzero rider. */
   1980       if (b.kind == NATIVE_LOC_REG) b.shift = in->opnds[2].shift;
   1981       if (dst.kind != NATIVE_LOC_REG) {
   1982         NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type);
   1983         if (a.kind == NATIVE_LOC_REG && a.cls == (u8)dst_cls &&
   1984             temp_reg_is_leased(e, dst_cls, a.v.reg))
   1985           dst = loc_reg(in->opnds[0].type, dst_cls, a.v.reg);
   1986         else
   1987           dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, a.v.reg,
   1988                             loc_avoid_reg(b), in->loc);
   1989       }
   1990       e->target->binop(e->target, (BinOp)in->extra.imm, dst, a, b);
   1991       if (in->opnds[0].kind != OPK_REG)
   1992         write_operand(e, &in->opnds[0], dst,
   1993                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   1994       return;
   1995     case IR_UNOP:
   1996       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   1997       dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE;
   1998       a = materialize_operand(e, &in->opnds[1],
   1999                               class_for_type(e, in->opnds[1].type),
   2000                               in->opnds[1].type, dst_reg, REG_NONE, in->loc);
   2001       if (dst.kind != NATIVE_LOC_REG) {
   2002         NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type);
   2003         if (a.kind == NATIVE_LOC_REG && a.cls == (u8)dst_cls &&
   2004             temp_reg_is_leased(e, dst_cls, a.v.reg))
   2005           dst = loc_reg(in->opnds[0].type, dst_cls, a.v.reg);
   2006         else
   2007           dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, a.v.reg,
   2008                                  REG_NONE,
   2009                             in->loc);
   2010       }
   2011       e->target->unop(e->target, (UnOp)in->extra.imm, dst, a);
   2012       if (in->opnds[0].kind != OPK_REG)
   2013         write_operand(e, &in->opnds[0], dst,
   2014                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   2015       return;
   2016     case IR_CMP:
   2017       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   2018       dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE;
   2019       b = loc_from_operand(e, &in->opnds[2], in->loc);
   2020       a = materialize_operand(e, &in->opnds[1],
   2021                               class_for_type(e, in->opnds[1].type),
   2022                               in->opnds[1].type, dst_reg, loc_avoid_reg(b),
   2023                               in->loc);
   2024       b = operand_imm_or_reg(e, &in->opnds[2], NATIVE_IMM_CMP,
   2025                              (u32)in->extra.imm, a.v.reg, dst_reg, in->loc);
   2026       if (dst.kind != NATIVE_LOC_REG) {
   2027         NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type);
   2028         if (a.kind == NATIVE_LOC_REG && a.cls == (u8)dst_cls &&
   2029             temp_reg_is_leased(e, dst_cls, a.v.reg))
   2030           dst = loc_reg(in->opnds[0].type, dst_cls, a.v.reg);
   2031         else
   2032           dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, a.v.reg,
   2033                             loc_avoid_reg(b), in->loc);
   2034       }
   2035       e->target->cmp(e->target, (CmpOp)in->extra.imm, dst, a, b);
   2036       if (in->opnds[0].kind != OPK_REG)
   2037         write_operand(e, &in->opnds[0], dst,
   2038                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   2039       return;
   2040     case IR_CONVERT:
   2041       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   2042       dst_reg = dst.kind == NATIVE_LOC_REG ? dst.v.reg : REG_NONE;
   2043       src = materialize_operand(e, &in->opnds[1],
   2044                                 class_for_type(e, in->opnds[1].type),
   2045                                 in->opnds[1].type, dst_reg, REG_NONE, in->loc);
   2046       if (dst.kind != NATIVE_LOC_REG) {
   2047         NativeAllocClass dst_cls = class_for_type(e, in->opnds[0].type);
   2048         if (src.kind == NATIVE_LOC_REG && src.cls == (u8)dst_cls &&
   2049             temp_reg_is_leased(e, dst_cls, src.v.reg))
   2050           dst = loc_reg(in->opnds[0].type, dst_cls, src.v.reg);
   2051         else
   2052           dst = temp_loc_acquire(e, in->opnds[0].type, dst_cls, src.v.reg,
   2053                                  REG_NONE,
   2054                             in->loc);
   2055       }
   2056       e->target->convert(e->target, (ConvKind)in->extra.imm, dst, src);
   2057       if (in->opnds[0].kind != OPK_REG)
   2058         write_operand(e, &in->opnds[0], dst,
   2059                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   2060       return;
   2061     case IR_CALL:
   2062       emit_call(e, in);
   2063       return;
   2064     case IR_BR:
   2065       e->target->jump(e->target,
   2066                       ensure_label(e, e->f->blocks[block].succ[0], in->loc));
   2067       return;
   2068     case IR_CMP_BRANCH: {
   2069       u32 next = order_index + 1u < e->f->emit_order_n
   2070                      ? e->f->emit_order[order_index + 1u]
   2071                      : UINT32_MAX;
   2072       b = loc_from_operand(e, &in->opnds[1], in->loc);
   2073       a = materialize_operand(e, &in->opnds[0],
   2074                               class_for_type(e, in->opnds[0].type),
   2075                               in->opnds[0].type, REG_NONE, loc_avoid_reg(b),
   2076                               in->loc);
   2077       b = operand_imm_or_reg(e, &in->opnds[1], NATIVE_IMM_CMP,
   2078                              (u32)in->extra.imm, a.v.reg, REG_NONE, in->loc);
   2079       e->target->cmp_branch(
   2080           e->target, (CmpOp)in->extra.imm, a, b,
   2081           ensure_label(e, e->f->blocks[block].succ[0], in->loc));
   2082       if (e->f->blocks[block].nsucc > 1u && e->f->blocks[block].succ[1] != next)
   2083         e->target->jump(e->target,
   2084                         ensure_label(e, e->f->blocks[block].succ[1], in->loc));
   2085       return;
   2086     }
   2087     case IR_SWITCH: {
   2088       IRSwitchAux* aux = (IRSwitchAux*)in->extra.aux;
   2089       /* Lease the selector in one register for the whole chain. */
   2090       NativeLoc sel = materialize_operand(
   2091           e, &in->opnds[0], class_for_type(e, in->opnds[0].type),
   2092           in->opnds[0].type, REG_NONE, REG_NONE, in->loc);
   2093       /* Each case value is interpreted at the selector type (matches the CG
   2094        * fallback chain in cg/native_direct_target.c). Route each through the
   2095        * same NATIVE_IMM_CMP immediate-or-register logic IR_CMP_BRANCH uses, so
   2096        * a legal compare immediate becomes `cmp sel, #k` with no per-case
   2097        * materialization; an illegal immediate (e.g. too large for the target's
   2098        * compare encoding) falls back to load_imm into a temp that avoids the
   2099        * leased selector register. */
   2100       KitCgTypeId case_type =
   2101           aux && aux->selector_type ? aux->selector_type : in->opnds[0].type;
   2102       NativeEmitTempMark case_mark = temp_scope_mark(e);
   2103       for (u32 i = 0; aux && i < aux->ncases; ++i) {
   2104         OptOperand case_op;
   2105         NativeLoc imm;
   2106         memset(&case_op, 0, sizeof case_op);
   2107         case_op.kind = OPK_IMM;
   2108         case_op.type = case_type;
   2109         case_op.v.imm = (i64)aux->cases[i].value;
   2110         imm = operand_imm_or_reg(e, &case_op, NATIVE_IMM_CMP, (u32)CMP_EQ,
   2111                                  sel.v.reg, REG_NONE, in->loc);
   2112         e->target->cmp_branch(e->target, CMP_EQ, sel, imm,
   2113                               ensure_label(e, aux->cases[i].block, in->loc));
   2114         temp_scope_rewind(e, case_mark);
   2115       }
   2116       if (aux)
   2117         e->target->jump(e->target,
   2118                         ensure_label(e, aux->default_block, in->loc));
   2119       return;
   2120     }
   2121     case IR_INDIRECT_BRANCH: {
   2122       IRIndirectAux* aux = (IRIndirectAux*)in->extra.aux;
   2123       MCLabel* labels = aux && aux->ntargets
   2124                             ? arena_array(e->f->arena, MCLabel, aux->ntargets)
   2125                             : NULL;
   2126       for (u32 i = 0; aux && i < aux->ntargets; ++i)
   2127         labels[i] = ensure_label(e, aux->targets[i], in->loc);
   2128       src = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT,
   2129                                 in->opnds[0].type, REG_NONE, REG_NONE,
   2130                                 in->loc);
   2131       e->target->indirect_branch(e->target, src, labels,
   2132                                  aux ? aux->ntargets : 0u);
   2133       return;
   2134     }
   2135     case IR_LOAD_LABEL_ADDR: {
   2136       NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc);
   2137       dst = real;
   2138       if (dst.kind != NATIVE_LOC_REG)
   2139         dst = temp_loc_acquire(e, in->opnds[0].type, NATIVE_REG_INT, REG_NONE,
   2140                           REG_NONE, in->loc);
   2141       e->target->load_label_addr(e->target, dst,
   2142                                  ensure_label(e, (u32)in->extra.imm, in->loc));
   2143       if (real.kind != NATIVE_LOC_REG)
   2144         write_operand(e, &in->opnds[0], dst,
   2145                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   2146       return;
   2147     }
   2148     case IR_LOCAL_STATIC_DATA_BEGIN: {
   2149       CgIrLocalStaticBeginAux* aux = (CgIrLocalStaticBeginAux*)in->extra.aux;
   2150       emit_local_static_begin(e, aux ? &aux->desc : NULL, in->loc);
   2151       return;
   2152     }
   2153     case IR_LOCAL_STATIC_DATA_WRITE: {
   2154       CgIrLocalStaticWriteAux* aux = (CgIrLocalStaticWriteAux*)in->extra.aux;
   2155       if (!aux) emit_panic(e, in->loc, "missing local static data write");
   2156       emit_local_static_write(e, aux->has_data ? aux->data : NULL, aux->len,
   2157                               in->loc);
   2158       return;
   2159     }
   2160     case IR_LOCAL_STATIC_DATA_LABEL_ADDR: {
   2161       CgIrLocalStaticLabelAux* aux = (CgIrLocalStaticLabelAux*)in->extra.aux;
   2162       if (!aux) emit_panic(e, in->loc, "missing local static label data");
   2163       (void)aux->address_space;
   2164       emit_local_static_label_addr(e,
   2165                                    ensure_label(e, (u32)aux->target, in->loc),
   2166                                    aux->addend, aux->width, in->loc);
   2167       return;
   2168     }
   2169     case IR_LOCAL_STATIC_DATA_END:
   2170       emit_local_static_end(e, in->loc);
   2171       return;
   2172     case IR_RET:
   2173       emit_ret(e, in, fd);
   2174       return;
   2175     case IR_ALLOCA: {
   2176       NativeLoc real = loc_from_operand(e, &in->opnds[0], in->loc);
   2177       dst = real;
   2178       src = materialize_operand(e, &in->opnds[1], NATIVE_REG_INT,
   2179                                 in->opnds[1].type, REG_NONE, REG_NONE,
   2180                                 in->loc);
   2181       if (dst.kind != NATIVE_LOC_REG)
   2182         dst = temp_loc_acquire(e, in->opnds[0].type, NATIVE_REG_INT, src.v.reg,
   2183                           REG_NONE, in->loc);
   2184       e->target->alloca_(e->target, dst, src, (u32)in->extra.imm);
   2185       if (real.kind != NATIVE_LOC_REG)
   2186         write_operand(e, &in->opnds[0], dst,
   2187                       mem_for_type(e->c, in->opnds[0].type), in->loc);
   2188       return;
   2189     }
   2190     case IR_ATOMIC_LOAD: {
   2191       IRAtomicAux* aux = (IRAtomicAux*)in->extra.aux;
   2192       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   2193       addr = pointer_addr_from_operand(e, &in->opnds[1], in->loc, REG_NONE,
   2194                                        REG_NONE);
   2195       legalize_addr(e, &addr, aux->mem, in->loc);
   2196       if (dst.kind != NATIVE_LOC_REG)
   2197         dst = acquire_frame_load_result(e, in->opnds[0].type, &addr,
   2198                                         in->loc);
   2199       e->target->atomic_load(e->target, dst, addr, aux->mem, aux->mo);
   2200       if (in->opnds[0].kind != OPK_REG)
   2201         write_operand(e, &in->opnds[0], dst, aux->mem, in->loc);
   2202       return;
   2203     }
   2204     case IR_ATOMIC_STORE: {
   2205       IRAtomicAux* aux = (IRAtomicAux*)in->extra.aux;
   2206       addr = pointer_addr_from_operand(e, &in->opnds[0], in->loc, REG_NONE,
   2207                                        REG_NONE);
   2208       legalize_addr(e, &addr, aux->mem, in->loc);
   2209       src = loc_from_operand(e, &in->opnds[1], in->loc);
   2210       if (src.kind != NATIVE_LOC_REG)
   2211         src = materialize_operand_away_from_addr(e, &in->opnds[1], &addr,
   2212                                                  in->loc);
   2213       e->target->atomic_store(e->target, addr, src, aux->mem, aux->mo);
   2214       return;
   2215     }
   2216     case IR_ATOMIC_RMW: {
   2217       IRAtomicAux* aux = (IRAtomicAux*)in->extra.aux;
   2218       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   2219       addr = pointer_addr_from_operand(e, &in->opnds[1], in->loc, REG_NONE,
   2220                                        REG_NONE);
   2221       legalize_addr(e, &addr, aux->mem, in->loc);
   2222       src = materialize_operand(e, &in->opnds[2],
   2223                                 class_for_type(e, in->opnds[2].type),
   2224                                 in->opnds[2].type, REG_NONE, REG_NONE, in->loc);
   2225       if (dst.kind != NATIVE_LOC_REG)
   2226         dst = temp_loc_acquire(e, in->opnds[0].type,
   2227                           class_for_type(e, in->opnds[0].type), src.v.reg,
   2228                           REG_NONE, in->loc);
   2229       e->target->atomic_rmw(e->target, (KitCgAtomicOp)aux->op, dst, addr, src,
   2230                             aux->mem, aux->mo);
   2231       if (in->opnds[0].kind != OPK_REG)
   2232         write_operand(e, &in->opnds[0], dst, aux->mem, in->loc);
   2233       return;
   2234     }
   2235     case IR_ATOMIC_CAS: {
   2236       IRCasAux* aux = (IRCasAux*)in->extra.aux;
   2237       NativeLoc ok;
   2238       NativeLoc expected;
   2239       NativeLoc desired;
   2240       dst = loc_from_operand(e, &in->opnds[0], in->loc);
   2241       ok = loc_from_operand(e, &in->opnds[1], in->loc);
   2242       addr = pointer_addr_from_operand(e, &in->opnds[2], in->loc, REG_NONE,
   2243                                        REG_NONE);
   2244       legalize_addr(e, &addr, aux->mem, in->loc);
   2245       expected = materialize_operand(
   2246           e, &in->opnds[3], class_for_type(e, in->opnds[3].type),
   2247           in->opnds[3].type, REG_NONE, REG_NONE, in->loc);
   2248       desired = materialize_operand(
   2249           e, &in->opnds[4], class_for_type(e, in->opnds[4].type),
   2250           in->opnds[4].type, expected.v.reg, REG_NONE, in->loc);
   2251       if (dst.kind != NATIVE_LOC_REG)
   2252         dst = temp_loc_acquire(e, in->opnds[0].type,
   2253                           class_for_type(e, in->opnds[0].type), expected.v.reg,
   2254                           desired.v.reg, in->loc);
   2255       if (ok.kind != NATIVE_LOC_REG)
   2256         ok = temp_loc_acquire(e, in->opnds[1].type,
   2257                          class_for_type(e, in->opnds[1].type), dst.v.reg,
   2258                          expected.v.reg, in->loc);
   2259       e->target->atomic_cas(e->target, dst, ok, addr, expected, desired,
   2260                             aux->mem, aux->success, aux->failure);
   2261       if (in->opnds[0].kind != OPK_REG)
   2262         write_operand(e, &in->opnds[0], dst, aux->mem, in->loc);
   2263       if (in->opnds[1].kind != OPK_REG)
   2264         write_operand(e, &in->opnds[1], ok,
   2265                       mem_for_type(e->c, in->opnds[1].type), in->loc);
   2266       return;
   2267     }
   2268     case IR_VA_START: {
   2269       NativeLoc ap = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT,
   2270                                          in->opnds[0].type, REG_NONE, REG_NONE,
   2271                                          in->loc);
   2272       e->target->va_start_(e->target, ap);
   2273       return;
   2274     }
   2275     case IR_VA_END: {
   2276       NativeLoc ap = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT,
   2277                                          in->opnds[0].type, REG_NONE, REG_NONE,
   2278                                          in->loc);
   2279       e->target->va_end_(e->target, ap);
   2280       return;
   2281     }
   2282     case IR_VA_COPY: {
   2283       NativeLoc d = materialize_operand(e, &in->opnds[0], NATIVE_REG_INT,
   2284                                         in->opnds[0].type, REG_NONE, REG_NONE,
   2285                                         in->loc);
   2286       NativeLoc s = materialize_operand(e, &in->opnds[1], NATIVE_REG_INT,
   2287                                         in->opnds[1].type, d.v.reg, REG_NONE,
   2288                                         in->loc);
   2289       e->target->va_copy_(e->target, d, s);
   2290       return;
   2291     }
   2292     case IR_VA_ARG: {
   2293       KitCgTypeId ty = in->opnds[0].type;
   2294       NativeLoc ap = materialize_operand(e, &in->opnds[1], NATIVE_REG_INT,
   2295                                          in->opnds[1].type, REG_NONE, REG_NONE,
   2296                                          in->loc);
   2297       NativeLoc res;
   2298       if (type_is_aggregate_or_large(e, ty)) {
   2299         /* A value too wide for one register (an 8-byte i64/double on a 32-bit
   2300          * target, or an aggregate) cannot pass through one instruction-local
   2301          * register; hand the target its memory destination so it can copy the
   2302          * value directly. */
   2303         e->target->va_arg_(e->target,
   2304                            loc_from_operand(e, &in->opnds[0], in->loc), ap, ty);
   2305         return;
   2306       }
   2307       /* The result must land in a register distinct from the va_list pointer;
   2308        * fetch into an instruction-local temp, then write to the real
   2309        * destination. */
   2310       res = temp_loc_acquire(e, ty, class_for_type(e, ty), ap.v.reg, REG_NONE,
   2311                         in->loc);
   2312       e->target->va_arg_(e->target, res, ap, ty);
   2313       write_operand(e, &in->opnds[0], res, mem_for_type(e->c, ty), in->loc);
   2314       return;
   2315     }
   2316     case IR_ASM_BLOCK: {
   2317       IRAsmAux* aux = (IRAsmAux*)in->extra.aux;
   2318       NativeLoc* out_locs = aux && aux->nout
   2319                                 ? arena_array(e->f->arena, NativeLoc, aux->nout)
   2320                                 : NULL;
   2321       NativeLoc* real_out_locs =
   2322           aux && aux->nout
   2323               ? arena_array(e->f->arena, NativeLoc, aux->nout)
   2324               : NULL;
   2325       NativeLoc* in_locs = aux && aux->nin
   2326                                ? arena_array(e->f->arena, NativeLoc, aux->nin)
   2327                                : NULL;
   2328       int* staged_outs =
   2329           aux && aux->nout ? arena_zarray(e->f->arena, int, aux->nout) : NULL;
   2330       if (e->temps)
   2331         e->temps->allow_asm_temps =
   2332             (u8)!(aux && aux->has_memory_constraint);
   2333       /* Register constraints are an instruction-local placement requirement,
   2334        * not a property of the value's whole live range. Keep regalloc's chosen
   2335        * hard register when it satisfies the constraint; otherwise stage the
   2336        * value through a scoped lease. The backend asm binder validates and
   2337        * binds concrete register operands; only backend-private materialization
   2338        * for memory constraints remains on its side of the boundary. */
   2339       for (u32 i = 0; aux && i < aux->nout; ++i) {
   2340         real_out_locs[i] =
   2341             loc_from_operand(e, &aux->out_ops[i], in->loc);
   2342         out_locs[i] = asm_stage_reg_operand(
   2343             e, &aux->outs[i],
   2344             aux->out_reg_reqs ? &aux->out_reg_reqs[i] : NULL,
   2345             real_out_locs[i],
   2346             aux->outs[i].dir == KIT_CG_ASM_INOUT, &staged_outs[i], in->loc);
   2347       }
   2348       for (u32 i = 0; aux && i < aux->nin; ++i) {
   2349         NativeLoc real = loc_from_operand(e, &aux->in_ops[i], in->loc);
   2350         int matched = native_asm_match_index(aux->ins[i].str);
   2351         if (matched >= 0) {
   2352           if ((u32)matched >= aux->nout ||
   2353               out_locs[matched].kind != NATIVE_LOC_REG)
   2354             emit_panic(e, in->loc, "invalid matching asm operand");
   2355           if (native_asm_constraint_early(aux->outs[matched].str))
   2356             emit_panic(e, in->loc,
   2357                        "matching input names early-clobber output");
   2358           load_loc_into_reg(e, out_locs[matched], real, in->loc);
   2359           in_locs[i] = out_locs[matched];
   2360         } else {
   2361           in_locs[i] = asm_stage_reg_operand(
   2362               e, &aux->ins[i],
   2363               aux->in_reg_reqs ? &aux->in_reg_reqs[i] : NULL, real, 1, NULL,
   2364               in->loc);
   2365         }
   2366       }
   2367       e->target->asm_block(e->target, aux ? aux->tmpl : "",
   2368                            aux ? aux->outs : NULL, aux ? aux->nout : 0,
   2369                            out_locs, aux ? aux->ins : NULL, aux ? aux->nin : 0,
   2370                            in_locs, aux ? aux->clobbers : NULL,
   2371                            aux ? aux->nclob : 0);
   2372       for (u32 i = 0; aux && i < aux->nout; ++i)
   2373         if (staged_outs[i])
   2374           write_operand(e, &aux->out_ops[i], out_locs[i],
   2375                         mem_for_type(e->c, out_locs[i].type), in->loc);
   2376       return;
   2377     }
   2378     case IR_BREAK_TO:
   2379     case IR_CONTINUE_TO:
   2380       emit_panic(e, in->loc, "operation is not wired to NativeTarget yet");
   2381     case IR_FENCE:
   2382       e->target->fence(e->target, (KitCgMemOrder)in->extra.imm);
   2383       return;
   2384     case IR_UNREACHABLE:
   2385       e->target->trap(e->target);
   2386       return;
   2387     case IR_INTRINSIC: {
   2388       IRIntrinAux* aux = (IRIntrinAux*)in->extra.aux;
   2389       NativeLoc* dsts = aux && aux->ndst
   2390                             ? arena_array(e->f->arena, NativeLoc, aux->ndst)
   2391                             : NULL;
   2392       NativeLoc* real_dsts =
   2393           aux && aux->ndst
   2394               ? arena_array(e->f->arena, NativeLoc, aux->ndst)
   2395               : NULL;
   2396       NativeLoc* args = aux && aux->narg
   2397                             ? arena_array(e->f->arena, NativeLoc, aux->narg)
   2398                             : NULL;
   2399       for (u32 i = 0; aux && i < aux->narg; ++i) {
   2400         if (aux->args[i].kind == OPK_IMM &&
   2401             native_intrinsic_arg_accepts_imm(e->target, aux->kind, i)) {
   2402           args[i] = loc_from_operand(e, &aux->args[i], in->loc);
   2403         } else {
   2404           args[i] = materialize_operand(
   2405               e, &aux->args[i], class_for_type(e, aux->args[i].type),
   2406               aux->args[i].type, REG_NONE, REG_NONE, in->loc);
   2407         }
   2408       }
   2409       for (u32 i = 0; aux && i < aux->ndst; ++i) {
   2410         NativeAllocClass cls = class_for_type(e, aux->dsts[i].type);
   2411         real_dsts[i] = loc_from_operand(e, &aux->dsts[i], in->loc);
   2412         dsts[i] = real_dsts[i];
   2413         if (dsts[i].kind != NATIVE_LOC_NONE &&
   2414             dsts[i].kind != NATIVE_LOC_REG)
   2415           dsts[i] = temp_loc_acquire(e, aux->dsts[i].type, cls, REG_NONE,
   2416                                      REG_NONE,
   2417                                 in->loc);
   2418       }
   2419       e->target->intrinsic(e->target, aux->kind, dsts, aux->ndst, args,
   2420                            aux->narg);
   2421       for (u32 i = 0; aux && i < aux->ndst; ++i) {
   2422         if (real_dsts[i].kind == NATIVE_LOC_NONE ||
   2423             real_dsts[i].kind == NATIVE_LOC_REG)
   2424           continue;
   2425         write_operand(e, &aux->dsts[i], dsts[i],
   2426                       mem_for_type(e->c, aux->dsts[i].type), in->loc);
   2427       }
   2428       return;
   2429     }
   2430     default:
   2431       emit_panic(e, in->loc, "unknown IR op");
   2432   }
   2433 }
   2434 
   2435 static void emit_inst(NativeEmitCtx* e, u32 block, u32 order_index, Inst* in,
   2436                       const CGFuncDesc* fd) {
   2437   NativeEmitTempScope scope;
   2438   temp_scope_begin(e, &scope, in);
   2439   emit_inst_body(e, block, order_index, in, fd);
   2440   temp_scope_end(e, &scope);
   2441 }
   2442 
   2443 static int native_emit_terminates(const Inst* in) {
   2444   if (!in) return 0;
   2445   switch ((IROp)in->op) {
   2446     case IR_BR:
   2447     case IR_CONDBR:
   2448     case IR_CMP_BRANCH:
   2449     case IR_SWITCH:
   2450     case IR_INDIRECT_BRANCH:
   2451     case IR_RET:
   2452     case IR_UNREACHABLE:
   2453     case IR_BREAK_TO:
   2454     case IR_CONTINUE_TO:
   2455       return 1;
   2456     case IR_INTRINSIC: {
   2457       IRIntrinAux* aux = (IRIntrinAux*)in->extra.aux;
   2458       return aux && (aux->kind == INTRIN_LONGJMP || aux->kind == INTRIN_TRAP);
   2459     }
   2460     default:
   2461       return 0;
   2462   }
   2463 }
   2464 
   2465 static void emit_block(NativeEmitCtx* e, u32 block, u32 order_index,
   2466                        const CGFuncDesc* fd) {
   2467   if (block >= e->f->nblocks) return;
   2468   /* No cache fact crosses a control-flow edge. Even a single-predecessor block
   2469    * may be entered through a layout-independent branch, so block entry is the
   2470    * simple, explicit ownership boundary. */
   2471   frame_cache_clear(e);
   2472   if (!e->label_placed[block]) {
   2473     e->label_placed[block] = 1u;
   2474     e->target->label_place(e->target,
   2475                            ensure_label(e, block, (SrcLoc){0, 0, 0}));
   2476   }
   2477   Block* bl = &e->f->blocks[block];
   2478   int is_last_block = order_index + 1u == e->f->emit_order_n;
   2479   for (u32 i = 0; i < bl->ninsts; ++i) {
   2480     e->emitting_terminal_ret = is_last_block && i + 1u == bl->ninsts &&
   2481                                (IROp)bl->insts[i].op == IR_RET;
   2482     emit_inst(e, block, order_index, &bl->insts[i], fd);
   2483   }
   2484   e->emitting_terminal_ret = 0;
   2485   if (bl->nsucc == 1u &&
   2486       (bl->ninsts == 0 ||
   2487        !native_emit_terminates(&bl->insts[bl->ninsts - 1u]))) {
   2488     u32 next = order_index + 1u < e->f->emit_order_n
   2489                    ? e->f->emit_order[order_index + 1u]
   2490                    : UINT32_MAX;
   2491     if (bl->succ[0] != next)
   2492       e->target->jump(e->target,
   2493                       ensure_label(e, bl->succ[0], (SrcLoc){0, 0, 0}));
   2494   }
   2495 }
   2496 
   2497 #define EMIT_MAX_REG_CLASSES 4u
   2498 
   2499 static void collect_used_reg(Func* f, Inst* in, OptOperand* op, int is_def,
   2500                              void* ctx) {
   2501   u32* used = (u32*)ctx;
   2502   (void)f;
   2503   (void)in;
   2504   (void)is_def;
   2505   if (op && op->kind == OPT_OPK_REG && op->cls < EMIT_MAX_REG_CLASSES &&
   2506       op->v.reg < 32u)
   2507     used[op->cls] |= 1u << op->v.reg;
   2508 }
   2509 
   2510 static void compute_emit_live_after(NativeEmitCtx* e) {
   2511   OptHardBlockLive* blocks = opt_maybe_build_hard_live(e->f);
   2512   e->live_after_ready = 1u;
   2513   e->live_after_cap = e->f->next_inst_id;
   2514   if (!e->live_after_cap) return;
   2515   e->live_after_by_inst =
   2516       arena_zarray(e->f->arena, OptHardRegSet, e->live_after_cap);
   2517   for (u32 b = 0; b < e->f->nblocks; ++b) {
   2518     Block* bl = &e->f->blocks[b];
   2519     OptHardRegSet live =
   2520         opt_hard_live_out_for_block(blocks ? &blocks[b] : NULL);
   2521     for (u32 ri = bl->ninsts; ri > 0; --ri) {
   2522       Inst* in = &bl->insts[ri - 1u];
   2523       OptRegEffectMasks effects;
   2524       OptHardRegSet kills;
   2525       if (in->id != INST_ID_NONE && in->id < e->live_after_cap)
   2526         e->live_after_by_inst[in->id] = live;
   2527       opt_inst_reg_effect_masks(e->f, in, &effects);
   2528       opt_reg_effect_mask_kills(&effects, &kills);
   2529       opt_hard_live_step(&live, &effects.uses, &kills);
   2530     }
   2531   }
   2532 }
   2533 
   2534 static NativeLoc asm_planning_loc(const OptOperand* op) {
   2535   NativeLoc loc = loc_none();
   2536   if (!op) return loc;
   2537   loc.type = op->type;
   2538   if (op->kind == OPK_REG)
   2539     loc = loc_reg(op->type, (NativeAllocClass)op->cls, op->v.reg);
   2540   return loc;
   2541 }
   2542 
   2543 /* Dry-run one instruction's asm allocator before frame finalization. The same
   2544  * candidate order and unavailable masks are used by final emission, so retain
   2545  * only the ABI-relevant result: callee-saved staging registers overwritten. */
   2546 static void plan_one_asm_staging(NativeEmitCtx* e, Inst* in, IRAsmAux* aux) {
   2547   NativeEmitTempScope scope;
   2548   if (!aux) return;
   2549   temp_scope_begin(e, &scope, in);
   2550   scope.allow_asm_temps = (u8)!aux->has_memory_constraint;
   2551   for (u32 k = 0; k < aux->nout; ++k) {
   2552     (void)asm_stage_reg_operand(
   2553         e, &aux->outs[k],
   2554         aux->out_reg_reqs ? &aux->out_reg_reqs[k] : NULL,
   2555         asm_planning_loc(&aux->out_ops[k]), 0, NULL, in->loc);
   2556   }
   2557   for (u32 k = 0; k < aux->nin; ++k) {
   2558     if (native_asm_match_index(aux->ins[k].str) >= 0) continue;
   2559     (void)asm_stage_reg_operand(
   2560         e, &aux->ins[k],
   2561         aux->in_reg_reqs ? &aux->in_reg_reqs[k] : NULL,
   2562         asm_planning_loc(&aux->in_ops[k]), 0, NULL, in->loc);
   2563   }
   2564   for (u32 c = 0; c < OPT_REG_CLASSES; ++c)
   2565     e->asm_stage_callee_used[c] |=
   2566         scope.leased[c] & e->f->opt_callee_saved[c];
   2567   temp_scope_end(e, &scope);
   2568 }
   2569 
   2570 static NativeLoc* marshal_call_args_reserve(NativeEmitCtx* e, NativeLoc* args,
   2571                                          u32* cap, u32 want) {
   2572   u32 grown;
   2573   if (want <= *cap) return args;
   2574   grown = *cap ? *cap : want;
   2575   while (grown < want && grown <= 0x7fffffffu) grown *= 2u;
   2576   if (grown < want) grown = want;
   2577   *cap = grown;
   2578   return arena_zarray(e->f->arena, NativeLoc, grown);
   2579 }
   2580 
   2581 static void plan_asm_clobber_push(NativeEmitCtx* e, Sym** clobbers, u32* count,
   2582                                   u32* cap, Sym clobber) {
   2583   if (*count == *cap) {
   2584     u32 grown = *cap ? *cap * 2u : 8u;
   2585     Sym* next;
   2586     if (grown < *cap) grown = *count + 1u;
   2587     next = arena_array(e->f->arena, Sym, grown);
   2588     if (*count) memcpy(next, *clobbers, sizeof *next * *count);
   2589     *clobbers = next;
   2590     *cap = grown;
   2591   }
   2592   (*clobbers)[(*count)++] = clobber;
   2593 }
   2594 
   2595 /* Plan the complete call frame before any code is emitted, then hand it to the
   2596  * backend via func_begin_known_frame so the prologue is emitted final. The
   2597  * optimizer knows everything the frame needs after register allocation and MIR
   2598  * lowering: the callee-saved set (scanned from the MIR), every static frame
   2599  * slot (f->frame_slots), and the outgoing-arg area (the max over all calls of
   2600  * the pure call_stack_bytes query). The body therefore allocates no slots, so
   2601  * the frame is final up front and nothing is back-patched. Populates
   2602  * e->slot_map from the backend-assigned slot handles for the body to use. */
   2603 static void plan_frame(NativeEmitCtx* e, const CGFuncDesc* fd) {
   2604   NativeTarget* t = e->target;
   2605   NativeKnownFrameDesc frame;
   2606   NativeFrameSlotDesc* slots = NULL;
   2607   NativeFrameSlot* out_slots = NULL;
   2608   u32 used[EMIT_MAX_REG_CLASSES];
   2609   u32 nclasses;
   2610   NativeLoc* call_args = NULL;
   2611   u32 call_args_cap = 0;
   2612   u32 max_outgoing = 0;
   2613   u8 has_alloca = 0;
   2614   u8 needs_scratch_spill = 0;
   2615   u8 has_call = 0;
   2616   u8 has_asm = 0;
   2617   u8 reads_frame = 0;
   2618   u32 nasm_clob = 0;
   2619   u32 asm_clob_cap = 0;
   2620   u32 asm_clobber_abi_sets = 0;
   2621   Sym* asm_clobbers = NULL;
   2622   memset(&frame, 0, sizeof frame);
   2623   memset(used, 0, sizeof used);
   2624   memset(e->asm_stage_callee_used, 0, sizeof e->asm_stage_callee_used);
   2625   memset(e->callee_saved_used, 0, sizeof e->callee_saved_used);
   2626   /* One primary MIR scan collects allocated hard registers, asm staging, and
   2627    * all frame-shape facts. This used to be three independent full walks. */
   2628   for (u32 b = 0; b < e->f->nblocks; ++b) {
   2629     Block* bl = &e->f->blocks[b];
   2630     for (u32 i = 0; i < bl->ninsts; ++i) {
   2631       Inst* in = &bl->insts[i];
   2632       if (t->reserve_callee_saves)
   2633         opt_walk_inst_operands(e->f, in, collect_used_reg, used);
   2634       if ((IROp)in->op == IR_ALLOCA) {
   2635         has_alloca = 1;
   2636       } else if ((IROp)in->op == IR_ATOMIC_RMW) {
   2637         needs_scratch_spill = 1;
   2638       } else if ((IROp)in->op == IR_CALL) {
   2639         IRCallAux* aux = (IRCallAux*)in->extra.aux;
   2640         /* Any call (regular or sibling/tail) means the function is not a leaf:
   2641          * it clobbers the return-address register and the stack below sp. */
   2642         has_call = 1;
   2643         if (aux && t->call_stack_bytes) {
   2644           NativeCallDesc d;
   2645           u32 sb;
   2646           call_args = marshal_call_args_reserve(
   2647               e, call_args, &call_args_cap, aux->desc.nargs);
   2648           memset(&d, 0, sizeof d);
   2649           d.fn_type = aux->desc.fn_type;
   2650           d.flags = aux->desc.flags;
   2651           d.nargs = aux->desc.nargs;
   2652           for (u32 k = 0; k < aux->desc.nargs; ++k) {
   2653             memset(&call_args[k], 0, sizeof call_args[k]);
   2654             call_args[k].type = aux->desc.args[k].type;
   2655           }
   2656           d.args = call_args;
   2657           sb = t->call_stack_bytes(t, &d);
   2658           if (sb > max_outgoing) max_outgoing = sb;
   2659         }
   2660       } else if ((IROp)in->op == IR_ASM_BLOCK) {
   2661         /* Inline asm may clobber the return-address register or the red zone
   2662          * opaquely; disqualifies the frame-eliding tiers (see has_asm). Its
   2663          * callee-saved register clobbers and named-register operand
   2664          * requirements are equally opaque to the operand scan below; collect
   2665          * them now so the backend can fold them into the saved set. */
   2666         IRAsmAux* aux = (IRAsmAux*)in->extra.aux;
   2667         has_asm = 1;
   2668         if (aux) {
   2669           plan_one_asm_staging(e, in, aux);
   2670           for (u32 k = 0; k < aux->nclob; ++k)
   2671             plan_asm_clobber_push(e, &asm_clobbers, &nasm_clob,
   2672                                   &asm_clob_cap, aux->clobbers[k]);
   2673           for (u32 k = 0; k < aux->nout; ++k)
   2674             if (aux->outs[k].reg)
   2675               plan_asm_clobber_push(e, &asm_clobbers, &nasm_clob,
   2676                                     &asm_clob_cap, aux->outs[k].reg);
   2677           for (u32 k = 0; k < aux->nin; ++k)
   2678             if (aux->ins[k].reg)
   2679               plan_asm_clobber_push(e, &asm_clobbers, &nasm_clob,
   2680                                     &asm_clob_cap, aux->ins[k].reg);
   2681           asm_clobber_abi_sets |= aux->clobber_abi_sets;
   2682         }
   2683       } else if ((IROp)in->op == IR_INTRINSIC) {
   2684         /* __builtin_frame_address / __builtin_return_address read the frame
   2685          * record, so the function must keep one (disables the rv64 frameless
   2686          * leaf tier; see NativeKnownFrameDesc.reads_frame). */
   2687         IRIntrinAux* aux = (IRIntrinAux*)in->extra.aux;
   2688         if (aux && (aux->kind == INTRIN_FRAME_ADDRESS ||
   2689                     aux->kind == INTRIN_RETURN_ADDRESS))
   2690           reads_frame = 1;
   2691       } else if ((IROp)in->op == IR_TLS_ADDR_OF) {
   2692         /* The Mach-O TLV descriptor access calls the resolver thunk — a hidden
   2693          * call the leaf analysis must see, or the frame-eliding leaf tiers (the
   2694          * x64 SysV red zone, which keeps spills below sp; any return-address
   2695          * elision) miscompile: the thunk's `call`/`blr` clobbers the return
   2696          * address and the bytes below sp. ELF Local-Exec and Windows TEB
   2697          * accesses make no call, so only the descriptor model disqualifies. */
   2698         if (obj_format_tls_via_descriptor(t->c)) has_call = 1;
   2699       }
   2700     }
   2701   }
   2702   nclasses = t->reserve_callee_saves && t->regs
   2703                  ? (t->regs->nclasses < EMIT_MAX_REG_CLASSES
   2704                         ? t->regs->nclasses
   2705                         : EMIT_MAX_REG_CLASSES)
   2706                  : 0u;
   2707   for (u32 i = 0; t->regs && i < t->regs->nclasses; ++i) {
   2708     const NativeAllocClassInfo* ci = &t->regs->classes[i];
   2709     if (ci->cls >= EMIT_MAX_REG_CLASSES) continue;
   2710     used[ci->cls] &=
   2711         native_target_callee_saved_mask(t, (NativeAllocClass)ci->cls);
   2712     used[ci->cls] |= e->asm_stage_callee_used[ci->cls];
   2713   }
   2714   for (u32 c = 0; c < nclasses && c < OPT_REG_CLASSES; ++c)
   2715     e->callee_saved_used[c] = used[c];
   2716   e->slot_map =
   2717       arena_zarray(e->f->arena, NativeFrameSlot, e->f->nframe_slots + 1u);
   2718   if (e->f->nframe_slots) {
   2719     slots = arena_zarray(e->f->arena, NativeFrameSlotDesc, e->f->nframe_slots);
   2720     out_slots = arena_zarray(e->f->arena, NativeFrameSlot, e->f->nframe_slots);
   2721     for (u32 i = 0; i < e->f->nframe_slots; ++i) {
   2722       IRFrameSlot* s = &e->f->frame_slots[i];
   2723       NativeFrameSlotDesc* d = &slots[i];
   2724       memset(d, 0, sizeof *d);
   2725       d->type = s->type;
   2726       d->name = s->name;
   2727       d->loc = s->loc;
   2728       d->size = s->size;
   2729       d->align = s->align;
   2730       d->priority = s->priority;
   2731       d->kind = s->kind;
   2732       d->flags = s->flags;
   2733     }
   2734   }
   2735   /* W1.0 hot-slot-low ordering. The backend's known-frame path bump-allocates
   2736    * each body slot in the order it is presented (cum_off is monotonic, see
   2737    * native_frame_slot_alloc), so slots presented first get the smallest final
   2738    * frame displacement. Present the body slots in DESCENDING priority order so
   2739    * the hottest spills land low: disp8 on x64 (-4 bytes/access), inside the
   2740    * scaled reach on aa64, inside the +/-2KB imm12 window on rv64.
   2741    *
   2742    * `order[k]` is the original slot index now presented at position k. Fixed-
   2743    * offset slots keep their original positions (their displacement is not
   2744    * order-derived); the remaining slots are stable-sorted by descending priority
   2745    * with an original-index tie-break (so the sort is deterministic and, among
   2746    * equal priorities, byte-stable). The native slot the backend returns for
   2747    * position k therefore belongs to original IR slot `order[k]`, and the slot_map
   2748    * is written back through `order` so e->slot_map[ir_id] stays exact -- a
   2749    * transposed map silently miscompiles every spill, so it is asserted below. */
   2750   u32 nfs = e->f->nframe_slots;
   2751   u32* slot_order = NULL; /* slot_order[k] = original IR slot index at position k */
   2752   if (nfs > 1u) {
   2753     u32* order = arena_array(e->f->arena, u32, nfs);
   2754     u32* movable = arena_array(e->f->arena, u32, nfs);
   2755     u32 nmovable = 0;
   2756     for (u32 i = 0; i < nfs; ++i) {
   2757       order[i] = i; /* fixed slots stay pinned at their original index */
   2758       if (!(slots[i].flags & NATIVE_FRAME_SLOT_FIXED_OFFSET))
   2759         movable[nmovable++] = i;
   2760     }
   2761     /* Stable descending-priority sort of the movable indices (insertion sort:
   2762      * nslots is small per function and this is off the per-instruction path; if
   2763      * a profile shows it material, swap in a radix over the integer priority --
   2764      * see O1.md W1.0 "Linear?"). Insertion sort is naturally stable, preserving
   2765      * original order among equal priorities. */
   2766     for (u32 i = 1; i < nmovable; ++i) {
   2767       u32 cur = movable[i];
   2768       u32 cur_pri = slots[cur].priority;
   2769       u32 j = i;
   2770       while (j > 0 && slots[movable[j - 1]].priority < cur_pri) {
   2771         movable[j] = movable[j - 1];
   2772         --j;
   2773       }
   2774       movable[j] = cur;
   2775     }
   2776     /* Drop the sorted movable indices back into the non-fixed positions. */
   2777     u32 mi = 0;
   2778     for (u32 k = 0; k < nfs; ++k)
   2779       if (!(slots[k].flags & NATIVE_FRAME_SLOT_FIXED_OFFSET))
   2780         order[k] = movable[mi++];
   2781     /* Reorder the desc array in place via a scratch copy so frame.slots is the
   2782      * contiguous, permuted view the backend iterates. */
   2783     NativeFrameSlotDesc* ordered =
   2784         arena_array(e->f->arena, NativeFrameSlotDesc, nfs);
   2785     for (u32 k = 0; k < nfs; ++k) ordered[k] = slots[order[k]];
   2786     slot_order = order;
   2787     slots = ordered;
   2788   }
   2789   frame.slots = slots;
   2790   frame.nslots = e->f->nframe_slots;
   2791   frame.max_outgoing = max_outgoing;
   2792   frame.callee_saved_used = nclasses ? used : NULL;
   2793   frame.ncallee_classes = nclasses;
   2794   frame.has_alloca = has_alloca;
   2795   frame.needs_scratch_spill = needs_scratch_spill;
   2796   frame.is_leaf = !has_call;
   2797   frame.has_asm = has_asm;
   2798   frame.reads_frame = reads_frame;
   2799   frame.asm_clobbers = asm_clobbers;
   2800   frame.nasm_clobbers = nasm_clob;
   2801   frame.asm_clobber_abi_sets = asm_clobber_abi_sets;
   2802   t->func_begin_known_frame(t, fd, &frame, out_slots);
   2803   /* Map each backend native slot (returned for presentation position k) back to
   2804    * its original IR frame slot id. With ordering, position k holds IR slot
   2805    * order[k]; without it (nfs<=1 or no reorder), position k == IR slot k. */
   2806   for (u32 k = 0; k < e->f->nframe_slots; ++k) {
   2807     u32 ir_idx = slot_order ? slot_order[k] : k;
   2808     e->slot_map[e->f->frame_slots[ir_idx].id] = out_slots[k];
   2809   }
   2810 #ifndef NDEBUG
   2811   /* The slot_map must stay an exact 1:1 of IR frame slots to the native slots
   2812    * the backend returned -- a transposed/dropped map silently miscompiles every
   2813    * spill. The actual native slot ids are NOT bounded by nframe_slots (the
   2814    * backend reserves callee-saves / entry-saves / scratch around the body slots,
   2815    * so body native ids are an arbitrary distinct block), so verify the real
   2816    * correctness condition instead: `order` is a permutation of [0, nfs) -- i.e.
   2817    * every IR frame slot is presented exactly once -- and each presented position
   2818    * received a non-NONE native slot. Both are O(nslots) (seen array sized by the
   2819    * IR index domain), off the per-instruction path. With identity ordering this
   2820    * is vacuously the original code's invariant. */
   2821   if (nfs) {
   2822     u8* seen = arena_zarray(e->f->arena, u8, nfs);
   2823     for (u32 k = 0; k < nfs; ++k) {
   2824       u32 ir_idx = slot_order ? slot_order[k] : k;
   2825       if (ir_idx >= nfs || seen[ir_idx])
   2826         compiler_panic(e->c, (SrcLoc){0, 0, 0},
   2827                        "opt W1.0: slot order is not a permutation at pos %u",
   2828                        (unsigned)k);
   2829       seen[ir_idx] = 1u;
   2830       if (out_slots[k] == NATIVE_FRAME_SLOT_NONE)
   2831         compiler_panic(e->c, (SrcLoc){0, 0, 0},
   2832                        "opt W1.0: backend returned no native slot for pos %u",
   2833                        (unsigned)k);
   2834     }
   2835   }
   2836 #endif
   2837 }
   2838 
   2839 void opt_emit_native(Compiler* c, Func* f, NativeTarget* target) {
   2840   NativeEmitCtx e;
   2841   Func view;
   2842   CGFuncDesc fd;
   2843   if (!f || !target) return;
   2844   memset(&e, 0, sizeof e);
   2845   e.f = opt_mir_view(f, &view) ? &view : f;
   2846   e.c = c;
   2847   e.target = target;
   2848   metrics_scope_begin(c, "opt.native_emit.setup");
   2849   e.labels = arena_array(e.f->arena, MCLabel, e.f->nblocks ? e.f->nblocks : 1u);
   2850   e.label_placed =
   2851       arena_zarray(e.f->arena, u8, e.f->nblocks ? e.f->nblocks : 1u);
   2852   for (u32 i = 0; i < e.f->nblocks; ++i) e.labels[i] = MC_LABEL_NONE;
   2853   fd = semantic_func_desc(&e);
   2854   metrics_scope_end(c, "opt.native_emit.setup");
   2855 
   2856   metrics_scope_begin(c, "opt.native_emit.func_begin");
   2857   /* The optimizer has the whole frame after regalloc + MIR lowering, so it
   2858    * plans it up front (plan_frame) and drives func_begin_known_frame: the
   2859    * backend emits a final prologue with no reserved NOP region and no
   2860    * back-patching. The body allocates no frame slots, so the frame stays final;
   2861    * allocas and tail epilogues are emitted final too. (Contrast the
   2862    * single-pass NativeDirectTarget path, which reserves and patches.) */
   2863   plan_frame(&e, &fd);
   2864   bind_params(&e);
   2865   metrics_scope_end(c, "opt.native_emit.func_begin");
   2866 
   2867   metrics_scope_begin(c, "opt.native_emit.body");
   2868   for (u32 i = 0; i < e.f->emit_order_n; ++i)
   2869     emit_block(&e, e.f->emit_order[i], i, &fd);
   2870   metrics_scope_end(c, "opt.native_emit.body");
   2871 
   2872   metrics_scope_begin(c, "opt.native_emit.func_end");
   2873   target->func_end(target);
   2874   metrics_scope_end(c, "opt.native_emit.func_end");
   2875 }