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}