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 }