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}