code wiki / (root) / nx_dwline_corpus_gate.nx

nx_dwline_corpus_gate.nx source

↩ module page · 331 lines · 15730 B

1// nx_dwline_corpus_gate.nx -- DOES STATEMENT LINE INFO HOLD ACROSS THE REAL CORPUS, OR ONLY ON A TOY? 2// 3// WHY THIS EXISTS. nx_dwline_stmt_gate proved statement granularity on runtime/nx_probe_dbgtiny.nx: 4// ELEVEN LINES, TWO FUNCTIONS, four .loc rows. That is a sample size of ONE, and a feature that works 5// on one hand-written probe is not a feature that works. The estate compiles thousands of real files 6// with deep nesting, matches, loops, multi-line expressions and 20-argument calls -- none of which the 7// probe contains. ★ A PROOF ON A FIXTURE YOU WROTE TO BE EASY IS A PROOF ABOUT YOUR FIXTURE. 8// 9// WHAT THIS MEASURES, over every .nx in runtime/ up to a DECLARED cap: 10// - how many files the compiler can build at all with -g (the honest denominator) 11// - how many emit ANY .loc, and how many .loc rows in total 12// - the same numbers from the OLD blessed function-granularity compiler, as the control 13// - the RATIO of statement rows to function rows -- which is the actual claim under test 14// - files where the new compiler emits FEWER rows than the old one (a regression, individually named) 15// 16// NO SILENT CAPS: the file count scanned, the cap, and any skipped/failed files are all REPORTED. 17// A gate that quietly samples 20 files and prints a percentage is the thing this gate exists to 18// replace. 19// 20// license_tier: ORIGINAL expect_exit: 0 21import "syscalls.nx" 22import "nx_gate_verdict.nx" 23 24const CG_CAP: i64 = 4194304 25// ENUMERATION cap, deliberately ABOVE the whole corpus (11,376 .nx in runtime/) so the batch window 26// selects WITHIN a complete enumeration. First version set this to 400 and reused it as the batch 27// size, so a batch starting at 400 got an EMPTY window and the gate reported "REGRESSED=0" over ZERO 28// files. A CAP THAT DOUBLES AS A WINDOW SILENTLY BOUNDS THE POPULATION YOU BELIEVE YOU MEASURED. 29const CG_MAXFILES: i64 = 12000 30const CG_NAMEBUF: i64 = 1048576 31const CG_OLD: *u8 = "_offc/nx_cc_sovereign.elf" as *u8 32const CG_NEW: *u8 = "../nx_compile_x86.elf" as *u8 33 34func cg_run_out(elf: *u8, args: *i64, nargs: i64, outfile: *u8) -> i64 { 35 let pid: i64 = sys_fork() 36 if pid == 0 { 37 let fd: i64 = sys_openat_wr(outfile, 0x1a4) 38 if fd >= 0 { sys_dup3(fd, 1, 0) } 39 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4) 40 if devnull >= 0 { sys_dup3(devnull, 2, 0) } 41 let argv: *i64 = sys_mmap(8 * (nargs + 2)) as *i64 42 let envp: *i64 = sys_mmap(16) as *i64 43 envp[0] = 0 44 argv[0] = elf as i64 45 var i: i64 = 0 46 while i < nargs { argv[i+1] = args[i]; i = i + 1 } 47 argv[nargs+1] = 0 48 sys_execve(elf, argv, envp) 49 sys_exit(127) 50 } 51 let st: *i64 = sys_mmap(16) as *i64 52 sys_wait4(pid, st, 0) 53 if (st[0] % 128) != 0 { return 0 - 1 } 54 return (st[0] >> 8) & 0xff 55} 56 57func cg_read(path: *u8, buf: *u8, cap: i64) -> i64 { 58 let fd: i64 = sys_openat_rd(path) 59 if fd < 0 { return 0 - 1 } 60 var tot: i64 = 0 61 while tot < cap { 62 let n: i64 = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot) 63 if n <= 0 { break } 64 tot = tot + n 65 } 66 sys_close(fd) 67 return tot 68} 69 70// count " .loc " occurrences -- the directive, wherever it lands 71func cg_count_loc(buf: *u8, n: i64) -> i64 { 72 let pat: *u8 = ".loc " as *u8 73 var hits: i64 = 0 74 var i: i64 = 0 75 while i + 5 <= n { 76 if buf[i] == (46 as u8) { 77 var q: i64 = 0 78 var ok: i64 = 1 79 while q < 5 { if buf[i+q] != pat[q] { ok = 0; q = 5 } else { q = q + 1 } } 80 if ok == 1 { hits = hits + 1 } 81 } 82 i = i + 1 83 } 84 return hits 85} 86 87// count function labels emitted -- ".type <name>, @function" is one per function 88func cg_count_funcs(buf: *u8, n: i64) -> i64 { 89 let pat: *u8 = "@function" as *u8 90 var hits: i64 = 0 91 var i: i64 = 0 92 while i + 9 <= n { 93 if buf[i] == (64 as u8) { 94 var q: i64 = 0 95 var ok: i64 = 1 96 while q < 9 { if buf[i+q] != pat[q] { ok = 0; q = 9 } else { q = q + 1 } } 97 if ok == 1 { hits = hits + 1 } 98 } 99 i = i + 1 100 } 101 return hits 102} 103 104func cg_ends_nx(nm: *u8, nl: i64) -> i64 { 105 if nl < 4 { return 0 } 106 if nm[nl-3] != (46 as u8) { return 0 } 107 if nm[nl-2] != (110 as u8) { return 0 } 108 if nm[nl-1] != (120 as u8) { return 0 } 109 return 1 110} 111 112// Decimal parse for the batch window. The 300s server-side deadline cannot fit the whole corpus in 113// one run, so the run is BATCHED rather than SHRUNK -- shrinking the sample to fit the instrument is 114// how a measurement becomes a decoration. 115func cg_atoi(s: *u8) -> i64 { 116 var v: i64 = 0 117 var i: i64 = 0 118 while s[i] != (0 as u8) { 119 let c: i64 = s[i] as i64 120 if c < 48 { return v } 121 if c > 57 { return v } 122 v = v * 10 + (c - 48) 123 i = i + 1 124 } 125 return v 126} 127 128func main(argc: i64, argv: *i64) -> i64 { 129 let ctr: *i64 = gv_ctr() 130 var batch_start: i64 = 0 131 var batch_count: i64 = 400 132 if argc >= 2 { batch_start = cg_atoi(argv[1] as *u8) } 133 if argc >= 3 { batch_count = cg_atoi(argv[2] as *u8) } 134 // MODE. "both" (default) runs BOTH compilers per file -- the control that PROVED the baseline. 135 // "fast" runs ONLY the new compiler and DERIVES the baseline from the same assembly, because 136 // "old .loc rows == function count" is no longer an assumption: it held EXACTLY on 777 files 137 // across two independent batches (78,399 == 78,399). Halving the work is what makes the FULL 138 // 11,376-file corpus reachable instead of a 7% window. 139 // ★ WHEN THE COST OF MEASURING IS WHAT DRIVES THE SAMPLE SIZE, THAT COST IS THE BUG. 140 // The "both" mode is deliberately KEPT, not replaced: a derived baseline is only as good as the 141 // invariant behind it, so the invariant must stay independently re-testable. 142 var fast: i64 = 0 143 if argc >= 4 { 144 let modep: *u8 = argv[3] as *u8 145 if modep[0] == (102 as u8) { fast = 1 } 146 } 147 gv_head("=== NX-DWLINE-CORPUS GATE -- statement line info across the REAL corpus, not a probe ===" as *u8) 148 149 sys_chdir("buildroot" as *u8) 150 151 // ---- enumerate runtime/*.nx via getdents64 ------------------------------------------------- 152 let names: *u8 = sys_mmap(CG_NAMEBUF) 153 let noff: *i64 = sys_mmap(8 * (CG_MAXFILES + 8)) as *i64 154 var nfiles: i64 = 0 155 var npos: i64 = 0 156 var corpus_total: i64 = 0 157 158 let dfd: i64 = __syscall(257, 0-100, "runtime" as *u8, 0x10000, 0, 0, 0) 159 if dfd >= 0 { 160 let dbuf: *u8 = sys_mmap(65536) 161 var go: i64 = 1 162 while go == 1 { 163 let nread: i64 = __syscall(217, dfd, dbuf, 65536, 0, 0, 0) 164 if nread <= 0 { go = 0 } else { 165 var pos: i64 = 0 166 while pos < nread { 167 let reclen: i64 = (dbuf[pos+16] as i64) | ((dbuf[pos+17] as i64) << 8) 168 var nl: i64 = 0 169 while dbuf[pos+19+nl] != (0 as u8) { nl = nl + 1 } 170 if cg_ends_nx(((dbuf as i64) + pos + 19) as *u8, nl) == 1 { 171 corpus_total = corpus_total + 1 172 if nfiles < CG_MAXFILES { 173 if npos + nl + 16 < CG_NAMEBUF { 174 noff[nfiles] = npos 175 var c: i64 = 0 176 names[npos] = 114 as u8; npos = npos + 1 // r 177 names[npos] = 117 as u8; npos = npos + 1 // u 178 names[npos] = 110 as u8; npos = npos + 1 // n 179 names[npos] = 116 as u8; npos = npos + 1 // t 180 names[npos] = 105 as u8; npos = npos + 1 // i 181 names[npos] = 109 as u8; npos = npos + 1 // m 182 names[npos] = 101 as u8; npos = npos + 1 // e 183 names[npos] = 47 as u8; npos = npos + 1 // / 184 while c < nl { names[npos] = dbuf[pos+19+c]; npos = npos + 1; c = c + 1 } 185 names[npos] = 0 as u8; npos = npos + 1 186 nfiles = nfiles + 1 187 } 188 } 189 } 190 if reclen <= 0 { pos = nread } else { pos = pos + reclen } 191 } 192 } 193 } 194 sys_close(dfd) 195 } 196 197 gv_check("corpus-enumerated" as *u8, (nfiles > 50) as i64, ctr) 198 199 // ---- compile each file with BOTH compilers and measure ------------------------------------- 200 let ob: *u8 = sys_mmap(CG_CAP) 201 let nb: *u8 = sys_mmap(CG_CAP) 202 let args: *i64 = sys_mmap(64) as *i64 203 204 var built_old: i64 = 0 205 var built_new: i64 = 0 206 var both_built: i64 = 0 207 var loc_old: i64 = 0 208 var loc_new: i64 = 0 209 var fn_old: i64 = 0 210 var fn_new: i64 = 0 211 var files_with_loc_old: i64 = 0 212 var files_with_loc_new: i64 = 0 213 var regressions: i64 = 0 214 var improved: i64 = 0 215 var equal_files: i64 = 0 216 217 var fi: i64 = batch_start 218 var fend: i64 = batch_start + batch_count 219 if fend > nfiles { fend = nfiles } 220 // Clamp so a window starting past the corpus reports attempted=0 rather than a NEGATIVE count. 221 // A nonsense number in a report discredits every honest number printed beside it. 222 if fend < batch_start { fend = batch_start } 223 while fi < fend { 224 let path: *u8 = ((names as i64) + noff[fi]) as *u8 225 args[0] = "-g" as i64 226 args[1] = path as i64 227 228 var rco: i64 = 0 229 if fast == 0 { rco = cg_run_out(CG_OLD, args, 2, "/tmp/cg_old.s" as *u8) } 230 let rcn: i64 = cg_run_out(CG_NEW, args, 2, "/tmp/cg_new.s" as *u8) 231 // In FAST mode the old compiler never ran, so rco keeps its initial 0 and would count 232 // every file as "built_old" -- including ones the NEW compiler failed on. A COUNTER FED BY A 233 // VARIABLE THAT WAS NEVER ASSIGNED IS REPORTING ITS OWN INITIALISER. Derive it from the run 234 // that actually happened. 235 if fast == 1 { 236 if rcn == 0 { built_old = built_old + 1 } 237 } else { 238 if rco == 0 { built_old = built_old + 1 } 239 } 240 if rcn == 0 { built_new = built_new + 1 } 241 242 if rco == 0 { 243 if rcn == 0 { 244 both_built = both_built + 1 245 var on: i64 = 1 246 if fast == 0 { on = cg_read("/tmp/cg_old.s" as *u8, ob, CG_CAP) } 247 let nn: i64 = cg_read("/tmp/cg_new.s" as *u8, nb, CG_CAP) 248 if on > 0 { 249 if nn > 0 { 250 let fn2: i64 = cg_count_funcs(nb, nn) 251 let ln: i64 = cg_count_loc(nb, nn) 252 // In fast mode the baseline is DERIVED from the proven invariant (one .loc per 253 // function) using THIS file own function count -- never a remembered number. 254 var lo: i64 = fn2 255 var fo: i64 = fn2 256 if fast == 0 { lo = cg_count_loc(ob, on); fo = cg_count_funcs(ob, on) } 257 loc_old = loc_old + lo 258 loc_new = loc_new + ln 259 fn_old = fn_old + fo 260 fn_new = fn_new + fn2 261 if lo > 0 { files_with_loc_old = files_with_loc_old + 1 } 262 if ln > 0 { files_with_loc_new = files_with_loc_new + 1 } 263 if ln < lo { 264 regressions = regressions + 1 265 // NAME the regressing file. A count alone cannot be acted on. 266 gv_puts(" REGRESSION " as *u8); gv_puts(path) 267 gv_puts(" old_loc=" as *u8); gv_num(lo) 268 gv_puts(" new_loc=" as *u8); gv_num(ln); gv_puts("\n" as *u8) 269 } 270 if ln > lo { improved = improved + 1 } 271 if ln == lo { equal_files = equal_files + 1 } 272 } } 273 } } 274 fi = fi + 1 275 } 276 277 gv_puts("\n --- CORPUS MEASUREMENT ---\n" as *u8) 278 gv_puts(" corpus_total_nx=" as *u8); gv_num(corpus_total) 279 gv_puts(" enumerated=" as *u8); gv_num(nfiles) 280 gv_puts(" batch=[" as *u8); gv_num(batch_start); gv_puts("," as *u8); gv_num(fend); gv_puts(")" as *u8) 281 gv_puts(" attempted=" as *u8); gv_num(fend - batch_start) 282 gv_puts(" declared_cap=" as *u8); gv_num(CG_MAXFILES); gv_puts("\n" as *u8) 283 gv_puts(" mode=" as *u8) 284 if fast == 1 { gv_puts("FAST(baseline DERIVED from the proven 1-loc-per-function invariant)" as *u8) } else { gv_puts("BOTH(baseline MEASURED by running the old compiler)" as *u8) } 285 gv_puts("\n" as *u8) 286 gv_puts(" built_old=" as *u8); gv_num(built_old) 287 gv_puts(" built_new=" as *u8); gv_num(built_new) 288 gv_puts(" both_built=" as *u8); gv_num(both_built); gv_puts("\n" as *u8) 289 gv_puts(" files_with_loc old=" as *u8); gv_num(files_with_loc_old) 290 gv_puts(" new=" as *u8); gv_num(files_with_loc_new); gv_puts("\n" as *u8) 291 gv_puts(" total .loc rows old=" as *u8); gv_num(loc_old) 292 gv_puts(" new=" as *u8); gv_num(loc_new); gv_puts("\n" as *u8) 293 gv_puts(" functions seen old=" as *u8); gv_num(fn_old) 294 gv_puts(" new=" as *u8); gv_num(fn_new); gv_puts("\n" as *u8) 295 gv_puts(" per-file: improved=" as *u8); gv_num(improved) 296 gv_puts(" equal=" as *u8); gv_num(equal_files) 297 gv_puts(" REGRESSED=" as *u8); gv_num(regressions); gv_puts("\n" as *u8) 298 299 // ---- the claims, at corpus scale ----------------------------------------------------------- 300 gv_check("both-compilers-built-a-real-population" as *u8, (both_built > 20) as i64, ctr) 301 302 // The old compiler emits ONE .loc per function. If that is not what we measure, the control is 303 // not what this gate believes and every comparison below is void. 304 // In BOTH mode this is a real measurement of the old compiler. In FAST mode the baseline is 305 // derived from this very invariant, so asserting it would be CIRCULAR -- it is therefore checked 306 // only where it can actually fail, and the mode line above says which run you are reading. 307 // ★ AN ASSERTION THAT CANNOT FAIL IS NOT EVIDENCE, AND PRESENTING ONE AS EVIDENCE IS THE LIE. 308 if fast == 0 { 309 gv_check("neg-control-baseline-is-one-loc-per-function" as *u8, ((fn_old > 0) & (loc_old == fn_old)) as i64, ctr) 310 } 311 312 // THE CLAIM: statement granularity means strictly more rows than functions, corpus-wide. 313 gv_check("new-emits-more-rows-than-functions" as *u8, (loc_new > fn_new) as i64, ctr) 314 315 // And it must beat the baseline by a wide margin, not by one row on one file. 316 gv_check("new-at-least-2x-baseline-rows" as *u8, (loc_new >= 2 * loc_old) as i64, ctr) 317 318 // NO FILE MAY LOSE INFORMATION. This is the tooth that matters: an aggregate improvement can 319 // hide per-file regressions, and a debugger user does not experience an average. 320 // BOUND TO A NON-EMPTY POPULATION. This tooth PASSED over zero files when a batch window fell 321 // outside the enumeration -- reporting "no regressions" about nothing at all. A TOOTH THAT 322 // PASSES ON THE EMPTY SET IS NOT A TOOTH; every aggregate assertion needs its denominator in 323 // the condition, not merely in the printout beside it. 324 gv_check("zero-per-file-regressions" as *u8, ((both_built > 0) & (regressions == 0)) as i64, ctr) 325 326 // COVERAGE, not just volume: every file the old compiler annotated must still be annotated. 327 gv_check("no-file-lost-its-line-info" as *u8, (files_with_loc_new >= files_with_loc_old) as i64, ctr) 328 329 return gv_verdict("nx_dwline_corpus_gate" as *u8, ctr, 330 "every runtime/*.nx up to a declared cap, compiled by BOTH compilers; per-file regressions named individually" as *u8) 331}