code wiki / (root) / nx_cc_equiv_gate.nx

nx_cc_equiv_gate.nx source

↩ module page · 675 lines · 35165 B

1// nx_cc_equiv_gate.nx -- COMPILER DIFFERENTIAL-BEHAVIOR ORACLE: miscompile-detection for the sovereign toolchain (EMI-style baseline-vs-challenger build + stdout byte-equal + self-host stage). 2// 3// Team organ (Engineer-owned) born from the dom/licm dormant-paths 4// discovery (ledger T#opt-dom-licm-dormant-paths, 2026-06-10): the 5// deref-cast precedence bug had silently disabled 4 optimizer reads 6// since they were written, and AWAKENING them broke the self-host at 7// generation 3 -- a class of defect no single-binary test catches. 8// This gate judges ANY compiler mutation by BEHAVIOR, not by asm: 9// 10// For each corpus row (deterministic, network-free modules): 11// build with BASELINE cc -> run -> raw wait4 status + stdout bytes 12// build with CHALLENGER cc -> run -> same 13// row PASS iff both build, statuses EQUAL, stdout BYTE-EQUAL. 14// (Different codegen/hoisting is fine; different BEHAVIOR is not.) 15// 16// SELF-HOST stage (the gen3-class catch): 17// challenger compiles runtime/nx_compile_x86.nx -> gen2 binary; 18// gen2 must then compile a probe module whose binary runs exit 0. 19// This is exactly where the awakened-LICM breakage surfaced 20// (gen3: "unknown type name 'nx_size'"). 21// 22// Usage: nx_cc_equiv_gate <challenger_elf> [feature_source.nx] 23// challenger: REQUIRED (a stale default once produced ten confident FAIL rows about a leftover). 24// baseline: _offc/nx_cc_sovereign.elf (the LIVE toolchain) unless knowledge/cc_equiv_baseline.conf 25// names another compiler on its first line. A node names the HUB's promoted compiler there 26// (pulled through nx_content_get, digest-verified) so a candidate that ADDS a language 27// feature can still be judged DIFFERENTIALLY against a baseline that builds the whole 28// corpus (2026-09-15: __size_of on struct types left 5 of 10 rows unbuildable by the live 29// laptop compiler, and "both sides must build" could never pass any capability-adding 30// release -- loosening the rule was the only other exit, and it is the wrong one). 31// ⚠PATHS ARE RESOLVED AFTER THE BUILDROOT ANCHOR: from nxc2/ a relative `_build/x.elf` means 32// buildroot/_build/x.elf. Measured 2026-09-15: a relative challenger path silently named a Sep-4 33// fossil in buildroot/_build instead of the candidate in nxc2/_build, and the gate printed the 34// caller's path as if it were that binary. Pass an ABSOLUTE path; the gate prints the byte size of 35// every compiler it will exec so the identity is checkable, and refuses a challenger byte-identical 36// to the baseline (a self-comparison measures nothing). 37// Run with CWD=nxc2/ or nishihost/ (it anchors into buildroot/). Exit 0 iff all rows + self-host PASS. 38// Verdicts stream to stdout AND append to the durable log 39// knowledge/status/cc_equiv_gate.log (Archivist rule). 40// 41// ORCHESTRATION is pure NishiLang fork/dup3/execve/wait4 -- the 42// sovereign exit-judging law (no shell, no $?). 43// 44// license_tier: ORIGINAL 45 46import "nx_syscalls.nx" 47import "nx_gate_verdict.nx" 48 49const EQ_ROWS: i64 = 10 50// One path line; PATH_MAX-sized on purpose (the baseline conf holds one compiler path, never a body). 51const EQ_CONF_CAP: i64 = 4096 52 53// Length of the first line of buf[0,n): stops at LF, CR or NUL. Reads the one-line baseline conf. 54func eq_first_line(buf: *u8, n: i64) -> i64 { 55 var i: i64 = 0 56 while i < n { 57 if buf[i] == (10 as u8) { return i } 58 if buf[i] == (13 as u8) { return i } 59 if buf[i] == (0 as u8) { return i } 60 i = i + 1 61 } 62 return n 63} 64 65func eq_puts2(logfd: i64, s: *u8) -> i64 { 66 var n: i64 = 0 67 while s[n] != (0 as u8) { n = n + 1 } 68 sys_write(1, s, n) 69 if logfd > 0 { sys_write(logfd, s, n) } 70 return 0 71} 72 73func eq_putn2(logfd: i64, v: i64) -> i64 { 74 let t: *u8 = sys_mmap(28) 75 let o: *u8 = sys_mmap(28) 76 var m: i64 = v 77 var neg: i64 = 0 78 if m < 0 { neg = 1; m = 0 - m } 79 var k: i64 = 0 80 if m == 0 { t[0] = 48 as u8; k = 1 } 81 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 82 var w: i64 = 0 83 if neg == 1 { o[0] = 45 as u8; w = 1 } 84 var i: i64 = 0 85 while i < k { o[w + i] = t[k - 1 - i]; i = i + 1 } 86 sys_write(1, o, w + k) 87 if logfd > 0 { sys_write(logfd, o, w + k) } 88 return 0 89} 90 91func eq_cat(dst: *u8, off: i64, s: *u8) -> i64 { 92 var i: i64 = 0 93 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 } 94 return off + i 95} 96 97// Decimal-append v into dst at off; returns new off (buffer sibling of eq_putn2). 98func eq_catn(dst: *u8, off: i64, v: i64) -> i64 { 99 let t: *u8 = sys_mmap(28) 100 var m: i64 = v 101 if m < 0 { m = 0 - m } 102 var k: i64 = 0 103 if m == 0 { t[0] = 48 as u8; k = 1 } 104 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 105 var i: i64 = 0 106 while i < k { dst[off + i] = t[k - 1 - i]; i = i + 1 } 107 return off + k 108} 109 110// "<pre><nonce><suf>" heap path. PER-RUN UNIQUE TEMPS (2026-08-05): every temp below used to 111// be one FIXED shared path, so two concurrent gate runs (two seats, or one transport retry -- 112// OBSERVED live: epochs 1785972850/1785972865, 15s apart) overwrote each other's products; 113// worst, both raced the SAME _build/_eq_asm_out.elf rename source, so run A could rename run 114// B's assembled binary into its own slot and compare cross-wired artifacts. Same fixed-shared- 115// capture-path class as the mgmt /api/build seq236 bug. A differential oracle whose two sides 116// can belong to DIFFERENT runs is not a measurement. 117func eq_uniq(pre: *u8, nonce: i64, suf: *u8) -> *u8 { 118 let b: *u8 = sys_mmap(128) 119 var o: i64 = eq_cat(b, 0, pre) 120 o = eq_catn(b, o, nonce) 121 o = eq_cat(b, o, suf) 122 b[o] = 0 as u8 123 return b 124} 125 126// Run nonce: 8 CSPRNG bytes -> positive decimal < 1e9; epoch-seconds fallback. Sub-second 127// double-fires (the transport-retry shape) get distinct nonces where epoch alone collides. 128func eq_nonce() -> i64 { 129 let fd: i64 = sys_openat_rd("/dev/urandom\x00" as *u8) 130 if fd < 0 { return sys_now_realtime_sec() } 131 let b: *u8 = sys_mmap(16) 132 let n: i64 = sys_read(fd, b, 8) 133 sys_close(fd) 134 if n < 8 { return sys_now_realtime_sec() } 135 var v: i64 = 0 136 var i: i64 = 0 137 while i < 8 { v = (v << 8) | (b[i] as i64); i = i + 1 } 138 if v < 0 { v = 0 - v } 139 if v < 0 { v = 0 } 140 return v % 1000000000 141} 142 143// fork + redirects + execve; parent waits; returns RAW wait status. 144func eq_run(path: *u8, argv: *i64, envp: *i64, redir_out: i64, redir_err: i64) -> i64 { 145 let pid: i64 = sys_fork() 146 if pid == 0 { 147 if redir_out >= 0 { sys_dup3(redir_out, 1, 0) } 148 if redir_err >= 0 { sys_dup3(redir_err, 2, 0) } 149 sys_execve(path, argv, envp) 150 sys_exit(127) 151 } 152 let st: *i64 = sys_mmap(16) as *i64 153 sys_wait4(pid, st, 0) 154 return st[0] 155} 156 157// Compile src_path with cc_path -> elf_out via as+ld. 158// Returns 0 ok, 1 compile-fail, 2 as-fail, 3 ld-fail. 159// -1 missing, else byte size. The 127 hunt: name WHICH stage produced nothing. 160func eq_fsize(path: *u8) -> i64 { 161 let fd: i64 = sys_openat_rd(path) 162 if fd < 0 { return 0 - 1 } 163 let sz: i64 = sys_lseek(fd, 0, 2) 164 sys_close(fd) 165 return sz 166} 167 168// ⚠THE `devnull` ARG IS THE COMPILER'S STDERR AND IT USED TO SWALLOW EVERY DIAGNOSTIC. 169// A row could report build_b=1 (challenger refused) with NO WAY to learn WHY -- the exact 170// nx_parse message that names the offending call/callee was written to /dev/null, so a RED 171// row said only THAT it failed. That turned a 30-second read into a multi-session hunt on the 172// call-arg type residue (2026-08-05). ★A GATE THAT REPORTS A FAILURE WITHOUT ITS DIAGNOSTIC 173// MAKES THE FAILURE UNACTIONABLE. Callers now pass a real log fd; the cc's stderr lands in 174// /tmp/eq_cc_a_<nonce>.log (baseline) and /tmp/eq_cc_b_<nonce>.log (challenger, per-run nonce'd 175// -- kept on RED, named in the CC-EQUIV cc-logs-kept line), and their sizes are reported 176// per row so a nonzero cclog is visible in the verdict line itself. 177func eq_build(cc_path: *u8, src_path: *u8, s_tmp: *u8, o_tmp: *u8, 178 elf_out: *u8, envp: *i64, devnull: i64, 179 asm_tmp: *u8, asm_log: *u8) -> i64 { 180 let a1: *i64 = sys_mmap(8 * 4) as *i64 181 a1[0] = cc_path as i64; a1[1] = src_path as i64; a1[2] = 0 182 let sfd: i64 = sys_openat_wr(s_tmp, 0x1a4) 183 let st1: i64 = eq_run(cc_path, a1, envp, sfd, devnull) 184 sys_close(sfd) 185 if st1 != 0 { return 1 } 186 187 // SOVEREIGN assemble+link with nxasm (NO gcc/binutils) -- re-parents the self-host PROOF 188 // itself to Nishi (X-SOV-EQUIV-NXASM; before this the proof ran THROUGH GNU as+ld, so the 189 // sovereignty proof was non-sovereign). nxasm emits the ELF directly (.s -> ELF, no .o). 190 // Output to a NEUTRAL /tmp temp then rename over elf_out -- the R1-T1-004 ctx-x-output-path 191 // workaround nx_sov_build_run uses (certain target strings exit 6 from sovereign parents). 192 let nxasm: *u8 = "_offc/nxasm_x86_main.elf\x00" 193 let tmpe: *u8 = asm_tmp 194 let a2: *i64 = sys_mmap(8 * 6) as *i64 195 a2[0] = nxasm as i64 196 a2[1] = s_tmp as i64 197 a2[2] = tmpe as i64 198 a2[3] = 0 199 let alog: i64 = sys_openat_wr(asm_log, 0x1a4) 200 let arc: i64 = eq_run(nxasm, a2, envp, alog, alog) 201 sys_close(alog) 202 if arc != 0 { return 2 } 203 // the rename result was DISCARDED -- a failed rename left elf_out missing while eq_build 204 // still returned 0, so the exec-127 downstream looked like a compiler fault. 205 if sys_renameat(tmpe, elf_out) != 0 { return 3 } 206 // nxasm writes 0644, so the product was NOT EXECUTABLE and every exec died 127. Both sides died 207 // identically, so out_eq compared two empty files and every row reported PASS -- a GREEN that 208 // measured nothing. ★TWO FAILURES THAT AGREE ARE NOT AN EQUIVALENCE. 209 sys_fchmodat(elf_out, 0x1ed) 210 return 0 211} 212 213// Run elf with stdout captured to out_path; returns RAW wait status. 214func eq_run_capture(elf: *u8, out_path: *u8, envp: *i64, devnull: i64) -> i64 { 215 let ofd: i64 = sys_openat_wr(out_path, 0x1a4) 216 let a: *i64 = sys_mmap(8 * 2) as *i64 217 a[0] = elf as i64; a[1] = 0 218 let st: i64 = eq_run(elf, a, envp, ofd, devnull) 219 sys_close(ofd) 220 return st 221} 222 223// Byte-compare two files. 1 equal, 0 different / unreadable. 224func eq_files_equal(pa: *u8, pb: *u8) -> i64 { 225 let fa: i64 = sys_openat_rd(pa) 226 let fb: i64 = sys_openat_rd(pb) 227 if fa < 0 { if fb < 0 { return 1 } } 228 if fa < 0 { sys_close(fb); return 0 } 229 if fb < 0 { sys_close(fa); return 0 } 230 let ba: *u8 = sys_mmap(65536) 231 let bb: *u8 = sys_mmap(65536) 232 var equal: i64 = 1 233 var more: i64 = 1 234 while more == 1 { 235 let na: i64 = sys_read(fa, ba, 65536) 236 let nb: i64 = sys_read(fb, bb, 65536) 237 if na != nb { equal = 0; more = 0 } 238 if more == 1 { 239 if na <= 0 { more = 0 } 240 var i: i64 = 0 241 while i < na { 242 if ba[i] != bb[i] { equal = 0; i = na; more = 0 } 243 i = i + 1 244 } 245 } 246 } 247 sys_close(fa) 248 sys_close(fb) 249 return equal 250} 251 252// A GATE WHOSE VERDICT DEPENDS ON THE CALLER'S WORKING DIRECTORY IS NOT A MEASUREMENT. 253// Every source path below is relative to the buildroot. Run from anywhere else -- e.g. nx_job_run, 254// which fixes cwd at ~/nishihost -- and EVERY build fails to FIND its source, so all rows report 255// build!=0 and the run-status sentinels (-1/-2) are never overwritten. That prints verdict=RED and 256// reads as a broken compiler when nothing is wrong at all. Anchor to the tree, THEN measure. 257func eq_probe(logfd: i64, tag: *u8, path: *u8) -> i64 { 258 let fd: i64 = sys_openat_rd(path) 259 eq_puts2(logfd, " " as *u8); eq_puts2(logfd, tag); eq_puts2(logfd, "=" as *u8) 260 if fd < 0 { eq_puts2(logfd, "NO" as *u8); return 0 } 261 sys_close(fd) 262 eq_puts2(logfd, "YES" as *u8) 263 return 1 264} 265 266func eq_anchor_root() -> i64 { 267 let fd: i64 = sys_openat_rd("buildroot/runtime/nx_compile_x86.nx\x00" as *u8) 268 if fd < 0 { return 0 } 269 sys_close(fd) 270 return sys_chdir("buildroot\x00" as *u8) 271} 272 273func main(argc: i64, argv: *i64) -> i64 { 274 // SIGPIPE IMMUNITY (2026-08-05). The gate streams every line to stdout AND the durable 275 // log. When the CALLER's transport drops (observed twice live: epochs 1785972850/65 and 276 // 1785975277/91 -- headers in the log, then silence), the next stdout write hits a closed 277 // pipe and SIGPIPE kills the run MID-MEASUREMENT, so the verdict never reaches the log 278 // either. A GATE WHOSE VERDICT DEPENDS ON ITS CALLER'S CONNECTION SURVIVING IS NOT A 279 // MEASUREMENT: ignore SIGPIPE so a dead caller costs the stream, never the verdict. 280 sys_ignore_sigpipe() 281 eq_anchor_root() 282 var challenger: *u8 = "/tmp/cc_challenger.elf\x00" 283 var challenger_given: i64 = 0 284 if argc >= 2 { challenger = argv[1] as *u8; challenger_given = 1 } 285 var baseline: *u8 = "_offc/nx_cc_sovereign.elf\x00" // the LIVE toolchain; the old name has not existed for a long time 286 var baseline_src: *u8 = "default-live\x00" 287 288 let names: *i64 = sys_mmap(8 * (EQ_ROWS + 1)) as *i64 289 names[0] = "_derefcast_minrepro\x00" as *u8 as i64 290 names[1] = "_arg9_delegate_probe\x00" as *u8 as i64 291 names[2] = "_arg7_minrepro\x00" as *u8 as i64 292 names[3] = "nx_tls13_kdf_test\x00" as *u8 as i64 293 names[4] = "nx_tls13_schedule_test\x00" as *u8 as i64 294 names[5] = "nx_jpeg_ascii_test\x00" as *u8 as i64 295 names[6] = "nx_tls13_ext_test\x00" as *u8 as i64 296 names[7] = "nx_tls13_client_session_recv_sh_test\x00" as *u8 as i64 297 names[8] = "nx_p256_keyshare_test\x00" as *u8 as i64 298 names[9] = "nx_tls13_p256_loopback_test\x00" as *u8 as i64 299 300 let envp: *i64 = sys_mmap(8 * 2) as *i64 301 envp[0] = "PATH=/usr/bin:/bin\x00" as *u8 as i64 302 envp[1] = 0 303 let devnull: i64 = sys_openat_wr("/dev/null\x00" as *u8, 0x1a4) 304 // LOG DURABILITY (2026-09-15). nx_bless_compiler reads this gate's verdict FROM THE LOG. On a fresh node 305 // buildroot/knowledge/status did not exist, so two full runs streamed to stdout only while the consumer 306 // read a Sep-4 fossil as the current verdict. Create the directory (idempotent; EEXIST is the common 307 // case) and REFUSE if the log still cannot open: an unrecorded verdict is not a verdict. 308 sys_mkdir("knowledge\x00" as *u8, 0x1ed) 309 sys_mkdir("knowledge/status\x00" as *u8, 0x1ed) 310 let logfd: i64 = sys_openat_append("knowledge/status/cc_equiv_gate.log\x00" as *u8, 0x1a4) 311 if logfd < 0 { 312 eq_puts2(0 - 1, "CC-EQUIV verdict=LOG-UNWRITABLE path=knowledge/status/cc_equiv_gate.log -- the durable verdict could not be opened; nothing measured\n\x00" as *u8) 313 sys_exit(4) 314 } 315 316 eq_puts2(logfd, "CC-EQUIV epoch=\x00" as *u8) 317 eq_putn2(logfd, sys_now_realtime_sec()) 318 eq_puts2(logfd, " challenger=\x00" as *u8) 319 eq_puts2(logfd, challenger) 320 eq_puts2(logfd, "\nCC-EQUIV cwdprobe" as *u8) 321 eq_probe(logfd, "runtime" as *u8, "runtime/nx_compile_x86.nx\x00" as *u8) 322 eq_probe(logfd, "br_runtime" as *u8, "buildroot/runtime/nx_compile_x86.nx\x00" as *u8) 323 eq_probe(logfd, "offc" as *u8, "_offc/nx_compile_x86_native.elf\x00" as *u8) 324 eq_probe(logfd, "br_offc" as *u8, "buildroot/_offc/nx_compile_x86_native.elf\x00" as *u8) 325 eq_puts2(logfd, "\n" as *u8) 326 327 // CHALLENGER PREFLIGHT (2026-08-05). A challenger path that does not resolve from the 328 // gate's own CWD (buildroot -- eq_anchor_root chdirs there) used to run all 10 rows 329 // against a binary that NEVER EXECUTED: RED on every row, empty challenger logs 330 // (cclogb=0), identical eb bytes on every row -- two failures agreeing with each other, 331 // wearing the shape of an equivalence verdict. A gate whose instrument is missing must 332 // refuse to run: fail LOUD, name the path AND the fix, exit a distinct code (4). 333 // NO SILENT DEFAULT CHALLENGER (2026-08-14). The preflight below proves the path is READABLE. 334 // It does NOT prove the file is the artifact the caller meant, and on 2026-08-14 that gap produced 335 // a fully-formed wrong answer: the gate was invoked with no argument, fell back to this default, 336 // found a STALE 635,099 B /tmp/cc_challenger.elf left by some earlier run, and returned ten 337 // confident FAIL rows -- build_b=1 on every one -- about a binary nobody had asked it to test. The 338 // caller then read that as a verdict on their own compiler change, which the gate had never seen. 339 // -- A STALE ARTIFACT IS WORSE THAN AN ABSENT ONE: absence refuses, staleness ANSWERS. 340 // A default that silently names somebody else's leftover is not a convenience, it is a 341 // false-verdict generator, so the challenger is now REQUIRED and its identity is printed. 342 if challenger_given == 0 { 343 eq_puts2(logfd, "CC-EQUIV verdict=NO-CHALLENGER-GIVEN\n\x00" as *u8) 344 eq_puts2(logfd, " this gate compares a CANDIDATE compiler against the live one and cannot guess which candidate you mean.\n\x00" as *u8) 345 eq_puts2(logfd, " gate CWD is buildroot; /api/build stages into the nishihost ROOT -- pass ../<target>.sov.elf.new\n\x00" as *u8) 346 sys_exit(5) 347 } 348 let cpre: i64 = sys_openat_rd(challenger) 349 if cpre < 0 { 350 eq_puts2(logfd, "CC-EQUIV verdict=CHALLENGER-UNREADABLE path=\x00" as *u8) 351 eq_puts2(logfd, challenger) 352 eq_puts2(logfd, "\n gate CWD is buildroot; /api/build stages into the nishihost ROOT -- pass ../<target>.sov.elf.new\n\x00" as *u8) 353 sys_exit(4) 354 } 355 sys_close(cpre) 356 357 // BASELINE AS DATA (2026-09-15). The default baseline is the LIVE compiler. A candidate that ADDS a 358 // language feature (measured: __size_of over struct types, 5 of 10 corpus rows) can never pass "both 359 // sides build" against a baseline that lacks the feature -- the gate would refuse every capability- 360 // adding release forever, or a seat would loosen the rule. Neither is acceptable. A node names a 361 // baseline that CAN build the corpus in knowledge/cc_equiv_baseline.conf (line 1 = path, resolved 362 // AFTER the buildroot anchor so the conf lives beside the log it governs); the natural baseline is 363 // the HUB's promoted compiler pulled through nx_content_get, whose whole-file digest the client 364 // verified before writing a byte. Identity is PRINTED (path, source, bytes) for every compiler this 365 // run execs, so a reader can check WHICH binary was judged. 366 let bconf: i64 = sys_openat_rd("knowledge/cc_equiv_baseline.conf\x00" as *u8) 367 if bconf >= 0 { 368 let bbuf: *u8 = sys_mmap(EQ_CONF_CAP) 369 let bn: i64 = sys_read(bconf, bbuf, EQ_CONF_CAP - 1) 370 sys_close(bconf) 371 var bl: i64 = 0 372 if bn > 0 { bl = eq_first_line(bbuf, bn) } 373 bbuf[bl] = 0 as u8 374 if bl > 0 { 375 let bfd: i64 = sys_openat_rd(bbuf) 376 if bfd < 0 { 377 eq_puts2(logfd, "CC-EQUIV verdict=BASELINE-CONF-UNREADABLE path=\x00" as *u8) 378 eq_puts2(logfd, bbuf) 379 eq_puts2(logfd, " -- knowledge/cc_equiv_baseline.conf names a compiler that does not resolve from the anchored root; fix the conf or remove it\n\x00" as *u8) 380 sys_exit(4) 381 } 382 sys_close(bfd) 383 baseline = bbuf 384 baseline_src = "conf\x00" 385 } 386 } 387 eq_puts2(logfd, "CC-EQUIV baseline=\x00" as *u8) 388 eq_puts2(logfd, baseline) 389 eq_puts2(logfd, " baseline_src=\x00" as *u8) 390 eq_puts2(logfd, baseline_src) 391 eq_puts2(logfd, " baseline_bytes=\x00" as *u8) 392 eq_putn2(logfd, eq_fsize(baseline)) 393 eq_puts2(logfd, " challenger_bytes=\x00" as *u8) 394 eq_putn2(logfd, eq_fsize(challenger)) 395 eq_puts2(logfd, "\n\x00" as *u8) 396 // A challenger byte-identical to the baseline compares a compiler with itself: every row PASSES and 397 // nothing was measured. Refuse it by name, with the same byte ruler the rows use. 398 if eq_files_equal(baseline, challenger) == 1 { 399 eq_puts2(logfd, "CC-EQUIV verdict=BASELINE-IS-CHALLENGER -- the two compilers are byte-identical; a self-comparison measures nothing\n\x00" as *u8) 400 sys_exit(4) 401 } 402 403 // A release may require a capability the baseline intentionally lacks. 404 // Keep that feature's correctness separate from baseline equivalence. 405 var feature_source: *u8 = 0 as *u8 406 if argc >= 3 { 407 feature_source = argv[2] as *u8 408 let feature_fd: i64 = sys_openat_rd(feature_source) 409 if feature_fd < 0 { 410 eq_puts2(logfd, "CC-EQUIV verdict=FEATURE-SOURCE-UNREADABLE path=" as *u8) 411 eq_puts2(logfd, feature_source) 412 eq_puts2(logfd, " open_result=" as *u8) 413 eq_putn2(logfd, feature_fd) 414 eq_puts2(logfd, "\n" as *u8) 415 sys_exit(4) 416 } 417 sys_close(feature_fd) 418 } 419 420 // PER-RUN UNIQUE TEMPS -- see eq_uniq. One nonce names every temp this run owns. 421 let uq: i64 = eq_nonce() 422 let p_as: *u8 = eq_uniq("/tmp/eqa_\x00" as *u8, uq, ".s\x00" as *u8) 423 let p_ao: *u8 = eq_uniq("/tmp/eqa_\x00" as *u8, uq, ".o\x00" as *u8) 424 let p_ae: *u8 = eq_uniq("_build/eqa_\x00" as *u8, uq, ".elf\x00" as *u8) 425 let p_aout: *u8 = eq_uniq("/tmp/eqa_\x00" as *u8, uq, ".out\x00" as *u8) 426 let p_alog: *u8 = eq_uniq("/tmp/eq_cc_a_\x00" as *u8, uq, ".log\x00" as *u8) 427 let p_bs: *u8 = eq_uniq("/tmp/eqb_\x00" as *u8, uq, ".s\x00" as *u8) 428 let p_bo: *u8 = eq_uniq("/tmp/eqb_\x00" as *u8, uq, ".o\x00" as *u8) 429 let p_be: *u8 = eq_uniq("_build/eqb_\x00" as *u8, uq, ".elf\x00" as *u8) 430 let p_bout: *u8 = eq_uniq("/tmp/eqb_\x00" as *u8, uq, ".out\x00" as *u8) 431 let p_blog: *u8 = eq_uniq("/tmp/eq_cc_b_\x00" as *u8, uq, ".log\x00" as *u8) 432 let p_asm: *u8 = eq_uniq("_build/_eq_asm_\x00" as *u8, uq, ".elf\x00" as *u8) 433 let p_nxl: *u8 = eq_uniq("/tmp/eq_nxasm_\x00" as *u8, uq, ".log\x00" as *u8) 434 let p_g2s: *u8 = eq_uniq("/tmp/eqg2_\x00" as *u8, uq, ".s\x00" as *u8) 435 let p_g2o: *u8 = eq_uniq("/tmp/eqg2_\x00" as *u8, uq, ".o\x00" as *u8) 436 let p_g2e: *u8 = eq_uniq("_build/eqg2_\x00" as *u8, uq, ".elf\x00" as *u8) 437 let p_prs: *u8 = eq_uniq("/tmp/eqpr_\x00" as *u8, uq, ".s\x00" as *u8) 438 let p_pro: *u8 = eq_uniq("/tmp/eqpr_\x00" as *u8, uq, ".o\x00" as *u8) 439 let p_pre: *u8 = eq_uniq("_build/eqpr_\x00" as *u8, uq, ".elf\x00" as *u8) 440 let p_prout: *u8 = eq_uniq("/tmp/eqpr_\x00" as *u8, uq, ".out\x00" as *u8) 441 // TREE-STABILITY CONTROL temps (2026-08-06, debt 1786067358) -- a SECOND baseline pass per row. 442 let p_as2: *u8 = eq_uniq("/tmp/eqa2_\x00" as *u8, uq, ".s\x00" as *u8) 443 let p_ao2: *u8 = eq_uniq("/tmp/eqa2_\x00" as *u8, uq, ".o\x00" as *u8) 444 let p_ae2: *u8 = eq_uniq("_build/eqa2_\x00" as *u8, uq, ".elf\x00" as *u8) 445 446 var passed: i64 = 0 447 var moved: i64 = 0 // rows whose INPUT changed mid-row -- see the tree-stability control 448 var base_cannot: i64 = 0 // rows the BASELINE could not build while the challenger could: no differential exists 449 var ri: i64 = 0 450 while ri < EQ_ROWS { 451 let mod: *u8 = names[ri] as *u8 452 let src: *u8 = sys_mmap(512) 453 var o: i64 = 0 454 o = eq_cat(src, o, "runtime/\x00" as *u8) 455 o = eq_cat(src, o, mod) 456 o = eq_cat(src, o, ".nx\x00" as *u8) 457 src[o] = 0 as u8 458 459 // A FAILED BUILD LEAVES THE PREVIOUS ROW'S PRODUCT IN PLACE (measured 2026-09-15: five failing rows 460 // all reported ea=14712, the size of row 3), so ea/eb must be THIS row's size or absent (-1). 461 sys_unlinkat(p_ae); sys_unlinkat(p_be) 462 // per-side compiler stderr (see the eq_build header): a refused row must be able to say WHY. 463 let cca: i64 = sys_openat_wr(p_alog, 0x1a4) 464 let ba_rc: i64 = eq_build(baseline, src, 465 p_as, p_ao, 466 p_ae, envp, cca, p_asm, p_nxl) 467 sys_close(cca) 468 let ccb: i64 = sys_openat_wr(p_blog, 0x1a4) 469 let ch_rc: i64 = eq_build(challenger, src, 470 p_bs, p_bo, 471 p_be, envp, ccb, p_asm, p_nxl) 472 sys_close(ccb) 473 474 // ---- TREE-STABILITY CONTROL (2026-08-06, debt 1786067358) ---------------------- 475 // A and B are compiled SEQUENTIALLY over the LIVE buildroot tree. If a sibling seat 476 // rewrites any file in the closure between the two compiles, the two sides did not 477 // see the same input -- and this gate reports that as the CHALLENGER disagreeing. 478 // That is not hypothetical: on 2026-08-06 it produced a RED that survived an 479 // immediate byte-identical re-run (a continuous oscillation straddles both runs, so 480 // reproducibility across two attempts only rules out noise SLOWER than your retry), 481 // cost a 4-step bisect whose every step was confounded, and got a sev-8 compiler bug 482 // filed against a defect that does not exist (retracted, 1786067328). 483 // The control uses the SAME instrument rather than a second one to trust: recompile 484 // the BASELINE and require its assembly to be byte-identical to its first pass. A 485 // difference means the input moved, so this row yields NO verdict about the challenger. 486 // ★A VERDICT ABOUT AN EXPERIMENT WHOSE INPUT CHANGED IS NOT A VERDICT. 487 // Cost is one extra compile per row (~+50% runtime). That is the correct trade for a 488 // gate guarding the crown-jewel compiler, where a false RED costs an hour and a lie. 489 let a2_rc: i64 = eq_build(baseline, src, 490 p_as2, p_ao2, 491 p_ae2, envp, devnull, p_asm, p_nxl) 492 var tree_moved: i64 = 0 493 if a2_rc != ba_rc { tree_moved = 1 } 494 if eq_files_equal(p_as, p_as2) == 0 { tree_moved = 1 } 495 496 var st_a: i64 = 0 - 1 497 var st_b: i64 = 0 - 2 498 var out_eq: i64 = 0 499 if ba_rc == 0 { 500 if ch_rc == 0 { 501 st_a = eq_run_capture(p_ae, p_aout, envp, devnull) 502 st_b = eq_run_capture(p_be, p_bout, envp, devnull) 503 out_eq = eq_files_equal(p_aout, p_bout) 504 } 505 } 506 507 var pass: i64 = 0 508 // NON-VACUITY: exit 127 means execve never ran the product. Two sides that BOTH fail to 509 // run agree perfectly and compare two empty outputs, which this gate used to report as PASS 510 // -- 10/10 GREEN while measuring nothing. A gate that can pass on a non-measurement is worse 511 // than a red one, because it is trusted. Require a REAL run before equivalence can mean 512 // anything. ★TWO FAILURES THAT AGREE ARE NOT AN EQUIVALENCE. 513 var ran: i64 = 1 514 if ((st_a >> 8) & 255) == 127 { ran = 0 } 515 if ((st_b >> 8) & 255) == 127 { ran = 0 } 516 if ran == 0 { eq_puts2(logfd, " VACUOUS(exec-127)" as *u8) } 517 if ran == 1 { if ba_rc == 0 { if ch_rc == 0 { if st_a == st_b { if out_eq == 1 { pass = 1 } } } } } 518 // A moved input outranks every other reading of this row: whatever the comparison 519 // said, it was not comparing two runs of the same experiment. 520 if tree_moved == 1 { 521 pass = 0 522 moved = moved + 1 523 } 524 525 eq_puts2(logfd, "CC-EQUIV row=\x00" as *u8) 526 eq_puts2(logfd, mod) 527 eq_puts2(logfd, " sa=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_as)) 528 eq_puts2(logfd, " ea=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_ae)) 529 eq_puts2(logfd, " eb=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_be)) 530 eq_puts2(logfd, " alog=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_nxl)) 531 eq_puts2(logfd, " ccloga=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_alog)) 532 eq_puts2(logfd, " cclogb=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_blog)) 533 eq_puts2(logfd, " build_a=\x00" as *u8) 534 eq_putn2(logfd, ba_rc) 535 eq_puts2(logfd, " build_b=\x00" as *u8) 536 eq_putn2(logfd, ch_rc) 537 eq_puts2(logfd, " st_a=\x00" as *u8) 538 eq_putn2(logfd, st_a) 539 eq_puts2(logfd, " st_b=\x00" as *u8) 540 eq_putn2(logfd, st_b) 541 eq_puts2(logfd, " out_eq=\x00" as *u8) 542 eq_putn2(logfd, out_eq) 543 if pass == 1 { eq_puts2(logfd, " verdict=PASS\n\x00" as *u8) } 544 // A moved input is NOT a FAIL, and calling it one is precisely how a reader spends an 545 // hour attributing a tree change to a code change. Give it its own word. 546 // A FAIL that does not name WHICH failure sends the reader at the wrong subsystem: on 2026-09-15 five 547 // rows the baseline could not even COMPILE read as the challenger disagreeing. Name the class. 548 if pass != 1 { if tree_moved == 0 { 549 if ba_rc != 0 { if ch_rc == 0 { 550 eq_puts2(logfd, " verdict=FAIL(baseline-cannot-build -- the challenger built this row; no differential is possible against this baseline; remedy: knowledge/cc_equiv_baseline.conf naming a compiler that builds the corpus)\n\x00" as *u8) 551 base_cannot = base_cannot + 1 552 } } 553 if ba_rc != 0 { if ch_rc != 0 { eq_puts2(logfd, " verdict=FAIL(neither-side-builds)\n\x00" as *u8) } } 554 if ba_rc == 0 { if ch_rc != 0 { eq_puts2(logfd, " verdict=FAIL(challenger-cannot-build)\n\x00" as *u8) } } 555 if ba_rc == 0 { if ch_rc == 0 { eq_puts2(logfd, " verdict=FAIL(behaviour-differs-or-vacuous)\n\x00" as *u8) } } 556 } } 557 if tree_moved == 1 { eq_puts2(logfd, " verdict=TREE-MOVED(input changed between the A and B compiles -- no verdict on the challenger)\n\x00" as *u8) } 558 if pass == 1 { passed = passed + 1 } 559 ri = ri + 1 560 } 561 562 // ---- SELF-HOST stage: challenger -> gen2 -> probe runs 0 ---- 563 var sh_pass: i64 = 0 564 let g2_rc: i64 = eq_build(challenger, "runtime/nx_compile_x86.nx\x00" as *u8, 565 p_g2s, p_g2o, 566 p_g2e, envp, devnull, p_asm, p_nxl) 567 var pr_rc: i64 = 0 - 1 568 var pr_st: i64 = 0 - 1 569 if g2_rc == 0 { 570 pr_rc = eq_build(p_g2e, "runtime/_derefcast_minrepro.nx\x00" as *u8, 571 p_prs, p_pro, 572 p_pre, envp, devnull, p_asm, p_nxl) 573 if pr_rc == 0 { 574 pr_st = eq_run_capture(p_pre, p_prout, envp, devnull) 575 if pr_st == 0 { sh_pass = 1 } 576 } 577 } 578 eq_puts2(logfd, "CC-EQUIV selfhost sizes g2s=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_g2s)) 579 eq_puts2(logfd, " g2elf=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_g2e)) 580 eq_puts2(logfd, " asmtmp=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_asm)) 581 eq_puts2(logfd, " alog=\x00" as *u8); eq_putn2(logfd, eq_fsize(p_nxl)) 582 eq_puts2(logfd, "\n\x00" as *u8) 583 eq_puts2(logfd, "CC-EQUIV selfhost g2_build=\x00" as *u8) 584 eq_putn2(logfd, g2_rc) 585 eq_puts2(logfd, " probe_build=\x00" as *u8) 586 eq_putn2(logfd, pr_rc) 587 eq_puts2(logfd, " probe_st=\x00" as *u8) 588 eq_putn2(logfd, pr_st) 589 if sh_pass == 1 { eq_puts2(logfd, " verdict=PASS\n\x00" as *u8) } 590 if sh_pass != 1 { eq_puts2(logfd, " verdict=FAIL\n\x00" as *u8) } 591 592 var feature_pass: i64 = 1 593 if feature_source != (0 as *u8) { 594 feature_pass = 0 595 let feature_log: i64 = sys_openat_wr(p_blog, 0x1a4) 596 let feature_build: i64 = eq_build(challenger, feature_source, 597 p_prs, p_pro, p_pre, envp, feature_log, p_asm, p_nxl) 598 var feature_status: i64 = 0 - 1 599 if feature_build == 0 { 600 feature_status = eq_run_capture(p_pre, p_prout, envp, feature_log) 601 } 602 sys_close(feature_log) 603 eq_puts2(logfd, "CC-EQUIV feature source=" as *u8) 604 eq_puts2(logfd, feature_source) 605 eq_puts2(logfd, " build=" as *u8) 606 eq_putn2(logfd, feature_build) 607 eq_puts2(logfd, " raw_wait_status=" as *u8) 608 eq_putn2(logfd, feature_status) 609 if feature_build == 0 { if feature_status == 0 { feature_pass = 1 } } 610 if feature_pass == 1 { eq_puts2(logfd, " verdict=PASS\n" as *u8) } 611 if feature_pass == 0 { 612 eq_puts2(logfd, " verdict=FAIL diagnostics=" as *u8) 613 eq_puts2(logfd, p_blog) 614 eq_puts2(logfd, " stdout=" as *u8) 615 eq_puts2(logfd, p_prout) 616 eq_puts2(logfd, "\n" as *u8) 617 } 618 } 619 620 eq_puts2(logfd, "CC-EQUIV rows=\x00" as *u8) 621 eq_putn2(logfd, EQ_ROWS) 622 eq_puts2(logfd, " passed=\x00" as *u8) 623 eq_putn2(logfd, passed) 624 eq_puts2(logfd, " selfhost=\x00" as *u8) 625 eq_putn2(logfd, sh_pass) 626 eq_puts2(logfd, " tree_moved_rows=\x00" as *u8) 627 eq_putn2(logfd, moved) 628 eq_puts2(logfd, " baseline_cannot_build=\x00" as *u8) 629 eq_putn2(logfd, base_cannot) 630 var verdict_green: i64 = 0 631 if passed == EQ_ROWS { if sh_pass == 1 { verdict_green = 1 } } 632 // A run in which ANY row's input moved cannot be reported as a clean verdict about the 633 // challenger, in EITHER direction. Emitting RED here is what sent a reader (me) hunting a 634 // code change that did not cause it; emitting GREEN would be worse still. Name the state 635 // and the remedy instead. ★AN INSTRUMENT THAT CANNOT SAY "I WAS DISTURBED" WILL SAY 636 // SOMETHING ELSE INSTEAD, AND IT WILL BE BELIEVED. 637 if moved > 0 { verdict_green = 0 } 638 if feature_pass == 0 { verdict_green = 0 } 639 if verdict_green == 1 { eq_puts2(logfd, " verdict=GREEN\n\x00" as *u8) } 640 if verdict_green != 1 { if moved == 0 { eq_puts2(logfd, " verdict=RED\n\x00" as *u8) } } 641 if moved > 0 { 642 eq_puts2(logfd, " verdict=ADVISORY-TREE-MOVED -- the closure changed between the A and B compiles on \x00" as *u8) 643 eq_putn2(logfd, moved) 644 eq_puts2(logfd, " row(s); this is NOT a verdict about the challenger. Re-run when the tree is quiet.\n\x00" as *u8) 645 } 646 // TEMP CLEANUP (2026-08-05): nonce'd temps would otherwise accumulate one set per run 647 // (the old fixed names self-overwrote); NAS /tmp persists until reboot, so unlink is not 648 // optional. On RED the two per-side compiler logs are KEPT and named -- they are the 649 // diagnosis, and a gate that reports a failure without its diagnostic is unactionable. 650 if verdict_green != 1 { 651 eq_puts2(logfd, "CC-EQUIV cc-logs-kept=\x00" as *u8) 652 eq_puts2(logfd, p_alog) 653 eq_puts2(logfd, " \x00" as *u8) 654 eq_puts2(logfd, p_blog) 655 eq_puts2(logfd, "\n\x00" as *u8) 656 } 657 sys_unlinkat(p_as); sys_unlinkat(p_ao); sys_unlinkat(p_ae); sys_unlinkat(p_aout) 658 sys_unlinkat(p_bs); sys_unlinkat(p_bo); sys_unlinkat(p_be); sys_unlinkat(p_bout) 659 sys_unlinkat(p_as2); sys_unlinkat(p_ao2); sys_unlinkat(p_ae2) 660 sys_unlinkat(p_asm); sys_unlinkat(p_nxl) 661 sys_unlinkat(p_g2s); sys_unlinkat(p_g2o); sys_unlinkat(p_g2e) 662 sys_unlinkat(p_prs); sys_unlinkat(p_pro); sys_unlinkat(p_pre) 663 if feature_pass == 1 { sys_unlinkat(p_prout) } 664 if verdict_green == 1 { sys_unlinkat(p_alog); sys_unlinkat(p_blog) } 665 if logfd > 0 { sys_close(logfd) } 666 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check 667 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled 668 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify. 669 let ctr__dry: *i64 = gv_ctr() 670 ctr__dry[0] = verdict_green 671 ctr__dry[1] = 1 672 let rc__dry: i64 = gv_verdict("CC-EQUIV-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 673 sys_exit(rc__dry) 674 return rc__dry 675}