code wiki / (root) / nx_x86_regalloc_use_gate.nx

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}