code wiki / (root) / nx_x86_regalloc.nx

nx_x86_regalloc.nx source

↩ module page · 1833 lines · 84573 B

1// nx_x86_regalloc.nx -- genuine x86_64 register allocation (G1 keystone). 2// 3// FRESH, x86_-prefixed, self-host-isolated: re-implements the proven IDEAS of 4// nx_regalloc.nx (Interval shape, def-to-last-use scan, mark_crosses_call, 5// ValueLoc lowering) with ZERO shared symbols and NO `import "nx_regalloc.nx"`, 6// because regalloc_function is on the live RISC-V self-host bootstrap host 7// (nx_nxc.nx:139, nx_main.nx:64) that compiles the x86 compiler -- re-binding a 8// shared name there would corrupt the host. See docs/NX_G1_X86_REGALLOC_PLAN. 9// 10// ALLOCATE-not-COPY: a homed value LIVES in a callee-saved GPR for its whole 11// live range (producing op computes straight into it -- zero copy), unlike the 12// reverted K=1 cache (movq %reg,%rbx per result, +2018 insns -> net-negative). 13// 14// Home pool = the 5 SysV callee-saved GPRs {r12,r13,r14,r15,rbx} (home idx 15// 0..4) -- none used as scratch/operand/arg/clobber by the x86 backend, so they 16// survive every CALL/SYSCALL/TAIL_CALL/clone with no save-around-call logic. 17// FIX-12 INVARIANT: never add rcx/rdx/rbp/r10/r11 to this pool. 18// 19// THIS IS STEP 1b: full interval analysis (build_intervals + mark_crosses_call 20// + single_block) RUNS over every function -- crash-tested by the self-host 21// fixpoint over the whole corpus -- but its result is DISCARDED: every value 22// lowers to {VL_SPILLED,-1}, mask 0, so emitted code is byte-identical to the 23// pre-regalloc backend. Home selection + emit-path retarget land in STEP 2. 24 25import "nx_syscalls.nx" 26import "nx_types.nx" 27 28// ----- home register pool (SysV callee-saved GPRs) ----------------- 29const X86_HOME_CAP: i64 = 5 // {r12,r13,r14,r15,rbx} 30// ----- caller-saved pool (arc stage 2) ----------------------------- 31// {rsi,rdi,r8,r9} = home idx 5..8. Only for crosses_call==0 intervals in 32// clobber-intrinsic-free functions; NOT saved in prologue. 33// STAGE 2+3 EXPERIMENTS (2026-07-15) -- BOTH gated OFF (byte-identical to 34// blessed, NO regression). PROVEN by measurement, both crypto-CORRECT (gauntlet 35// 19/19 + fixpoint + bit-exact vs gcc/clang): 36// - CALLER_POOL (single-block, +4 regs): PERF-NEUTRAL (matmul's pressure is 37// cross-block, not single-block temps). 38// - ALLOW_CROSSBLOCK (relax single_block): REGRESSES matmul 4.7x->5.1x (+~6%) 39// but IMPROVES fnv 0.97x->0.93x. Root: this allocator gives each homed value 40// a DEDICATED reg for its whole [start,end]; a cross-block value (i*N) then 41// ties up a reg across the blocks between def and use where it's unused, 42// starving the k-loop. Crude whole-range homing wastes registers. 43// CONCLUSION: the matmul win needs a REAL allocator (loop-weighted spill cost + 44// live-interval mgmt with holes/splitting), NOT these flags. See 45// project-nishi-nxcc-register-allocation-arc-2026-07-15 "STAGE 4 = the rewrite". 46const X86_CALLER_POOL_ENABLE: i64 = 1 47const X86_ALLOW_CROSSBLOCK: i64 = 1 48// Caller-saved is only worth its home-move overhead for DEEP-loop values. A 49// call-free value may take a caller-saved reg only if wcost >= this (=~ a use at 50// loop depth 2+, 16^2). Keeps tight scalar loops (fnv/divpow2, depth 1) on the 51// baseline callee-only path while matmul's depth-3 k-loop temps (wcost ~4096) 52// still get the extra registers. Callee-saved keeps the lower X86_HOME_MIN_USES. 53const X86_CALLER_MIN_WCOST: i64 = 256 54// NEGATIVE CONTROL: 1 = let cross-call SSA values take caller-saved regs (rsi/rdi/ 55// r8/r9) that the call clobbers -> the gauntlet register-survival KAT + crypto 56// KATs MUST go RED. Proves the crosses_call gate is load-bearing. Ships at 0. 57const X86_NEGCTL_CALLER_IGNORE_CALL: i64 = 0 58const X86_CALLER_BASE: i64 = 5 59const X86_CALLER_CAP: i64 = 4 // idx 5,6,7,8 60// Caller-saved homes have NO save/restore cost, so even a single-use value wins 61// (avoids a spill store + a reload). Lower than X86_HOME_MIN_USES(3), which only 62// existed to amortize the callee-saved prologue push/pop. 63const X86_CALLER_MIN_USES: i64 = 1 64// G20 SLACK ADMISSION -- ATTEMPTED 2026-07-16, REFUTED BY MEASUREMENT, REVERTED. 65// Homing in-loop temps (div results + non-adjacent single-use) into idle caller 66// regs converted divpow2's 12 spill/reload into +7 copies (39->38, ~nothing) 67// AND regressed collatz 17->18 + fnv 13->14: a home INSERTS a movq exactly 68// where the G1/G4/G16 rax-forwarding was already zero-cost, and no static 69// adjacency test can see emitter forwarding state (phase order). The honest 70// fix is the STAGE-4 allocator rewrite; until then, EXTEND forwarding 71// (commute-consume, cmov-dance) instead of blanket-homing. Arc memo rung 26. 72 73func x86_home_reg_name(idx: i64) -> *u8 { 74 if idx == 0 { return "r12" as *u8 } 75 if idx == 1 { return "r13" as *u8 } 76 if idx == 2 { return "r14" as *u8 } 77 if idx == 3 { return "r15" as *u8 } 78 if idx == 4 { return "rbx" as *u8 } 79 // CALLER-SAVED pool (2026-07-15, reg-alloc arc stage 2): home idx 5..8. 80 // Assigned ONLY to intervals with crosses_call==0 in functions with no 81 // GP-clobbering intrinsic -> never live across a call/intrinsic that would 82 // clobber them -> need NO prologue save (kept OUT of used_cs_mask). Verified 83 // the common emit path (load/store/binop) never touches these; they appear 84 // only in intrinsics (thread-clone=call-class, f32x8-dot, byte-shuffle, 85 // mul256) and _start. rdx/rcx/r10/r11 stay scratch (FIX-12). 86 if idx == 5 { return "rsi" as *u8 } 87 if idx == 6 { return "rdi" as *u8 } 88 if idx == 7 { return "r8" as *u8 } 89 if idx == 8 { return "r9" as *u8 } 90 return 0 as *u8 91} 92 93// Compare two short register-name strings (<= 7 chars + NUL). 1 = equal. 94func x86_reg_eq(a: *u8, b: *u8) -> i64 { 95 var i: i64 = 0 96 while i < 8 { 97 if a[i] != b[i] { return 0 } 98 if a[i] == 0 { return 1 } 99 i = i + 1 100 } 101 return 1 102} 103 104// FIX-6: COMPACTED save offset, keyed on the running slot_index 0..n_saved-1 105// (ascending mask-bit order), NOT on the home index -- ONE scheme for save AND 106// restore so a non-contiguous home subset still maps cleanly. 107func x86_home_save_off(slot_index: i64) -> i64 { 108 return 0 - (8 * (slot_index + 1)) 109} 110 111func x86_popcount(mask: i64) -> i64 { 112 var n: i64 = 0 113 var m: i64 = mask 114 var b: i64 = 0 115 while b < X86_HOME_CAP { 116 if (m & 1) == 1 { n = n + 1 } 117 m = m >> 1 118 b = b + 1 119 } 120 return n 121} 122 123// ----- per-value live interval (x86-local; 7 i64 = 56 bytes) ------- 124struct X86Interval { 125 v: i64, 126 start: i64, 127 end: i64, 128 reg: i64, // home index 0..4, or -1 129 slot: i64, 130 crosses_call: i64, 131 single_block: i64, // 1 iff every def/use of v lives in one basic block 132 use_count: i64, // # of operand appearances (reload-density proxy) 133} 134 135func x86_intv_at(buf: *X86Interval, id: i64) -> *X86Interval { 136 let base: i64 = buf as i64 137 return (base + id * 64) as *X86Interval 138} 139 140// value/block accessors (own copies -- NOT the ctx's, which is imported AFTER us) 141func x86_val_at(f: *Function, id: i64) -> *Value { 142 let base: i64 = f.values as i64 143 return (base + id * 48) as *Value // Value = 48 bytes 144} 145func x86_block_at(f: *Function, id: i64) -> *BasicBlock { 146 let base: i64 = f.blocks as i64 147 return (base + id * 96) as *BasicBlock // BasicBlock = 96 bytes 148} 149 150// k-th operand slot (op0..op15). 151func x86_operand_k(inst: *Instr, k: i64) -> i64 { 152 if k == 0 { return inst.op0 } 153 if k == 1 { return inst.op1 } 154 if k == 2 { return inst.op2 } 155 if k == 3 { return inst.op3 } 156 if k == 4 { return inst.op4 } 157 if k == 5 { return inst.op5 } 158 if k == 6 { return inst.op6 } 159 if k == 7 { return inst.op7 } 160 if k == 8 { return inst.op8 } 161 if k == 9 { return inst.op9 } 162 if k == 10 { return inst.op10 } 163 if k == 11 { return inst.op11 } 164 if k == 12 { return inst.op12 } 165 if k == 13 { return inst.op13 } 166 if k == 14 { return inst.op14 } 167 if k == 15 { return inst.op15 } 168 if k == 16 { return inst.op16 } 169 if k == 17 { return inst.op17 } 170 if k == 18 { return inst.op18 } 171 if k == 19 { return inst.op19 } 172 if k == 20 { return inst.op20 } 173 if k == 21 { return inst.op21 } 174 if k == 22 { return inst.op22 } 175 if k == 23 { return inst.op23 } 176 return 0 - 1 177} 178 179// FIX-3: how many LEADING operands are real Value ids (not block ids). 180// OP_BR's operand is a block id; OP_BR_COND is (cond_value, true_bb, false_bb) 181// -> only op0 is a Value. Everything else: op0..op(n_operands-1), capped at 24. 182func x86_n_value_operands(inst: *Instr) -> i64 { 183 if inst.op == OP_BR { return 0 } 184 if inst.op == OP_BR_COND { return 1 } 185 let n: i64 = inst.n_operands 186 if n > 24 { return 24 } 187 if n < 0 { return 0 } 188 return n 189} 190 191// True for caller-saved-clobbering ops whose value operands must be flagged 192// not-homeable in cut 1 (FIX-3/FIX-5): CALL, TAIL_CALL, SYSCALL, THREAD_CLONE. 193func x86_is_call_class(op: i64) -> i64 { 194 if op == OP_CALL { return 1 } 195 if op == OP_TAIL_CALL { return 1 } 196 if op == OP_SYSCALL { return 1 } 197 if op == OP_THREAD_CLONE { return 1 } 198 if op == OP_CALL_INDIRECT { return 1 } 199 return 0 200} 201 202func x86_touch_bb(first_bb: *i64, last_bb: *i64, v: i64, bi: i64) -> i64 { 203 if first_bb[v] < 0 { first_bb[v] = bi } 204 last_bb[v] = bi // blocks scanned ascending -> max 205 return 0 206} 207 208// ===== SOTA allocator core (2026-07-15): loop-depth-weighted spill cost ===== 209// The matmul win: rank homing candidates by DYNAMIC hotness (uses weighted by 210// loop nesting depth) so the deep-loop values (s,k,i*N at depth 3) beat cold 211// setup temps (depth 0) for the scarce registers. Without this, ranking by raw 212// textual use_count homes cold high-count values and spills the hot loop-carried 213// ones. Loop depth is a back-edge heuristic (SAFE: only affects PRIORITY, never 214// correctness -- a wrong depth = suboptimal alloc, never a miscompile). 215 216// Loop nesting depth per block. A back-edge = a terminator branch to a block 217// with id <= the source (RPO-ish block order); its body = [tid, bi] gets +1. 218func x86_compute_loop_depth(f: *Function, depth: *i64) -> i64 { 219 var bi: i64 = 0 220 while bi < f.n_blocks { depth[bi] = 0; bi = bi + 1 } 221 bi = 0 222 while bi < f.n_blocks { 223 let b: *BasicBlock = x86_block_at(f, bi) 224 let t: *Instr = b.tail 225 if t != (0 as *Instr) { 226 var nt: i64 = 0 227 var t0: i64 = 0 - 1 228 var t1: i64 = 0 - 1 229 if t.op == OP_BR { t0 = t.op0; nt = 1 } 230 if t.op == OP_BR_COND { t0 = t.op1; t1 = t.op2; nt = 2 } 231 var ti: i64 = 0 232 while ti < nt { 233 var tid: i64 = t0 234 if ti == 1 { tid = t1 } 235 if tid >= 0 { if tid <= bi { 236 var j: i64 = tid 237 while j <= bi { depth[j] = depth[j] + 1; j = j + 1 } 238 } } 239 ti = ti + 1 240 } 241 } 242 bi = bi + 1 243 } 244 return 0 245} 246 247// 16^depth, capped at depth 10 (=2^40, safe in i64). Each loop level ~16x. 248func x86_weight_for_depth(d: i64) -> i64 { 249 var dd: i64 = d 250 if dd > 10 { dd = 10 } 251 return 1 << (dd * 4) 252} 253 254// wcost[v] = Σ over every operand appearance of v of 16^(loop depth of that use's 255// block). This is the spill-cost proxy that ranks homing candidates. 256func x86_compute_wcost(f: *Function, depth: *i64, wcost: *i64) -> i64 { 257 let n: i64 = f.n_values 258 var v: i64 = 0 259 while v < n { wcost[v] = 0; v = v + 1 } 260 var bi: i64 = 0 261 while bi < f.n_blocks { 262 let w: i64 = x86_weight_for_depth(depth[bi]) 263 let b: *BasicBlock = x86_block_at(f, bi) 264 var inst: *Instr = b.head 265 while inst != (0 as *Instr) { 266 let nv: i64 = x86_n_value_operands(inst) 267 var k: i64 = 0 268 while k < nv { 269 let u: i64 = x86_operand_k(inst, k) 270 if u >= 0 { if u < n { wcost[u] = wcost[u] + w } } 271 k = k + 1 272 } 273 inst = inst.next 274 } 275 bi = bi + 1 276 } 277 return 0 278} 279 280// ----- interval construction (def-to-last-use + single_block) ------ 281// Mirrors nx_regalloc.nx:175-267 with FIX-3 (opcode-aware operands), FIX-4 282// (independent single_block scan), FIX-5 (all call-class sites), FIX-8 (bounds). 283func x86_build_intervals(f: *Function, intv: *X86Interval, 284 calls: *i64, n_calls_ptr: *i64, any_call: *i64, 285 bb_start: *i64, bb_end: *i64) -> i64 { 286 let n: i64 = f.n_values 287 var v: i64 = 0 288 while v < n { 289 let iv: *X86Interval = x86_intv_at(intv, v) 290 iv.v = v 291 iv.start = 0 - 1 292 iv.end = 0 - 1 293 iv.reg = 0 - 1 294 iv.slot = 0 - 1 295 iv.crosses_call = 0 296 iv.single_block = 0 297 iv.use_count = 0 298 any_call[v] = 0 299 v = v + 1 300 } 301 // Parameters are live from index 0. 302 v = 0 303 while v < n { 304 let vv: *Value = x86_val_at(f, v) 305 if vv.kind == VK_PARAM { 306 let iv: *X86Interval = x86_intv_at(intv, v) 307 iv.start = 0 308 iv.end = 0 309 } 310 v = v + 1 311 } 312 313 // Pass 1: linear def-to-last-use, call-site recording, bb ranges. 314 var idx: i64 = 1 315 var bi: i64 = 0 316 var n_calls: i64 = 0 317 while bi < f.n_blocks { 318 let b: *BasicBlock = x86_block_at(f, bi) 319 bb_start[bi] = idx 320 var inst: *Instr = b.head 321 while inst != (0 as *Instr) { 322 // Def: the instruction's (non-void) result. 323 if inst.ty != (0 as *Type) { 324 if inst.ty.kind != 0 { 325 let r: i64 = inst.result 326 if r >= 0 { if r < n { 327 let rv: *X86Interval = x86_intv_at(intv, r) 328 if rv.start < 0 { rv.start = idx } 329 if rv.end < idx { rv.end = idx } 330 } } 331 } 332 } 333 // Uses: opcode-aware value operands (FIX-3), bounds-guarded (FIX-8). 334 let nv: i64 = x86_n_value_operands(inst) 335 var k: i64 = 0 336 while k < nv { 337 let u: i64 = x86_operand_k(inst, k) 338 if u >= 0 { if u < n { 339 let uv: *X86Interval = x86_intv_at(intv, u) 340 if uv.start < 0 { uv.start = idx } 341 if uv.end < idx { uv.end = idx } 342 uv.use_count = uv.use_count + 1 343 } } 344 k = k + 1 345 } 346 // Call-class sites (FIX-5) + flag their value operands (FIX-3). 347 if x86_is_call_class(inst.op) == 1 { 348 calls[n_calls] = idx 349 n_calls = n_calls + 1 350 var ck: i64 = 0 351 while ck < nv { 352 let cu: i64 = x86_operand_k(inst, ck) 353 if cu >= 0 { if cu < n { any_call[cu] = 1 } } 354 ck = ck + 1 355 } 356 } 357 idx = idx + 1 358 inst = inst.next 359 } 360 bb_end[bi] = idx - 1 361 bi = bi + 1 362 } 363 *n_calls_ptr = n_calls 364 365 // Pass 2 (FIX-4): single_block via an INDEPENDENT full op0..op(nv-1) scan of 366 // every instruction -- NOT derived from the (possibly truncated) use record. 367 let fb_raw: *u8 = sys_mmap(n * 8 + 16) 368 let first_bb: *i64 = fb_raw as *i64 369 let lb_raw: *u8 = sys_mmap(n * 8 + 16) 370 let last_bb: *i64 = lb_raw as *i64 371 v = 0 372 while v < n { first_bb[v] = 0 - 1; last_bb[v] = 0 - 1; v = v + 1 } 373 bi = 0 374 while bi < f.n_blocks { 375 let b: *BasicBlock = x86_block_at(f, bi) 376 var inst: *Instr = b.head 377 while inst != (0 as *Instr) { 378 if inst.ty != (0 as *Type) { 379 if inst.ty.kind != 0 { 380 let r: i64 = inst.result 381 if r >= 0 { if r < n { x86_touch_bb(first_bb, last_bb, r, bi) } } 382 } 383 } 384 let nv2: i64 = x86_n_value_operands(inst) 385 var k2: i64 = 0 386 while k2 < nv2 { 387 let u2: i64 = x86_operand_k(inst, k2) 388 if u2 >= 0 { if u2 < n { x86_touch_bb(first_bb, last_bb, u2, bi) } } 389 k2 = k2 + 1 390 } 391 inst = inst.next 392 } 393 bi = bi + 1 394 } 395 v = 0 396 while v < n { 397 let iv: *X86Interval = x86_intv_at(intv, v) 398 if first_bb[v] >= 0 { 399 if first_bb[v] == last_bb[v] { iv.single_block = 1 } 400 } 401 v = v + 1 402 } 403 return idx 404} 405 406// ===== BACK-EDGE INTERVAL EXTENSION (2026-07-15, STOP-SHIP fix) ====== 407// ROOT CAUSE of the br_table-zeroing miscompile (found by S18 via the wat 408// lane; bisected to R10): build_intervals' [start,end] is def-to-last-use in 409// LINEAR order, but a value live ACROSS A BACK-EDGE (def before/hoisted out 410// of a loop, used inside it) is live through the WHOLE loop body at runtime 411// -- including CALLS positioned after its last linear use. crosses_call then 412// under-counts -> the value gets a CALLER-SAVED reg -> the uncounted call 413// clobbers it -> next iteration reads garbage/zero. The old single_block gate 414// masked exactly this; R10's cross-block relaxation exposed it. FIX: for 415// every backward branch bi -> tid, any interval that overlaps the loop span 416// [bb_start[tid], bb_end[bi]] but ends inside it is EXTENDED to the loop end, 417// so crosses_call sees every call in the loop. Conservative: extension can 418// only DENY caller-saved (push a value to callee-saved/spill), never allow 419// more. Callee-saved homes were never affected (dedicated + call-preserved). 420// ⚠MOVED UP 2026-07-20 -- THIS NEGATIVE CONTROL COULD NEVER FIRE. It was declared ~827 lines BELOW 421// (beside its siblings) while its ONLY reader is the very next line. A module const read ABOVE its 422// declaration silently resolves to 0, so flipping it to 1 was ignored: the negctl could not 423// reintroduce the miscompile it exists to reintroduce, and any gate row leaning on it was VACUOUSLY 424// GREEN. Its own sibling comment says this family "kills the 'the guard never fires, green is 425// vacuous' liar" -- this member WAS that liar. Found by the new forward-const diagnostic. 426// NEGATIVE CONTROL: 1 = skip the back-edge interval extension -> reintroduces the br_table-zeroing 427// miscompile (caller-saved clobbered across a back-edge) -> the gauntlet's WAT byte-exact row goes 428// RED. Ships at 0. 429const X86_NEGCTL_NO_BACKEDGE_EXT: i64 = 0 430 431func x86_extend_backedge_intervals(f: *Function, intv: *X86Interval, 432 bb_start: *i64, bb_end: *i64) -> i64 { 433 if X86_NEGCTL_NO_BACKEDGE_EXT == 1 { return 0 } 434 var bi: i64 = 0 435 while bi < f.n_blocks { 436 let b: *BasicBlock = x86_block_at(f, bi) 437 let t: *Instr = b.tail 438 if t != (0 as *Instr) { 439 var nt: i64 = 0 440 var t0: i64 = 0 - 1 441 var t1: i64 = 0 - 1 442 if t.op == OP_BR { t0 = t.op0; nt = 1 } 443 if t.op == OP_BR_COND { t0 = t.op1; t1 = t.op2; nt = 2 } 444 var ti: i64 = 0 445 while ti < nt { 446 var tid: i64 = t0 447 if ti == 1 { tid = t1 } 448 if tid >= 0 { if tid <= bi { if tid < f.n_blocks { 449 let H: i64 = bb_start[tid] 450 let P: i64 = bb_end[bi] 451 var v: i64 = 0 452 while v < f.n_values { 453 let iv: *X86Interval = x86_intv_at(intv, v) 454 if iv.start >= 0 { 455 if iv.start <= P { if iv.end >= H { if iv.end < P { 456 iv.end = P 457 } } } 458 } 459 v = v + 1 460 } 461 } } } 462 ti = ti + 1 463 } 464 } 465 bi = bi + 1 466 } 467 return 0 468} 469 470// crosses_call: any call index strictly after start and at/before end. 471func x86_mark_crosses_call(intv: *X86Interval, n_values: i64, 472 calls: *i64, n_calls: i64) -> i64 { 473 var v: i64 = 0 474 while v < n_values { 475 let iv: *X86Interval = x86_intv_at(intv, v) 476 if iv.start >= 0 { 477 var c: i64 = 0 478 while c < n_calls { 479 let ci: i64 = calls[c] 480 if ci > iv.start { if ci <= iv.end { iv.crosses_call = 1 } } 481 c = c + 1 482 } 483 } 484 v = v + 1 485 } 486 return 0 487} 488 489// ----- home selection --------------------------------------------- 490// Cut-1 restricts homeable values to results of PURE-RAX binops (the ops whose 491// emitter threads FIX-1 x86ctx_result_reg). Everything else stays spilled. 492func x86_is_pure_rax_binop(op: i64) -> i64 { 493 if op == OP_ADD { return 1 } 494 if op == OP_SUB { return 1 } 495 if op == OP_MUL { return 1 } 496 if op == OP_AND { return 1 } 497 if op == OP_OR { return 1 } 498 if op == OP_XOR { return 1 } 499 return 0 500} 501 502// A value is homeable (cut-1) iff it has a live interval, is single-basic-block, 503// is not a call/syscall/tail/clone operand (FIX-3), is not an alloca (FIX-10), 504// and is produced by a pure-rax binop (so result_reg threading covers it). 505func x86_is_homeable(f: *Function, intv: *X86Interval, alloca_off: *i64, 506 any_call: *i64, v: i64) -> i64 { 507 let iv: *X86Interval = x86_intv_at(intv, v) 508 if iv.start < 0 { return 0 } 509 // STAGE 3: cross-block values are homeable too (dedicated reg for the whole 510 // live range is sound under SSA dominance). Old code required single_block. 511 if X86_ALLOW_CROSSBLOCK == 0 { if iv.single_block != 1 { return 0 } } 512 // NB: a value that is a CALL operand is STILL homeable -- the home is a 513 // callee-saved register, so it survives the call (loaded into the arg reg 514 // from %home at the call site, retained after). any_call is computed but no 515 // longer excludes (the cut-1 exclusion was over-conservative); the register- 516 // survival KAT + the self-host fixpoint guard correctness. 517 if alloca_off[v] >= 0 { return 0 } 518 let val: *Value = x86_val_at(f, v) 519 if val.kind != VK_INSTR { return 0 } 520 let inst: *Instr = val.instr 521 if inst == (0 as *Instr) { return 0 } 522 // STAGE 5 attempt (2026-07-15) REVERTED: homing 8-byte LOAD results (emit_load 523 // targets result_reg) showed NO matmul benefit (3.40× vs 3.42×) AND broke the 524 // self-host fixpoint [C] (miscompiles the compiler on some pattern the KATs 525 // don't hit). Not worth a self-host miscompile for zero gain. The emit_load 526 // dst-plumbing is left in (inert: result_reg = rax for every unhomed value, so 527 // dst==rax for all loads now that loads aren't homeable -> byte-identical). To 528 // retry: re-add OP_LOAD here + root-cause the self-host miscompile first. 529 if x86_is_pure_rax_binop(inst.op) == 0 { return 0 } 530 return 1 531} 532 533// Does function f contain any op whose hand-written intrinsic emitter clobbers a 534// GP caller-saved reg (rsi/rdi/r8/r9)? CONSERVATIVE: the whole SIMD/crypto 535// intrinsic family (nx_x86_64_ctx dispatch 2439-2455) is treated as clobbering, 536// so a function using ANY of them keeps the current 5-callee-saved behavior and 537// the caller-saved pool is disabled for it. OP_THREAD_CLONE/SYSCALL are already 538// call-class (caught by crosses_call), so they need not appear here. matmul uses 539// none -> gets the full pool. The gauntlet + negative control validate soundness. 540func x86_op_clobbers_caller_saved(op: i64) -> i64 { 541 if op == OP_F32X4_DOT { return 1 } 542 if op == OP_I8DOT32 { return 1 } 543 if op == OP_I8DOT32A { return 1 } 544 if op == OP_I8FMA32 { return 1 } 545 if op == OP_Q8ROWDOT { return 1 } 546 if op == OP_Q5UNPACK32 { return 1 } 547 if op == OP_F32X8_DOT { return 1 } 548 if op == OP_F32X8_FMA { return 1 } 549 if op == OP_F32X8_HSUM { return 1 } 550 if op == OP_I16X16_MADD { return 1 } 551 if op == OP_AES128_ENC_BLOCK { return 1 } 552 if op == OP_SHA256_NI_BLOCK { return 1 } 553 if op == OP_MUL256_WIDE { return 1 } 554 if op == OP_CLMUL_LL { return 1 } 555 if op == OP_CLMUL_HH { return 1 } 556 if op == OP_CLMUL_LH { return 1 } 557 if op == OP_CLMUL_HL { return 1 } 558 return 0 559} 560 561func x86_fn_has_clobber_intrinsic(f: *Function) -> i64 { 562 var bi: i64 = 0 563 while bi < f.n_blocks { 564 let b: *BasicBlock = x86_block_at(f, bi) 565 var inst: *Instr = b.head 566 while inst != (0 as *Instr) { 567 if x86_op_clobbers_caller_saved(inst.op) == 1 { return 1 } 568 inst = inst.next 569 } 570 bi = bi + 1 571 } 572 return 0 573} 574 575// HOMING GATE -- ON. The full register-allocation machinery (consume-side op1 576// folding, use-count top-K selection, multi-home r12..rbx, call-operand values 577// allowed since homes are callee-saved) COMPOSES with the optimizer (opt_run, 578// enabled in nx_compile_x86.nx) for the WIN: opt's mem2reg promotes alloca'd 579// state to SSA values, and this allocator homes them. Measured on the point-loop 580// vs the pre-opt baseline (bench/nx_codegen_ab.sh): -672 insns (-13%), -266 581// reloads (-14%) [noise-free], wall-clock 2.5-5.9% faster. CORRECT + determin- 582// istic: full self-host gauntlet green (13/13 differential KATs + byte-stable 583// 2nd-gen fixpoint + register-survival KAT), 5/5 identical sha256 compiles. 584// Neither pass alone wins -- regalloc-alone is neutral (allocas stay in memory), 585// opt-alone REGRESSES ~6% (promoted SSA values spill without a register home); 586// only TOGETHER do they win. See docs/NX_G1_X86_REGALLOC_PLAN_2026_05_29.md. 587const X86_HOME_CAP_ENABLE: i64 = 1 // ON: composes with opt_run for the measured crypto win 588const X86_HOME_MIN_USES: i64 = 3 // require >= this many uses to beat save/restore 589 590// Home up to X86_HOME_CAP eligible values, highest use_count first (most reloads 591// to eliminate), requiring use_count >= X86_HOME_MIN_USES so the per-function 592// callee-saved save/restore is amortised. Assigns home indices 0..K-1 (r12..rbx) 593// and sets the corresponding mask bits. Deterministic: strict > keeps the lowest 594// id on ties (ascending scan). 595func x86_select_homes(f: *Function, intv: *X86Interval, alloca_off: *i64, 596 any_call: *i64, used_cs_mask_out: *i64, wcost: *i64) -> i64 { 597 *used_cs_mask_out = 0 598 if X86_HOME_CAP_ENABLE == 0 { return 0 } 599 var assigned: i64 = 0 600 var stop0: i64 = 0 601 while assigned < X86_HOME_CAP { 602 if stop0 == 0 { 603 var best: i64 = 0 - 1 604 var best_uc: i64 = 0 - 1 605 var v: i64 = 0 606 while v < f.n_values { 607 let iv: *X86Interval = x86_intv_at(intv, v) 608 if iv.reg < 0 { 609 if x86_is_homeable(f, intv, alloca_off, any_call, v) == 1 { 610 // RANK BY LOOP-DEPTH-WEIGHTED COST (SOTA): a deep-loop 611 // value beats a cold high-textual-count one. 612 if wcost[v] > best_uc { 613 best = v 614 best_uc = wcost[v] 615 } 616 } 617 } 618 v = v + 1 619 } 620 if best < 0 { stop0 = 1 } 621 if best_uc < X86_HOME_MIN_USES { stop0 = 1 } 622 if stop0 == 0 { 623 let biv: *X86Interval = x86_intv_at(intv, best) 624 biv.reg = assigned 625 *used_cs_mask_out = *used_cs_mask_out | (1 << assigned) 626 assigned = assigned + 1 627 } 628 } 629 if stop0 == 1 { assigned = X86_HOME_CAP } 630 } 631 // ===== CALLER-SAVED pool (arc stage 2, 2026-07-15) ============== 632 // Runs only when all 5 callee-saved were assigned (>5 homeable values = 633 // exactly when leftovers exist). Assigns idx 5..8 (rsi/rdi/r8/r9) to the 634 // next-highest-use homeable values that DON'T cross a call, in functions 635 // with no GP-clobbering intrinsic. NO used_cs_mask bit -> no prologue save 636 // (sound: the interval never spans a call/intrinsic that clobbers them). 637 if X86_CALLER_POOL_ENABLE == 1 { 638 if x86_fn_has_clobber_intrinsic(f) == 0 { 639 var cassigned: i64 = 0 640 var cstop: i64 = 0 641 while cassigned < X86_CALLER_CAP { 642 if cstop == 0 { 643 var cbest: i64 = 0 - 1 644 var cbest_uc: i64 = 0 - 1 645 var cv: i64 = 0 646 while cv < f.n_values { 647 let civ: *X86Interval = x86_intv_at(intv, cv) 648 if civ.reg < 0 { 649 if civ.crosses_call == 0 { 650 if x86_is_homeable(f, intv, alloca_off, any_call, cv) == 1 { 651 if wcost[cv] > cbest_uc { 652 cbest = cv 653 cbest_uc = wcost[cv] 654 } 655 } 656 } 657 } 658 cv = cv + 1 659 } 660 if cbest < 0 { cstop = 1 } 661 if cbest_uc < X86_CALLER_MIN_USES { cstop = 1 } 662 if cstop == 0 { 663 let cbiv: *X86Interval = x86_intv_at(intv, cbest) 664 cbiv.reg = X86_CALLER_BASE + cassigned 665 } 666 } 667 cassigned = cassigned + 1 668 } 669 } 670 } 671 return 0 672} 673 674// ===== G2: ALLOCA HOMING (loop-carried scalar registerization) ====== 675// The 2026-05-29 plan's DEEPEST FINDING: in this SSA-no-phi IR the loop- 676// carried hot state lives in ALLOCAS (memory), untouched by SSA-value homing 677// and unreachable by opt_mem2reg_simple (needs phis). G2 homes the alloca's 678// STORAGE ITSELF in a leftover callee-saved register: every full-qword 679// load/store of the var becomes a register move -- no phis needed; loop- 680// carried correctness is by construction (the register IS the storage). 681// Measured motivation: the LCG grounding loop (reference-nishilang-vs-c- 682// codegen-grounding-2026-07-14) runs 20 memory ops/iter, 6 of them the 683// x/acc/i alloca round-trips on the critical dependence chain. 684// 685// Eligibility is a strict WHITELIST -- anything unrecognised disqualifies: 686// base: the alloca's declared storage type is exactly 8 bytes (i64/ptr). 687// uses: every appearance of the alloca's value id must be one of 688// OP_LOAD op0, 8-byte type (var read; emitted as movq %home,%rax) 689// OP_STORE op0, 8-byte type (var write; emitted as movq %src,%home) 690// OP_STORE op1 (value-read; load_value_v derefs the home) 691// a value-consume operand of a whitelisted op (binop/unop/cmp/br_cond/ 692// return/call/tail_call/call_indirect/copy, syscall args k>=1) -- all 693// of these materialise operands via x86ctx_load_value_v, which reads 694// the home register directly. 695// disqualifiers: OP_GEP base, OP_ADDR_OF (the address escapes), OP_SYSCALL 696// k==0 (as-address load path), subword load/store, and EVERY op not 697// whitelisted (SIMD/crypto/atomic/clone: unknown consumption idiom -> 698// conservatively keep the stack slot). 699// Belt+braces: the as-address path (x86ctx_load_value) plants an undefined- 700// label jump for a homed alloca, so an eligibility miss FAILS THE ASSEMBLE 701// loudly instead of silently miscompiling (the 2026-05-30 SEV1 lesson). 702 703// Value-consume whitelist: ops PROVEN to materialise operands via 704// x86ctx_load_value_v (the home-aware as-value path). 705func x86_ah_value_consume_ok(op: i64) -> i64 { 706 if op == OP_ADD { return 1 } 707 if op == OP_SUB { return 1 } 708 if op == OP_MUL { return 1 } 709 if op == OP_DIV_S { return 1 } 710 if op == OP_DIV_U { return 1 } 711 if op == OP_REM_S { return 1 } 712 if op == OP_REM_U { return 1 } 713 if op == OP_NEG { return 1 } 714 if op == OP_NOT { return 1 } 715 if op == OP_AND { return 1 } 716 if op == OP_OR { return 1 } 717 if op == OP_XOR { return 1 } 718 if op == OP_SHL { return 1 } 719 if op == OP_SHR_S { return 1 } 720 if op == OP_SHR_U { return 1 } 721 if op == OP_EQ { return 1 } 722 if op == OP_NE { return 1 } 723 if op == OP_LT_S { return 1 } 724 if op == OP_LE_S { return 1 } 725 if op == OP_GT_S { return 1 } 726 if op == OP_GE_S { return 1 } 727 if op == OP_BR_COND { return 1 } 728 if op == OP_RETURN { return 1 } 729 if op == OP_CALL { return 1 } 730 if op == OP_TAIL_CALL { return 1 } 731 if op == OP_CALL_INDIRECT { return 1 } 732 if op == OP_SYSCALL { return 1 } 733 if op == OP_COPY { return 1 } 734 return 0 735} 736 737// Base eligibility: an OP_ALLOCA whose declared storage is an 8-byte scalar. 738func x86_ah_base_ok(f: *Function, v: i64) -> i64 { 739 let val: *Value = x86_val_at(f, v) 740 if val.kind != VK_INSTR { return 0 } 741 let inst: *Instr = val.instr 742 if inst == (0 as *Instr) { return 0 } 743 if inst.op != OP_ALLOCA { return 0 } 744 let t: *Type = inst.ty 745 if t == (0 as *Type) { return 0 } 746 if t.kind == TY_VOID { return 0 } 747 if t.size != 8 { return 0 } 748 return 1 749} 750 751// 1 iff this load/store moves a full 8-byte qword (subword access of a 752// homed register-var has no lowering -> disqualifies). 753func x86_ah_ldst_sz8(inst: *Instr) -> i64 { 754 let t: *Type = inst.ty 755 if t == (0 as *Type) { return 0 } 756 if t.size != 8 { return 0 } 757 return 1 758} 759 760// Scan every operand appearance of every alloca; leave elig[v]=1 only for 761// allocas whose EVERY use is whitelisted. cnt[v] = safe-use count (the 762// reload-elimination payoff proxy that ranks candidates). 763func x86_ah_scan(f: *Function, alloca_off: *i64, elig: *i64, cnt: *i64) -> i64 { 764 let n: i64 = f.n_values 765 var v: i64 = 0 766 while v < n { 767 elig[v] = 0 768 cnt[v] = 0 769 if alloca_off[v] >= 0 { elig[v] = x86_ah_base_ok(f, v) } 770 v = v + 1 771 } 772 var bi: i64 = 0 773 while bi < f.n_blocks { 774 let b: *BasicBlock = x86_block_at(f, bi) 775 var inst: *Instr = b.head 776 while inst != (0 as *Instr) { 777 let nv: i64 = x86_n_value_operands(inst) 778 var k: i64 = 0 779 while k < nv { 780 let u: i64 = x86_operand_k(inst, k) 781 if u >= 0 { if u < n { if alloca_off[u] >= 0 { 782 var ok: i64 = 0 783 if inst.op == OP_LOAD { if k == 0 { ok = x86_ah_ldst_sz8(inst) } } 784 if inst.op == OP_STORE { 785 if k == 0 { ok = x86_ah_ldst_sz8(inst) } 786 if k == 1 { ok = 1 } 787 } 788 if ok == 0 { ok = x86_ah_value_consume_ok(inst.op) } 789 if inst.op == OP_SYSCALL { if k == 0 { ok = 0 } } 790 if ok == 1 { cnt[u] = cnt[u] + 1 } 791 if ok == 0 { elig[u] = 0 } 792 } } } 793 k = k + 1 794 } 795 inst = inst.next 796 } 797 bi = bi + 1 798 } 799 return 0 800} 801 802// Assign LEFTOVER callee-saved homes (bits not taken by SSA-value homing) 803// to eligible allocas, highest safe-use count first, >= X86_HOME_MIN_USES. 804// Deterministic: strict > keeps the lowest value id on ties; lowest free 805// bit each round. 806func x86_ah_select(f: *Function, elig: *i64, cnt: *i64, 807 alloca_home: *i64, mask_ptr: *i64, wcost: *i64) -> i64 { 808 if X86_HOME_CAP_ENABLE == 0 { return 0 } 809 var round: i64 = 0 810 var stopr: i64 = 0 811 while round < X86_HOME_CAP { 812 if stopr == 0 { 813 var bit: i64 = 0 - 1 814 var b: i64 = 0 815 while b < X86_HOME_CAP { 816 if bit < 0 { if ((mask_ptr[0] >> b) & 1) == 0 { bit = b } } 817 b = b + 1 818 } 819 if bit < 0 { stopr = 1 } 820 if stopr == 0 { 821 var best: i64 = 0 - 1 822 var best_c: i64 = 0 - 1 823 var v: i64 = 0 824 while v < f.n_values { 825 if elig[v] == 1 { if alloca_home[v] < 0 { 826 // RANK BY LOOP-DEPTH-WEIGHTED COST (SOTA): a loop-carried 827 // alloca (s,k used every k-iter) beats a cold one; cnt 828 // (safe-use count) stays the eligibility signal via elig. 829 if wcost[v] > best_c { best = v; best_c = wcost[v] } 830 } } 831 v = v + 1 832 } 833 if best < 0 { stopr = 1 } 834 if best_c < X86_HOME_MIN_USES { stopr = 1 } 835 if stopr == 0 { 836 alloca_home[best] = bit 837 mask_ptr[0] = mask_ptr[0] | (1 << bit) 838 round = round + 1 839 } 840 } 841 } 842 if stopr == 1 { round = X86_HOME_CAP } 843 } 844 return 0 845} 846 847// ===== G4: single-use next-instruction temp ELISION ================= 848// The emitter materialises EVERY instruction result into a stack slot even 849// when the value is consumed once by the IMMEDIATELY FOLLOWING instruction 850// through the G1 rax-forwarding path -- a dead store per temp (the dominant 851// remaining memory traffic after G2: ~8 dead stores/iter in the LCG loop). 852// elide[v]=1 marks values whose slot store may be SKIPPED because the single 853// consumer is guaranteed to take the G1 path: use_count==1, the use is in 854// the textually NEXT instruction (same block), at a position whose emission 855// loads that operand into RAX as its FIRST rax-touching action. store_result 856// keeps the G1 contract (sets G1_RAX_SLOT without the store); any slot LOAD 857// of an elided value = criterion bug -> loud undefined-label tripwire. 858 859// Ops whose emit loads op0 into rax first via load_value_v (or load_value 860// with the G1 check, for GEP). Binops additionally require the consumer's 861// result to be UNHOMED (dst==rax); cmp/unop/gep results are never homed. 862func x86_g4_is_binop(op: i64) -> i64 { 863 if op == OP_ADD { return 1 } 864 if op == OP_SUB { return 1 } 865 if op == OP_MUL { return 1 } 866 if op == OP_DIV_S { return 1 } 867 if op == OP_DIV_U { return 1 } 868 if op == OP_REM_S { return 1 } 869 if op == OP_REM_U { return 1 } 870 if op == OP_AND { return 1 } 871 if op == OP_OR { return 1 } 872 if op == OP_XOR { return 1 } 873 if op == OP_SHL { return 1 } 874 if op == OP_SHR_S { return 1 } 875 if op == OP_SHR_U { return 1 } 876 return 0 877} 878func x86_g4_is_cmp(op: i64) -> i64 { 879 if op == OP_EQ { return 1 } 880 if op == OP_NE { return 1 } 881 if op == OP_LT_S { return 1 } 882 if op == OP_LE_S { return 1 } 883 if op == OP_GT_S { return 1 } 884 if op == OP_GE_S { return 1 } 885 return 0 886} 887// dst OP src == src OP dst -- the ops the emitter may operand-swap (G11 chain 888// commute + G21 commuted rax-consume). Defined here (before x86_g4_pos_ok and 889// x86_chain_scan, its two caller clusters) per the defined-before-use rule. 890func x86_chain_op_commutative(op: i64) -> i64 { 891 if op == OP_ADD { return 1 } 892 if op == OP_MUL { return 1 } 893 if op == OP_AND { return 1 } 894 if op == OP_OR { return 1 } 895 if op == OP_XOR { return 1 } 896 return 0 897} 898// PRODUCER whitelist: ops whose emit leaves the result in RAX with 899// G1_RAX_SLOT still valid at the NEXT instruction -- the only ops whose slot 900// store is safe to elide. EXCLUDES every op whose dispatch clears 901// G1_RAX_SLOT after store (CPUID/RDTSC/CALL/SYSCALL/f32/SIMD/crypto) and every 902// op that produces in a non-rax reg (REM/UMULHI -> rdx). Mirrors the 903// consumer whitelist; both sides must be clean-rax for forwarding to hold. 904func x86_g4_producer_ok(op: i64) -> i64 { 905 if op == OP_ADD { return 1 } 906 if op == OP_SUB { return 1 } 907 if op == OP_MUL { return 1 } 908 if op == OP_DIV_S { return 1 } 909 if op == OP_DIV_U { return 1 } 910 if op == OP_AND { return 1 } 911 if op == OP_OR { return 1 } 912 if op == OP_XOR { return 1 } 913 if op == OP_SHL { return 1 } 914 if op == OP_SHR_S { return 1 } 915 if op == OP_SHR_U { return 1 } 916 if op == OP_EQ { return 1 } 917 if op == OP_NE { return 1 } 918 if op == OP_LT_S { return 1 } 919 if op == OP_LE_S { return 1 } 920 if op == OP_GT_S { return 1 } 921 if op == OP_GE_S { return 1 } 922 if op == OP_NEG { return 1 } 923 if op == OP_NOT { return 1 } 924 if op == OP_LOAD { return 1 } 925 if op == OP_GEP { return 1 } 926 if op == OP_COPY { return 1 } 927 return 0 928} 929func x86_g4_pos_ok(jop: i64, k: i64, jres_homed: i64) -> i64 { 930 if x86_g4_is_binop(jop) == 1 { 931 if jres_homed == 1 { return 0 } 932 if k == 0 { return 1 } 933 // G21 COMMUTED CONSUME: a commutative consumer reads its op1 straight 934 // from rax -- the emitter swaps operands (op0 becomes the src, rax is 935 // the dst seed), so the op1 slot store is dead exactly like the k==0 936 // case. MUST mirror the emit-side g21 gate in x86ctx_emit_binop (both 937 // sides clean-rax, unhomed result); the .G4_elided_slot_load_bug 938 // tripwire catches any divergence loudly. 939 if k == 1 { if x86_chain_op_commutative(jop) == 1 { return 1 } } 940 return 0 941 } 942 if x86_g4_is_cmp(jop) == 1 { 943 if k == 0 { return 1 } 944 return 0 945 } 946 if jop == OP_NEG { if k == 0 { return 1 } return 0 } 947 if jop == OP_NOT { if k == 0 { return 1 } return 0 } 948 if jop == OP_BR_COND { if k == 0 { return 1 } return 0 } 949 if jop == OP_RETURN { if k == 0 { return 1 } return 0 } 950 if jop == OP_COPY { if k == 0 { return 1 } return 0 } 951 if jop == OP_GEP { if k == 0 { return 1 } return 0 } 952 if jop == OP_STORE { if k == 1 { return 1 } return 0 } 953 return 0 954} 955 956func x86_g4_elide_scan(f: *Function, locs: *ValueLoc, alloca_off: *i64, 957 intv: *X86Interval, elide: *i64, 958 chain_home: *i64, chain_swap: *i64, fwd_home: *i64) -> i64 { 959 let n: i64 = f.n_values 960 var v: i64 = 0 961 while v < n { elide[v] = 0; v = v + 1 } 962 var bi: i64 = 0 963 while bi < f.n_blocks { 964 let b: *BasicBlock = x86_block_at(f, bi) 965 var inst: *Instr = b.head 966 while inst != (0 as *Instr) { 967 let nxt: *Instr = inst.next 968 if nxt != (0 as *Instr) { 969 let r: i64 = inst.result 970 if r >= 0 { if r < n { 971 var ok: i64 = 1 972 if inst.ty == (0 as *Type) { ok = 0 } 973 if ok == 1 { if inst.ty.kind == TY_VOID { ok = 0 } } 974 if x86_g4_producer_ok(inst.op) == 0 { ok = 0 } 975 let rv: *Value = x86_val_at(f, r) 976 if rv.kind != VK_INSTR { ok = 0 } 977 if alloca_off[r] >= 0 { ok = 0 } 978 if ok == 1 { 979 let rl: *ValueLoc = ((locs as i64) + r * 16) as *ValueLoc 980 if rl.kind == VL_REGISTER { ok = 0 } 981 } 982 if ok == 1 { 983 let riv: *X86Interval = x86_intv_at(intv, r) 984 if riv.use_count != 1 { ok = 0 } 985 } 986 if ok == 1 { 987 var jres_homed: i64 = 0 988 let jr: i64 = nxt.result 989 if jr >= 0 { if jr < n { 990 let jl: *ValueLoc = ((locs as i64) + jr * 16) as *ValueLoc 991 if jl.kind == VL_REGISTER { jres_homed = 1 } 992 } } 993 let nv: i64 = x86_n_value_operands(nxt) 994 var found: i64 = 0 995 var k: i64 = 0 996 while k < nv { 997 if x86_operand_k(nxt, k) == r { 998 if found == 0 { 999 if x86_g4_pos_ok(nxt.op, k, jres_homed) == 1 { found = 1 } 1000 } 1001 // G16: a CHAIN-FUSED consumer materializes its 1002 // SRC operand via load_value_v(rax) as its FIRST 1003 // rax touch (op0 is never materialized -- it IS 1004 // the home), so the G1 contract holds at the src 1005 // position (op1 normal, op0 swapped). r must not 1006 // be homed (checked above) nor forwarded (would 1007 // read a home, not rax). 1008 if found == 0 { 1009 let cj: i64 = nxt.result 1010 if cj >= 0 { if cj < n { if chain_home[cj] >= 0 { 1011 var srcpos: i64 = 1 1012 if chain_swap[cj] == 1 { srcpos = 0 } 1013 if k == srcpos { 1014 if fwd_home[r] < 0 { found = 1 } 1015 } 1016 } } } 1017 } 1018 } 1019 k = k + 1 1020 } 1021 if found == 1 { elide[r] = 1 } 1022 } 1023 } } 1024 } 1025 inst = nxt 1026 } 1027 bi = bi + 1 1028 } 1029 return 0 1030} 1031 1032// ===== G5: BIG-CONSTANT HOMING ====================================== 1033// A VK_CONST_INT too big for imm32 is re-materialised with a 10-byte movabsq 1034// at EVERY use (per-iteration in loops). Home the hottest such constants in 1035// LEFTOVER callee-saved regs: one movabsq in the prologue, and op1-direct 1036// binop consumption reads the home register with ZERO per-use instructions 1037// (imulq %r15,%rax instead of movabsq+imulq). imm32-fitting constants are 1038// handled by the immediate-folding path in the emitter instead (no reg cost). 1039func x86_g5_imm32_ok(v: i64) -> i64 { 1040 if v > 2147483647 { return 0 } 1041 if v < (0 - 2147483648) { return 0 } 1042 return 1 1043} 1044 1045func x86_g5_select_const_homes(f: *Function, intv: *X86Interval, 1046 locs: *ValueLoc, mask_ptr: *i64) -> i64 { 1047 if X86_HOME_CAP_ENABLE == 0 { return 0 } 1048 let n: i64 = f.n_values 1049 // Loop-block map: for every BACKWARD branch bj -> bi (bi <= bj), blocks 1050 // bi..bj are loop-resident (blocks lay out in creation order; a NishiLang 1051 // while parses to a contiguous cond..body range, so the interval IS the 1052 // loop body). A constant's payoff is per-EXECUTION, not per-site: one 1053 // in-loop movabsq costs every iteration, so in-loop uses weigh 8x -- 1054 // a single hot-loop use clears X86_HOME_MIN_USES; cold single uses don't. 1055 let lb_raw: *u8 = sys_mmap(f.n_blocks * 8 + 16) 1056 let lb: *i64 = lb_raw as *i64 1057 var lbi: i64 = 0 1058 while lbi < f.n_blocks { 1059 let lbb: *BasicBlock = x86_block_at(f, lbi) 1060 var linst: *Instr = lbb.head 1061 while linst != (0 as *Instr) { 1062 var tgt: i64 = 0 - 1 1063 var tgt2: i64 = 0 - 1 1064 if linst.op == OP_BR { tgt = linst.op0 } 1065 if linst.op == OP_BR_COND { tgt = linst.op1; tgt2 = linst.op2 } 1066 if tgt >= 0 { if tgt <= lbi { if tgt < f.n_blocks { 1067 var m: i64 = tgt 1068 while m <= lbi { lb[m] = 1; m = m + 1 } 1069 } } } 1070 if tgt2 >= 0 { if tgt2 <= lbi { if tgt2 < f.n_blocks { 1071 var m2: i64 = tgt2 1072 while m2 <= lbi { lb[m2] = 1; m2 = m2 + 1 } 1073 } } } 1074 linst = linst.next 1075 } 1076 lbi = lbi + 1 1077 } 1078 // direct-consumable op1 uses per value (the payoff metric), loop-weighted 1079 let du_raw: *u8 = sys_mmap(n * 8 + 16) 1080 let du: *i64 = du_raw as *i64 1081 var bi: i64 = 0 1082 while bi < f.n_blocks { 1083 let b: *BasicBlock = x86_block_at(f, bi) 1084 var inst: *Instr = b.head 1085 while inst != (0 as *Instr) { 1086 if x86_is_pure_rax_binop(inst.op) == 1 { 1087 let u: i64 = inst.op1 1088 if u >= 0 { if u < n { 1089 if lb[bi] == 1 { du[u] = du[u] + 8 } 1090 if lb[bi] == 0 { du[u] = du[u] + 1 } 1091 } } 1092 } 1093 inst = inst.next 1094 } 1095 bi = bi + 1 1096 } 1097 var round: i64 = 0 1098 while round < X86_HOME_CAP { 1099 var bit: i64 = 0 - 1 1100 var b2: i64 = 0 1101 while b2 < X86_HOME_CAP { 1102 if bit < 0 { if ((mask_ptr[0] >> b2) & 1) == 0 { bit = b2 } } 1103 b2 = b2 + 1 1104 } 1105 if bit < 0 { return 0 } 1106 var best: i64 = 0 - 1 1107 var best_c: i64 = 0 - 1 1108 var v: i64 = 0 1109 while v < n { 1110 let val: *Value = x86_val_at(f, v) 1111 if val.kind == VK_CONST_INT { 1112 let lv: *ValueLoc = ((locs as i64) + v * 16) as *ValueLoc 1113 if lv.kind != VL_REGISTER { 1114 if x86_g5_imm32_ok(val.const_int) == 0 { 1115 if du[v] > best_c { best = v; best_c = du[v] } 1116 } 1117 } 1118 } 1119 v = v + 1 1120 } 1121 if best < 0 { return 0 } 1122 if best_c < X86_HOME_MIN_USES { return 0 } 1123 let bl: *ValueLoc = ((locs as i64) + best * 16) as *ValueLoc 1124 bl.kind = VL_REGISTER 1125 bl.idx = bit 1126 mask_ptr[0] = mask_ptr[0] | (1 << bit) 1127 round = round + 1 1128 } 1129 return 0 1130} 1131 1132// ===== G17: NON-NEGATIVE RANGE ANALYSIS (2026-07-16) ================= 1133// The auto-scientist's PROVEN 2.063x spot: signed division by 2^k pays a 1134// 4-instruction sign-bias dance that a provably NON-NEGATIVE dividend never 1135// needs -- gcc -O2 pays it too wherever range isn't visible. This lattice is 1136// DELIBERATELY conservative: only ops that STRUCTURALLY clear or preserve a 1137// zero sign bit qualify; anything that can overflow into the sign bit 1138// (ADD/SUB/MUL/SHL) is EXCLUDED by design, loads are excluded in v1 (the 1139// alloca-store fixpoint is G17b). Wrong answers here are MISCOMPILES -- the 1140// negctl (treat everything as nonneg) must go RED on the battery. 1141const X86_NEGCTL_NONNEG_ALWAYS: i64 = 0 1142 1143func x86_const_ge(f: *Function, v: i64, lo: i64) -> i64 { 1144 if v < 0 { return 0 } 1145 if v >= f.n_values { return 0 } 1146 let val: *Value = x86_val_at(f, v) 1147 if val.kind != VK_CONST_INT { return 0 } 1148 if val.const_int >= lo { return 1 } 1149 return 0 1150} 1151 1152func x86_val_nonneg(f: *Function, v: i64, depth: i64) -> i64 { 1153 if X86_NEGCTL_NONNEG_ALWAYS == 1 { return 1 } 1154 if depth <= 0 { return 0 } 1155 if v < 0 { return 0 } 1156 if v >= f.n_values { return 0 } 1157 let val: *Value = x86_val_at(f, v) 1158 if val.kind == VK_CONST_INT { 1159 if val.const_int >= 0 { return 1 } 1160 return 0 1161 } 1162 if val.kind != VK_INSTR { return 0 } 1163 let inst: *Instr = val.instr 1164 if inst == (0 as *Instr) { return 0 } 1165 // AND with ANY nonneg operand: a zero sign bit ANDed in stays zero. 1166 if inst.op == OP_AND { 1167 if x86_val_nonneg(f, inst.op0, depth - 1) == 1 { return 1 } 1168 if x86_val_nonneg(f, inst.op1, depth - 1) == 1 { return 1 } 1169 return 0 1170 } 1171 // Logical shift right by a CONST >= 1 kills the sign bit outright; by any 1172 // count it preserves nonneg. Arithmetic shift right preserves the sign. 1173 if inst.op == OP_SHR_U { 1174 if x86_const_ge(f, inst.op1, 1) == 1 { return 1 } 1175 if x86_val_nonneg(f, inst.op0, depth - 1) == 1 { return 1 } 1176 return 0 1177 } 1178 if inst.op == OP_SHR_S { 1179 if x86_val_nonneg(f, inst.op0, depth - 1) == 1 { return 1 } 1180 return 0 1181 } 1182 // Signed div of a nonneg by a positive const is nonneg (magnitude shrinks). 1183 if inst.op == OP_DIV_S { 1184 if x86_const_ge(f, inst.op1, 1) == 1 { 1185 if x86_val_nonneg(f, inst.op0, depth - 1) == 1 { return 1 } 1186 } 1187 return 0 1188 } 1189 // Unsigned div by a const >= 2 clears the top bit regardless of input. 1190 if inst.op == OP_DIV_U { 1191 if x86_const_ge(f, inst.op1, 2) == 1 { return 1 } 1192 return 0 1193 } 1194 // rem_u by a positive const is in [0, const) -- nonneg. 1195 if inst.op == OP_REM_U { 1196 if x86_const_ge(f, inst.op1, 1) == 1 { return 1 } 1197 return 0 1198 } 1199 // rem_s follows the dividend's sign. 1200 if inst.op == OP_REM_S { 1201 if x86_val_nonneg(f, inst.op0, depth - 1) == 1 { return 1 } 1202 return 0 1203 } 1204 return 0 1205} 1206 1207// ===== G10: HOME-FORWARDING (2026-07-15) ============================ 1208// The gap tool's verdict: 59-68% of every hot loop is materialization waste, 1209// led by the load-of-homed-alloca triple `movq %home,%rax; movq %rax,slot; 1210// movq slot,%reg`. G10 kills it: a SINGLE-USE, 8-byte LOAD of a G2-HOMED 1211// alloca whose one consumer is IN THE SAME BLOCK at an AUDITED operand 1212// position, with NO intervening store to that alloca and NO intervening 1213// call-class op, emits NOTHING -- the consumer reads the alloca's home 1214// register directly (load_value_v/load_value check fwd_home first). 1215// SOUNDNESS: the home register is DEDICATED to the alloca (never shared, 1216// callee-saved, never scratch), so between the load site and the consumer 1217// site its content changes ONLY on a store-to-the-alloca -- exactly what the 1218// scan forbids. The audited-consumer whitelist guarantees the operand is 1219// materialized via load_value_v/load_value (where the fwd check lives), and 1220// the SIB-shape exclusions below keep displaced-emission folds out. 1221const X86_FWD_ENABLE: i64 = 1 1222// NEGATIVE CONTROL: 1 = scan ignores intervening stores-to-the-alloca -> a 1223// STALE home is forwarded -> nx_homefwd_adversary T1 answers WRONG and the 1224// battery goes RED. Proves the store-guard is load-bearing. Ships at 0. 1225const X86_NEGCTL_FWD_IGNORE_STORE: i64 = 0 1226// NEGATIVE CONTROL 2: 1 = forward from the WRONG home register (idx rotated 1227// within the callee-saved pool) -> every forwarded read yields garbage -> the 1228// battery MUST go RED wherever forwarding fires. Kills the "forwarding never 1229// actually fires, green is vacuous" liar. Ships at 0. 1230const X86_NEGCTL_FWD_WRONG_HOME: i64 = 0 1231 1232// ===== G11: CHAIN IN-PLACE FUSION (2026-07-15, rung 12) ============= 1233// The gap tool's next rock (cp=7-9 copies/loop): the loop-carried update 1234// `load A -> binop chain -> store A` still costs a head copy (movq %hA,%dst) 1235// and a tail copy (movq %dst,%hA) plus per-step dst shuffling. G11 fuses the 1236// WHOLE chain onto the home register: each chain binop emits `op src1,%hA` 1237// IN PLACE (op0 never materialized -- it IS hA), the head load and the tail 1238// store emit NOTHING. `x = x*A + C` -> imulq+addq straight on %hx; `i = i+1` 1239// -> one `addq $1,%hi`. SOUNDNESS: the scan requires (a) head load of a 1240// G2-homed 8-byte alloca, result single-use consumed at op0 of an 1241// ADD/SUB/MUL/AND/OR/XOR whose result is again single-use at the next step's 1242// op0, ... terminating in `store A, final`; (b) NO other access to A (load 1243// OR store) inside the window -- hA holds INTERMEDIATE values mid-chain, so 1244// any other reader would see garbage; (c) no call-class op inside; (d) all 1245// same-block. Interleaved instructions that don't touch A are fine (they 1246// write rax/rcx/their own homes, never hA -- homes are dedicated). 1247const X86_CHAIN_ENABLE: i64 = 1 1248// NEGATIVE CONTROL: 1 = ignore the mid-window load-of-A abort -> a chain 1249// containing a second read of A fuses anyway -> that read sees the MUTATED 1250// home -> nx_homefwd_adversary T4 answers WRONG -> battery RED. Ships at 0. 1251const X86_NEGCTL_CHAIN_IGNORE_READ: i64 = 0 1252// NEGATIVE CONTROL: 1 = drop the self-ref chain guard -> `z = z & (z-1)` fuses 1253// in place again (subq $1,%home; andq %home,%home) -> Kernighan popcount 1254// degrades to a decrement -> nx_chain_selfref_adversary goes RED. Kills the 1255// "the guard never fires, green is vacuous" liar. Ships at 0. 1256const X86_NEGCTL_CHAIN_SELFREF_OFF: i64 = 0 1257// (X86_NEGCTL_NO_BACKEDGE_EXT MOVED UP to sit beside its only reader -- it was declared here and 1258// read 827 lines earlier, so it silently resolved to 0 and could never fire. Do NOT move it back.) 1259 1260func x86_chain_op_ok(op: i64) -> i64 { 1261 if op == OP_ADD { return 1 } 1262 if op == OP_SUB { return 1 } 1263 if op == OP_MUL { return 1 } 1264 if op == OP_AND { return 1 } 1265 if op == OP_OR { return 1 } 1266 if op == OP_XOR { return 1 } 1267 return 0 1268} 1269 1270// Commutative subset: the chain value may sit at op1 (const-first canonical 1271// forms like MUL(3,n)); `op src,dst` then computes dst OP src = src OP dst. 1272// SUB excluded (3-n != n-3). 1273// Marks: chain_home[v] = the home idx for IN-PLACE emission of chain binop 1274// results (-1 otherwise); the head load is killed via fwd_home[head]=hA (the 1275// existing G10 kill path); the tail store is suppressed in emit_store via 1276// chain_home[op1] == alloca_home[op0]. Runs AFTER x86_fwd_scan (which inits 1277// fwd_home) so the head-kill write survives. 1278func x86_chain_scan(f: *Function, alloca_off: *i64, alloca_home: *i64, 1279 intv: *X86Interval, fwd_home: *i64, chain_home: *i64, 1280 chain_swap: *i64) -> i64 { 1281 var vz: i64 = 0 1282 while vz < f.n_values { chain_home[vz] = 0 - 1; chain_swap[vz] = 0; vz = vz + 1 } 1283 if X86_CHAIN_ENABLE == 0 { return 0 } 1284 var bi: i64 = 0 1285 while bi < f.n_blocks { 1286 let b: *BasicBlock = x86_block_at(f, bi) 1287 var inst: *Instr = b.head 1288 while inst != (0 as *Instr) { 1289 if inst.op == OP_LOAD { 1290 let a: i64 = inst.op0 1291 if a >= 0 { if a < f.n_values { if alloca_home[a] >= 0 { 1292 let lt: *Type = inst.ty 1293 if lt != (0 as *Type) { if lt.size == 8 { 1294 let r: i64 = inst.result 1295 if r >= 0 { if r < f.n_values { 1296 let riv: *X86Interval = x86_intv_at(intv, r) 1297 if riv.use_count == 1 { 1298 // Walk the op0-thread to a store-back of A. 1299 var cur: i64 = r 1300 var steps: i64 = 0 1301 var J: *Instr = inst.next 1302 var verdict: i64 = 0 // 0=scan 1=fuse 2=no 1303 var term: *Instr = 0 as *Instr 1304 while verdict == 0 { 1305 if J == (0 as *Instr) { verdict = 2 } 1306 if verdict == 0 { 1307 if J.op == OP_LOAD { if J.op0 == a { 1308 if X86_NEGCTL_CHAIN_IGNORE_READ == 0 { verdict = 2 } 1309 } } 1310 } 1311 if verdict == 0 { 1312 if J.op == OP_STORE { if J.op0 == a { 1313 if J.op1 == cur { 1314 if steps >= 1 { verdict = 1; term = J } 1315 if steps < 1 { verdict = 2 } 1316 } 1317 if J.op1 != cur { verdict = 2 } 1318 } } 1319 } 1320 if verdict == 0 { 1321 if x86_is_call_class(J.op) == 1 { verdict = 2 } 1322 } 1323 if verdict == 0 { 1324 // Does J consume cur anywhere? 1325 let nv: i64 = x86_n_value_operands(J) 1326 var kk: i64 = 0 1327 var found: i64 = 0 1328 while kk < nv { 1329 if x86_operand_k(J, kk) == cur { found = 1 } 1330 kk = kk + 1 1331 } 1332 if found == 1 { 1333 var ok: i64 = 0 1334 if x86_chain_op_ok(J.op) == 1 { 1335 // straight: chain value at op0 1336 if J.op0 == cur { if J.op1 != cur { ok = 1 } } 1337 // commuted: chain value at op1 1338 // of a COMMUTATIVE op (const- 1339 // first canonical MUL(3,n) etc.) 1340 if ok == 0 { 1341 if x86_chain_op_commutative(J.op) == 1 { 1342 if J.op1 == cur { if J.op0 != cur { ok = 1 } } 1343 } 1344 } 1345 if ok == 1 { 1346 let civ: *X86Interval = x86_intv_at(intv, cur) 1347 if civ.use_count != 1 { ok = 0 } 1348 let jr: i64 = J.result 1349 if jr < 0 { ok = 0 } 1350 if jr >= f.n_values { ok = 0 } 1351 // SELF-REF GUARD (fixes z = z & (z-1) miscompile): 1352 // the SRC operand (the non-chain operand) must NOT read 1353 // the alloca `a` being chained. The chain mutates a's home 1354 // IN PLACE, so any src that resolves to a's value (the head 1355 // load r, OR any other LOAD of a) reads the MUTATED home, 1356 // not the original z -- `z&(z-1)`, `(z-1)&z`, `z^(z+1)` all 1357 // route a second read of z into a step's src. Legit chains 1358 // (s += arr[i]) load a DIFFERENT alloca, so are unaffected. 1359 if X86_NEGCTL_CHAIN_SELFREF_OFF == 0 { 1360 var chsrc: i64 = J.op1 1361 if J.op0 != cur { chsrc = J.op0 } 1362 if chsrc == r { ok = 0 } 1363 if chsrc >= 0 { if chsrc < f.n_values { 1364 let sv: *Value = x86_val_at(f, chsrc) 1365 if sv.kind == VK_INSTR { 1366 let si: *Instr = sv.instr 1367 if si != (0 as *Instr) { 1368 if si.op == OP_LOAD { if si.op0 == a { ok = 0 } } 1369 } 1370 } 1371 } } 1372 } 1373 } 1374 } 1375 if ok == 1 { cur = J.result; steps = steps + 1 } 1376 if ok == 0 { verdict = 2 } 1377 } 1378 } 1379 if verdict == 0 { J = J.next } 1380 } 1381 if verdict == 1 { 1382 // Final value must be single-use (the store). 1383 let fiv: *X86Interval = x86_intv_at(intv, cur) 1384 if fiv.use_count == 1 { 1385 let hA: i64 = alloca_home[a] 1386 fwd_home[r] = hA // kill the head load 1387 // Mark every chain binop for in-place emit. 1388 var C2: *Instr = inst.next 1389 var prev: i64 = r 1390 var marking: i64 = 1 1391 while marking == 1 { 1392 if C2 == (0 as *Instr) { marking = 0 } 1393 if marking == 1 { 1394 if C2 == term { marking = 0 } 1395 } 1396 if marking == 1 { 1397 if x86_chain_op_ok(C2.op) == 1 { 1398 var mk: i64 = 0 1399 var sw: i64 = 0 1400 if C2.op0 == prev { if C2.op1 != prev { mk = 1 } } 1401 if mk == 0 { 1402 if x86_chain_op_commutative(C2.op) == 1 { 1403 if C2.op1 == prev { if C2.op0 != prev { mk = 1; sw = 1 } } 1404 } 1405 } 1406 if mk == 1 { 1407 chain_home[C2.result] = hA 1408 chain_swap[C2.result] = sw 1409 prev = C2.result 1410 } 1411 } 1412 C2 = C2.next 1413 } 1414 } 1415 } 1416 } 1417 } 1418 } } 1419 } } 1420 } } } 1421 } 1422 inst = inst.next 1423 } 1424 bi = bi + 1 1425 } 1426 return 0 1427} 1428 1429// Is value id `u` a VK_CONST_INT equal to one of up to 4 given values? 1430func x86_fwd_const_in(f: *Function, u: i64, a: i64, b: i64, c2: i64, d: i64) -> i64 { 1431 if u < 0 { return 0 } 1432 if u >= f.n_values { return 0 } 1433 let v: *Value = x86_val_at(f, u) 1434 if v.kind != VK_CONST_INT { return 0 } 1435 let x: i64 = v.const_int 1436 if x == a { return 1 } 1437 if x == b { return 1 } 1438 if x == c2 { return 1 } 1439 if x == d { return 1 } 1440 return 0 1441} 1442 1443// Is value id `u` produced by an instruction that a SIB fold could consume 1444// (SHL by 0..3 / MUL by 1,2,4,8)? Used to keep forwarded values out of any 1445// shape the G8 SIB probe might fold (folds emit DISPLACED, at the load/store 1446// site, possibly past a store-to-the-alloca -- unsound for forwarding). 1447func x86_fwd_is_sibish(f: *Function, u: i64) -> i64 { 1448 if u < 0 { return 0 } 1449 if u >= f.n_values { return 0 } 1450 let v: *Value = x86_val_at(f, u) 1451 if v.kind != VK_INSTR { return 0 } 1452 let inst: *Instr = v.instr 1453 if inst == (0 as *Instr) { return 0 } 1454 if inst.op == OP_SHL { 1455 if x86_fwd_const_in(f, inst.op1, 0, 1, 2, 3) == 1 { return 1 } 1456 } 1457 if inst.op == OP_MUL { 1458 if x86_fwd_const_in(f, inst.op1, 1, 2, 4, 8) == 1 { return 1 } 1459 if x86_fwd_const_in(f, inst.op0, 1, 2, 4, 8) == 1 { return 1 } 1460 } 1461 return 0 1462} 1463 1464// AUDITED consumer positions: (op, k) pairs whose emission materializes the 1465// operand via load_value_v (verified in nx_x86_64_ctx.nx: binop op0 -> dst, 1466// binop op1 -> rcx/direct, cmp op0 -> rax + op1 -> rcx/direct, STORE op1 on 1467// all four paths, RETURN op0). Everything else (GEP/LOAD/STORE addresses, 1468// calls, syscalls, branches, unops, SIMD) is NOT forwarded. SIB exclusions: 1469// - (SHL,0) with shift 0..3 and (MUL,*) by 1/2/4/8: the instr itself can be 1470// folded into a SIB address -> displaced emission -> skip. 1471// - (ADD,k) whose OTHER operand is such a SHL/MUL: LOAD(ADD(ptr,SHL(i,3))) 1472// folds the ADD too -> skip. (ADD with a non-sib MUL, e.g. k*192, is fine.) 1473func x86_fwd_consumer_ok(f: *Function, J: *Instr, k: i64) -> i64 { 1474 let op: i64 = J.op 1475 if op == OP_STORE { 1476 if k == 1 { return 1 } 1477 return 0 1478 } 1479 if op == OP_RETURN { 1480 if k == 0 { return 1 } 1481 return 0 1482 } 1483 var binop: i64 = 0 1484 if op == OP_ADD { binop = 1 } 1485 if op == OP_SUB { binop = 1 } 1486 if op == OP_MUL { binop = 1 } 1487 if op == OP_AND { binop = 1 } 1488 if op == OP_OR { binop = 1 } 1489 if op == OP_XOR { binop = 1 } 1490 if op == OP_SHL { binop = 1 } 1491 if op == OP_SHR_S { binop = 1 } 1492 if op == OP_SHR_U { binop = 1 } 1493 if op == OP_DIV_S { binop = 1 } 1494 if op == OP_DIV_U { binop = 1 } 1495 if op == OP_REM_S { binop = 1 } 1496 if op == OP_REM_U { binop = 1 } 1497 var cmp: i64 = 0 1498 if op == OP_EQ { cmp = 1 } 1499 if op == OP_NE { cmp = 1 } 1500 if op == OP_LT_S { cmp = 1 } 1501 if op == OP_LE_S { cmp = 1 } 1502 if op == OP_GT_S { cmp = 1 } 1503 if op == OP_GE_S { cmp = 1 } 1504 if binop == 0 { if cmp == 0 { return 0 } } 1505 if k > 1 { return 0 } 1506 // SIB exclusion 1: the consumer itself is a sib-foldable SHL/MUL. 1507 if op == OP_SHL { 1508 if x86_fwd_const_in(f, J.op1, 0, 1, 2, 3) == 1 { return 0 } 1509 } 1510 if op == OP_MUL { 1511 if x86_fwd_const_in(f, J.op1, 1, 2, 4, 8) == 1 { return 0 } 1512 if x86_fwd_const_in(f, J.op0, 1, 2, 4, 8) == 1 { return 0 } 1513 } 1514 // SIB exclusion 2: an ADD whose OTHER operand is a sib-ish SHL/MUL. 1515 if op == OP_ADD { 1516 var other: i64 = J.op1 1517 if k == 1 { other = J.op0 } 1518 if x86_fwd_is_sibish(f, other) == 1 { return 0 } 1519 } 1520 return 1 1521} 1522 1523// The scan. fwd_home[v] = the source alloca's home index for a forwarded 1524// load result, else -1. Runs after select_all (alloca_home final). 1525func x86_fwd_scan(f: *Function, alloca_off: *i64, alloca_home: *i64, 1526 intv: *X86Interval, fwd_home: *i64) -> i64 { 1527 var vz: i64 = 0 1528 while vz < f.n_values { fwd_home[vz] = 0 - 1; vz = vz + 1 } 1529 if X86_FWD_ENABLE == 0 { return 0 } 1530 var bi: i64 = 0 1531 while bi < f.n_blocks { 1532 let b: *BasicBlock = x86_block_at(f, bi) 1533 var inst: *Instr = b.head 1534 while inst != (0 as *Instr) { 1535 var hidx: i64 = 0 - 1 1536 if inst.op == OP_LOAD { 1537 let a: i64 = inst.op0 1538 if a >= 0 { if a < f.n_values { if alloca_home[a] >= 0 { 1539 let lt: *Type = inst.ty 1540 if lt != (0 as *Type) { if lt.size == 8 { 1541 let r: i64 = inst.result 1542 if r >= 0 { if r < f.n_values { 1543 let riv: *X86Interval = x86_intv_at(intv, r) 1544 if riv.use_count == 1 { 1545 // Walk forward to the single consumer. 1546 var J: *Instr = inst.next 1547 var verdict: i64 = 0 // 0=scan 1=fwd 2=no 1548 while verdict == 0 { 1549 if J == (0 as *Instr) { verdict = 2 } 1550 if verdict == 0 { 1551 if J.op == OP_STORE { if J.op0 == a { 1552 if X86_NEGCTL_FWD_IGNORE_STORE == 0 { verdict = 2 } 1553 } } 1554 } 1555 if verdict == 0 { 1556 if x86_is_call_class(J.op) == 1 { verdict = 2 } 1557 } 1558 if verdict == 0 { 1559 let nv: i64 = x86_n_value_operands(J) 1560 var kk: i64 = 0 1561 var found: i64 = 0 1562 var okpos: i64 = 1 1563 while kk < nv { 1564 if x86_operand_k(J, kk) == r { 1565 found = 1 1566 if x86_fwd_consumer_ok(f, J, kk) == 0 { okpos = 0 } 1567 } 1568 kk = kk + 1 1569 } 1570 if found == 1 { 1571 if okpos == 1 { verdict = 1 } 1572 if okpos == 0 { verdict = 2 } 1573 } 1574 } 1575 if verdict == 0 { J = J.next } 1576 } 1577 if verdict == 1 { 1578 hidx = alloca_home[a] 1579 if X86_NEGCTL_FWD_WRONG_HOME == 1 { 1580 hidx = hidx + 1 1581 if hidx >= X86_HOME_CAP { hidx = 0 } 1582 } 1583 } 1584 } 1585 } } 1586 } } 1587 } } } 1588 } 1589 if hidx >= 0 { fwd_home[inst.result] = hidx } 1590 inst = inst.next 1591 } 1592 bi = bi + 1 1593 } 1594 return 0 1595} 1596 1597// ===== UNIFIED SOTA SELECTION (2026-07-15) ========================== 1598// Rank ALL homing candidates -- homeable SSA values (iv.reg) AND eligible 1599// allocas (alloca_home) -- TOGETHER by loop-weighted wcost, greedily giving each 1600// the best LEGAL register. Constraints: allocas home their whole-function 1601// storage so they MUST use callee-saved (survives the setup calls); a 1602// cross-call SSA value also needs callee-saved; a call-free SSA value prefers a 1603// caller-saved reg (rsi/rdi/r8/r9, no prologue save) to leave callee-saved for 1604// those that need them. Each homed value gets a DEDICATED register for its whole 1605// range (no two share) -> sound under SSA dominance. This replaces the old 1606// SSA-first-then-alloca two-pass scheme that let cold single-use SSA temps grab 1607// the callee-saved regs the hot loop-carried allocas needed. 1608func x86_select_all(f: *Function, intv: *X86Interval, alloca_off: *i64, 1609 any_call: *i64, elig: *i64, alloca_home: *i64, 1610 used_cs_mask_out: *i64, wcost: *i64) -> i64 { 1611 *used_cs_mask_out = 0 1612 if X86_HOME_CAP_ENABLE == 0 { return 0 } 1613 let fn_clob: i64 = x86_fn_has_clobber_intrinsic(f) 1614 var caller_mask: i64 = 0 1615 var done: i64 = 0 1616 while done == 0 { 1617 // Current register availability. 1618 var free_callee: i64 = 0 1619 var cb: i64 = 0 1620 while cb < X86_HOME_CAP { 1621 if ((used_cs_mask_out[0] >> cb) & 1) == 0 { free_callee = free_callee + 1 } 1622 cb = cb + 1 1623 } 1624 var free_caller: i64 = 0 1625 if X86_CALLER_POOL_ENABLE == 1 { if fn_clob == 0 { 1626 var qb: i64 = 0 1627 while qb < X86_CALLER_CAP { 1628 if ((caller_mask >> qb) & 1) == 0 { free_caller = free_caller + 1 } 1629 qb = qb + 1 1630 } 1631 } } 1632 // Pick the highest-wcost PLACEABLE candidate. 1633 var best: i64 = 0 - 1 1634 var best_w: i64 = 0 - 1 1635 var best_alloca: i64 = 0 1636 var best_needs_callee: i64 = 0 1637 var v: i64 = 0 1638 while v < f.n_values { 1639 let iv: *X86Interval = x86_intv_at(intv, v) 1640 var is_cand: i64 = 0 1641 var is_alloca: i64 = 0 1642 if alloca_off[v] >= 0 { 1643 if elig[v] == 1 { if alloca_home[v] < 0 { is_cand = 1; is_alloca = 1 } } 1644 } 1645 if alloca_off[v] < 0 { 1646 if iv.reg < 0 { if x86_is_homeable(f, intv, alloca_off, any_call, v) == 1 { is_cand = 1 } } 1647 // G13 (2026-07-16): BIG CONSTANTS compete for homes by wcost 1648 // like everything else -- a non-imm32 const re-materializes a 1649 // 10-byte movabsq at EVERY use (per-iteration in loops). The 1650 // prologue materializer + VL_REGISTER read path already exist 1651 // (G5); the old G5 selector only got LEFTOVER regs, which the 1652 // unified allocator never leaves. crosses_call gates caller- 1653 // saved exactly as for SSA values (read-only, but a clobbered 1654 // caller-saved home would still read garbage). 1655 if is_cand == 0 { if iv.reg < 0 { if iv.start >= 0 { 1656 let g13v: *Value = x86_val_at(f, v) 1657 if g13v.kind == VK_CONST_INT { 1658 if x86_g5_imm32_ok(g13v.const_int) == 0 { is_cand = 1 } 1659 } 1660 } } } 1661 } 1662 if is_cand == 1 { 1663 var nc: i64 = 0 1664 if is_alloca == 1 { nc = 1 } 1665 if is_alloca == 0 { if iv.crosses_call == 1 { if X86_NEGCTL_CALLER_IGNORE_CALL == 0 { nc = 1 } } } 1666 var placeable: i64 = 0 1667 if nc == 1 { if free_callee > 0 { placeable = 1 } } 1668 if nc == 0 { 1669 if free_callee > 0 { placeable = 1 } 1670 if free_caller > 0 { if wcost[v] >= X86_CALLER_MIN_WCOST { placeable = 1 } } 1671 // G13: call-free CONSTANTS take caller-saved at ANY wcost -- 1672 // read-only, so a caller-saved home has zero move overhead 1673 // (no store-back ever; one prologue movabsq). MUST mirror 1674 // the chosen-branch exemption below or the pick loop hangs. 1675 if placeable == 0 { if free_caller > 0 { 1676 let g13p: *Value = x86_val_at(f, v) 1677 if g13p.kind == VK_CONST_INT { placeable = 1 } 1678 } } 1679 } 1680 if placeable == 1 { if wcost[v] > best_w { 1681 best = v; best_w = wcost[v]; best_alloca = is_alloca; best_needs_callee = nc 1682 } } 1683 } 1684 v = v + 1 1685 } 1686 if best < 0 { done = 1 } 1687 if best >= 0 { if best_w < X86_HOME_MIN_USES { done = 1 } } 1688 if done == 0 { 1689 var chosen: i64 = 0 - 1 1690 // Deep-loop call-free SSA prefers caller-saved (leave callee for those 1691 // needing it); shallow values (wcost < threshold) fall through to 1692 // callee-only, matching the baseline for tight scalar loops. 1693 // G13: constants are caller-eligible at ANY wcost (mirrors placeable). 1694 var cpref: i64 = 0 1695 if best_needs_callee == 0 { if free_caller > 0 { 1696 if best_w >= X86_CALLER_MIN_WCOST { cpref = 1 } 1697 if cpref == 0 { 1698 let g13b: *Value = x86_val_at(f, best) 1699 if g13b.kind == VK_CONST_INT { cpref = 1 } 1700 } 1701 } } 1702 if cpref == 1 { 1703 var qb2: i64 = 0 1704 while qb2 < X86_CALLER_CAP { 1705 if chosen < 0 { if ((caller_mask >> qb2) & 1) == 0 { 1706 chosen = X86_CALLER_BASE + qb2 1707 caller_mask = caller_mask | (1 << qb2) 1708 } } 1709 qb2 = qb2 + 1 1710 } 1711 } 1712 if chosen < 0 { 1713 var cb2: i64 = 0 1714 while cb2 < X86_HOME_CAP { 1715 if chosen < 0 { if ((used_cs_mask_out[0] >> cb2) & 1) == 0 { 1716 chosen = cb2 1717 used_cs_mask_out[0] = used_cs_mask_out[0] | (1 << cb2) 1718 } } 1719 cb2 = cb2 + 1 1720 } 1721 } 1722 if best_alloca == 1 { alloca_home[best] = chosen } 1723 if best_alloca == 0 { let biv: *X86Interval = x86_intv_at(intv, best); biv.reg = chosen } 1724 } 1725 } 1726 return 0 1727} 1728 1729// ----- allocator entry (STEP 2b + G2: SSA homes, then alloca homes) ---- 1730// alloca_off (authoritative alloca map, FIX-10). alloca_home[v] receives the 1731// G2 home index (0..4) for a registerized alloca, or -1. elide[v]=1 marks 1732// G4 dead-store temps (see x86_g4_elide_scan). 1733func x86_regalloc_function(f: *Function, alloca_off: *i64, 1734 locs: *ValueLoc, used_cs_mask_out: *i64, 1735 alloca_home: *i64, elide: *i64, 1736 fwd_home: *i64, chain_home: *i64, chain_swap: *i64) -> i64 { 1737 let n: i64 = f.n_values 1738 let intv_raw: *u8 = sys_mmap(n * 64 + 16) 1739 let intv: *X86Interval = intv_raw as *X86Interval 1740 let calls_raw: *u8 = sys_mmap(f.n_instrs * 8 + 32) 1741 let calls: *i64 = calls_raw as *i64 1742 let nc_raw: *u8 = sys_mmap(16) 1743 let nc: *i64 = nc_raw as *i64 1744 *nc = 0 1745 let ac_raw: *u8 = sys_mmap(n * 8 + 16) 1746 let any_call: *i64 = ac_raw as *i64 1747 let bs_raw: *u8 = sys_mmap(f.n_blocks * 8 + 16) 1748 let bb_start: *i64 = bs_raw as *i64 1749 let be_raw: *u8 = sys_mmap(f.n_blocks * 8 + 16) 1750 let bb_end: *i64 = be_raw as *i64 1751 1752 x86_build_intervals(f, intv, calls, nc, any_call, bb_start, bb_end) 1753 // G13: a homed CONST is materialized in the PROLOGUE -- its effective live 1754 // range is [0, last use], NOT [first use, last use]. Stretch starts to 0 so 1755 // crosses_call counts SETUP calls too (a caller-saved const home clobbered 1756 // by a pre-loop syscall's arg setup = the sha256/lcg/fnv miscompile the 1757 // battery caught 2026-07-16; call-free functions still go caller-saved). 1758 var g13i: i64 = 0 1759 while g13i < n { 1760 let g13iv: *X86Interval = x86_intv_at(intv, g13i) 1761 if g13iv.start > 0 { 1762 let g13val: *Value = x86_val_at(f, g13i) 1763 if g13val.kind == VK_CONST_INT { g13iv.start = 0 } 1764 } 1765 g13i = g13i + 1 1766 } 1767 // STOP-SHIP fix: extend intervals across back-edges BEFORE the call scan, 1768 // so loop-internal calls are counted for values live across the edge. 1769 x86_extend_backedge_intervals(f, intv, bb_start, bb_end) 1770 x86_mark_crosses_call(intv, n, calls, *nc) 1771 // SOTA: loop-depth-weighted spill cost drives BOTH SSA and alloca selection. 1772 let dep_raw: *u8 = sys_mmap(f.n_blocks * 8 + 16) 1773 let depth: *i64 = dep_raw as *i64 1774 let wc_raw: *u8 = sys_mmap(n * 8 + 16) 1775 let wcost: *i64 = wc_raw as *i64 1776 x86_compute_loop_depth(f, depth) 1777 x86_compute_wcost(f, depth, wcost) 1778 // UNIFIED SOTA selection: SSA values AND allocas ranked TOGETHER by 1779 // loop-weighted wcost. ah_scan first (computes alloca eligibility `elig`), 1780 // then one pass gives each candidate the best legal register (allocas + 1781 // cross-call SSA -> callee-saved; call-free SSA prefers caller-saved). 1782 var ai: i64 = 0 1783 while ai < n { alloca_home[ai] = 0 - 1; ai = ai + 1 } 1784 let el_raw: *u8 = sys_mmap(n * 8 + 16) 1785 let elig: *i64 = el_raw as *i64 1786 let ct_raw: *u8 = sys_mmap(n * 8 + 16) 1787 let acnt: *i64 = ct_raw as *i64 1788 x86_ah_scan(f, alloca_off, elig, acnt) 1789 x86_select_all(f, intv, alloca_off, any_call, elig, alloca_home, used_cs_mask_out, wcost) 1790 // G10: home-forwarding scan (needs the FINAL alloca_home + use counts). 1791 x86_fwd_scan(f, alloca_off, alloca_home, intv, fwd_home) 1792 // G11: chain fusion scan (runs AFTER fwd_scan -- it writes fwd_home[head] 1793 // for the chain-head kill and must not be wiped by fwd_scan's init). 1794 x86_chain_scan(f, alloca_off, alloca_home, intv, fwd_home, chain_home, chain_swap) 1795 1796 // Lower SSA intervals to ValueLocs: homed (iv.reg>=0) -> VL_REGISTER else spill. 1797 let lbase: i64 = locs as i64 1798 var k: i64 = 0 1799 while k < n { 1800 let iv: *X86Interval = x86_intv_at(intv, k) 1801 let l: *ValueLoc = (lbase + k * 16) as *ValueLoc 1802 if iv.reg >= 0 { 1803 l.kind = VL_REGISTER 1804 l.idx = iv.reg 1805 } 1806 if iv.reg < 0 { 1807 l.kind = VL_SPILLED 1808 l.idx = 0 - 1 1809 } 1810 k = k + 1 1811 } 1812 1813 // G5: big constants take whatever homes remain. 1814 x86_g5_select_const_homes(f, intv, locs, used_cs_mask_out) 1815 1816 // G4: dead single-use temp stores (needs the FINAL locs -- runs last). 1817 x86_g4_elide_scan(f, locs, alloca_off, intv, elide, chain_home, chain_swap, fwd_home) 1818 // G10 tripwire: a forwarded (killed) load's slot is NEVER written; flag it 1819 // elide so any unaudited slot read hits the loud .G4_elided_slot_load_bug 1820 // link failure instead of silently reading stale stack garbage. (The fwd 1821 // check in load_value_v/load_value runs BEFORE the elide tripwire, so the 1822 // one legitimate consumer is unaffected.) 1823 var tw: i64 = 0 1824 while tw < n { 1825 if fwd_home[tw] >= 0 { elide[tw] = 1 } 1826 // G11 tripwire: chain-internal values never materialize anywhere (the 1827 // next chain step operates on the home in place); an unaudited slot 1828 // read must fail loud, not read stale garbage. 1829 if chain_home[tw] >= 0 { elide[tw] = 1 } 1830 tw = tw + 1 1831 } 1832 return 0 1833}