nx_x86_regalloc_use_gate.nx source
↩ module page · 153 lines · 8947 B
1// nx_x86_regalloc_use_gate.nx -- GATE for LN7 "backend consumes its register allocation"
2// (lang.plan rung LN7, watch symbol x86_use_regalloc in nx_x86_64_ctx.nx).
3//
4// THE RUNG'S DONE-RULE: the x86 emitter reads a homed/forwarded value IN PLACE as a source
5// operand of a compare/binop/SIB instead of copying it into a scratch register first, and the
6// allocator no longer spends a home register on a load it can forward. Measured on the estate's
7// own instrument runtime/nx_probe_bchk_asm.nx (a bounds-checked hot loop over [1024]i64):
8// pre-LN7 loop head: movq %r12, %r15 ; cmpq $1024, %r15 (copy the homed counter, THEN compare)
9// LN7 loop head: cmpq $1024, %r12 (compare the homed counter in place)
10// main() real instructions (crash-guard ON, the default build this gate uses): 62 -> 58
11// (--no-crash-guard shape is 64 -> 60; the guard adds a fixed prologue to both)
12// the loop-bound check reads the homed counter DIRECTLY: pre-LN7 copies it to a scratch reg
13// first (movq %r12,%r15 ; cmpq $1024,%r15), LN7 emits `cmpq $1024, %r12` -- present iff LN7.
14// checked-loop bench nx_bounds_perf: 8229/8278/8385 us -> 6939/6904/6912 us (median -16.5%)
15// All measured 2026-08-23 on the SOUND rule (X86_LN7_BLOCK_FWD_ENABLE with the cut-the-load-block
16// availability check; an earlier merge-unsound draft forwarded more but miscompiled self-host and
17// was rewritten). Instruction method = the awk equivalent ln7_main_instrs below.
18//
19// Subject = the compiler under test (argv[1], default the live builder). This gate does NOT mutate
20// its own source to bite: the discriminator is the COMPILER, so the bite is running it against the
21// banked pre-LN7 compiler (T2 and T3 go RED) vs the LN7 compiler (all GREEN). Drive it as:
22// nx_x86_regalloc_use_gate _offc/nx_cc_sovereign.elf (LN7 live -> GREEN)
23// nx_x86_regalloc_use_gate /path/to/pre-ln7-compiler.elf (bite -> RED on T2,T3)
24//
25// Driver = nx_ccgate_lib (the one compiler-gate driver): .s capture in /tmp/nxln7/, per-pid paths,
26// runnables under _build/ (NAS /tmp is noexec). ASCII-only source. license_tier: ORIGINAL. No hw
27// writes (Rule 26).
28import "nx_syscalls.nx"
29import "nx_gate_verdict.nx"
30import "nx_ccgate_lib.nx"
31
32// Recorded baseline (2026-08-23, crash-guard ON -- the default build ccg_build produces). Method:
33// real instructions in main() of the probe's emitted asm = ln7_main_instrs below (awk equivalent
34// '/^main:/{m=1} /^.size main/{m=0} m && !/^[.]/ && !/:$/ && NF' | wc -l). Pre-LN7 = 62, LN7 = 58.
35// The ceiling sits strictly between the two, so the LN7 compiler passes and a compiler emitting the
36// pre-LN7 copy-before-compare fails. Derived from the fixture, not chosen: 62 is what the incumbent
37// emits. T2 (the in-place compare) and T3 (this count) measure DIFFERENT axes -- a specific
38// instruction shape vs the aggregate size -- so a trivial change cannot satisfy both by accident.
39const LN7_PROBE_MAIN_MAX: i64 = 60
40// The probe declares [1024]i64, so 1024 is the loop bound it compares against -- a fixture-derived
41// constant, not a tuned one. r12 is the first callee-saved home (X86_HOME_CAP order r12..rbx), which
42// the allocator deterministically assigns to the loop counter of this single-counter loop.
43const LN7_DIRECT_CMP: *u8 = "cmpq $1024, %r12\x00" // LN7 in-place compare of the homed counter
44
45// Count real instructions in main() of an emitted .s file (labels/directives/blanks excluded).
46// -1 if unreadable, so "could not look" is distinct from "zero".
47func ln7_main_instrs(path: *u8) -> i64 {
48 let szp: *i64 = sys_mmap(16) as *i64
49 let b: *u8 = sys_read_file(path, szp)
50 if (b as i64) == 0 { return 0 - 1 }
51 let n: i64 = szp[0]
52 var i: i64 = 0
53 var ls: i64 = 0
54 var inmain: i64 = 0
55 var count: i64 = 0
56 var go: i64 = 1
57 while go == 1 {
58 var at_end: i64 = 0
59 if i >= n { at_end = 1 }
60 var is_nl: i64 = 0
61 if at_end == 1 { is_nl = 1 }
62 if at_end == 0 { if b[i] == (10 as u8) { is_nl = 1 } }
63 if is_nl == 1 {
64 // line is b[ls, i)
65 let len: i64 = i - ls
66 if ccg_mem_has(b, ls, i, "main:\x00" as *u8, 5) == 1 { if len == 5 { inmain = 1 } }
67 if ccg_mem_has(b, ls, i, ".size main\x00" as *u8, 10) == 1 { inmain = 0 }
68 if inmain == 1 {
69 if len > 0 {
70 let c0: i64 = b[ls] as i64
71 // skip labels (a line beginning "main:" or ".L..." ends in ':'), directives ('.'),
72 // and blank/indented-empty lines. Instruction lines begin with a space then a mnemonic.
73 var is_label: i64 = 0
74 if b[i - 1] == (58 as u8) { is_label = 1 } // ends in ':'
75 var is_dir: i64 = 0
76 if c0 == 46 { is_dir = 1 } // '.'
77 // find first non-space
78 var p: i64 = ls
79 var seen: i64 = 0
80 while p < i { if b[p] != (32 as u8) { if b[p] != (9 as u8) { seen = 1; p = i } } p = p + 1 }
81 if seen == 1 { if is_label == 0 { if is_dir == 0 { count = count + 1 } } }
82 }
83 }
84 ls = i + 1
85 if at_end == 1 { go = 0 }
86 }
87 i = i + 1
88 }
89 return count
90}
91
92func main(argc: i64, argv: *i64) -> i64 {
93 var cc: *u8 = "_offc/nx_cc_sovereign.elf\x00"
94 if argc >= 2 { cc = argv[1] as *u8 }
95 ccg_anchor_root()
96 sys_mkdir("/tmp/nxln7\x00" as *u8, CCG_MODE_X)
97 let pid: i64 = ccg_pid()
98
99 let p_s: *u8 = ccg_path("/tmp/nxln7/probe_\x00" as *u8, pid, ".s\x00" as *u8)
100 let p_e: *u8 = ccg_path("/tmp/nxln7/probe_\x00" as *u8, pid, ".err\x00" as *u8)
101 let p_elf: *u8 = ccg_path("_build/nxln7_probe_\x00" as *u8, pid, ".elf\x00" as *u8)
102 let p_asm: *u8 = ccg_path("_build/nxln7_asm_\x00" as *u8, pid, ".elf\x00" as *u8)
103 let pf_s: *u8 = ccg_path("/tmp/nxln7/perf_\x00" as *u8, pid, ".s\x00" as *u8)
104 let pf_e: *u8 = ccg_path("/tmp/nxln7/perf_\x00" as *u8, pid, ".err\x00" as *u8)
105 let pf_elf: *u8 = ccg_path("_build/nxln7_perf_\x00" as *u8, pid, ".elf\x00" as *u8)
106
107 let ctr: *i64 = gv_ctr()
108 gv_head("=== nx_x86_regalloc_use_gate -- LN7 backend consumes its register allocation: the homed loop counter is compared IN PLACE, the load of a homed alloca is forwarded not re-homed, the checked loop shrinks, behaviour unchanged ===" as *u8)
109
110 // -- build the probe with the compiler under test --
111 let fix: *u8 = "runtime/nx_probe_bchk_asm.nx\x00"
112 let bp: i64 = ccg_build(cc, 0 as *u8, fix, p_s, p_elf, p_e, p_asm)
113 ccg_val("probe_build_rc" as *u8, bp)
114 var t1: i64 = 0
115 if bp == 0 { t1 = 1 }
116 gv_check("T1 probe-compiles-under-the-compiler-under-test (a normal citizen of the tree)" as *u8, t1, ctr)
117
118 // -- the emitted asm: the LN7 outcome vs the pre-LN7 copy --
119 let direct: i64 = ccg_file_has(p_s, LN7_DIRECT_CMP) // in-place compare of the homed counter
120 let instrs: i64 = ln7_main_instrs(p_s)
121 ccg_val("direct_cmp_present" as *u8, direct)
122 ccg_val("main_instrs" as *u8, instrs)
123 ccg_val("main_instrs_ceiling" as *u8, LN7_PROBE_MAIN_MAX)
124
125 var t2: i64 = 0
126 if bp == 0 { if direct == 1 { t2 = 1 } }
127 gv_check("T2 homed-counter-compared-IN-PLACE (the LN7 done-rule: cmpq $1024,%r12 with no scratch copy before the compare; a pre-LN7 compiler copies the counter to a scratch reg first and FAILS here)" as *u8, t2, ctr)
128
129 var t3: i64 = 0
130 if bp == 0 { if instrs >= 0 { if instrs <= LN7_PROBE_MAIN_MAX { t3 = 1 } } }
131 gv_check("neg-control-T3-main-instruction-count-BELOW-the-copy-before-compare-baseline (<=60; LN7=58, a compiler that copies the counter before comparing emits >=62 and FAILS -- an independent axis from T2)" as *u8, t3, ctr)
132
133 // -- behaviour: the optimised probe runs to a clean exit (no forwarding miscompile) --
134 var pr_exit: i64 = 0 - 1
135 if bp == 0 { pr_exit = ccg_phase(p_elf, 0 as *u8) }
136 ccg_val("probe_exit" as *u8, pr_exit)
137 var t5: i64 = 0
138 if pr_exit == 0 { t5 = 1 }
139 gv_check("T4 probe-runs-exit0 (the forwarded/in-place code executes; a forwarding miscompile would trap or return nonzero)" as *u8, t5, ctr)
140
141 // -- behaviour: the checked-loop bench compiles and runs clean under the same compiler --
142 let bperf: i64 = ccg_build(cc, 0 as *u8, "runtime/nx_bounds_perf.nx\x00" as *u8, pf_s, pf_elf, pf_e, p_asm)
143 var pf_exit: i64 = 0 - 1
144 if bperf == 0 { pf_exit = ccg_phase(pf_elf, 0 as *u8) }
145 ccg_val("perf_build_rc" as *u8, bperf)
146 ccg_val("perf_exit" as *u8, pf_exit)
147 var t6: i64 = 0
148 if pf_exit == 0 { t6 = 1 }
149 gv_check("T5 bounds-perf-bench-runs-exit0 (the checked hot loop the rung speeds up still computes its checksum)" as *u8, t6, ctr)
150
151 let rc: i64 = gv_verdict("X86-REGALLOC-USE-LN7" as *u8, ctr, "LN7 backend uses its register allocation" as *u8)
152 return rc
153}