kit

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

native_direct_target.c (114087B)


      1 #include "cg/native_direct_target.h"
      2 
      3 /* NativeDirectTarget is intentionally single-pass: semantic CG calls are
      4  * lowered immediately to NativeTarget operations, MCEmitter owns label fixups,
      5  * and function-end calls note_frame_state()/patch_apply() let the native
      6  * backend patch deferred frame/prologue details after max outgoing space is
      7  * known. Direct lowering currently forwards final frame state but does not
      8  * author generic NativePatch records itself.
      9  *
     10  * Remaining direct/backend cutover work: stack arguments, tail/musttail,
     11  * varargs, typed inline-asm register/memory bindings and outputs,
     12  * label-address data and computed gotos, records/sret/large aggregates, FP and
     13  * rounding conversions, fuller scalar intrinsics, and production-grade atomic
     14  * RMW/CAS lowering. */
     15 
     16 #include <string.h>
     17 
     18 #include "abi/abi.h"
     19 #include "cg/native_asm.h"
     20 #include "cg/type.h"
     21 #include "core/arena.h"
     22 #include "core/pool.h"
     23 #include "core/slice.h"
     24 
     25 #define NATIVE_DIRECT_MAGIC 0x4e445447u
     26 
     27 static NativeDirectTarget* nd_of(CgTarget* t) { return (NativeDirectTarget*)t; }
     28 
     29 static _Noreturn void nd_panic(NativeDirectTarget* d, const char* what) {
     30   compiler_panic(d->base.c, d->loc, "native direct target: %s", what);
     31 }
     32 
     33 static void* nd_arena(NativeDirectTarget* d, size_t size, size_t align) {
     34   void* p = arena_zalloc(d->base.c->tu, size, align);
     35   if (!p) nd_panic(d, "out of memory");
     36   return p;
     37 }
     38 
     39 /* Pick a transient NativeLoc array for one op: the on-struct buffer when N
     40  * fits, else a one-shot arena allocation. N == 0 yields NULL. */
     41 static NativeLoc* nd_loc_buf(NativeDirectTarget* d, NativeLoc* buf, u32 cap,
     42                              u32 n) {
     43   if (!n) return NULL;
     44   if (n <= cap) return buf;
     45   return nd_arena(d, sizeof(NativeLoc) * n, _Alignof(NativeLoc));
     46 }
     47 
     48 void native_direct_project_tail_call_desc(NativeDirectTarget* d,
     49                                           const CGCallDesc* call,
     50                                           NativeCallDesc* nd) {
     51   NativeLoc* args = NULL;
     52   NativeLoc* results = NULL;
     53   u32 nresults = call->result != CG_LOCAL_NONE ? 1u : 0u;
     54   u32 i;
     55   memset(nd, 0, sizeof *nd);
     56   if (call->nargs) args = arena_zarray(d->base.c->tu, NativeLoc, call->nargs);
     57   if (nresults) results = arena_zarray(d->base.c->tu, NativeLoc, nresults);
     58   for (i = 0; i < call->nargs; ++i) {
     59     args[i].kind = NATIVE_LOC_FRAME;
     60     args[i].type = d->locals[call->args[i] - 1u].type;
     61     args[i].cls = d->locals[call->args[i] - 1u].cls;
     62     args[i].v.frame = d->locals[call->args[i] - 1u].home;
     63   }
     64   if (nresults) {
     65     results[0].kind = NATIVE_LOC_FRAME;
     66     results[0].type = d->locals[call->result - 1u].type;
     67     results[0].cls = d->locals[call->result - 1u].cls;
     68     results[0].v.frame = d->locals[call->result - 1u].home;
     69   }
     70   nd->fn_type = call->fn_type;
     71   nd->args = args;
     72   nd->results = results;
     73   nd->nargs = call->nargs;
     74   nd->nresults = nresults;
     75 }
     76 
     77 static void nd_grow_locals(NativeDirectTarget* d, u32 want) {
     78   NativeDirectLocal* next;
     79   u32 cap;
     80   if (d->locals_cap >= want) return;
     81   cap = d->locals_cap ? d->locals_cap : 32u;
     82   while (cap < want) cap *= 2u;
     83   next = nd_arena(d, sizeof(*next) * cap, _Alignof(NativeDirectLocal));
     84   if (d->locals) memcpy(next, d->locals, sizeof(*next) * d->nlocals);
     85   d->locals = next;
     86   d->locals_cap = cap;
     87 }
     88 
     89 static void nd_grow_transient(NativeDirectTarget* d, u32 want) {
     90   CGLocal* next;
     91   u32 cap;
     92   if (d->transient_cap >= want) return;
     93   cap = d->transient_cap ? d->transient_cap : 32u;
     94   while (cap < want) cap *= 2u;
     95   next = nd_arena(d, sizeof(*next) * cap, _Alignof(CGLocal));
     96   if (d->transient_locals)
     97     memcpy(next, d->transient_locals, sizeof(*next) * d->ntransient);
     98   d->transient_locals = next;
     99   d->transient_cap = cap;
    100 }
    101 
    102 static void nd_grow_labels(NativeDirectTarget* d, u32 want) {
    103   MCLabel* next;
    104   u32 cap;
    105   if (d->labels_cap >= want) return;
    106   cap = d->labels_cap ? d->labels_cap : 32u;
    107   while (cap < want) cap *= 2u;
    108   next = nd_arena(d, sizeof(*next) * cap, _Alignof(MCLabel));
    109   if (d->labels) memcpy(next, d->labels, sizeof(*next) * d->labels_cap);
    110   d->labels = next;
    111   d->labels_cap = cap;
    112 }
    113 
    114 static void nd_grow_scopes(NativeDirectTarget* d, u32 want) {
    115   NativeDirectScope* next;
    116   u32 cap;
    117   if (d->scopes_cap >= want) return;
    118   cap = d->scopes_cap ? d->scopes_cap : 16u;
    119   while (cap < want) cap *= 2u;
    120   next = nd_arena(d, sizeof(*next) * cap, _Alignof(NativeDirectScope));
    121   if (d->scopes) memcpy(next, d->scopes, sizeof(*next) * d->nscopes);
    122   d->scopes = next;
    123   d->scopes_cap = cap;
    124 }
    125 
    126 static NativeDirectLocal* nd_local(NativeDirectTarget* d, CGLocal local) {
    127   if (local == CG_LOCAL_NONE || local > d->nlocals)
    128     nd_panic(d, "bad semantic local");
    129   return &d->locals[local - 1u];
    130 }
    131 
    132 static NativeAllocClass nd_class_for_type(NativeDirectTarget* d,
    133                                           KitCgTypeId type) {
    134   if (d->native && d->native->class_for_type)
    135     return d->native->class_for_type(d->native, type);
    136   return NATIVE_REG_INT;
    137 }
    138 
    139 static const NativeAllocClassInfo* nd_class_info(NativeDirectTarget* d,
    140                                                  NativeAllocClass cls) {
    141   const NativeAllocClassInfo* ci =
    142       (u32)cls < NATIVE_REG_CLASS_COUNT ? d->class_info[cls] : NULL;
    143   if (!ci) nd_panic(d, "target has no requested register class");
    144   return ci;
    145 }
    146 
    147 /* Register-location constructor is shared as native_loc_reg in
    148  * native_target.h (arg order: type, cls, reg). nd_loc_reg wraps it to stamp
    149  * the cached scalar-width descriptor (szinfo) once, here at the single
    150  * register-loc choke point, so the per-arch binop/move/convert/cmp hot path
    151  * reads loc.szinfo instead of re-crossing the type bridge per operand. */
    152 static inline NativeLoc nd_loc_reg(NativeDirectTarget* d, KitCgTypeId type,
    153                                    NativeAllocClass cls, Reg reg) {
    154   NativeLoc loc = native_loc_reg(type, cls, reg);
    155   if (d->native) native_loc_stamp_size(d->native, &loc);
    156   return loc;
    157 }
    158 
    159 static void nd_flush_local(NativeDirectTarget* d, CGLocal local);
    160 static Reg nd_cache_reg_for(NativeDirectTarget* d, CGLocal local,
    161                             KitCgTypeId access_type);
    162 static Reg nd_pick_cache_victim(NativeDirectTarget* d, NativeAllocClass cls);
    163 
    164 static u32 nd_callee_saved_mask(NativeDirectTarget* d, NativeAllocClass cls) {
    165   return native_target_callee_saved_mask(d->native, cls);
    166 }
    167 
    168 static u32 nd_caller_saved_mask(NativeDirectTarget* d, NativeAllocClass cls) {
    169   return native_target_caller_saved_mask(d->native, cls);
    170 }
    171 
    172 static void nd_note_reg_used(NativeDirectTarget* d, NativeAllocClass cls,
    173                              Reg reg) {
    174   if ((u32)cls >= NATIVE_REG_CLASS_COUNT || reg >= NATIVE_MAX_HARD_REGS) return;
    175   if (nd_callee_saved_mask(d, cls) & (1u << reg))
    176     d->callee_saved_used[cls] |= 1u << reg;
    177 }
    178 
    179 /* Mask of registers the single-pass path may hand out as scratch in `cls` — its
    180  * dedicated scratch list plus the allocable pool. A multi-register (wide) value's
    181  * whole register run must lie within this set so each lane is a real, writable
    182  * GPR the NDT owns: this is what keeps a wide value off a base whose high lane is
    183  * a reserved register (the arm32 trap, where the lone scratch ip(r12) would pair
    184  * its high lane onto r13/sp). On 64-bit targets every value is one register, so
    185  * the run is the base alone and this never narrows the choice. */
    186 static u32 nd_acquirable_mask(const NativeAllocClassInfo* ci) {
    187   u32 m = 0u, i;
    188   for (i = 0; i < ci->nscratch; ++i)
    189     if (ci->scratch[i] < 32u) m |= 1u << ci->scratch[i];
    190   for (i = 0; i < ci->ndt_allocable_count; ++i)
    191     if (ci->ndt_allocable[i] < 32u) m |= 1u << ci->ndt_allocable[i];
    192   return m;
    193 }
    194 
    195 /* A `span`-register value placed at base `r` is legal iff the high lanes
    196  * r+1..r+span-1 are all acquirable registers (never sp/pc/fp/lr). The base lane
    197  * itself is whatever the caller's scratch/allocable scan already vetted (it may
    198  * legitimately be a reserved-from-allocation scratch like ip). */
    199 static int nd_span_high_acquirable(u32 acq, Reg r, u32 span) {
    200   u32 k;
    201   for (k = 1u; k < span; ++k)
    202     if ((u32)(r + k) >= 32u || !(acq & (1u << (r + k)))) return 0;
    203   return 1;
    204 }
    205 
    206 /* Acquire a run of `span` consecutive registers (span==1 is the common case)
    207  * whose base is returned; the whole run is pinned. span>1 only arises for a wide
    208  * (8-byte) integer value on a 32-bit target, which occupies a register pair. */
    209 static Reg nd_scratch_acquire_span(NativeDirectTarget* d, NativeAllocClass cls,
    210                                    u32 span) {
    211   const NativeAllocClassInfo* ci = nd_class_info(d, cls);
    212   const Reg* regs = ci->scratch;
    213   u32 nregs = ci->nscratch;
    214   u32 acq = span > 1u ? nd_acquirable_mask(ci) : 0u;
    215   if (span < 1u) span = 1u;
    216   /* Prefer a register (run) that is neither pinned (scratch_used) nor caching a
    217    * live local (reg_owner). */
    218   for (u32 pass = 0; pass < 2u; ++pass) {
    219     for (u32 i = 0; i < nregs; ++i) {
    220       Reg r = regs[i];
    221       u32 k;
    222       int run_ok = 1;
    223       if (r >= 32u) continue;
    224       /* On targets whose register model declares ndt_caller_saved_only, the
    225        * single-pass (-O0) path never takes a callee-saved register as scratch
    226        * (the `allocable` pass would otherwise offer them under pressure);
    227        * pressure is absorbed by evicting a cached local instead. This keeps
    228        * callee_saved_used empty so the backend can reserve only a tiny
    229        * deferred-prologue region (see nd_func_end). Targets without a rich
    230        * enough caller-saved pool (x64, riscv today) leave it clear and keep the
    231        * historical callee-saved-as-scratch fallback. */
    232       if (d->reg_info && d->reg_info->ndt_caller_saved_only &&
    233           (nd_callee_saved_mask(d, cls) & (1u << r)))
    234         continue;
    235       if (span > 1u && !nd_span_high_acquirable(acq, r, span)) continue;
    236       /* The whole run must be free: none pinned, none caching a live local. */
    237       for (k = 0; k < span; ++k) {
    238         Reg h = r + k;
    239         if ((d->scratch_used[cls] & (1u << h)) != 0 ||
    240             d->reg_owner[cls][h] != CG_LOCAL_NONE) {
    241           run_ok = 0;
    242           break;
    243         }
    244       }
    245       if (!run_ok) continue;
    246       for (k = 0; k < span; ++k) {
    247         d->scratch_used[cls] |= 1u << (r + k);
    248         nd_note_reg_used(d, cls, r + k);
    249       }
    250       return r;
    251     }
    252     regs = ci->ndt_allocable;
    253     nregs = ci->ndt_allocable_count;
    254   }
    255   /* Under pressure, evict the LRU non-pinned cached local (spilling it to its
    256    * home) and reuse its register as a scratch temporary. A wide run evicts every
    257    * cached local across the run; it never crosses a pinned register. */
    258   if (span <= 1u) {
    259     Reg r = nd_pick_cache_victim(d, cls);
    260     if (r != REG_NONE) {
    261       nd_flush_local(d, d->reg_owner[cls][r]);
    262       d->scratch_used[cls] |= 1u << r;
    263       nd_note_reg_used(d, cls, r);
    264       return r;
    265     }
    266   } else {
    267     const Reg* alloc = ci->ndt_allocable;
    268     for (u32 i = 0; i < ci->ndt_allocable_count; ++i) {
    269       Reg r = alloc[i];
    270       u32 k;
    271       int run_ok = 1;
    272       if (r >= 32u || !nd_span_high_acquirable(acq, r, span)) continue;
    273       for (k = 0; k < span; ++k)
    274         if ((d->scratch_used[cls] & (1u << (r + k))) != 0) { run_ok = 0; break; }
    275       if (!run_ok) continue;
    276       for (k = 0; k < span; ++k)
    277         if (d->reg_owner[cls][r + k] != CG_LOCAL_NONE)
    278           nd_flush_local(d, d->reg_owner[cls][r + k]);
    279       for (k = 0; k < span; ++k) {
    280         d->scratch_used[cls] |= 1u << (r + k);
    281         nd_note_reg_used(d, cls, r + k);
    282       }
    283       return r;
    284     }
    285   }
    286   nd_panic(d, "out of scratch registers");
    287 }
    288 
    289 static Reg nd_scratch_acquire(NativeDirectTarget* d, NativeAllocClass cls) {
    290   return nd_scratch_acquire_span(d, cls, 1u);
    291 }
    292 
    293 /* Number of consecutive registers a value of `type` occupies in class `cls`. On
    294  * a 32-bit target an 8-byte integer (i64 / soft-double) is a register pair; a
    295  * value that fits the register width — and any value in a non-integer class,
    296  * which holds wide data in a single vector register — is one register. */
    297 static u32 nd_reg_span(NativeDirectTarget* d, NativeAllocClass cls,
    298                        KitCgTypeId type) {
    299   u32 ptr;
    300   u64 size;
    301   if (cls != NATIVE_REG_INT || type == 0) return 1u;
    302   ptr = (u32)d->base.c->target.ptr_size;
    303   size = cg_type_size(d->base.c, type);
    304   return (ptr && size > (u64)ptr) ? (u32)((size + ptr - 1u) / ptr) : 1u;
    305 }
    306 
    307 static void nd_scratch_release(NativeDirectTarget* d, NativeAllocClass cls,
    308                                Reg reg) {
    309   if (reg < 32u) d->scratch_used[cls] &= ~(1u << reg);
    310 }
    311 
    312 static NativeFrameSlot nd_alloc_frame_slot(NativeDirectTarget* d,
    313                                            const NativeFrameSlotDesc* desc) {
    314   NativeFrameSlot slot = NATIVE_FRAME_SLOT_NONE;
    315   if (d->native && d->native->frame_slot)
    316     slot = d->native->frame_slot(d->native, desc);
    317   else
    318     nd_panic(d, "target does not allocate frame slots");
    319   if (slot == NATIVE_FRAME_SLOT_NONE)
    320     nd_panic(d, "frame slot allocation failed");
    321   return slot;
    322 }
    323 
    324 /* Ensure LOCAL has a frame home, allocating one lazily on first demand.
    325  * Transient temps are created without a home (nd_alloc_local) so a value that
    326  * stays register-resident never reserves frame space; the slot is minted here
    327  * the first time the value must live in memory (a spill, or a reader that needs
    328  * the home address). The TRANSIENT flag wires the lazy slot into the same
    329  * free-list reuse as eager transient homes (native_frame_slot_alloc pops a free
    330  * bin first; nd_reclaim_temps recycles it at the next statement boundary).
    331  * Non-transient locals and address-taken/memory-required temps are homed
    332  * eagerly, so for them this is a pure pass-through. */
    333 static NativeFrameSlot nd_home(NativeDirectTarget* d, CGLocal local) {
    334   NativeDirectLocal* l = nd_local(d, local);
    335   if (l->home == NATIVE_FRAME_SLOT_NONE) {
    336     NativeFrameSlotDesc fsd = {.type = l->type,
    337                                .size = l->size,
    338                                .align = l->align,
    339                                .kind = NATIVE_FRAME_SLOT_LOCAL,
    340                                .flags = NATIVE_FRAME_SLOT_TRANSIENT};
    341     l->home = nd_alloc_frame_slot(d, &fsd);
    342   }
    343   return l->home;
    344 }
    345 
    346 /* Ensure the home(s) of every local an operand references are allocated before
    347  * handing the operand to arch code that reads `home` directly (va_arg / asm
    348  * helpers). A no-op for IMM/GLOBAL operands. */
    349 static void nd_home_operand(NativeDirectTarget* d, Operand op) {
    350   switch ((OpKind)op.kind) {
    351     case OPK_LOCAL:
    352       nd_home(d, op.v.local);
    353       break;
    354     case OPK_INDIRECT:
    355       nd_home(d, op.v.ind.base);
    356       if (op.v.ind.index != CG_LOCAL_NONE) nd_home(d, op.v.ind.index);
    357       break;
    358     default:
    359       break;
    360   }
    361 }
    362 
    363 static NativeFrameSlotDesc nd_slot_desc_local(const CGLocalDesc* in) {
    364   NativeFrameSlotDesc out = {.type = in->type,
    365                              .name = in->name,
    366                              .loc = in->loc,
    367                              .size = in->size,
    368                              .align = in->align,
    369                              .kind = NATIVE_FRAME_SLOT_LOCAL};
    370   if (in->flags & CG_LOCAL_ADDR_TAKEN)
    371     out.flags |= NATIVE_FRAME_SLOT_ADDR_TAKEN;
    372   if (in->flags & CG_LOCAL_MEMORY_REQUIRED)
    373     out.flags |= NATIVE_FRAME_SLOT_MEMORY_REQUIRED;
    374   if (in->flags & CG_LOCAL_TRANSIENT)
    375     out.flags |= NATIVE_FRAME_SLOT_TRANSIENT;
    376   return out;
    377 }
    378 
    379 static NativeFrameSlotDesc nd_slot_desc_param(const CGParamDesc* in) {
    380   NativeFrameSlotDesc out = {.type = in->type,
    381                              .name = in->name,
    382                              .loc = in->loc,
    383                              .size = in->size,
    384                              .align = in->align,
    385                              .kind = NATIVE_FRAME_SLOT_PARAM};
    386   if (in->flags & CG_LOCAL_ADDR_TAKEN)
    387     out.flags |= NATIVE_FRAME_SLOT_ADDR_TAKEN;
    388   if (in->flags & CG_LOCAL_MEMORY_REQUIRED)
    389     out.flags |= NATIVE_FRAME_SLOT_MEMORY_REQUIRED;
    390   return out;
    391 }
    392 
    393 static CGLocal nd_alloc_local(NativeDirectTarget* d, const CGLocalDesc* desc) {
    394   NativeDirectLocal* l;
    395   NativeFrameSlotDesc fsd;
    396   CGLocal id;
    397   nd_grow_locals(d, d->nlocals + 1u);
    398   id = d->nlocals + 1u;
    399   l = &d->locals[d->nlocals++];
    400   memset(l, 0, sizeof *l);
    401   l->type = desc->type;
    402   l->size = desc->size;
    403   l->align = desc->align;
    404   l->flags = desc->flags;
    405   l->reg = REG_NONE;
    406   l->address_taken = (desc->flags & CG_LOCAL_ADDR_TAKEN) != 0;
    407   l->memory_required = (desc->flags & CG_LOCAL_MEMORY_REQUIRED) != 0;
    408   l->transient = (desc->flags & CG_LOCAL_TRANSIENT) != 0;
    409   l->cls = (u8)nd_class_for_type(d, desc->type);
    410   if (l->transient && !l->address_taken && !l->memory_required) {
    411     /* Lazy home: a plain transient temp reserves no frame slot until it must
    412      * actually live in memory (nd_home, on first spill / home read). Address-
    413      * taken and memory-required (wide8) temps need a stable home, so they stay
    414      * eager. */
    415     l->home = NATIVE_FRAME_SLOT_NONE;
    416   } else {
    417     fsd = nd_slot_desc_local(desc);
    418     l->home = nd_alloc_frame_slot(d, &fsd);
    419   }
    420   if (l->transient) {
    421     nd_grow_transient(d, d->ntransient + 1u);
    422     d->transient_locals[d->ntransient++] = id;
    423   }
    424   return id;
    425 }
    426 
    427 static MCLabel nd_mc_label(NativeDirectTarget* d, Label label) {
    428   if (label == LABEL_NONE || label > d->nlabels || !d->labels[label])
    429     nd_panic(d, "bad label");
    430   return d->labels[label];
    431 }
    432 
    433 static Label nd_label_new_raw(NativeDirectTarget* d) {
    434   Label id;
    435   if (!d->native || !d->native->label_new)
    436     nd_panic(d, "target does not allocate labels");
    437   id = d->nlabels + 1u;
    438   nd_grow_labels(d, id + 1u);
    439   d->labels[id] = d->native->label_new(d->native);
    440   d->nlabels = id;
    441   return id;
    442 }
    443 
    444 /* Designated compound literals: value-identical to the former memset + field
    445  * stores (unnamed fields zero-fill), but the compiler emits only the live
    446  * stores rather than an out-of-line memset of the ~64-byte descriptor on this
    447  * per-operand path. */
    448 static NativeLoc nd_loc_frame(NativeDirectTarget* d, CGLocal local,
    449                               KitCgTypeId type) {
    450   NativeDirectLocal* l = nd_local(d, local);
    451   return (NativeLoc){.kind = NATIVE_LOC_FRAME,
    452                      .cls = l->cls,
    453                      .type = type ? type : l->type,
    454                      .v.frame = nd_home(d, local)};
    455 }
    456 
    457 static NativeLoc nd_loc_imm(i64 imm, KitCgTypeId type) {
    458   return (NativeLoc){.kind = NATIVE_LOC_IMM, .type = type, .v.imm = imm};
    459 }
    460 
    461 static NativeLoc nd_loc_global(ObjSymId sym, i64 addend, KitCgTypeId type) {
    462   return (NativeLoc){.kind = NATIVE_LOC_GLOBAL,
    463                      .type = type,
    464                      .v.global = {.sym = sym, .addend = addend}};
    465 }
    466 
    467 static NativeLoc nd_loc_operand(NativeDirectTarget* d, Operand op) {
    468   switch ((OpKind)op.kind) {
    469     case OPK_IMM:
    470       return nd_loc_imm(op.v.imm, op.type);
    471     case OPK_LOCAL:
    472       return nd_loc_frame(d, op.v.local, op.type);
    473     case OPK_GLOBAL:
    474       return nd_loc_global(op.v.global.sym, op.v.global.addend, op.type);
    475     case OPK_INDIRECT: {
    476       NativeDirectLocal* bl = nd_local(d, op.v.ind.base);
    477       NativeLoc out = (NativeLoc){
    478           .kind = NATIVE_LOC_ADDR,
    479           .type = op.type,
    480           .v.addr = {.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE,
    481                      .base.frame = nd_home(d, op.v.ind.base),
    482                      .cls = bl->cls,
    483                      .base_type = bl->type,
    484                      .log2_scale = op.v.ind.log2_scale,
    485                      .offset = op.v.ind.ofs}};
    486       if (op.v.ind.index != CG_LOCAL_NONE) {
    487         NativeDirectLocal* il = nd_local(d, op.v.ind.index);
    488         out.v.addr.index_kind = NATIVE_ADDR_INDEX_FRAME_VALUE;
    489         out.v.addr.index.frame = nd_home(d, op.v.ind.index);
    490         out.v.addr.index_cls = il->cls;
    491         out.v.addr.index_type = il->type;
    492       }
    493       return out;
    494     }
    495     default:
    496       nd_panic(d, "bad operand kind");
    497   }
    498 }
    499 
    500 static NativeAddr nd_addr_storage(NativeDirectTarget* d, Operand op) {
    501   switch ((OpKind)op.kind) {
    502     case OPK_LOCAL: {
    503       /* The local's home is addressed directly (a memory access reads/writes
    504        * the frame slot itself, e.g. by-value aggregate field extraction). This
    505        * is not pointer aliasing, but it does read the home, so a cached value
    506        * must be made current: spill if dirty and drop the entry. */
    507       NativeDirectLocal* l;
    508       nd_flush_local(d, op.v.local);
    509       l = nd_local(d, op.v.local);
    510       return (NativeAddr){.base_kind = NATIVE_ADDR_BASE_FRAME,
    511                           .base.frame = nd_home(d, op.v.local),
    512                           .cls = l->cls,
    513                           .base_type = l->type};
    514     }
    515     case OPK_GLOBAL:
    516       return (NativeAddr){
    517           .base_kind = NATIVE_ADDR_BASE_GLOBAL,
    518           .base.global = {.sym = op.v.global.sym, .addend = op.v.global.addend},
    519           .base_type = op.type};
    520     case OPK_INDIRECT: {
    521       NativeDirectLocal* bl = nd_local(d, op.v.ind.base);
    522       Reg br = nd_cache_reg_for(d, op.v.ind.base, bl->type);
    523       NativeAddr out = (NativeAddr){.cls = bl->cls,
    524                                     .base_type = bl->type,
    525                                     .log2_scale = op.v.ind.log2_scale,
    526                                     .offset = op.v.ind.ofs};
    527       if (br != REG_NONE) {
    528         out.base_kind = NATIVE_ADDR_BASE_REG;
    529         out.base.reg = br;
    530         d->scratch_used[bl->cls] |= 1u
    531                                     << br; /* pin; unpinned at temps release */
    532       } else {
    533         out.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
    534         out.base.frame = nd_home(d, op.v.ind.base);
    535       }
    536       if (op.v.ind.index != CG_LOCAL_NONE) {
    537         NativeDirectLocal* il = nd_local(d, op.v.ind.index);
    538         Reg ir = nd_cache_reg_for(d, op.v.ind.index, il->type);
    539         out.index_cls = il->cls;
    540         out.index_type = il->type;
    541         if (ir != REG_NONE) {
    542           out.index_kind = NATIVE_ADDR_INDEX_REG;
    543           out.index.reg = ir;
    544           d->scratch_used[il->cls] |= 1u << ir;
    545         } else {
    546           out.index_kind = NATIVE_ADDR_INDEX_FRAME_VALUE;
    547           out.index.frame = nd_home(d, op.v.ind.index);
    548         }
    549       }
    550       return out;
    551     }
    552     default:
    553       nd_panic(d, "operand is not addressable storage");
    554   }
    555 }
    556 
    557 static NativeAddr nd_addr_pointer(NativeDirectTarget* d, Operand op) {
    558   switch ((OpKind)op.kind) {
    559     case OPK_LOCAL: {
    560       NativeDirectLocal* l = nd_local(d, op.v.local);
    561       NativeAddr out = (NativeAddr){.cls = l->cls, .base_type = l->type};
    562       if (cg_type_is_ptr(d->base.c, op.type)) {
    563         /* Pointer value lives in the local: use its live register if cached
    564          * (a dirty cached pointer is a valid base), else load from the home. */
    565         Reg r = nd_cache_reg_for(d, op.v.local, l->type);
    566         if (r != REG_NONE) {
    567           out.base_kind = NATIVE_ADDR_BASE_REG;
    568           out.base.reg = r;
    569           d->scratch_used[l->cls] |= 1u << r;
    570         } else {
    571           out.base_kind = NATIVE_ADDR_BASE_FRAME_VALUE;
    572           out.base.frame = nd_home(d, op.v.local);
    573         }
    574       } else {
    575         /* The local's home is addressed directly; make it current first (see
    576          * nd_addr_storage OPK_LOCAL). */
    577         nd_flush_local(d, op.v.local);
    578         out.base_kind = NATIVE_ADDR_BASE_FRAME;
    579         out.base.frame = nd_home(d, op.v.local);
    580       }
    581       return out;
    582     }
    583     case OPK_GLOBAL:
    584       return (NativeAddr){
    585           .base_kind = NATIVE_ADDR_BASE_GLOBAL,
    586           .base.global = {.sym = op.v.global.sym, .addend = op.v.global.addend},
    587           .base_type = op.type};
    588     case OPK_INDIRECT:
    589       return nd_addr_storage(d, op);
    590     default:
    591       nd_panic(d, "operand is not a pointer address");
    592   }
    593 }
    594 
    595 #define ND_REQUIRE_NATIVE(d, member, name)                           \
    596   do {                                                               \
    597     if (!(d)->native || !(d)->native->member) nd_panic((d), (name)); \
    598   } while (0)
    599 
    600 typedef struct NdAddrTemps {
    601   Reg base;
    602   Reg index;
    603   NativeAllocClass base_cls;
    604   NativeAllocClass index_cls;
    605 } NdAddrTemps;
    606 
    607 static void nd_addr_temps_release(NativeDirectTarget* d,
    608                                   const NdAddrTemps* temps);
    609 
    610 static MemAccess nd_scalar_mem(KitCgTypeId type, u32 size, u32 align) {
    611   return (MemAccess){.type = type, .size = size, .align = align};
    612 }
    613 
    614 static MemAccess nd_type_mem(NativeDirectTarget* d, KitCgTypeId type) {
    615   u64 size;
    616   if (!type) type = builtin_id(KIT_CG_BUILTIN_I64);
    617   size = cg_type_size(d->base.c, type);
    618   if (size > 0xffffffffu) nd_panic(d, "scalar type is too large");
    619   return nd_scalar_mem(type, (u32)size, cg_type_align(d->base.c, type));
    620 }
    621 
    622 static void nd_barrier(NativeDirectTarget* d, u32 flags) {
    623   if (d->ops && d->ops->barrier) d->ops->barrier(d, flags);
    624 }
    625 
    626 static void nd_load_frame_to_reg(NativeDirectTarget* d, NativeLoc dst,
    627                                  NativeFrameSlot frame, KitCgTypeId type) {
    628   NativeAddr addr = {.base_kind = NATIVE_ADDR_BASE_FRAME,
    629                      .base.frame = frame,
    630                      .base_type = type};
    631   MemAccess mem = nd_type_mem(d, type);
    632   ND_REQUIRE_NATIVE(d, load, "target does not emit loads");
    633   d->native->load(d->native, dst, addr, mem);
    634 }
    635 
    636 static void nd_store_reg_to_frame(NativeDirectTarget* d, NativeFrameSlot frame,
    637                                   KitCgTypeId type, NativeLoc src) {
    638   NativeAddr addr = {.base_kind = NATIVE_ADDR_BASE_FRAME,
    639                      .base.frame = frame,
    640                      .base_type = type};
    641   MemAccess mem = nd_type_mem(d, type);
    642   ND_REQUIRE_NATIVE(d, store, "target does not emit stores");
    643   d->native->store(d->native, addr, src, mem);
    644 }
    645 
    646 static void nd_copy_to_reg(NativeDirectTarget* d, NativeLoc dst, NativeLoc src);
    647 static void nd_release_materialized(NativeDirectTarget* d, NativeLoc loc);
    648 static void nd_store_operand_from_reg(NativeDirectTarget* d, Operand dst,
    649                                       NativeLoc src);
    650 
    651 /* --- Local register cache (write-back, basic-block-scoped) ---------------- *
    652  * Only scalar, non-address-taken locals are cached, and only in caller-saved
    653  * allocable registers. Entries are created solely by pure-compute destinations
    654  * (nd_dst_reg/nd_dst_writeback) and are always dirty; reads hit a live entry or
    655  * fall back to a frame load without creating one. nd_flush_all spills and
    656  * empties the cache at the top of every non-pure-compute op, so the cache only
    657  * survives across straight-line runs of compute ops. Caching prefers the
    658  * register-file caller-saved mask; if the live OS ABI treats one of those
    659  * registers as callee-saved, nd_note_reg_used reports it to the backend before
    660  * the deferred prologue is patched. */
    661 
    662 static int nd_local_cacheable(NativeDirectTarget* d,
    663                               const NativeDirectLocal* l) {
    664   return !l->address_taken && !l->memory_required && l->size != 0 &&
    665          !cg_type_is_aggregate(d->base.c, l->type) &&
    666          l->size <= (u32)d->base.c->target.ptr_size;
    667 }
    668 
    669 /* If LOCAL is currently cached and the access reads it at its cached (storage)
    670  * width, return its live register; else REG_NONE. Used by the address builders
    671  * to point an address at a base/index local's live register instead of reading
    672  * a possibly-stale frame home. Base/index reads are always of the local's own
    673  * type, so the width check is trivially met for that use; the value-read width
    674  * hazard is handled separately in nd_materialize_operand. */
    675 /* Stamp a cache touch (def/read/addressing use) for LRU victim selection. Every
    676  * caller holds l->reg set (l is currently cached), so the flat reg_last_use
    677  * mirror the victim scan reads stays in lockstep with l->last_use. */
    678 static void nd_touch_local(NativeDirectTarget* d, NativeDirectLocal* l) {
    679   l->last_use = ++d->use_tick;
    680   d->reg_last_use[l->cls][l->reg] = l->last_use;
    681 }
    682 
    683 static Reg nd_cache_reg_for(NativeDirectTarget* d, CGLocal local,
    684                             KitCgTypeId access_type) {
    685   NativeDirectLocal* l = nd_local(d, local);
    686   if (l->reg == REG_NONE) return REG_NONE;
    687   if (!nd_local_cacheable(d, l)) return REG_NONE;
    688   if (access_type && access_type != l->type) return REG_NONE;
    689   nd_touch_local(d, l);
    690   return l->reg;
    691 }
    692 
    693 /* Pick the least-recently-used non-pinned cached local in CLS as a spill victim
    694  * (its register can then be reused). REG_NONE if every owned reg is pinned.
    695  * Pressure is real in Design B (the cache survives across memory ops), so an
    696  * arbitrary victim would thrash a hot local; LRU keeps the live set resident.
    697  */
    698 static Reg nd_pick_cache_victim(NativeDirectTarget* d, NativeAllocClass cls) {
    699   const NativeAllocClassInfo* ci = nd_class_info(d, cls);
    700   Reg best = REG_NONE;
    701   u32 best_use = 0;
    702   for (u32 i = 0; i < ci->ndt_allocable_count; ++i) {
    703     Reg r = ci->ndt_allocable[i];
    704     u32 use;
    705     if (r >= 32u) continue;
    706     if (d->reg_owner[cls][r] == CG_LOCAL_NONE) continue;
    707     if (d->scratch_used[cls] & (1u << r)) continue; /* pinned: never a victim */
    708     /* Flat mirror: equals nd_local(owner)->last_use for the owning local. */
    709     use = d->reg_last_use[cls][r];
    710     if (best == REG_NONE || use < best_use) {
    711       best = r;
    712       best_use = use;
    713     }
    714   }
    715   return best;
    716 }
    717 
    718 /* Pick an allocable register to cache a local in: a free one from the cache
    719  * pool, else evict the LRU non-pinned cached local. REG_NONE means use the
    720  * frame-only path. */
    721 static Reg nd_cache_alloc(NativeDirectTarget* d, NativeAllocClass cls) {
    722   const Reg* pool = d->cache_pool[cls];
    723   u32 npool = d->ncache_pool[cls];
    724   Reg victim;
    725   /* Scan the precomputed cache pool (allocable order preserved; on
    726    * ndt_caller_saved_only targets it is the caller-saved subset). */
    727   for (u32 i = 0; i < npool; ++i) {
    728     Reg r = pool[i];
    729     if (d->reg_owner[cls][r] == CG_LOCAL_NONE &&
    730         (d->scratch_used[cls] & (1u << r)) == 0) {
    731       nd_note_reg_used(d, cls, r);
    732       return r;
    733     }
    734   }
    735   /* Under pressure, evict the LRU non-pinned cached local and reuse its
    736    * register. Every cached local was allocated from cache_pool, so the victim
    737    * is always a valid pool register — no caller/callee guard needed. */
    738   victim = nd_pick_cache_victim(d, cls);
    739   if (victim != REG_NONE) {
    740     nd_flush_local(d, d->reg_owner[cls][victim]);
    741     nd_note_reg_used(d, cls, victim);
    742     return victim;
    743   }
    744   return REG_NONE;
    745 }
    746 
    747 /* Append LOCAL to the tail of the cached-locals list (O(1)). Called only on the
    748  * REG_NONE -> reg transition in nd_dst_reg. */
    749 static void nd_cache_link(NativeDirectTarget* d, CGLocal local) {
    750   i32 idx = (i32)(local - 1u);
    751   i32 prev = d->cache_tail;
    752   d->locals[idx].cache_next = -1;
    753   d->locals[idx].cache_prev = prev;
    754   if (prev >= 0)
    755     d->locals[prev].cache_next = idx;
    756   else
    757     d->cache_head = idx;
    758   d->cache_tail = idx;
    759   d->ncached++;
    760 }
    761 
    762 /* Remove LOCAL (which must currently be cached) from the cached-locals list. */
    763 static void nd_cache_unlink(NativeDirectTarget* d, CGLocal local) {
    764   i32 idx = (i32)(local - 1u);
    765   i32 next = d->locals[idx].cache_next;
    766   i32 prev = d->locals[idx].cache_prev;
    767   if (next >= 0)
    768     d->locals[next].cache_prev = prev;
    769   else
    770     d->cache_tail = prev;
    771   if (prev >= 0)
    772     d->locals[prev].cache_next = next;
    773   else
    774     d->cache_head = next;
    775   d->ncached--;
    776 }
    777 
    778 /* Write a cached local back to its home (if dirty) and drop the entry. Safe to
    779  * call on an uncached local. */
    780 static void nd_flush_local(NativeDirectTarget* d, CGLocal local) {
    781   NativeDirectLocal* l = nd_local(d, local);
    782   if (l->reg == REG_NONE) return;
    783   if (l->dirty)
    784     nd_store_reg_to_frame(
    785         d, nd_home(d, local), l->type,
    786         nd_loc_reg(d, l->type, (NativeAllocClass)l->cls, l->reg));
    787   nd_cache_unlink(d, local);
    788   d->reg_owner[l->cls][l->reg] = CG_LOCAL_NONE;
    789   l->reg = REG_NONE;
    790   l->dirty = 0;
    791   l->load_zext = 0;
    792   l->load_sext = 0;
    793 }
    794 
    795 /* Drop a cache entry without writing it back, for when a store supersedes the
    796  * cached value. */
    797 static void nd_invalidate_local(NativeDirectTarget* d, CGLocal local) {
    798   NativeDirectLocal* l = nd_local(d, local);
    799   if (l->reg == REG_NONE) return;
    800   nd_cache_unlink(d, local);
    801   d->reg_owner[l->cls][l->reg] = CG_LOCAL_NONE;
    802   l->reg = REG_NONE;
    803   l->dirty = 0;
    804   l->load_zext = 0;
    805   l->load_sext = 0;
    806 }
    807 
    808 /* Spill the whole cache to memory and empty it. The list is sorted ascending,
    809  * so this spills in the same order as the former O(nlocals) index scan. */
    810 static void nd_flush_all(NativeDirectTarget* d) {
    811   while (d->cache_head >= 0) nd_flush_local(d, (CGLocal)(d->cache_head + 1));
    812 }
    813 
    814 /* Discard the whole cache WITHOUT writing entries back. Only sound where every
    815  * cached value is provably dead — at a return the frame is about to be
    816  * destroyed, so no home is ever read again. Lets nd_ret source the return value
    817  * from its live register and skip the round-trip + the dead writebacks a
    818  * flush-all would emit. */
    819 static void nd_drop_all(NativeDirectTarget* d) {
    820   while (d->cache_head >= 0)
    821     nd_invalidate_local(d, (CGLocal)(d->cache_head + 1));
    822 }
    823 
    824 /* A "kept" argument is one nd_call leaves register-resident across the call: it
    825  * is named in desc->args and flagged dead in arg_dead_mask (so dropping it after
    826  * the call loses nothing). A live or shared arg is not kept and is spilled. */
    827 static int nd_call_arg_kept(const CGCallDesc* desc, CGLocal local) {
    828   for (u32 i = 0; i < desc->nargs; ++i)
    829     if (desc->args[i] == local)
    830       return i < 64u && ((desc->arg_dead_mask >> i) & 1u);
    831   return 0;
    832 }
    833 
    834 /* Spill every cached local EXCEPT the kept (dead) argument locals to its home
    835  * and empty those entries. Kept args stay cached so the marshalling can source
    836  * them from their live registers; everything else is potentially live across
    837  * the call and, on the -O0 caller-saved-only cache, sits in a call-clobbered
    838  * register, so it must reach memory now. */
    839 static void nd_flush_all_except_kept_args(NativeDirectTarget* d,
    840                                           const CGCallDesc* desc) {
    841   i32 idx = d->cache_head;
    842   while (idx >= 0) {
    843     i32 next = d->locals[idx].cache_next; /* capture before flush unlinks idx */
    844     CGLocal local = (CGLocal)(idx + 1);
    845     if (!nd_call_arg_kept(desc, local)) nd_flush_local(d, local);
    846     idx = next;
    847   }
    848 }
    849 
    850 static NativeAddr nd_addr_materialize(NativeDirectTarget* d, NativeAddr in,
    851                                       NdAddrTemps* temps, MemAccess mem) {
    852   NativeAddr out = in;
    853   memset(temps, 0, sizeof *temps);
    854   temps->base = REG_NONE;
    855   temps->index = REG_NONE;
    856   /* A base/index that arrives already in a register is a pinned live cache reg
    857    * (the addr builders are the only producers of REG-kind storage addresses).
    858    * Record it so the temps release unpins it afterward — without storing or
    859    * invalidating, leaving the cache entry intact. */
    860   if (out.base_kind == NATIVE_ADDR_BASE_REG) {
    861     temps->base = out.base.reg;
    862     temps->base_cls = (NativeAllocClass)out.cls;
    863   }
    864   if (out.index_kind == NATIVE_ADDR_INDEX_REG) {
    865     temps->index = out.index.reg;
    866     temps->index_cls = (NativeAllocClass)out.index_cls;
    867   }
    868   if (out.base_kind == NATIVE_ADDR_BASE_FRAME_VALUE) {
    869     NativeAllocClass cls = (NativeAllocClass)out.cls;
    870     Reg r = nd_scratch_acquire(d, cls);
    871     NativeLoc dst = nd_loc_reg(d, out.base_type, cls, r);
    872     nd_load_frame_to_reg(d, dst, out.base.frame, out.base_type);
    873     out.base_kind = NATIVE_ADDR_BASE_REG;
    874     out.base.reg = r;
    875     temps->base = r;
    876     temps->base_cls = cls;
    877   }
    878   if (out.index_kind == NATIVE_ADDR_INDEX_FRAME_VALUE) {
    879     NativeAllocClass cls = (NativeAllocClass)out.index_cls;
    880     Reg r = nd_scratch_acquire(d, cls);
    881     NativeLoc dst = nd_loc_reg(d, out.index_type, cls, r);
    882     nd_load_frame_to_reg(d, dst, out.index.frame, out.index_type);
    883     out.index_kind = NATIVE_ADDR_INDEX_REG;
    884     out.index.reg = r;
    885     temps->index = r;
    886     temps->index_cls = cls;
    887   }
    888   if (d->native && d->native->addr_legal &&
    889       !d->native->addr_legal(d->native, &out, mem)) {
    890     NativeAllocClass cls = NATIVE_REG_INT;
    891     Reg r = nd_scratch_acquire(d, cls);
    892     NativeLoc dst = nd_loc_reg(
    893         d, out.base_type ? out.base_type : builtin_id(KIT_CG_BUILTIN_I64), cls,
    894         r);
    895     ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
    896     d->native->load_addr(d->native, dst, out);
    897     nd_addr_temps_release(d, temps);
    898     memset(temps, 0, sizeof *temps);
    899     temps->base = r;
    900     temps->index = REG_NONE;
    901     temps->base_cls = cls;
    902     out = (NativeAddr){.base_kind = NATIVE_ADDR_BASE_REG,
    903                        .base.reg = r,
    904                        .cls = (u8)cls,
    905                        .base_type = dst.type};
    906     if (d->native && d->native->addr_legal &&
    907         !d->native->addr_legal(d->native, &out, mem))
    908       nd_panic(d, "native address is not legal");
    909   }
    910   return out;
    911 }
    912 
    913 static void nd_addr_temps_release(NativeDirectTarget* d,
    914                                   const NdAddrTemps* temps) {
    915   if (temps->base != REG_NONE)
    916     nd_scratch_release(d, temps->base_cls, temps->base);
    917   if (temps->index != REG_NONE)
    918     nd_scratch_release(d, temps->index_cls, temps->index);
    919 }
    920 
    921 static NativeLoc nd_materialize_loc(NativeDirectTarget* d, NativeLoc src,
    922                                     NativeAllocClass cls, KitCgTypeId type) {
    923   Reg r;
    924   NativeLoc dst;
    925   KitCgTypeId dt;
    926   if (src.kind == NATIVE_LOC_REG) return src;
    927   dt = type ? type : src.type;
    928   r = nd_scratch_acquire_span(d, cls, nd_reg_span(d, cls, dt));
    929   dst = nd_loc_reg(d, dt, cls, r);
    930   nd_copy_to_reg(d, dst, src);
    931   return dst;
    932 }
    933 
    934 static void nd_copy_to_reg(NativeDirectTarget* d, NativeLoc dst,
    935                            NativeLoc src) {
    936   if (dst.kind != NATIVE_LOC_REG) nd_panic(d, "copy destination is not a reg");
    937   switch ((NativeLocKind)src.kind) {
    938     case NATIVE_LOC_REG:
    939       if (src.v.reg != dst.v.reg || src.cls != dst.cls) {
    940         if (d->native->move_rr)
    941           d->native->move_rr(d->native, native_reg_loc_of(dst),
    942                              native_reg_loc_of(src));
    943         else {
    944           ND_REQUIRE_NATIVE(d, move, "target does not emit register moves");
    945           d->native->move(d->native, dst, src);
    946         }
    947       }
    948       break;
    949     case NATIVE_LOC_FRAME:
    950       nd_load_frame_to_reg(d, dst, src.v.frame, dst.type);
    951       break;
    952     case NATIVE_LOC_STACK: {
    953       MemAccess mem = nd_type_mem(d, dst.type);
    954       NativeAddr addr = {.base_kind = NATIVE_ADDR_BASE_FRAME,
    955                          .base.frame = src.v.stack.slot,
    956                          .base_type = dst.type,
    957                          .offset = src.v.stack.offset};
    958       ND_REQUIRE_NATIVE(d, load, "target does not emit loads");
    959       d->native->load(d->native, dst, addr, mem);
    960       break;
    961     }
    962     case NATIVE_LOC_IMM:
    963       ND_REQUIRE_NATIVE(d, load_imm, "target does not emit immediates");
    964       d->native->load_imm(d->native, dst, src.v.imm);
    965       break;
    966     case NATIVE_LOC_GLOBAL: {
    967       NativeAddr addr = {.base_kind = NATIVE_ADDR_BASE_GLOBAL,
    968                          .base.global = {.sym = src.v.global.sym,
    969                                          .addend = src.v.global.addend},
    970                          .base_type = dst.type};
    971       ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
    972       d->native->load_addr(d->native, dst, addr);
    973       break;
    974     }
    975     case NATIVE_LOC_ADDR: {
    976       NdAddrTemps temps;
    977       MemAccess mem = nd_scalar_mem(dst.type, d->base.c->target.ptr_size,
    978                                     d->base.c->target.ptr_align);
    979       NativeAddr addr = nd_addr_materialize(d, src.v.addr, &temps, mem);
    980       ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
    981       d->native->load_addr(d->native, dst, addr);
    982       nd_addr_temps_release(d, &temps);
    983       break;
    984     }
    985     default:
    986       nd_panic(d, "cannot materialize native location");
    987   }
    988 }
    989 
    990 static void nd_write_loc(NativeDirectTarget* d, NativeLoc dst, NativeLoc src,
    991                          MemAccess mem) {
    992   switch ((NativeLocKind)dst.kind) {
    993     case NATIVE_LOC_REG:
    994       nd_copy_to_reg(d, dst, src);
    995       break;
    996     case NATIVE_LOC_FRAME: {
    997       NativeLoc val =
    998           nd_materialize_loc(d, src, (NativeAllocClass)dst.cls, dst.type);
    999       nd_store_reg_to_frame(d, dst.v.frame, dst.type, val);
   1000       nd_release_materialized(d, val);
   1001       break;
   1002     }
   1003     case NATIVE_LOC_STACK: {
   1004       NativeLoc val =
   1005           nd_materialize_loc(d, src, (NativeAllocClass)dst.cls, dst.type);
   1006       NativeAddr addr = {.base_kind = NATIVE_ADDR_BASE_FRAME,
   1007                          .base.frame = dst.v.stack.slot,
   1008                          .base_type = dst.type,
   1009                          .offset = dst.v.stack.offset};
   1010       ND_REQUIRE_NATIVE(d, store, "target does not emit stores");
   1011       d->native->store(d->native, addr, val, mem);
   1012       nd_release_materialized(d, val);
   1013       break;
   1014     }
   1015     case NATIVE_LOC_ADDR: {
   1016       NdAddrTemps temps;
   1017       NativeAddr addr = nd_addr_materialize(d, dst.v.addr, &temps, mem);
   1018       NativeAllocClass cls = nd_class_for_type(d, src.type);
   1019       NativeLoc val = nd_materialize_loc(d, src, cls, src.type);
   1020       ND_REQUIRE_NATIVE(d, store, "target does not emit stores");
   1021       d->native->store(d->native, addr, val, mem);
   1022       nd_release_materialized(d, val);
   1023       nd_addr_temps_release(d, &temps);
   1024       break;
   1025     }
   1026     default:
   1027       nd_panic(d, "unsupported write destination");
   1028   }
   1029 }
   1030 
   1031 static void nd_release_materialized(NativeDirectTarget* d, NativeLoc loc) {
   1032   if (loc.kind == NATIVE_LOC_REG) {
   1033     u32 span = nd_reg_span(d, (NativeAllocClass)loc.cls, loc.type), k;
   1034     for (k = 0; k < span; ++k)
   1035       nd_scratch_release(d, (NativeAllocClass)loc.cls, (Reg)(loc.v.reg + k));
   1036   }
   1037 }
   1038 
   1039 /* Spill cached locals that back an INDIRECT operand's address before it is read
   1040  * from their frame homes. Compute ops normally receive only LOCAL/IMM/GLOBAL
   1041  * operands; this keeps the rare INDIRECT case correct without flushing all. */
   1042 static void nd_flush_operand_addr_locals(NativeDirectTarget* d, Operand op) {
   1043   if (op.kind != OPK_INDIRECT) return;
   1044   nd_flush_local(d, op.v.ind.base);
   1045   if (op.v.ind.index != CG_LOCAL_NONE) nd_flush_local(d, op.v.ind.index);
   1046 }
   1047 
   1048 /* Materialize OP's value into a register and return it — the tcc gv(rc) analog:
   1049  * "generalize this value into a register, doing nothing if it is already there."
   1050  *
   1051  *   want == NULL : ANY register of OP's class (gv with a generic class). A cache
   1052  *                  hit returns the live register pinned, with no code; otherwise
   1053  *                  a fresh scratch is acquired and OP loaded into it. The caller
   1054  *                  releases the pin with nd_release_materialized.
   1055  *   want != NULL : that SPECIFIC, already-acquired+pinned register (gv into a
   1056  *                  fixed reg). A cache-resident OP costs the one unavoidable
   1057  *                  reg-reg move; a memory / immediate / global / indirect OP is
   1058  *                  loaded straight into *want (no scratch round-trip). *want
   1059  *                  carries the destination type/width the copy uses, which may
   1060  *                  differ from OP's (e.g. a narrowing copy), so it — not OP —
   1061  *                  drives nd_copy_to_reg. Returns *want.
   1062  *
   1063  * Both modes share one residence resolution (cache hit, or width-mismatch flush
   1064  * then memory), so there is a single soundness argument for getting a value into
   1065  * a register at -O0. */
   1066 static NativeLoc nd_gv(NativeDirectTarget* d, Operand op, const NativeLoc* want) {
   1067   NativeAllocClass cls = nd_class_for_type(d, op.type);
   1068   if (op.kind == OPK_LOCAL) {
   1069     NativeDirectLocal* l = nd_local(d, op.v.local);
   1070     if (l->reg != REG_NONE && op.type == l->type && nd_local_cacheable(d, l)) {
   1071       nd_touch_local(d, l);
   1072       if (!want) {
   1073         /* Cache hit: pin and reuse the live register, no reload. */
   1074         d->scratch_used[l->cls] |= 1u << l->reg;
   1075         return nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg);
   1076       }
   1077       nd_copy_to_reg(d, *want,
   1078                      nd_loc_reg(d, op.type, (NativeAllocClass)l->cls, l->reg));
   1079       return *want;
   1080     }
   1081     /* A live entry under a different access width must reach memory before we
   1082      * bypass the cache for this access. */
   1083     if (l->reg != REG_NONE) nd_flush_local(d, op.v.local);
   1084   }
   1085   nd_flush_operand_addr_locals(d, op);
   1086   if (!want)
   1087     return nd_materialize_loc(d, nd_loc_operand(d, op), cls, op.type);
   1088   nd_copy_to_reg(d, *want, nd_loc_operand(d, op));
   1089   return *want;
   1090 }
   1091 
   1092 static NativeLoc nd_materialize_operand(NativeDirectTarget* d, Operand op) {
   1093   return nd_gv(d, op, NULL);
   1094 }
   1095 
   1096 /* Materialize OP straight into the already-acquired register WANT (gv into a
   1097  * fixed reg), skipping the scratch round-trip + separate move a memory /
   1098  * immediate / global / indirect source would otherwise make. WANT must already
   1099  * be pinned by the caller. */
   1100 static void nd_materialize_operand_into(NativeDirectTarget* d, NativeLoc want,
   1101                                         Operand op) {
   1102   nd_gv(d, op, &want);
   1103 }
   1104 
   1105 static NativeLoc nd_dst_scratch(NativeDirectTarget* d, Operand dst) {
   1106   NativeAllocClass cls = nd_class_for_type(d, dst.type);
   1107   Reg r = nd_scratch_acquire_span(d, cls, nd_reg_span(d, cls, dst.type));
   1108   return nd_loc_reg(d, dst.type, cls, r);
   1109 }
   1110 
   1111 /* Arithmetic/compare RHS: keep a constant operand as an immediate when the
   1112  * target can encode it for `use` (so no scratch register is spent
   1113  * materializing it), mirroring the optimizer's operand_imm_or_reg. Falls back
   1114  * to a register when there is no imm_legal hook (e.g. a recording mock target)
   1115  * or the constant is not target-legal for this op. */
   1116 static NativeLoc nd_rhs_imm_or_reg(NativeDirectTarget* d, NativeImmUse use,
   1117                                    u32 sub, Operand b) {
   1118   if (b.kind == OPK_IMM && d->native->imm_legal &&
   1119       d->native->imm_legal(d->native, use, sub, b.type, b.v.imm))
   1120     return nd_loc_imm(b.v.imm, b.type);
   1121   return nd_materialize_operand(d, b);
   1122 }
   1123 
   1124 /* Register a pure-compute op writes its result into. For a cacheable local that
   1125  * is the local's cache register (reused or freshly allocated), pinned for the
   1126  * instruction; nd_dst_writeback then marks it dirty without storing. Otherwise
   1127  * a scratch temporary that nd_dst_writeback spills to the frame home. */
   1128 static NativeLoc nd_dst_reg(NativeDirectTarget* d, Operand dst) {
   1129   if (dst.kind == OPK_LOCAL) {
   1130     NativeDirectLocal* l = nd_local(d, dst.v.local);
   1131     if (dst.type == l->type && nd_local_cacheable(d, l)) {
   1132       Reg r = l->reg;
   1133       if (r == REG_NONE) {
   1134         r = nd_cache_alloc(d, (NativeAllocClass)l->cls);
   1135         if (r != REG_NONE) {
   1136           d->reg_owner[l->cls][r] = dst.v.local;
   1137           l->reg = r;
   1138           nd_cache_link(d, dst.v.local);
   1139         }
   1140       }
   1141       if (r != REG_NONE) {
   1142         d->scratch_used[l->cls] |= 1u << r; /* pin for the instruction */
   1143         nd_touch_local(d, l);
   1144         return nd_loc_reg(d, dst.type, (NativeAllocClass)l->cls, r);
   1145       }
   1146     }
   1147   }
   1148   return nd_dst_scratch(d, dst);
   1149 }
   1150 
   1151 static void nd_dst_writeback(NativeDirectTarget* d, Operand dst, NativeLoc dr) {
   1152   if (dst.kind == OPK_LOCAL) {
   1153     NativeDirectLocal* l = nd_local(d, dst.v.local);
   1154     if (dr.kind == NATIVE_LOC_REG && l->reg == dr.v.reg &&
   1155         dst.type == l->type && nd_local_cacheable(d, l)) {
   1156       l->dirty = 1;
   1157       l->load_zext = 0; /* a fresh value; nd_load re-sets this for narrow loads */
   1158       l->load_sext = 0; /* "" (re-set only by an MF_SEXT_LOAD load) */
   1159       d->scratch_used[l->cls] &= ~(1u << dr.v.reg); /* unpin, keep cached */
   1160       return;
   1161     }
   1162     /* Bypassing the cache: drop any stale entry, then spill to the home. */
   1163     if (l->reg != REG_NONE) nd_invalidate_local(d, dst.v.local);
   1164   }
   1165   nd_store_operand_from_reg(d, dst, dr);
   1166   nd_release_materialized(d, dr);
   1167 }
   1168 
   1169 /* Coalescing rename: SRC (a transient flagged dead — OPK_FLAG_KILL) holds its
   1170  * live value in a cache register; transfer ownership of that register to DST so
   1171  * a copy needs no mov and a convert can run in place. Returns the now-DST-owned
   1172  * register (pinned for the consuming instruction; the caller passes it to
   1173  * nd_dst_writeback to unpin + mark dirty), or REG_NONE when a rename is not
   1174  * applicable and the caller must take its normal materialize path.
   1175  *
   1176  * Applicable only when: SRC is cached at its own (storage) width so the register
   1177  * truly holds SRC's value; SRC and DST are both cacheable scalars in the SAME
   1178  * register class (a width change within the class — e.g. a sxtb — is fine: the
   1179  * caller's convert rewrites the register in place; a class change GPR<->FPR is
   1180  * not, that needs a real cross-bank move). The register's bits become DST's, so
   1181  * DST is marked dirty (its home is now stale) and pinned; SRC is dropped without
   1182  * write-back (it is dead). A pre-existing DST cache entry is discarded first
   1183  * (the copy/convert supersedes it). Sound because OPK_FLAG_KILL is set only from
   1184  * api_temp_dead, which confirms no live value-stack reference to SRC remains. */
   1185 static Reg nd_rename_killed_to_dst(NativeDirectTarget* d, Operand dst,
   1186                                    Operand src) {
   1187   NativeDirectLocal* sl;
   1188   NativeDirectLocal* dl;
   1189   Reg r;
   1190   if (!(src.flags & OPK_FLAG_KILL) || src.kind != OPK_LOCAL ||
   1191       dst.kind != OPK_LOCAL || src.v.local == dst.v.local)
   1192     return REG_NONE;
   1193   sl = nd_local(d, src.v.local);
   1194   dl = nd_local(d, dst.v.local);
   1195   if (sl->reg == REG_NONE || sl->type != src.type || !nd_local_cacheable(d, sl))
   1196     return REG_NONE;
   1197   if (sl->cls != dl->cls || !nd_local_cacheable(d, dl)) return REG_NONE;
   1198   r = sl->reg;
   1199   /* Detach SRC from R without writing it back (dead) and discard any stale DST
   1200    * cache entry (its old value is about to be overwritten). */
   1201   nd_invalidate_local(d, src.v.local);
   1202   if (dl->reg != REG_NONE) nd_invalidate_local(d, dst.v.local);
   1203   /* R is now DST's: take ownership, mark dirty (home stale), pin for the op. */
   1204   d->reg_owner[dl->cls][r] = dst.v.local;
   1205   dl->reg = r;
   1206   dl->dirty = 1;
   1207   nd_cache_link(d, dst.v.local);
   1208   d->scratch_used[dl->cls] |= 1u << r;
   1209   nd_touch_local(d, dl);
   1210   return r;
   1211 }
   1212 
   1213 /* After an op has materialized and consumed OP, drop OP's cache entry WITHOUT a
   1214  * write-back when it carries OPK_FLAG_KILL — a transient the cg layer proved dead
   1215  * after this op (api_temp_dead). The op already read OP's value, so storing it at
   1216  * the next barrier would be wasted and holding its register inflates pressure;
   1217  * dropping it now both removes that dead spill and frees the register immediately
   1218  * (fewer eviction spills + reloads downstream). Sound for the same reason
   1219  * nd_rename_killed_to_dst is: OPK_FLAG_KILL is set only from api_temp_dead, which
   1220  * confirms no live value-stack reference to OP remains. A non-local or non-cached
   1221  * operand is a no-op (nd_invalidate_local early-returns on REG_NONE). The caller
   1222  * must have already unpinned OP's materialized register (nd_release_materialized);
   1223  * the cg layer never flags an operand that is also the op's destination. */
   1224 static void nd_drop_killed_operand(NativeDirectTarget* d, Operand op) {
   1225   if ((op.flags & OPK_FLAG_KILL) && op.kind == OPK_LOCAL)
   1226     nd_invalidate_local(d, op.v.local);
   1227 }
   1228 
   1229 static void nd_store_operand_from_reg(NativeDirectTarget* d, Operand dst,
   1230                                       NativeLoc src) {
   1231   if (dst.kind != OPK_LOCAL) nd_panic(d, "destination is not a semantic local");
   1232   /* This writes SRC to the local's frame home, bypassing the value cache (the
   1233    * result was produced in a scratch reg, e.g. a load / address-of). Any live
   1234    * cache entry for the local is now stale and must be dropped — the home write
   1235    * supersedes it. Drop without storing; storing back would clobber the new
   1236    * home value. Runs after SRC is produced, so a dst that was its own address
   1237    * base has already been consumed. */
   1238   {
   1239     NativeDirectLocal* l = nd_local(d, dst.v.local);
   1240     if (l->reg != REG_NONE) nd_invalidate_local(d, dst.v.local);
   1241     nd_store_reg_to_frame(d, nd_home(d, dst.v.local), dst.type, src);
   1242   }
   1243 }
   1244 
   1245 static void nd_func_begin(CgTarget* t, const CGFuncDesc* fd) {
   1246   NativeDirectTarget* d = nd_of(t);
   1247   d->func = fd;
   1248   d->nlocals = 0;
   1249   d->ntransient = 0;
   1250   d->nlabels = 0;
   1251   d->nscopes = 0;
   1252   d->max_outgoing = 0;
   1253   d->use_tick = 0;
   1254   d->cache_head = -1;
   1255   d->cache_tail = -1;
   1256   d->ncached = 0;
   1257   memset(d->scratch_used, 0, sizeof d->scratch_used);
   1258   memset(d->callee_saved_used, 0, sizeof d->callee_saved_used);
   1259   memset(d->reg_owner, 0, sizeof d->reg_owner);
   1260   if (d->native && d->native->func_begin) d->native->func_begin(d->native, fd);
   1261 }
   1262 
   1263 static void nd_func_end(CgTarget* t) {
   1264   NativeDirectTarget* d = nd_of(t);
   1265   NativeFramePatchState frame;
   1266   memset(&frame, 0, sizeof frame);
   1267   frame.max_outgoing = d->max_outgoing;
   1268   if (d->reg_info && d->reg_info->ndt_caller_saved_only) {
   1269     /* Caller-saved-only target (e.g. aa64): the -O0 path never allocates a
   1270      * callee-saved register — nd_scratch_acquire and the local cache restrict
   1271      * themselves to caller-saved regs. The prologue therefore never
   1272      * saves/restores any callee-save, which is what lets the backend reserve
   1273      * only a tiny tcc-style deferred-`sub` prologue region. Assert it so a
   1274      * future regression can't silently overflow that region. */
   1275     for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) {
   1276       if (d->callee_saved_used[cls])
   1277         nd_panic(d, "ndt_caller_saved_only target used a callee-saved register");
   1278     }
   1279   } else {
   1280     /* Default: NDT may use callee-saved registers as extra scratch under
   1281      * pressure; have the backend reserve save slots for them. The
   1282      * optimizer/known-frame path is separate and always callee-save-capable. */
   1283     u32 ncallee_classes = 0;
   1284     for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) {
   1285       if (d->callee_saved_used[cls]) ncallee_classes = cls + 1u;
   1286     }
   1287     if (ncallee_classes) {
   1288       if (!d->native || !d->native->reserve_callee_saves)
   1289         nd_panic(d, "target cannot preserve callee-saved scratch registers");
   1290       d->native->reserve_callee_saves(d->native, d->callee_saved_used,
   1291                                       ncallee_classes);
   1292     }
   1293   }
   1294   if (d->native && d->native->note_frame_state)
   1295     d->native->note_frame_state(d->native, &frame);
   1296   if (d->native && d->native->patch_apply) d->native->patch_apply(d->native);
   1297   if (d->native && d->native->func_end) d->native->func_end(d->native);
   1298   d->func = NULL;
   1299 }
   1300 
   1301 /* Reclaim dead transient temp slots at a statement boundary. cg calls this only
   1302  * when its value stack is empty, so every transient minted since the last
   1303  * reclaim is provably dead: nothing on the stack references it.
   1304  *
   1305  * For each such temp we DROP (not spill) any live cache entry — a pure-compute
   1306  * expression statement can leave a transient cached+dirty, and spilling it would
   1307  * write a dead value into a home we are about to recycle, then a later
   1308  * nd_flush_all could resurrect it into a different temp now sharing that slot.
   1309  * Then we return its frame home to the free list for the next statement's temps.
   1310  *
   1311  * An address-taken transient is excluded conservatively: nd_local_addr already
   1312  * flushed it and marked it uncacheable, and a pointer to its home may have
   1313  * escaped, so we keep its slot live (it is never cached here). At -O0 no C
   1314  * construct takes the address of a compiler temp, so this branch is belt-and-
   1315  * suspenders. Non-transient locals (declared locals/params) are untouched —
   1316  * their cached/dirty values legitimately live across statements. */
   1317 static void nd_reclaim_temps(CgTarget* t) {
   1318   NativeDirectTarget* d = nd_of(t);
   1319   for (u32 i = 0; i < d->ntransient; ++i) {
   1320     CGLocal local = d->transient_locals[i];
   1321     NativeDirectLocal* l = nd_local(d, local);
   1322     if (l->address_taken) continue;
   1323     nd_invalidate_local(d, local); /* drop cache entry, no write-back */
   1324     if (d->native && d->native->release_frame_slot &&
   1325         l->home != NATIVE_FRAME_SLOT_NONE)
   1326       d->native->release_frame_slot(d->native, l->home);
   1327   }
   1328   d->ntransient = 0;
   1329 }
   1330 
   1331 static void nd_alias(CgTarget* t, ObjSymId alias_sym, ObjSymId target_sym,
   1332                      KitCgTypeId type) {
   1333   (void)t;
   1334   (void)alias_sym;
   1335   (void)target_sym;
   1336   (void)type;
   1337 }
   1338 
   1339 static CGLocal nd_local_new(CgTarget* t, const CGLocalDesc* desc) {
   1340   return nd_alloc_local(nd_of(t), desc);
   1341 }
   1342 
   1343 static void nd_local_addr(CgTarget* t, Operand dst, const CGLocalDesc* desc,
   1344                           CGLocal local) {
   1345   NativeDirectTarget* d = nd_of(t);
   1346   NativeDirectLocal* l = nd_local(d, local);
   1347   Operand lv;
   1348   (void)desc;
   1349   /* Targeted flush: only this local escapes. Spill+drop its entry so the home
   1350    * is authoritative for the address computation, then mark it uncacheable. The
   1351    * rest of the cache is unaffected (other cached locals stay non-escaped). */
   1352   nd_flush_local(d, local);
   1353   l->address_taken = 1;
   1354   l->flags |= CG_LOCAL_ADDR_TAKEN;
   1355   memset(&lv, 0, sizeof lv);
   1356   lv.kind = OPK_LOCAL;
   1357   lv.type = l->type;
   1358   lv.v.local = local;
   1359   {
   1360     /* Route the &local address through the write-back cache (see nd_addr_of):
   1361      * write it into dst's cache register rather than a scratch copied to the
   1362      * home, killing the mov and keeping it resident for an immediate consumer. */
   1363     NativeLoc reg = nd_dst_reg(d, dst);
   1364     ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
   1365     d->native->load_addr(d->native, reg, nd_addr_storage(d, lv));
   1366     nd_dst_writeback(d, dst, reg);
   1367   }
   1368 }
   1369 
   1370 static CGLocal nd_param(CgTarget* t, const CGParamDesc* desc) {
   1371   NativeDirectTarget* d = nd_of(t);
   1372   NativeDirectLocal* l;
   1373   NativeFrameSlotDesc fsd;
   1374   CGLocal id;
   1375   nd_grow_locals(d, d->nlocals + 1u);
   1376   id = d->nlocals + 1u;
   1377   l = &d->locals[d->nlocals++];
   1378   memset(l, 0, sizeof *l);
   1379   l->type = desc->type;
   1380   l->size = desc->size;
   1381   l->align = desc->align;
   1382   l->flags = desc->flags;
   1383   l->reg = REG_NONE;
   1384   l->address_taken = (desc->flags & CG_LOCAL_ADDR_TAKEN) != 0;
   1385   l->memory_required = (desc->flags & CG_LOCAL_MEMORY_REQUIRED) != 0;
   1386   l->cls = (u8)nd_class_for_type(d, desc->type);
   1387   fsd = nd_slot_desc_param(desc);
   1388   l->home = nd_alloc_frame_slot(d, &fsd);
   1389   if (d->ops && d->ops->bind_param) d->ops->bind_param(d, desc, id, l);
   1390   return id;
   1391 }
   1392 
   1393 static int nd_local_debug_loc(CgTarget* t, CGLocal local, CGDebugLoc* out) {
   1394   NativeDirectTarget* d = nd_of(t);
   1395   NativeDirectLocal* l;
   1396   if (!out) return 0;
   1397   memset(out, 0, sizeof *out);
   1398   if (!d->native || !d->native->frame_slot_debug_loc) return 0;
   1399   l = nd_local(d, local);
   1400   if (l->home == NATIVE_FRAME_SLOT_NONE) return 0;
   1401   return d->native->frame_slot_debug_loc(d->native, l->home, out);
   1402 }
   1403 
   1404 static Label nd_label_new(CgTarget* t) { return nd_label_new_raw(nd_of(t)); }
   1405 
   1406 static void nd_label_place(CgTarget* t, Label label) {
   1407   NativeDirectTarget* d = nd_of(t);
   1408   nd_flush_all(d);
   1409   ND_REQUIRE_NATIVE(d, label_place, "target does not place labels");
   1410   d->native->label_place(d->native, nd_mc_label(d, label));
   1411 }
   1412 
   1413 static void nd_jump(CgTarget* t, Label label) {
   1414   NativeDirectTarget* d = nd_of(t);
   1415   nd_flush_all(d);
   1416   ND_REQUIRE_NATIVE(d, jump, "target does not emit jumps");
   1417   d->native->jump(d->native, nd_mc_label(d, label));
   1418 }
   1419 
   1420 static void nd_cmp_branch(CgTarget* t, CmpOp op, Operand a, Operand b,
   1421                           Label label) {
   1422   NativeDirectTarget* d = nd_of(t);
   1423   NativeLoc ar, br;
   1424   /* Materialize the compare operands BEFORE spilling the live-across set. A
   1425    * cache-resident operand is then read straight from its register instead of
   1426    * being spilled by the flush and immediately reloaded for the compare; a dead
   1427    * one (OPK_FLAG_KILL) is dropped without a write-back so the flush skips its
   1428    * store entirely. The operands stay pinned across the flush — their values
   1429    * survive in their registers (a spill store does not clobber them) — and the
   1430    * flush's stores are emitted before the compare, so they cannot clobber its
   1431    * condition flags. The post-branch cache state is still empty (the flush spills
   1432    * everything live), so control-flow-merge correctness is unchanged. */
   1433   ar = nd_materialize_operand(d, a);
   1434   br = nd_rhs_imm_or_reg(d, NATIVE_IMM_CMP, (u32)op, b);
   1435   nd_drop_killed_operand(d, a);
   1436   nd_drop_killed_operand(d, b);
   1437   nd_flush_all(d);
   1438   ND_REQUIRE_NATIVE(d, cmp_branch, "target does not emit compare branches");
   1439   d->native->cmp_branch(d->native, op, ar, br, nd_mc_label(d, label));
   1440   nd_release_materialized(d, br);
   1441   nd_release_materialized(d, ar);
   1442 }
   1443 
   1444 /* Cmp-and-branch chain with the selector pinned in ONE register across every
   1445  * case. This emits the same shape as cg_lower_switch_default (one CMP_EQ per
   1446  * case against cases[i].value at selector_type, then a jump to the default if
   1447  * any), but materializes the selector ONCE — before the flush — instead of
   1448  * letting each per-case cmp_branch reload it from its home (the flush empties
   1449  * the cache, so the shared lowering would re-materialize + re-flush per case).
   1450  *
   1451  * Soundness: after the initial flush the cache is empty. The chain falls through
   1452  * case-to-case with that same empty state — sel is a pinned scratch reg, not a
   1453  * cache entry, so nothing is re-cached between cases. Each case body and the
   1454  * default are reached through nd_label_place / nd_jump, which flush an
   1455  * already-empty cache. So the cache is empty along every out-edge: merge-correct
   1456  * exactly as the shared lowering is. The selector's value survives the flush in
   1457  * its pinned register (a spill store does not clobber it). Other backends and
   1458  * opt's IR replay still use cg_lower_switch_default unchanged. */
   1459 static void nd_switch(CgTarget* t, const CGSwitchDesc* desc) {
   1460   NativeDirectTarget* d = nd_of(t);
   1461   NativeLoc sel;
   1462   if (desc->ncases == 0) {
   1463     nd_flush_all(d);
   1464     if (desc->default_label != LABEL_NONE) {
   1465       ND_REQUIRE_NATIVE(d, jump, "target does not emit jumps");
   1466       d->native->jump(d->native, nd_mc_label(d, desc->default_label));
   1467     }
   1468     return;
   1469   }
   1470   sel = nd_materialize_operand(d, desc->selector);
   1471   nd_flush_all(d);
   1472   ND_REQUIRE_NATIVE(d, cmp_branch, "target does not emit compare branches");
   1473   for (u32 i = 0; i < desc->ncases; ++i) {
   1474     /* Same per-case immediate cg_lower_switch_default builds with api_op_imm:
   1475      * cases[i].value interpreted at selector_type. Constructed inline (the imm
   1476      * helper is not declared in this TU). */
   1477     Operand imm = {.kind = OPK_IMM,
   1478                    .type = desc->selector_type,
   1479                    .v.imm = (i64)desc->cases[i].value};
   1480     NativeLoc br = nd_rhs_imm_or_reg(d, NATIVE_IMM_CMP, (u32)CMP_EQ, imm);
   1481     d->native->cmp_branch(d->native, CMP_EQ, sel, br,
   1482                           nd_mc_label(d, desc->cases[i].label));
   1483     nd_release_materialized(d, br);
   1484   }
   1485   if (desc->default_label != LABEL_NONE) {
   1486     ND_REQUIRE_NATIVE(d, jump, "target does not emit jumps");
   1487     d->native->jump(d->native, nd_mc_label(d, desc->default_label));
   1488   }
   1489   nd_release_materialized(d, sel);
   1490 }
   1491 
   1492 static void nd_indirect_branch(CgTarget* t, Operand addr,
   1493                                const Label* valid_targets, u32 ntargets) {
   1494   NativeDirectTarget* d = nd_of(t);
   1495   MCLabel* native_targets;
   1496   NativeLoc addr_reg;
   1497   /* Materialize the target address BEFORE the flush (mirrors nd_cmp_branch): a
   1498    * cache-resident address is read from its register instead of being spilled
   1499    * by the flush and immediately reloaded. The reg stays pinned across the
   1500    * flush — its value survives (a spill store does not clobber it) — and the
   1501    * post-branch cache is still empty, so merge correctness is unchanged. */
   1502   addr_reg = nd_materialize_operand(d, addr);
   1503   nd_drop_killed_operand(d, addr);
   1504   nd_flush_all(d);
   1505   ND_REQUIRE_NATIVE(d, indirect_branch,
   1506                     "target does not emit indirect branches");
   1507   native_targets =
   1508       ntargets == 0 ? NULL
   1509       : ntargets <= ND_LBL_BUF
   1510           ? d->lblbuf
   1511           : nd_arena(d, sizeof(*native_targets) * ntargets, _Alignof(MCLabel));
   1512   for (u32 i = 0; i < ntargets; ++i)
   1513     native_targets[i] = nd_mc_label(d, valid_targets[i]);
   1514   d->native->indirect_branch(d->native, addr_reg, native_targets, ntargets);
   1515   nd_release_materialized(d, addr_reg);
   1516 }
   1517 
   1518 static void nd_load_label_addr(CgTarget* t, Operand dst, Label label) {
   1519   NativeDirectTarget* d = nd_of(t);
   1520   NativeLoc reg;
   1521   nd_flush_all(d);
   1522   reg = nd_dst_reg(d, dst);
   1523   ND_REQUIRE_NATIVE(d, load_label_addr,
   1524                     "target does not materialize label addresses");
   1525   d->native->load_label_addr(d->native, reg, nd_mc_label(d, label));
   1526   nd_dst_writeback(d, dst, reg);
   1527 }
   1528 
   1529 static int nd_local_static_data_begin(CgTarget* t,
   1530                                       const CGLocalStaticDataDesc* desc) {
   1531   NativeDirectTarget* d = nd_of(t);
   1532   Sym name;
   1533   SecKind kind;
   1534   u16 flags;
   1535   if (!d->native || !d->native->mc || !desc) return 0;
   1536   if (d->local_static_active) nd_panic(d, "nested local static data");
   1537   if (desc->attrs.section) {
   1538     name = (Sym)desc->attrs.section;
   1539     kind =
   1540         (desc->attrs.flags & KIT_CG_DATADEF_READONLY) ? SEC_RODATA : SEC_DATA;
   1541     flags = (desc->attrs.flags & KIT_CG_DATADEF_READONLY)
   1542                 ? SF_ALLOC
   1543                 : (SF_ALLOC | SF_WRITE);
   1544   } else if (desc->attrs.flags & KIT_CG_DATADEF_READONLY) {
   1545     name = pool_intern_slice(t->c->global, SLICE_LIT(".rodata"));
   1546     kind = SEC_RODATA;
   1547     flags = SF_ALLOC;
   1548   } else {
   1549     name = pool_intern_slice(t->c->global, SLICE_LIT(".data"));
   1550     kind = SEC_DATA;
   1551     flags = SF_ALLOC | SF_WRITE;
   1552   }
   1553   d->local_static_sec =
   1554       obj_section(t->obj, name, kind, flags, desc->align ? desc->align : 1u);
   1555   d->local_static_base =
   1556       obj_align_to(t->obj, d->local_static_sec, desc->align ? desc->align : 1u);
   1557   d->local_static_size = 0;
   1558   d->local_static_sym = desc->sym;
   1559   d->local_static_active = 1;
   1560   return 1;
   1561 }
   1562 
   1563 static void nd_local_static_data_write(CgTarget* t, const u8* data, u64 len) {
   1564   NativeDirectTarget* d = nd_of(t);
   1565   u8 zero[64];
   1566   u64 orig_len = len;
   1567   if (!d->local_static_active || !len) return;
   1568   if (data) {
   1569     obj_write(t->obj, d->local_static_sec, data, (size_t)len);
   1570   } else {
   1571     memset(zero, 0, sizeof zero);
   1572     while (len >= sizeof zero) {
   1573       obj_write(t->obj, d->local_static_sec, zero, sizeof zero);
   1574       len -= sizeof zero;
   1575     }
   1576     if (len) obj_write(t->obj, d->local_static_sec, zero, (size_t)len);
   1577   }
   1578   d->local_static_size += (u32)orig_len;
   1579 }
   1580 
   1581 static void nd_local_static_data_label_addr(CgTarget* t, Label target,
   1582                                             i64 addend, u32 width,
   1583                                             u32 address_space) {
   1584   NativeDirectTarget* d = nd_of(t);
   1585   u32 off;
   1586   u8 zero[8];
   1587   RelocKind kind;
   1588   (void)address_space;
   1589   if (!d->local_static_active)
   1590     nd_panic(d, "label address outside local static data");
   1591   /* A jump-table / label-address slot is one target pointer wide: 8 bytes
   1592    * (R_ABS64) on a 64-bit target, 4 bytes (R_ABS32) on rv32/ELFCLASS32. */
   1593   if (width == 8u)
   1594     kind = R_ABS64;
   1595   else if (width == 4u)
   1596     kind = R_ABS32;
   1597   else {
   1598     nd_panic(d, "unsupported local static label address width");
   1599     return;
   1600   }
   1601   memset(zero, 0, sizeof zero);
   1602   off = d->local_static_base + d->local_static_size;
   1603   obj_write(t->obj, d->local_static_sec, zero, width);
   1604   mc_emit_label_data_reloc(d->native->mc, d->local_static_sec, off,
   1605                                        nd_mc_label(d, target), kind, width,
   1606                                        addend);
   1607   d->local_static_size += width;
   1608 }
   1609 
   1610 static void nd_local_static_data_end(CgTarget* t) {
   1611   NativeDirectTarget* d = nd_of(t);
   1612   if (!d->local_static_active) return;
   1613   obj_symbol_define_live(t->obj, d->local_static_sym, d->local_static_sec,
   1614                          d->local_static_base, d->local_static_size);
   1615   d->local_static_active = 0;
   1616   d->local_static_sec = OBJ_SEC_NONE;
   1617   d->local_static_sym = OBJ_SYM_NONE;
   1618   d->local_static_base = 0;
   1619   d->local_static_size = 0;
   1620 }
   1621 
   1622 static const char* nd_data_label_addr_unsupported_msg(CgTarget* t) {
   1623   (void)t;
   1624   return NULL;
   1625 }
   1626 
   1627 static CGScope nd_scope_begin(CgTarget* t, const CGScopeDesc* desc) {
   1628   NativeDirectTarget* d = nd_of(t);
   1629   NativeDirectScope* s;
   1630   CGScope id;
   1631   nd_grow_scopes(d, d->nscopes + 1u);
   1632   id = d->nscopes + 1u;
   1633   s = &d->scopes[d->nscopes++];
   1634   memset(s, 0, sizeof *s);
   1635   s->kind = desc->kind;
   1636   s->owns_break = desc->break_label == LABEL_NONE;
   1637   s->break_label = desc->break_label ? desc->break_label : nd_label_new_raw(d);
   1638   s->continue_label = desc->continue_label;
   1639   if (desc->kind == SCOPE_LOOP && s->continue_label == LABEL_NONE)
   1640     s->continue_label = nd_label_new_raw(d);
   1641   return id;
   1642 }
   1643 
   1644 static NativeDirectScope* nd_scope(NativeDirectTarget* d, CGScope scope) {
   1645   if (scope == CG_SCOPE_NONE || scope > d->nscopes) nd_panic(d, "bad scope");
   1646   return &d->scopes[scope - 1u];
   1647 }
   1648 
   1649 static void nd_scope_end(CgTarget* t, CGScope scope) {
   1650   NativeDirectTarget* d = nd_of(t);
   1651   NativeDirectScope* s = nd_scope(d, scope);
   1652   if (s->owns_break) nd_label_place(t, s->break_label);
   1653 }
   1654 
   1655 static void nd_break_to(CgTarget* t, CGScope scope) {
   1656   nd_jump(t, nd_scope(nd_of(t), scope)->break_label);
   1657 }
   1658 
   1659 static void nd_continue_to(CgTarget* t, CGScope scope) {
   1660   NativeDirectScope* s = nd_scope(nd_of(t), scope);
   1661   if (s->continue_label == LABEL_NONE)
   1662     nd_panic(nd_of(t), "continue_to on scope without continue label");
   1663   nd_jump(t, s->continue_label);
   1664 }
   1665 
   1666 static int nd_is_wide64_int(NativeDirectTarget* d, KitCgTypeId ty);
   1667 static int nd_is_soft_double(NativeDirectTarget* d, KitCgTypeId ty);
   1668 
   1669 static void nd_load_imm(CgTarget* t, Operand dst, i64 imm) {
   1670   NativeDirectTarget* d = nd_of(t);
   1671   NativeLoc reg;
   1672   if (nd_is_wide64_int(d, dst.type))
   1673     nd_panic(d,
   1674              "64-bit integer immediate reached the backend un-lowered "
   1675              "(cg should materialize it as two 32-bit lanes)");
   1676   reg = nd_dst_reg(d, dst);
   1677   ND_REQUIRE_NATIVE(d, load_imm, "target does not emit immediates");
   1678   d->native->load_imm(d->native, reg, imm);
   1679   nd_dst_writeback(d, dst, reg);
   1680 }
   1681 
   1682 static void nd_load_const(CgTarget* t, Operand dst, ConstBytes cbytes) {
   1683   NativeDirectTarget* d = nd_of(t);
   1684   NativeLoc reg;
   1685   if (nd_is_wide64_int(d, dst.type) || nd_is_soft_double(d, dst.type))
   1686     nd_panic(d,
   1687              "8-byte constant reached the backend un-lowered (cg should "
   1688              "materialize it as two 32-bit lanes)");
   1689   reg = nd_dst_reg(d, dst);
   1690   ND_REQUIRE_NATIVE(d, load_const, "target does not emit byte constants");
   1691   d->native->load_const(d->native, reg, cbytes);
   1692   nd_dst_writeback(d, dst, reg);
   1693 }
   1694 
   1695 static void nd_copy(CgTarget* t, Operand dst, Operand src) {
   1696   NativeDirectTarget* d = nd_of(t);
   1697   u64 size = dst.type ? cg_type_size(t->c, dst.type) : 0;
   1698   if (size > (u64)t->c->target.ptr_size) {
   1699     NdAddrTemps dt, st;
   1700     AggregateAccess access;
   1701     /* Aggregate copy: addresses are built cache-aware (a directly-addressed
   1702      * cached local is flushed in nd_addr_storage), so no whole-cache flush. */
   1703     memset(&access, 0, sizeof access);
   1704     access.type = dst.type;
   1705     access.size = (u32)size;
   1706     access.align =
   1707         dst.type ? cg_type_align(t->c, dst.type) : (u32)t->c->target.ptr_align;
   1708     access.mem.type = dst.type;
   1709     access.mem.size = access.size;
   1710     access.mem.align = access.align;
   1711     NativeAddr da =
   1712         nd_addr_materialize(d, nd_addr_storage(d, dst), &dt, access.mem);
   1713     NativeAddr sa =
   1714         nd_addr_materialize(d, nd_addr_storage(d, src), &st, access.mem);
   1715     ND_REQUIRE_NATIVE(d, copy_bytes, "target does not copy bytes");
   1716     d->native->copy_bytes(d->native, da, sa, access);
   1717     nd_addr_temps_release(d, &st);
   1718     nd_addr_temps_release(d, &dt);
   1719     return;
   1720   }
   1721   /* Coalesce `x = t` when t is a dead transient resident in a register: rename
   1722    * its register to x (transfer ownership) instead of materialize + mov. Only
   1723    * for an exact same-type copy (the register bits become x's unchanged). */
   1724   if (dst.type == src.type) {
   1725     Reg r = nd_rename_killed_to_dst(d, dst, src);
   1726     if (r != REG_NONE) {
   1727       /* Register already holds the value as x's; just unpin + mark dirty. */
   1728       nd_dst_writeback(d, dst,
   1729                        nd_loc_reg(d, dst.type, nd_class_for_type(d, dst.type),
   1730                                   r));
   1731       return;
   1732     }
   1733   }
   1734   /* Acquire the destination register first, then materialize the source
   1735    * straight into it: a memory/immediate/global source loads directly into dr
   1736    * (no scratch + mov), a cached source costs the one unavoidable reg-reg move. */
   1737   NativeLoc dr = nd_dst_reg(d, dst);
   1738   nd_materialize_operand_into(d, dr, src);
   1739   nd_dst_writeback(d, dst, dr);
   1740 }
   1741 
   1742 /* Bit-fields ride the generic load/store (mem.bf_width != 0); this impl
   1743  * translates them to the physical NativeTarget bitfield_load/store below. */
   1744 static void nd_bitfield_load(CgTarget* t, Operand dst, Operand record_addr,
   1745                              BitFieldAccess access);
   1746 static void nd_bitfield_store(CgTarget* t, Operand record_addr, Operand src,
   1747                               BitFieldAccess access);
   1748 
   1749 static void nd_load(CgTarget* t, Operand dst, Operand addr, MemAccess mem) {
   1750   NativeDirectTarget* d = nd_of(t);
   1751   NdAddrTemps temps;
   1752   u64 size;
   1753   if (mem.bf_width != 0) {
   1754     nd_bitfield_load(t, dst, addr, bf_from_mem(mem));
   1755     return;
   1756   }
   1757   size = mem.size ? mem.size : (mem.type ? cg_type_size(t->c, mem.type) : 0);
   1758   /* No value-cache flush (2c): the cache holds only non-escaped locals, which no
   1759    * pointer can alias, so a volatile access — foreign memory — can neither observe
   1760    * a cached value's stale home nor overwrite it. The arch barrier hook still
   1761    * fires so an instrumentation seam can order the access. */
   1762   if (mem.flags & MF_VOLATILE)
   1763     nd_barrier(d, NATIVE_DIRECT_BARRIER_MEMORY | NATIVE_DIRECT_BARRIER_VOLATILE);
   1764   NativeAddr naddr =
   1765       nd_addr_materialize(d, nd_addr_storage(d, addr), &temps, mem);
   1766   if (size > (u64)t->c->target.ptr_size) {
   1767     NdAddrTemps dt;
   1768     AggregateAccess access;
   1769     memset(&access, 0, sizeof access);
   1770     access.type = mem.type ? mem.type : dst.type;
   1771     access.size = (u32)size;
   1772     access.align = mem.align;
   1773     access.mem = mem;
   1774     NativeAddr da = nd_addr_materialize(d, nd_addr_storage(d, dst), &dt, mem);
   1775     ND_REQUIRE_NATIVE(d, copy_bytes, "target does not copy bytes");
   1776     d->native->copy_bytes(d->native, da, naddr, access);
   1777     nd_addr_temps_release(d, &dt);
   1778     nd_addr_temps_release(d, &temps);
   1779     return;
   1780   }
   1781   /* The loaded value goes through the write-back cache (like a compute result)
   1782    * rather than straight to the frame home: a load feeds an immediate consumer
   1783    * far more often than not, so keeping it resident kills the store-then-reload
   1784    * pair. nd_dst_reg avoids the address temps (still pinned here) and falls back
   1785    * to a scratch + home store via nd_dst_writeback when dst is not cacheable. */
   1786   NativeLoc reg = nd_dst_reg(d, dst);
   1787   ND_REQUIRE_NATIVE(d, load, "target does not emit loads");
   1788   d->native->load(d->native, reg, naddr, mem);
   1789   nd_dst_writeback(d, dst, reg);
   1790   /* A narrow integer load fills the whole register: a plain ldrb/ldrh/ldr-w
   1791    * zero-extends, and (when the backend advertises load_sext and the access
   1792    * is MF_SEXT_LOAD) a sign-extending ldrsb/ldrsh sign-extends. Either way a
   1793    * subsequent matching widen (CV_ZEXT / CV_SEXT) is a no-op, so record which
   1794    * one this load produced — register-resident only — so nd_convert can drop the
   1795    * extend. (nd_dst_writeback just cleared both flags for this local.) */
   1796   if (dst.kind == OPK_LOCAL && size < (u64)t->c->target.ptr_size &&
   1797       nd_class_for_type(d, dst.type) == NATIVE_REG_INT &&
   1798       reg.kind == NATIVE_LOC_REG) {
   1799     NativeDirectLocal* l = nd_local(d, dst.v.local);
   1800     if (l->reg == reg.v.reg) {
   1801       if ((mem.flags & MF_SEXT_LOAD) && d->reg_info &&
   1802           d->reg_info->load_sext)
   1803         l->load_sext = 1;
   1804       else
   1805         l->load_zext = 1;
   1806     }
   1807   }
   1808   nd_addr_temps_release(d, &temps);
   1809 }
   1810 
   1811 static void nd_store(CgTarget* t, Operand addr, Operand src, MemAccess mem) {
   1812   NativeDirectTarget* d = nd_of(t);
   1813   NdAddrTemps temps;
   1814   u64 size;
   1815   if (mem.bf_width != 0) {
   1816     nd_bitfield_store(t, addr, src, bf_from_mem(mem));
   1817     return;
   1818   }
   1819   size = mem.size ? mem.size : (mem.type ? cg_type_size(t->c, mem.type) : 0);
   1820   /* No value-cache flush (2c, see nd_load): a store through a pointer cannot
   1821    * alias a cached non-escaped local. The store target is foreign memory, so
   1822    * there is no dst local entry to invalidate; SRC is read via
   1823    * nd_materialize_operand. The arch barrier hook still fires. */
   1824   if (mem.flags & MF_VOLATILE)
   1825     nd_barrier(d, NATIVE_DIRECT_BARRIER_MEMORY | NATIVE_DIRECT_BARRIER_VOLATILE);
   1826   NativeAddr naddr =
   1827       nd_addr_materialize(d, nd_addr_storage(d, addr), &temps, mem);
   1828   if (size > (u64)t->c->target.ptr_size) {
   1829     NdAddrTemps st;
   1830     AggregateAccess access;
   1831     memset(&access, 0, sizeof access);
   1832     access.type = mem.type ? mem.type : src.type;
   1833     access.size = (u32)size;
   1834     access.align = mem.align;
   1835     access.mem = mem;
   1836     NativeAddr sa = nd_addr_materialize(d, nd_addr_storage(d, src), &st, mem);
   1837     ND_REQUIRE_NATIVE(d, copy_bytes, "target does not copy bytes");
   1838     d->native->copy_bytes(d->native, naddr, sa, access);
   1839     nd_addr_temps_release(d, &st);
   1840     nd_addr_temps_release(d, &temps);
   1841     return;
   1842   }
   1843   NativeLoc val = nd_materialize_operand(d, src);
   1844   ND_REQUIRE_NATIVE(d, store, "target does not emit stores");
   1845   d->native->store(d->native, naddr, val, mem);
   1846   nd_release_materialized(d, val);
   1847   nd_addr_temps_release(d, &temps);
   1848   /* The store consumed SRC; if it was a dead transient, drop it (no write-back)
   1849    * so it is not flush-stored at the next barrier (eager dead-operand drop). */
   1850   nd_drop_killed_operand(d, src);
   1851 }
   1852 
   1853 static void nd_addr_of(CgTarget* t, Operand dst, Operand lv) {
   1854   NativeDirectTarget* d = nd_of(t);
   1855   NdAddrTemps temps;
   1856   MemAccess mem = nd_scalar_mem(dst.type, d->base.c->target.ptr_size,
   1857                                 d->base.c->target.ptr_align);
   1858   NativeAddr naddr;
   1859   /* Targeted: only an OPK_LOCAL lvalue escapes here — flush+mark just that
   1860    * local (its home becomes the authoritative address source). An INDIRECT
   1861    * lvalue's address is computed from base/index, which nd_addr_storage now
   1862    * reads from the cache directly; a GLOBAL needs nothing. The dst home write
   1863    * is handled by nd_store_operand_from_reg's invalidation. */
   1864   if (lv.kind == OPK_LOCAL) {
   1865     NativeDirectLocal* l = nd_local(d, lv.v.local);
   1866     nd_flush_local(d, lv.v.local);
   1867     l->address_taken = 1;
   1868     l->flags |= CG_LOCAL_ADDR_TAKEN;
   1869   }
   1870   naddr = nd_addr_materialize(d, nd_addr_storage(d, lv), &temps, mem);
   1871   /* Route the address through the write-back register cache (like nd_load):
   1872    * load_addr writes straight into the dst local's cache register instead of a
   1873    * scratch that is then copied to the home, killing the mov and keeping the
   1874    * address resident for an immediate consumer (e.g. the base of the load it
   1875    * feeds). nd_dst_reg falls back to a scratch + home store via nd_dst_writeback
   1876    * when dst is not cacheable. The address temps are still pinned, and
   1877    * nd_cache_alloc skips pinned regs, so the cache reg never aliases base/index. */
   1878   NativeLoc reg = nd_dst_reg(d, dst);
   1879   ND_REQUIRE_NATIVE(d, load_addr, "target does not materialize addresses");
   1880   d->native->load_addr(d->native, reg, naddr);
   1881   nd_dst_writeback(d, dst, reg);
   1882   nd_addr_temps_release(d, &temps);
   1883 }
   1884 
   1885 static void nd_tls_addr_of(CgTarget* t, Operand dst, ObjSymId sym, i64 addend) {
   1886   NativeDirectTarget* d = nd_of(t);
   1887   NativeLoc reg;
   1888   nd_flush_all(d);
   1889   reg = nd_dst_scratch(d, dst);
   1890   ND_REQUIRE_NATIVE(d, tls_addr_of,
   1891                     "target does not materialize TLS addresses");
   1892   d->native->tls_addr_of(d->native, reg, sym, addend);
   1893   nd_store_operand_from_reg(d, dst, reg);
   1894   nd_release_materialized(d, reg);
   1895 }
   1896 
   1897 static void nd_copy_bytes(CgTarget* t, Operand dst_addr, Operand src_addr,
   1898                           AggregateAccess access) {
   1899   NativeDirectTarget* d = nd_of(t);
   1900   NdAddrTemps dt, st;
   1901   NativeAddr dst;
   1902   /* Pointer-target memory; addresses are cache-aware. No whole-cache flush. */
   1903   dst = nd_addr_materialize(d, nd_addr_pointer(d, dst_addr), &dt, access.mem);
   1904   NativeAddr src =
   1905       nd_addr_materialize(d, nd_addr_pointer(d, src_addr), &st, access.mem);
   1906   ND_REQUIRE_NATIVE(d, copy_bytes, "target does not copy bytes");
   1907   d->native->copy_bytes(d->native, dst, src, access);
   1908   nd_addr_temps_release(d, &st);
   1909   nd_addr_temps_release(d, &dt);
   1910 }
   1911 
   1912 static void nd_set_bytes(CgTarget* t, Operand dst_addr, Operand byte_value,
   1913                          AggregateAccess access) {
   1914   NativeDirectTarget* d = nd_of(t);
   1915   NdAddrTemps temps;
   1916   NativeAddr dst;
   1917   NativeLoc byte;
   1918   /* Pointer-target memory; addresses are cache-aware. No whole-cache flush. */
   1919   dst =
   1920       nd_addr_materialize(d, nd_addr_pointer(d, dst_addr), &temps, access.mem);
   1921   byte = nd_materialize_operand(d, byte_value);
   1922   ND_REQUIRE_NATIVE(d, set_bytes, "target does not set bytes");
   1923   d->native->set_bytes(d->native, dst, byte, access);
   1924   nd_release_materialized(d, byte);
   1925   nd_addr_temps_release(d, &temps);
   1926 }
   1927 
   1928 static void nd_bitfield_load(CgTarget* t, Operand dst, Operand record_addr,
   1929                              BitFieldAccess access) {
   1930   NativeDirectTarget* d = nd_of(t);
   1931   NdAddrTemps temps;
   1932   NativeAddr addr;
   1933   NativeLoc reg;
   1934   /* Record (pointer-target) memory; addresses are cache-aware. The dst home
   1935    * write is handled by nd_store_operand_from_reg's invalidation. */
   1936   addr = nd_addr_materialize(d, nd_addr_storage(d, record_addr), &temps,
   1937                              access.storage);
   1938   reg = nd_dst_scratch(d, dst);
   1939   ND_REQUIRE_NATIVE(d, bitfield_load, "target does not load bitfields");
   1940   d->native->bitfield_load(d->native, reg, addr, access);
   1941   nd_store_operand_from_reg(d, dst, reg);
   1942   nd_release_materialized(d, reg);
   1943   nd_addr_temps_release(d, &temps);
   1944 }
   1945 
   1946 static void nd_bitfield_store(CgTarget* t, Operand record_addr, Operand src,
   1947                               BitFieldAccess access) {
   1948   NativeDirectTarget* d = nd_of(t);
   1949   NdAddrTemps temps;
   1950   NativeAddr addr;
   1951   NativeLoc val;
   1952   /* Record (pointer-target) memory; addresses are cache-aware, SRC reads the
   1953    * cache. No whole-cache flush. */
   1954   addr = nd_addr_materialize(d, nd_addr_storage(d, record_addr), &temps,
   1955                              access.storage);
   1956   val = nd_materialize_operand(d, src);
   1957   ND_REQUIRE_NATIVE(d, bitfield_store, "target does not store bitfields");
   1958   d->native->bitfield_store(d->native, addr, val, access);
   1959   nd_release_materialized(d, val);
   1960   nd_addr_temps_release(d, &temps);
   1961 }
   1962 
   1963 /* Last line of defense against an unlowered split-scalar/soft-float op reaching
   1964  * the machine backend. The cg-layer gates in src/cg/arith.c route split i64
   1965  * mul/div/shift and all soft-double arith/convert/compare to runtime calls; if
   1966  * one escapes, the native backend would silently emit wrong code. */
   1967 static int nd_is_split_wide8_scalar(NativeDirectTarget* d, KitCgTypeId ty) {
   1968   return abi_cg_scalar_split_lane_size(d->base.c->abi, ty) == 4u &&
   1969          native_type_size(d->native, ty) == 8u;
   1970 }
   1971 
   1972 static int nd_is_wide64_int(NativeDirectTarget* d, KitCgTypeId ty) {
   1973   if (!nd_is_split_wide8_scalar(d, ty)) return 0;
   1974   if (kit_cg_type_int_width((KitCompiler*)d->base.c, ty) == 0) return 0;
   1975   return 1;
   1976 }
   1977 
   1978 static int nd_is_soft_double(NativeDirectTarget* d, KitCgTypeId ty) {
   1979   if (!nd_is_split_wide8_scalar(d, ty)) return 0;
   1980   return kit_cg_type_float_width((KitCompiler*)d->base.c, ty) == 64;
   1981 }
   1982 
   1983 static void nd_binop(CgTarget* t, BinOp op, Operand dst, Operand a, Operand b) {
   1984   NativeDirectTarget* d = nd_of(t);
   1985   NativeLoc ar;
   1986   NativeLoc br;
   1987   NativeLoc dr;
   1988   /* No split-lane 8-byte value reaches a single-register op: the cg layer
   1989    * lowers i64 add/sub/and/or/xor to inline 2-word lane sequences and
   1990    * mul/div/rem/shift to __*di3 runtime calls (src/cg/arith.c). Anything that
   1991    * slips through here would silently compute only the low word, so fail
   1992    * loudly instead. */
   1993   if (nd_is_wide64_int(d, a.type) || nd_is_wide64_int(d, dst.type)) {
   1994     nd_panic(
   1995         d,
   1996         "64-bit integer arithmetic reached the backend un-lowered "
   1997         "(cg should emit a 2-word lane sequence or a __*di3 runtime call)");
   1998   }
   1999   if (nd_is_soft_double(d, a.type) || nd_is_soft_double(d, dst.type)) {
   2000     nd_panic(d,
   2001              "soft-float double arithmetic reached the backend un-lowered "
   2002              "(should be a __*df3 runtime call)");
   2003   }
   2004   ar = nd_materialize_operand(d, a);
   2005   br = nd_rhs_imm_or_reg(d, NATIVE_IMM_BINOP, (u32)op, b);
   2006   dr = nd_dst_reg(d, dst);
   2007   /* §E.3: cross the post-materialization operands as 16 B NativeRegLoc when the
   2008    * backend installs the narrow hook (every native arch does); fall back to the
   2009    * fat 48 B hook otherwise. Byte-identical — the arch reconstructs the same
   2010    * NativeLoc. */
   2011   if (d->native->binop_rr)
   2012     d->native->binop_rr(d->native, op, native_reg_loc_of(dr),
   2013                         native_reg_loc_of(ar), native_reg_loc_of(br));
   2014   else {
   2015     ND_REQUIRE_NATIVE(d, binop, "target does not emit binary ops");
   2016     d->native->binop(d->native, op, dr, ar, br);
   2017   }
   2018   nd_dst_writeback(d, dst, dr);
   2019   nd_release_materialized(d, br);
   2020   nd_release_materialized(d, ar);
   2021   /* Eager dead-operand drop: a/b flagged dead by the cg layer (OPK_FLAG_KILL) are
   2022    * consumed now — drop their cache entries without a write-back so they are not
   2023    * flush-stored at the next barrier and their registers free immediately. */
   2024   nd_drop_killed_operand(d, b);
   2025   nd_drop_killed_operand(d, a);
   2026 }
   2027 
   2028 static void nd_unop(CgTarget* t, UnOp op, Operand dst, Operand a) {
   2029   NativeDirectTarget* d = nd_of(t);
   2030   NativeLoc ar;
   2031   NativeLoc dr;
   2032   /* i64 neg/bnot stay inline as register pairs, and soft-double FNEG stays
   2033    * inline as a high-word sign-bit flip (v1), so both are allowlisted. Any
   2034    * OTHER soft-double unop reaching the backend is an unlowered escape. */
   2035   if (nd_is_wide64_int(d, a.type) || nd_is_wide64_int(d, dst.type)) {
   2036     nd_panic(d,
   2037              "64-bit integer unary op reached the backend un-lowered "
   2038              "(cg should emit a 2-word lane sequence)");
   2039   }
   2040   if (op != UO_FNEG &&
   2041       (nd_is_soft_double(d, a.type) || nd_is_soft_double(d, dst.type))) {
   2042     nd_panic(d, "soft-float double unary op reached the backend un-lowered");
   2043   }
   2044   ar = nd_materialize_operand(d, a);
   2045   dr = nd_dst_reg(d, dst);
   2046   ND_REQUIRE_NATIVE(d, unop, "target does not emit unary ops");
   2047   d->native->unop(d->native, op, dr, ar);
   2048   nd_dst_writeback(d, dst, dr);
   2049   nd_release_materialized(d, ar);
   2050   nd_drop_killed_operand(d, a);
   2051 }
   2052 
   2053 static void nd_cmp(CgTarget* t, CmpOp op, Operand dst, Operand a, Operand b) {
   2054   NativeDirectTarget* d = nd_of(t);
   2055   NativeLoc ar;
   2056   NativeLoc br;
   2057   NativeLoc dr;
   2058   /* i64 compares are lowered to inline 2-word lane sequences and soft-double
   2059    * compares to __*df2 runtime calls (src/cg/arith.c); neither reaches a single
   2060    * GPR compare here. */
   2061   if (nd_is_wide64_int(d, a.type) || nd_is_wide64_int(d, b.type)) {
   2062     nd_panic(d,
   2063              "64-bit integer compare reached the backend un-lowered "
   2064              "(cg should emit a 2-word lane sequence)");
   2065   }
   2066   if (nd_is_soft_double(d, a.type) || nd_is_soft_double(d, b.type)) {
   2067     nd_panic(d,
   2068              "soft-float double compare reached the backend un-lowered "
   2069              "(should be a __*df2 runtime call)");
   2070   }
   2071   ar = nd_materialize_operand(d, a);
   2072   br = nd_rhs_imm_or_reg(d, NATIVE_IMM_CMP, (u32)op, b);
   2073   dr = nd_dst_reg(d, dst);
   2074   if (d->native->cmp_rr)
   2075     d->native->cmp_rr(d->native, op, native_reg_loc_of(dr),
   2076                       native_reg_loc_of(ar), native_reg_loc_of(br));
   2077   else {
   2078     ND_REQUIRE_NATIVE(d, cmp, "target does not emit compares");
   2079     d->native->cmp(d->native, op, dr, ar, br);
   2080   }
   2081   nd_dst_writeback(d, dst, dr);
   2082   nd_release_materialized(d, br);
   2083   nd_release_materialized(d, ar);
   2084   nd_drop_killed_operand(d, b);
   2085   nd_drop_killed_operand(d, a);
   2086 }
   2087 
   2088 static void nd_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) {
   2089   NativeDirectTarget* d = nd_of(t);
   2090   NativeLoc sr;
   2091   NativeLoc dr;
   2092   /* i64<->i32 sext/zext/trunc are lowered to inline lane ops (src/cg/arith.c
   2093    * api_try_wide8_convert) and i64<->float / soft-double conversions to runtime
   2094    * calls; none reaches a single-register convert here. */
   2095   if (nd_is_wide64_int(d, src.type) || nd_is_wide64_int(d, dst.type)) {
   2096     nd_panic(d,
   2097              "64-bit integer conversion reached the backend un-lowered "
   2098              "(cg should emit a 2-word lane sequence or a runtime call)");
   2099   }
   2100   if (nd_is_soft_double(d, src.type) || nd_is_soft_double(d, dst.type)) {
   2101     nd_panic(d,
   2102              "soft-float double conversion reached the backend un-lowered "
   2103              "(should be a runtime call)");
   2104   }
   2105   /* Coalesce a convert whose source is a dead, register-resident transient of
   2106    * the same register class: rename the source's register to the destination
   2107    * and convert it in place (dr == sr). Saves the source materialize-into-a-
   2108    * pinned-reg + a separate destination scratch, and lets a width-preserving
   2109    * convert (e.g. a zext/sext/bitcast that is a no-op once dst==src reg)
   2110    * collapse to nothing in the arch move/convert path. A class-crossing convert
   2111    * (GPR<->FPR fmov) is excluded by nd_rename_killed_to_dst's same-class guard
   2112    * and falls through below. */
   2113   /* A CV_ZEXT (resp. CV_SEXT) whose source came straight from a narrow integer
   2114    * load is a no-op: the load already zero-extended (resp. sign-extended, via an
   2115    * MF_SEXT_LOAD ldrsb/ldrsh that filled the whole register) the value, so the
   2116    * widen is redundant for any wider destination. Detect it before the rename,
   2117    * which transfers (and would clear) the source's flag. */
   2118   int ext_noop = 0;
   2119   if (src.kind == OPK_LOCAL && (op == CV_ZEXT || op == CV_SEXT)) {
   2120     NativeDirectLocal* sl = nd_local(d, src.v.local);
   2121     u8 has = op == CV_ZEXT ? sl->load_zext : sl->load_sext;
   2122     ext_noop = sl->reg != REG_NONE && has && src.type == sl->type;
   2123   }
   2124   {
   2125     Reg r = nd_rename_killed_to_dst(d, dst, src);
   2126     if (r != REG_NONE) {
   2127       NativeAllocClass cls = nd_class_for_type(d, dst.type);
   2128       sr = nd_loc_reg(d, src.type, cls, r);
   2129       dr = nd_loc_reg(d, dst.type, cls, r);
   2130       /* Renaming the source register to the destination already placed the
   2131        * extended value where the result wants it — skip the redundant extend;
   2132        * the upper bits are provably the correct zero/sign fill. */
   2133       if (!ext_noop) {
   2134         if (d->native->convert_rr)
   2135           d->native->convert_rr(d->native, op, native_reg_loc_of(dr),
   2136                                 native_reg_loc_of(sr));
   2137         else {
   2138           ND_REQUIRE_NATIVE(d, convert, "target does not emit converts");
   2139           d->native->convert(d->native, op, dr, sr);
   2140         }
   2141       }
   2142       nd_dst_writeback(d, dst, dr);
   2143       return;
   2144     }
   2145   }
   2146   sr = nd_materialize_operand(d, src);
   2147   dr = nd_dst_reg(d, dst);
   2148   if (d->native->convert_rr)
   2149     d->native->convert_rr(d->native, op, native_reg_loc_of(dr),
   2150                           native_reg_loc_of(sr));
   2151   else {
   2152     ND_REQUIRE_NATIVE(d, convert, "target does not emit converts");
   2153     d->native->convert(d->native, op, dr, sr);
   2154   }
   2155   nd_dst_writeback(d, dst, dr);
   2156   nd_release_materialized(d, sr);
   2157 }
   2158 
   2159 static void nd_call(CgTarget* t, const CGCallDesc* desc) {
   2160   NativeDirectTarget* d = nd_of(t);
   2161   NativeCallPhase plan;
   2162   NativeCallDesc nd;
   2163   NativeLoc* args;
   2164   NativeLoc* results;
   2165   NativeLoc callee_tmp;
   2166   int release_callee_tmp = 0;
   2167   u32 nresults = desc->result != CG_LOCAL_NONE ? 1u : 0u;
   2168   memset(&plan, 0, sizeof plan);
   2169   memset(&nd, 0, sizeof nd);
   2170   memset(&callee_tmp, 0, sizeof callee_tmp);
   2171   args = nd_loc_buf(d, d->argbuf, ND_ARG_BUF, desc->nargs);
   2172   results = nd_loc_buf(d, d->retbuf, ND_RET_BUF, nresults);
   2173   /* Source each argument cache-aware BEFORE spilling. A cached arg that is dead
   2174    * after the call (arg_dead_mask) flows from its live register straight into
   2175    * the ABI arg register — the backend's parallel-copy scheduler
   2176    * (native_arg_shuffle) resolves reg<->arg-reg conflicts — avoiding the
   2177    * spill-to-home + reload round-trip the old flush-all forced. Pin its register
   2178    * so the callee materialize below cannot evict it as a scratch victim. A live
   2179    * (or uncached) arg keeps the home source and is spilled by the flush below. */
   2180   for (u32 i = 0; i < desc->nargs; ++i) {
   2181     NativeDirectLocal* l = nd_local(d, desc->args[i]);
   2182     int dead = i < 64u && ((desc->arg_dead_mask >> i) & 1u);
   2183     if (dead && l->reg != REG_NONE) {
   2184       args[i] = nd_loc_reg(d, l->type, (NativeAllocClass)l->cls, l->reg);
   2185       d->scratch_used[l->cls] |= 1u << l->reg;
   2186     } else {
   2187       args[i] = nd_loc_frame(d, desc->args[i], 0);
   2188     }
   2189   }
   2190   /* An indirect callee is read from its home; make it authoritative before the
   2191    * selective flush (the cg only ever produces an OPK_LOCAL or OPK_GLOBAL
   2192    * callee). */
   2193   if (desc->callee.kind == OPK_LOCAL) nd_flush_local(d, desc->callee.v.local);
   2194   /* Spill the live-across set: everything cached except the kept (dead) args. */
   2195   nd_flush_all_except_kept_args(d, desc);
   2196   /* Place the scalar result. With ndt_result_reg_stable (aa64/rv64) the result
   2197    * is cached directly in the ABI result register after the call — no mov, no
   2198    * home store; feed the plan a placeholder dst (never applied, the post-call
   2199    * step always skips rets[0]) so no home slot is allocated for a result
   2200    * consumed before the next flush. Without it (x86-64: RAX is an implicit
   2201    * div/mul operand, so a result left there is clobbered before its consumer)
   2202    * claim a general cache register now and let the post-call move write the
   2203    * result into it. A non-cacheable result (aggregate / sret) keeps the home. */
   2204   int result_in_abi_reg = 0;
   2205   if (nresults) {
   2206     NativeDirectLocal* rl = nd_local(d, desc->result);
   2207     if (!nd_local_cacheable(d, rl)) {
   2208       results[0] = nd_loc_frame(d, desc->result, 0);
   2209     } else if (d->reg_info && d->reg_info->ndt_result_reg_stable) {
   2210       result_in_abi_reg = 1;
   2211       results[0] = nd_loc_reg(d, rl->type, (NativeAllocClass)rl->cls, 0);
   2212     } else {
   2213       Reg r = nd_cache_alloc(d, (NativeAllocClass)rl->cls);
   2214       if (r != REG_NONE) {
   2215         d->reg_owner[rl->cls][r] = desc->result;
   2216         rl->reg = r;
   2217         rl->dirty = 1;
   2218         nd_cache_link(d, desc->result);
   2219         /* Pin until the post-call move so the callee materialize cannot evict
   2220          * this not-yet-valid entry as a scratch victim. */
   2221         d->scratch_used[rl->cls] |= 1u << r;
   2222         results[0] = nd_loc_reg(d, rl->type, (NativeAllocClass)rl->cls, r);
   2223       } else {
   2224         results[0] = nd_loc_frame(d, desc->result, 0);
   2225       }
   2226     }
   2227   }
   2228   nd_barrier(d, NATIVE_DIRECT_BARRIER_CALL | NATIVE_DIRECT_BARRIER_MEMORY);
   2229   nd.fn_type = desc->fn_type;
   2230   nd.callee = nd_loc_operand(d, desc->callee);
   2231   if (nd.callee.kind == NATIVE_LOC_FRAME) {
   2232     callee_tmp = nd_materialize_loc(
   2233         d, nd.callee, (NativeAllocClass)nd.callee.cls, nd.callee.type);
   2234     nd.callee = callee_tmp;
   2235     release_callee_tmp = 1;
   2236   }
   2237   nd.args = args;
   2238   nd.results = results;
   2239   nd.nargs = desc->nargs;
   2240   nd.nresults = nresults;
   2241   nd.flags = desc->flags;
   2242   nd.tail_policy = desc->tail_policy;
   2243   nd.inline_policy = desc->inline_policy;
   2244 
   2245   if (d->ops && d->ops->marshal_call)
   2246     d->ops->marshal_call(d, &nd, &plan);
   2247   else {
   2248     ND_REQUIRE_NATIVE(d, marshal_call, "target does not marshal calls");
   2249     d->native->marshal_call(d->native, &nd, &plan);
   2250   }
   2251   if (plan.stack_arg_size > d->max_outgoing)
   2252     d->max_outgoing = plan.stack_arg_size;
   2253   for (u32 i = 0; i < plan.nargs; ++i)
   2254     nd_write_loc(d, plan.args[i].dst, plan.args[i].src, plan.args[i].mem);
   2255   if (d->ops && d->ops->emit_call)
   2256     d->ops->emit_call(d, &plan);
   2257   else {
   2258     ND_REQUIRE_NATIVE(d, emit_call, "target does not emit calls");
   2259     d->native->emit_call(d->native, &plan);
   2260   }
   2261   /* The call clobbered the caller-saved registers, so the (dead-after) args'
   2262    * cached values are stale: unpin and drop their entries without writing back.
   2263    * args[i] is a reg only for args that were sourced from the cache. */
   2264   for (u32 i = 0; i < desc->nargs; ++i) {
   2265     if (args[i].kind == NATIVE_LOC_REG)
   2266       d->scratch_used[args[i].cls] &= ~(1u << args[i].v.reg);
   2267     nd_invalidate_local(d, desc->args[i]);
   2268   }
   2269   /* Stable-result-register path: cache the result directly in the ABI result
   2270    * register the call left it in (plan.rets[0].src) and skip its home store.
   2271    * That register may now be an ordinary cache-pool member (x0 on aa64 is -O0
   2272    * allocable), but it is free here regardless: the live-across flush + the
   2273    * dead-arg invalidation above cleared it, and the guard below evicts any
   2274    * residual owner before claiming it. The next call's flush spills it if still
   2275    * live, or drops it if dead. */
   2276   u32 ret_start = 0;
   2277   if (result_in_abi_reg) {
   2278     NativeDirectLocal* rl = nd_local(d, desc->result);
   2279     NativeLoc src = plan.rets[0].src;
   2280     if (plan.nrets != 1u || src.kind != NATIVE_LOC_REG ||
   2281         (NativeAllocClass)src.cls != (NativeAllocClass)rl->cls)
   2282       nd_panic(d, "cacheable scalar result not returned in one register");
   2283     if (d->reg_owner[rl->cls][src.v.reg] != CG_LOCAL_NONE)
   2284       nd_invalidate_local(d, d->reg_owner[rl->cls][src.v.reg]);
   2285     d->reg_owner[rl->cls][src.v.reg] = desc->result;
   2286     rl->reg = src.v.reg;
   2287     rl->dirty = 1;
   2288     nd_cache_link(d, desc->result);
   2289     nd_touch_local(d, rl);
   2290     ret_start = 1u;
   2291   }
   2292   for (u32 i = ret_start; i < plan.nrets; ++i)
   2293     nd_write_loc(d, plan.rets[i].dst, plan.rets[i].src, plan.rets[i].mem);
   2294   /* Non-stable path: the result was pre-claimed in a general cache register and
   2295    * the move above wrote the ABI reg into it; unpin it so it behaves as an
   2296    * ordinary write-back entry. */
   2297   if (nresults && !result_in_abi_reg && results[0].kind == NATIVE_LOC_REG)
   2298     d->scratch_used[results[0].cls] &= ~(1u << results[0].v.reg);
   2299   if (release_callee_tmp)
   2300     nd_scratch_release(d, (NativeAllocClass)callee_tmp.cls, callee_tmp.v.reg);
   2301 }
   2302 
   2303 static const char* nd_tail_call_unrealizable_reason(CgTarget* t,
   2304                                                     const CGCallDesc* desc) {
   2305   NativeDirectTarget* d = nd_of(t);
   2306   if (d->ops && d->ops->tail_call_unrealizable_reason)
   2307     return d->ops->tail_call_unrealizable_reason(d, desc);
   2308   return "target does not expose direct tail-call lowering";
   2309 }
   2310 
   2311 static void nd_ret(CgTarget* t, CGLocal value) {
   2312   NativeDirectTarget* d = nd_of(t);
   2313   NativeLoc loc;
   2314   const NativeLoc* locp = NULL;
   2315   NativeCallPhaseRet* rets = NULL;
   2316   u32 nrets = 0;
   2317   /* The emit_ret ops path reads `value` from its home, so it needs the cache
   2318    * spilled first (no production arch installs it today). */
   2319   if (d->ops && d->ops->emit_ret) {
   2320     nd_flush_all(d);
   2321     if (value != CG_LOCAL_NONE) nd_home(d, value); /* arch reads value from home */
   2322     d->ops->emit_ret(d, value);
   2323     return;
   2324   }
   2325   /* Default path: at a return every cached value is dead (the frame is about to
   2326    * be torn down), so source the return value straight from its live register
   2327    * when cached and drop the cache without spilling. This removes the
   2328    * per-function `stur <result>,[home]; ldur x0,[home]` round-trip and the dead
   2329    * writebacks the old flush-all emitted. A non-cached value (multi-part /
   2330    * aggregate / memory-required) is already authoritative in its home. */
   2331   if (value != CG_LOCAL_NONE) {
   2332     NativeDirectLocal* l = nd_local(d, value);
   2333     loc = l->reg != REG_NONE
   2334               ? nd_loc_reg(d, l->type, (NativeAllocClass)l->cls, l->reg)
   2335               : nd_loc_frame(d, value, 0);
   2336     locp = &loc;
   2337   }
   2338   nd_drop_all(d);
   2339   ND_REQUIRE_NATIVE(d, marshal_ret, "target does not marshal returns");
   2340   d->native->marshal_ret(d->native, d->func, locp, &rets, &nrets);
   2341   for (u32 i = 0; i < nrets; ++i)
   2342     nd_write_loc(d, rets[i].dst, rets[i].src, rets[i].mem);
   2343   ND_REQUIRE_NATIVE(d, ret, "target does not emit returns");
   2344   d->native->ret(d->native);
   2345 }
   2346 
   2347 static void nd_unreachable(CgTarget* t) {
   2348   NativeDirectTarget* d = nd_of(t);
   2349   nd_flush_all(d);
   2350   ND_REQUIRE_NATIVE(d, trap, "target does not emit traps");
   2351   d->native->trap(d->native);
   2352 }
   2353 
   2354 static void nd_alloca(CgTarget* t, Operand dst, Operand size, u32 align) {
   2355   NativeDirectTarget* d = nd_of(t);
   2356   NativeLoc sr, dr;
   2357   nd_flush_all(d);
   2358   sr = nd_materialize_operand(d, size);
   2359   dr = nd_dst_scratch(d, dst);
   2360   ND_REQUIRE_NATIVE(d, alloca_, "target does not emit alloca");
   2361   d->native->alloca_(d->native, dr, sr, align);
   2362   nd_store_operand_from_reg(d, dst, dr);
   2363   nd_release_materialized(d, dr);
   2364   nd_release_materialized(d, sr);
   2365 }
   2366 
   2367 static void nd_va_start(CgTarget* t, Operand ap_addr) {
   2368   NativeDirectTarget* d = nd_of(t);
   2369   nd_flush_all(d);
   2370   /* The arch va_* / asm helpers read operand locals' homes directly; with lazy
   2371    * homing a never-spilled transient operand (or a fresh store-target temp) has
   2372    * no home yet, so materialize one before crossing into arch code. */
   2373   nd_home_operand(d, ap_addr);
   2374   if (!d->ops || !d->ops->va_start_)
   2375     nd_panic(d, "target does not emit va_start");
   2376   d->ops->va_start_(d, ap_addr);
   2377 }
   2378 
   2379 static void nd_va_arg(CgTarget* t, Operand dst, Operand ap_addr,
   2380                       KitCgTypeId type) {
   2381   NativeDirectTarget* d = nd_of(t);
   2382   nd_flush_all(d);
   2383   nd_home_operand(d, dst); /* fresh store-target temp: home it before arch reads it */
   2384   nd_home_operand(d, ap_addr);
   2385   if (!d->ops || !d->ops->va_arg_) nd_panic(d, "target does not emit va_arg");
   2386   d->ops->va_arg_(d, dst, ap_addr, type);
   2387 }
   2388 
   2389 static void nd_va_end(CgTarget* t, Operand ap_addr) {
   2390   NativeDirectTarget* d = nd_of(t);
   2391   nd_flush_all(d);
   2392   nd_home_operand(d, ap_addr);
   2393   if (!d->ops || !d->ops->va_end_) nd_panic(d, "target does not emit va_end");
   2394   d->ops->va_end_(d, ap_addr);
   2395 }
   2396 
   2397 static void nd_va_copy(CgTarget* t, Operand dst_ap_addr, Operand src_ap_addr) {
   2398   NativeDirectTarget* d = nd_of(t);
   2399   nd_flush_all(d);
   2400   nd_home_operand(d, dst_ap_addr);
   2401   nd_home_operand(d, src_ap_addr);
   2402   if (!d->ops || !d->ops->va_copy_) nd_panic(d, "target does not emit va_copy");
   2403   d->ops->va_copy_(d, dst_ap_addr, src_ap_addr);
   2404 }
   2405 
   2406 static void nd_atomic_load(CgTarget* t, Operand dst, Operand addr,
   2407                            MemAccess mem, KitCgMemOrder order) {
   2408   NativeDirectTarget* d = nd_of(t);
   2409   NdAddrTemps temps;
   2410   /* No value-cache flush (2c): the atomic accesses foreign memory, which cannot
   2411    * alias a cached non-escaped local; the arch barrier hook still orders it. */
   2412   nd_barrier(d, NATIVE_DIRECT_BARRIER_MEMORY | NATIVE_DIRECT_BARRIER_ATOMIC);
   2413   NativeAddr naddr =
   2414       nd_addr_materialize(d, nd_addr_pointer(d, addr), &temps, mem);
   2415   NativeLoc dr = nd_dst_scratch(d, dst);
   2416   ND_REQUIRE_NATIVE(d, atomic_load, "target does not emit atomic loads");
   2417   d->native->atomic_load(d->native, dr, naddr, mem, order);
   2418   nd_store_operand_from_reg(d, dst, dr);
   2419   nd_release_materialized(d, dr);
   2420   nd_addr_temps_release(d, &temps);
   2421 }
   2422 
   2423 static void nd_atomic_store(CgTarget* t, Operand addr, Operand src,
   2424                             MemAccess mem, KitCgMemOrder order) {
   2425   NativeDirectTarget* d = nd_of(t);
   2426   NdAddrTemps temps;
   2427   /* No value-cache flush (2c): foreign atomic memory cannot alias a cached
   2428    * non-escaped local. */
   2429   nd_barrier(d, NATIVE_DIRECT_BARRIER_MEMORY | NATIVE_DIRECT_BARRIER_ATOMIC);
   2430   NativeAddr naddr =
   2431       nd_addr_materialize(d, nd_addr_pointer(d, addr), &temps, mem);
   2432   NativeLoc sr = nd_materialize_operand(d, src);
   2433   ND_REQUIRE_NATIVE(d, atomic_store, "target does not emit atomic stores");
   2434   d->native->atomic_store(d->native, naddr, sr, mem, order);
   2435   nd_release_materialized(d, sr);
   2436   nd_addr_temps_release(d, &temps);
   2437 }
   2438 
   2439 static void nd_atomic_rmw(CgTarget* t, KitCgAtomicOp op, Operand dst,
   2440                           Operand addr, Operand val, MemAccess mem,
   2441                           KitCgMemOrder order) {
   2442   NativeDirectTarget* d = nd_of(t);
   2443   NdAddrTemps temps;
   2444   /* No value-cache flush (2c): foreign atomic memory cannot alias a cached
   2445    * non-escaped local. */
   2446   nd_barrier(d, NATIVE_DIRECT_BARRIER_MEMORY | NATIVE_DIRECT_BARRIER_ATOMIC);
   2447   NativeAddr naddr =
   2448       nd_addr_materialize(d, nd_addr_pointer(d, addr), &temps, mem);
   2449   NativeLoc vr = nd_materialize_operand(d, val);
   2450   NativeLoc dr = nd_dst_scratch(d, dst);
   2451   ND_REQUIRE_NATIVE(d, atomic_rmw, "target does not emit atomic rmw");
   2452   d->native->atomic_rmw(d->native, op, dr, naddr, vr, mem, order);
   2453   nd_store_operand_from_reg(d, dst, dr);
   2454   nd_release_materialized(d, dr);
   2455   nd_release_materialized(d, vr);
   2456   nd_addr_temps_release(d, &temps);
   2457 }
   2458 
   2459 static void nd_atomic_cas(CgTarget* t, Operand prior, Operand ok, Operand addr,
   2460                           Operand expected, Operand desired, MemAccess mem,
   2461                           KitCgMemOrder success, KitCgMemOrder failure) {
   2462   NativeDirectTarget* d = nd_of(t);
   2463   NdAddrTemps temps;
   2464   /* No value-cache flush (2c): foreign atomic memory cannot alias a cached
   2465    * non-escaped local. */
   2466   nd_barrier(d, NATIVE_DIRECT_BARRIER_MEMORY | NATIVE_DIRECT_BARRIER_ATOMIC);
   2467   NativeAddr naddr =
   2468       nd_addr_materialize(d, nd_addr_pointer(d, addr), &temps, mem);
   2469   NativeLoc er = nd_materialize_operand(d, expected);
   2470   NativeLoc dr = nd_materialize_operand(d, desired);
   2471   NativeLoc pr = nd_dst_scratch(d, prior);
   2472   NativeLoc kr = nd_dst_scratch(d, ok);
   2473   ND_REQUIRE_NATIVE(d, atomic_cas,
   2474                     "target does not emit atomic compare-exchange");
   2475   d->native->atomic_cas(d->native, pr, kr, naddr, er, dr, mem, success,
   2476                         failure);
   2477   nd_store_operand_from_reg(d, prior, pr);
   2478   nd_store_operand_from_reg(d, ok, kr);
   2479   nd_release_materialized(d, kr);
   2480   nd_release_materialized(d, pr);
   2481   nd_release_materialized(d, dr);
   2482   nd_release_materialized(d, er);
   2483   nd_addr_temps_release(d, &temps);
   2484 }
   2485 
   2486 static void nd_fence(CgTarget* t, KitCgMemOrder order) {
   2487   NativeDirectTarget* d = nd_of(t);
   2488   /* No value-cache flush (2c): a fence orders memory visible to other agents; a
   2489    * cached local is non-escaped, so no other agent can observe its deferred home
   2490    * write. The arch fence instruction is still emitted. */
   2491   ND_REQUIRE_NATIVE(d, fence, "target does not emit fences");
   2492   d->native->fence(d->native, order);
   2493 }
   2494 
   2495 static void nd_intrinsic(CgTarget* t, IntrinKind kind, Operand* dsts, u32 ndst,
   2496                          const Operand* args, u32 narg) {
   2497   NativeDirectTarget* d = nd_of(t);
   2498   NativeLoc* ndsts = nd_loc_buf(d, d->retbuf, ND_RET_BUF, ndst);
   2499   NativeLoc* nargs = nd_loc_buf(d, d->argbuf, ND_ARG_BUF, narg);
   2500   nd_flush_all(d);
   2501   ND_REQUIRE_NATIVE(d, intrinsic, "target does not emit compiler intrinsics");
   2502   for (u32 i = 0; i < ndst; ++i) ndsts[i] = nd_dst_scratch(d, dsts[i]);
   2503   for (u32 i = 0; i < narg; ++i) {
   2504     nargs[i] = args[i].kind == OPK_IMM ? nd_loc_operand(d, args[i])
   2505                                        : nd_materialize_operand(d, args[i]);
   2506   }
   2507   d->native->intrinsic(d->native, kind, ndsts, ndst, nargs, narg);
   2508   for (u32 i = 0; i < ndst; ++i) {
   2509     nd_store_operand_from_reg(d, dsts[i], ndsts[i]);
   2510     nd_release_materialized(d, ndsts[i]);
   2511   }
   2512   for (u32 i = 0; i < narg; ++i) nd_release_materialized(d, nargs[i]);
   2513 }
   2514 
   2515 static void nd_asm_block(CgTarget* t, const char* tmpl,
   2516                          const AsmConstraint* outs, u32 nout, Operand* out_ops,
   2517                          const AsmConstraint* ins, u32 nin,
   2518                          const Operand* in_ops, const Sym* clobbers, u32 nclob,
   2519                          u32 clobber_abi_sets) {
   2520   NativeDirectTarget* d = nd_of(t);
   2521   nd_flush_all(d);
   2522   nd_barrier(d,
   2523              NATIVE_DIRECT_BARRIER_INLINE_ASM | NATIVE_DIRECT_BARRIER_MEMORY);
   2524   /* Outputs (incl. fresh reg-constraint store-targets) and inputs are bound by
   2525    * the arch helper reading their homes directly; materialize lazy homes now. */
   2526   for (u32 i = 0; i < nout; ++i) nd_home_operand(d, out_ops[i]);
   2527   for (u32 i = 0; i < nin; ++i) nd_home_operand(d, in_ops[i]);
   2528   if (d->ops && d->ops->asm_block) {
   2529     d->ops->asm_block(d, tmpl, outs, nout, out_ops, ins, nin, in_ops, clobbers,
   2530                       nclob, clobber_abi_sets);
   2531     return;
   2532   }
   2533   nd_panic(d, "target does not emit inline asm");
   2534 }
   2535 
   2536 static int nd_asm_is_reg_constraint(CgTarget* t, const char* constraint) {
   2537   NativeDirectTarget* d = nd_of(t);
   2538   return native_asm_constraint_is_reg(d->native, constraint);
   2539 }
   2540 
   2541 static void nd_file_scope_asm(CgTarget* t, const char* src, size_t len) {
   2542   NativeDirectTarget* d = nd_of(t);
   2543   ND_REQUIRE_NATIVE(d, file_scope_asm, "target does not emit file-scope asm");
   2544   d->native->file_scope_asm(d->native, src, len);
   2545 }
   2546 
   2547 static void nd_set_loc(CgTarget* t, SrcLoc loc) {
   2548   NativeDirectTarget* d = nd_of(t);
   2549   d->loc = loc;
   2550   if (d->native && d->native->set_loc) d->native->set_loc(d->native, loc);
   2551 }
   2552 
   2553 static void nd_finalize(CgTarget* t) {
   2554   NativeDirectTarget* d = nd_of(t);
   2555   if (d->native && d->native->finalize) d->native->finalize(d->native);
   2556 }
   2557 
   2558 static void nd_destroy(CgTarget* t) {
   2559   NativeDirectTarget* d = nd_of(t);
   2560   if (d->native && d->native->destroy) d->native->destroy(d->native);
   2561 }
   2562 
   2563 CgTarget* native_direct_target_new(Compiler* c, ObjBuilder* obj,
   2564                                    const NativeDirectTargetConfig* cfg) {
   2565   NativeDirectTarget* d;
   2566   if (!c || !cfg || !cfg->native)
   2567     compiler_panic(c, (SrcLoc){0, 0, 0},
   2568                    "native_direct_target_new: missing native target");
   2569   d = arena_znew(c->tu, NativeDirectTarget);
   2570   if (!d) return NULL;
   2571   d->base.c = c;
   2572   d->base.obj = obj;
   2573   d->magic = NATIVE_DIRECT_MAGIC;
   2574   d->native = cfg->native;
   2575   d->ops = cfg->ops;
   2576   d->user = cfg->user;
   2577 
   2578   /* Resolve register/class info once; it is constant for the program. */
   2579   d->reg_info = cfg->native ? cfg->native->regs : NULL;
   2580   native_reg_info_validate(c, d->reg_info);
   2581   for (u32 i = 0; i < NATIVE_REG_CLASS_COUNT; ++i) d->class_info[i] = NULL;
   2582   if (d->reg_info) {
   2583     const NativeRegInfo* ri = d->reg_info;
   2584     for (u32 i = 0; i < ri->nclasses; ++i) {
   2585       u32 cls = ri->classes[i].cls;
   2586       if (cls < NATIVE_REG_CLASS_COUNT) d->class_info[cls] = &ri->classes[i];
   2587     }
   2588   }
   2589 
   2590   /* Precompute, per class, the -O0 value-cache register pool (in allocable
   2591    * order), constant for the program. On ndt_caller_saved_only targets the pool
   2592    * is the caller-saved subset of allocable (the cache then never forces a
   2593    * callee-save spill into the deferred prologue); otherwise it is the full
   2594    * allocable list, with any callee-save in it reported to the backend on use
   2595    * (nd_note_reg_used) and reserved by reserve_callee_saves. */
   2596   {
   2597     int caller_only = d->reg_info && d->reg_info->ndt_caller_saved_only;
   2598     for (u32 cls = 0; cls < NATIVE_REG_CLASS_COUNT; ++cls) {
   2599       const NativeAllocClassInfo* ci = d->class_info[cls];
   2600       u32 mask, n = 0;
   2601       if (!ci) continue;
   2602       mask = nd_caller_saved_mask(d, (NativeAllocClass)cls);
   2603       for (u32 i = 0; i < ci->ndt_allocable_count; ++i) {
   2604         Reg r = ci->ndt_allocable[i];
   2605         if (r >= 32u) continue;
   2606         if (caller_only && !(mask & (1u << r))) continue;
   2607         d->cache_pool[cls][n++] = r;
   2608       }
   2609       d->ncache_pool[cls] = n;
   2610     }
   2611   }
   2612 
   2613   d->base.func_begin = nd_func_begin;
   2614   d->base.func_end = nd_func_end;
   2615   d->base.alias = nd_alias;
   2616   d->base.local = nd_local_new;
   2617   d->base.local_addr = nd_local_addr;
   2618   d->base.param = nd_param;
   2619   d->base.local_debug_loc = nd_local_debug_loc;
   2620   d->base.reclaim_temps = nd_reclaim_temps;
   2621   d->base.label_new = nd_label_new;
   2622   d->base.label_place = nd_label_place;
   2623   d->base.jump = nd_jump;
   2624   d->base.cmp_branch = nd_cmp_branch;
   2625   d->base.switch_ = nd_switch;
   2626   d->base.indirect_branch = nd_indirect_branch;
   2627   d->base.load_label_addr = nd_load_label_addr;
   2628   d->base.local_static_data_begin = nd_local_static_data_begin;
   2629   d->base.local_static_data_write = nd_local_static_data_write;
   2630   d->base.local_static_data_label_addr = nd_local_static_data_label_addr;
   2631   d->base.local_static_data_end = nd_local_static_data_end;
   2632   d->base.data_label_addr_unsupported_msg = nd_data_label_addr_unsupported_msg;
   2633   d->base.scope_begin = nd_scope_begin;
   2634   d->base.scope_end = nd_scope_end;
   2635   d->base.break_to = nd_break_to;
   2636   d->base.continue_to = nd_continue_to;
   2637   d->base.load_imm = nd_load_imm;
   2638   d->base.load_const = nd_load_const;
   2639   d->base.copy = nd_copy;
   2640   d->base.load = nd_load;
   2641   d->base.store = nd_store;
   2642   d->base.addr_of = nd_addr_of;
   2643   d->base.tls_addr_of = nd_tls_addr_of;
   2644   d->base.copy_bytes = nd_copy_bytes;
   2645   d->base.set_bytes = nd_set_bytes;
   2646   d->base.binop = nd_binop;
   2647   d->base.unop = nd_unop;
   2648   d->base.cmp = nd_cmp;
   2649   d->base.convert = nd_convert;
   2650   d->base.untyped_values = 1; /* register-resident values: no-op bitcasts elide */
   2651   d->base.call = nd_call;
   2652   d->base.tail_call_unrealizable_reason = nd_tail_call_unrealizable_reason;
   2653   d->base.ret = nd_ret;
   2654   d->base.unreachable = nd_unreachable;
   2655   d->base.alloca_ = nd_alloca;
   2656   d->base.va_start_ = nd_va_start;
   2657   d->base.va_arg_ = nd_va_arg;
   2658   d->base.va_end_ = nd_va_end;
   2659   d->base.va_copy_ = nd_va_copy;
   2660   d->base.atomic_load = nd_atomic_load;
   2661   d->base.atomic_store = nd_atomic_store;
   2662   d->base.atomic_rmw = nd_atomic_rmw;
   2663   d->base.atomic_cas = nd_atomic_cas;
   2664   d->base.fence = nd_fence;
   2665   d->base.intrinsic = nd_intrinsic;
   2666   d->base.asm_is_reg_constraint = nd_asm_is_reg_constraint;
   2667   d->base.asm_block = nd_asm_block;
   2668   d->base.file_scope_asm = nd_file_scope_asm;
   2669   d->base.set_loc = nd_set_loc;
   2670   d->base.finalize = nd_finalize;
   2671   d->base.destroy = nd_destroy;
   2672   return &d->base;
   2673 }
   2674 
   2675 NativeTarget* native_direct_target_native(CgTarget* t) {
   2676   NativeDirectTarget* d = t ? nd_of(t) : NULL;
   2677   return d && d->magic == NATIVE_DIRECT_MAGIC ? d->native : NULL;
   2678 }
   2679 
   2680 CgTarget* native_direct_backend_make(Compiler* c, ObjBuilder* o,
   2681                                      const KitCodeOptions* opts,
   2682                                      NativeTargetCtor ctor,
   2683                                      const NativeOps* ops) {
   2684   MCEmitter* mc = NULL;
   2685   Debug* debug = NULL;
   2686   CgTarget* t;
   2687   NativeTarget* native;
   2688   NativeDirectTargetConfig cfg;
   2689   if (cg_mc_debug_new(c, o, opts, &mc, &debug) != KIT_OK) return NULL;
   2690   native = ctor(c, o, mc);
   2691   if (!native) return NULL;
   2692   if (opts) native->disabled_backend_features = opts->disabled_backend_features;
   2693   memset(&cfg, 0, sizeof cfg);
   2694   cfg.native = native;
   2695   cfg.ops = ops;
   2696   t = native_direct_target_new(c, o, &cfg);
   2697   if (t) t->debug = debug;
   2698   return t;
   2699 }
   2700 
   2701 CgTarget* native_direct_semantic_target_new(Compiler* c, ObjBuilder* o,
   2702                                             MCEmitter* mc,
   2703                                             NativeTargetCtor ctor,
   2704                                             const NativeOps* ops) {
   2705   NativeTarget* native;
   2706   NativeDirectTargetConfig cfg;
   2707   if (!mc) mc = mc_new(c, o);
   2708   native = ctor(c, o, mc);
   2709   if (!native) return NULL;
   2710   memset(&cfg, 0, sizeof cfg);
   2711   cfg.native = native;
   2712   cfg.ops = ops;
   2713   return native_direct_target_new(c, o, &cfg);
   2714 }