code wiki / _hdl_build / nx_aritycheck_gate.nx

nx_aritycheck_gate.nx source

↩ module page · 542 lines · 28906 B

1// nx_aritycheck_gate.nx -- THE ARITY CENSUS: report every CALL SITE whose argument count disagrees with the 2// definition its own directory resolves. The axis nothing currently measures. 3// 4// WHY THIS EXISTS (measured 2026-07-31, lib-reconcile). nx_cc is fails-open on arity: seq1012 shows 5// undefined identifiers compile clean and 1785447657 shows duplicate definitions compile clean with zero 6// diagnostics. nx_undefscan closes the "called but never DEFINED" axis and is the right tool for it -- but 7// it reports VERDICT=CLEAN on nx_input_abstract_gate, which passes ONE argument to a TWO-parameter ia_init 8// at eight sites, because ia_init IS defined in the closure. The target also compiles. So nx_cc says fine, 9// undefscan says CLEAN, the artifact exists, and the call is still wrong. 10// ★★★★★CLEAN + BUILDS != CORRECT WHEN NO INSTRUMENT MEASURES THE AXIS THE DEFECT LIVES ON. 11// 12// SCOPE, stated so a caller never has to guess: ONE DIRECTORY. An import binds the importer's OWN 13// directory first (proven from compiler output at buildroot/_build/nx_f32_llm_serve.s:2511), so the 14// definitions visible to a file are, to first order, the ones beside it. That makes a per-directory census 15// sound for the common case and DELIBERATELY BLIND to cross-layer resolution -- which is reported as 16// UNKNOWN-NAME rather than silently ignored, because a name this organ cannot see is not a name it may 17// bless. 18// 19// KNOWN FALSE POSITIVES, bounded and declared (the nx_undefscan doctrine, inherited on purpose): 20// - a call through a FUNCTION-POINTER VARIABLE looks like a call to a name it cannot resolve 21// - a name that is BOTH a local variable and a function elsewhere in the dir 22// Both are bounded by the POSITIVE CONTROL: nx_m2d_engine calls ia_init(ia, 5) correctly in the SAME 23// directory against the SAME definition and MUST report zero. A checker that flagged everything fails it. 24// 25// ⚠STATUS 2026-07-31: DESK-CHECKED, NEVER COMPILED. Authored while /mcp was wedged, so it has not been 26// through nx_cc or its own selftest even once. Do NOT treat "reviewed" as "works" -- that is the exact 27// confusion this organ exists to remove. WHAT THE SELFTEST MUST PRINT on a first good build: 28// defs collected >= 3 (two_p=2, bad_caller=0, inner=2, good_caller=0) 29// EXACTLY 1 arity mismatch: bad.nx two_p called with 1 arg(s), defined with 2 30// 0 conflicting duplicates in /tmp/nxarity, and EXACTLY 1 in /tmp/nxarity2 31// If it reports 2 mismatches, ac_args lost its depth-awareness and counted the comma inside 32// inner(1, 9); if it reports 0, ac_find or ac_is_def_at is failing to see the definition at all. 33// Both failure modes are distinguishable from the message alone, on purpose. 34// FIRST REAL TARGET once built: buildroot/runtime/_hdl_build -- it must reproduce the 8 ia_init 35// mismatches in nx_input_abstract_gate (pre-move) and report ZERO for nx_m2d_engine. 36// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 when clean 37import "nx_syscalls.nx" 38 39const AC_MAXDEF: i64 = 4096 // distinct function names per directory 40const AC_NAMELEN: i64 = 64 // bytes per name slot 41const AC_FILECAP: i64 = 1048576 // 1MiB per source; over-cap is UNKNOWN, never "clean" 42const AC_DIRBUF: i64 = 131072 43const AC_PATHCAP: i64 = 4096 44const AC_STDOUT: i64 = 1 45 46// ---- tiny io ---- 47func ac_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 48func ac_w(s: *u8) -> i64 { sys_write(AC_STDOUT, s, ac_slen(s)); return 0 } 49func ac_wn(v: i64) -> i64 { 50 let b: *u8 = sys_mmap(32) 51 let t: *u8 = sys_mmap(32) 52 var m: i64 = v 53 if m < 0 { m = 0 - m; sys_write(AC_STDOUT, "-" as *u8, 1) } 54 var k: i64 = 0 55 if m == 0 { t[0] = 48 as u8; k = 1 } 56 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 57 var i: i64 = 0 58 while i < k { b[i] = t[k-1-i]; i = i + 1 } 59 sys_write(AC_STDOUT, b, k) 60 return 0 61} 62func ac_wb(p: *u8, n: i64) -> i64 { sys_write(AC_STDOUT, p, n); return 0 } 63 64// ---- identifier classification ---- 65func ac_isidst(c: i64) -> i64 { 66 if c >= 97 { if c <= 122 { return 1 } } 67 if c >= 65 { if c <= 90 { return 1 } } 68 if c == 95 { return 1 } 69 return 0 70} 71func ac_isidc(c: i64) -> i64 { 72 if ac_isidst(c) == 1 { return 1 } 73 if c >= 48 { if c <= 57 { return 1 } } 74 return 0 75} 76 77// ---- file read; -1 unreadable, -2 OVER CAP (never folded into a clean result) ---- 78func ac_slurp(path: *u8, buf: *u8, cap: i64) -> i64 { 79 let fd: i64 = sys_openat_rd(path) 80 if fd < 0 { return 0 - 1 } 81 var tot: i64 = 0 82 var go: i64 = 1 83 while go == 1 { 84 let r: i64 = sys_read(fd, ((buf as i64) + tot) as *u8, cap - tot) 85 if r <= 0 { go = 0 } else { tot = tot + r } 86 if tot >= cap { sys_close(fd); return 0 - 2 } 87 } 88 sys_close(fd) 89 return tot 90} 91 92// ---- name table: names[i] = NUL-terminated slot, arity[i] = parameter count ---- 93func ac_slot(names: *u8, i: i64) -> *u8 { return ((names as i64) + i * AC_NAMELEN) as *u8 } 94 95func ac_find(names: *u8, n: i64, p: *u8, plen: i64) -> i64 { 96 if plen >= AC_NAMELEN { return 0 - 1 } 97 var i: i64 = 0 98 while i < n { 99 let s: *u8 = ac_slot(names, i) 100 var k: i64 = 0 101 var same: i64 = 1 102 while k < plen { if s[k] != p[k] { same = 0; k = plen } else { k = k + 1 } } 103 if same == 1 { if s[plen] == (0 as u8) { return i } } 104 i = i + 1 105 } 106 return 0 - 1 107} 108 109// PARAMETER COUNT of a definition whose `func ` begins at `at`. Signatures carry no nested parens in this 110// corpus, so commas at one level are the whole story. -1 if unparseable; 0 = empty parens. 111func ac_params(buf: *u8, n: i64, at: i64) -> i64 { 112 var i: i64 = at 113 var go: i64 = 1 114 while go == 1 { 115 if i >= n { return 0 - 1 } 116 if buf[i] == (40 as u8) { go = 0 } else { i = i + 1 } 117 } 118 i = i + 1 119 var commas: i64 = 0 120 var any: i64 = 0 121 var go2: i64 = 1 122 while go2 == 1 { 123 if i >= n { return 0 - 1 } 124 let c: i64 = buf[i] as i64 125 if c == 41 { go2 = 0 } else { 126 if c == 44 { commas = commas + 1 } 127 if c != 32 { any = 1 } 128 i = i + 1 129 } 130 } 131 if any == 0 { return 0 } 132 return commas + 1 133} 134 135// ★ARGUMENT COUNT at a call site whose '(' is at `op`. DEPTH-AWARE: only commas at depth 1 separate 136// arguments, so ia_init(inp(base)) reads as ONE argument and NOT two. That single detail is the whole 137// difference between a checker that works and one that reports noise. -1 if the parens never close. 138// ★★★AND IT MUST SKIP STRING LITERALS. A call like 139// as_append(out, o, "font-family:system-ui,-apple-system,Segoe UI,sans-serif;background:" as *u8) 140// is THREE arguments, but the commas inside that string made it read as SIX. That produced a long, 141// plausible list of "defects" in nx_ad_banner / nx_activities / nx_ads that were ALL FALSE -- caught by 142// hand-verifying one row against the definition (3 params) and the real call (3 args). 143// THIS IS THE THIRD HEURISTIC GAP IN THIS ONE FUNCTION-FAMILY: comments, then directory scope, now 144// strings. ★A TEXT SCANNER PRETENDING TO BE A PARSER FINDS A NEW EXCEPTION ON EVERY REAL CORPUS. 145func ac_args(buf: *u8, n: i64, op: i64) -> i64 { 146 var i: i64 = op + 1 147 var depth: i64 = 1 148 var commas: i64 = 0 149 var any: i64 = 0 150 while i < n { 151 let c: i64 = buf[i] as i64 152 if c == 34 { 153 any = 1 154 i = i + 1 155 var g: i64 = 1 156 while g == 1 { 157 if i >= n { g = 0 } else { 158 if buf[i] == (92 as u8) { i = i + 2 } else { 159 if buf[i] == (34 as u8) { g = 0; i = i + 1 } else { i = i + 1 } 160 } 161 } 162 } 163 } else { 164 if c == 40 { depth = depth + 1 } 165 if c == 41 { 166 depth = depth - 1 167 if depth == 0 { if any == 0 { return 0 } return commas + 1 } 168 } 169 if depth == 1 { if c == 44 { commas = commas + 1 } } 170 if c != 32 { if c != 10 { if c != 9 { any = 1 } } } 171 i = i + 1 172 } 173 } 174 return 0 - 1 175} 176 177// is `func ` at column 0 here? Definitions are unindented in this corpus; a `//` comment mentioning 178// `func foo(` is not, so this removes the comment class without needing a tokenizer. 179func ac_is_def_at(buf: *u8, n: i64, i: i64) -> i64 { 180 if i + 5 > n { return 0 } 181 if buf[i] != (102 as u8) { return 0 } 182 if buf[i+1] != (117 as u8) { return 0 } 183 if buf[i+2] != (110 as u8) { return 0 } 184 if buf[i+3] != (99 as u8) { return 0 } 185 if buf[i+4] != (32 as u8) { return 0 } 186 if i > 0 { if buf[i-1] != (10 as u8) { return 0 } } 187 return 1 188} 189 190// collect every column-0 definition in `buf` into (names, arity); returns the new count. 191// ★A SECOND DEFINITION OF A NAME ALREADY SEEN IS NOT DROPPED SILENTLY. If its arity DIFFERS, that is a 192// duplicate definition with a conflicting signature -- compiler-root's 1785447657 class, which nx_cc 193// compiles with zero diagnostics -- and it would also silently corrupt this census, because ac_find 194// returns the FIRST match so every call written against the SECOND would false-flag. Reporting it is 195// both the honest thing and the only way the mismatch count stays meaningful. conflicts land in dup[0]. 196// ★★★SOUNDNESS FIX (2026-07-31, after the first real-corpus run produced ~100% false positives): a 197// DIRECTORY IS NOT A NAMESPACE. Every .nx is a separate program, so `main`, `g_w`, `chk` and `row` are 198// each defined in dozens of unrelated files and collide only in this table. The cure is to be SOUND 199// rather than COMPLETE: `dcount[i]` counts how many files define name i, and a call is judged ONLY when 200// its name is defined EXACTLY ONCE in the directory -- then same-dir resolution makes it unambiguous 201// which definition the caller meant. Ambiguous names are SKIPPED AND COUNTED, never guessed at. 202// ★A CHECKER THAT REPORTS WHAT IT COULD NOT JUDGE IS HONEST; ONE THAT GUESSES IS NOISE. 203// ★★★AND THE DUPLICATE AXIS HAD THE SAME GRANULARITY BUG. At DIRECTORY scope "defined twice with a 204// different arity" is NOT a defect -- `main` lives in every organ. The real class (1785447657, what nx_cc 205// swallows) is a name defined twice IN ONE FILE. `fseen[i]` holds the file-sequence number that last 206// defined name i, so a repeat within the SAME file is the only thing reported. Same lesson as the call 207// axis, one line further down: THE UNIT OF A DEFECT IS THE UNIT THE COMPILER ACTUALLY SEES. 208func ac_collect(buf: *u8, n: i64, names: *u8, arity: *i64, dcount: *i64, fseen: *i64, fileid: i64, cnt: i64, dup: *i64, fname: *u8) -> i64 { 209 var c: i64 = cnt 210 var i: i64 = 0 211 while i < n { 212 if ac_is_def_at(buf, n, i) == 1 { 213 let s: i64 = i + 5 214 var q: i64 = s 215 var go: i64 = 1 216 while go == 1 { 217 if q >= n { go = 0 } else { 218 if ac_isidc(buf[q] as i64) == 1 { q = q + 1 } else { go = 0 } 219 } 220 } 221 let nlen: i64 = q - s 222 if nlen > 0 { if nlen < AC_NAMELEN { 223 let p: *u8 = ((buf as i64) + s) as *u8 224 let ap: i64 = ac_params(buf, n, i) 225 let prev: i64 = ac_find(names, c, p, nlen) 226 // ★NEVER SILENTLY DROP A DEFINITION. Past the cap the census would go on reporting a 227 // confident mismatch count computed from an INCOMPLETE definition table -- a smaller 228 // clean, which is the exact sin this organ was built to expose. Count it and say so. 229 if prev < 0 { 230 if c >= AC_MAXDEF { dup[1] = dup[1] + 1 } 231 if c < AC_MAXDEF { 232 let dst: *u8 = ac_slot(names, c) 233 var k: i64 = 0 234 while k < nlen { dst[k] = p[k]; k = k + 1 } 235 dst[nlen] = 0 as u8 236 arity[c] = ap 237 dcount[c] = 1 238 fseen[c] = fileid 239 c = c + 1 240 } } else { 241 // SAME FILE defines this name again = the real 1785447657 class. Across files it is 242 // ordinary and must stay silent. 243 if fseen[prev] == fileid { 244 dup[0] = dup[0] + 1 245 ac_w(" DUP-DEF-IN-ONE-FILE " as *u8) 246 ac_w(fname) 247 ac_w(": " as *u8) 248 ac_wb(p, nlen) 249 ac_w(" defined twice in this file (" as *u8) 250 ac_wn(arity[prev]) 251 ac_w(" then " as *u8) 252 ac_wn(ap) 253 ac_w(" param(s)) -- nx_cc compiles this silently (1785447657)\n" as *u8) 254 } else { 255 fseen[prev] = fileid 256 dcount[prev] = dcount[prev] + 1 257 } 258 } 259 } } 260 i = q 261 } else { i = i + 1 } 262 } 263 return c 264} 265 266// check every call site in `buf` against (names, arity); returns mismatches found 267// amb[0] accumulates calls SKIPPED because the name is defined more than once in this directory. 268func ac_check(buf: *u8, n: i64, names: *u8, arity: *i64, dcount: *i64, cnt: i64, fname: *u8, amb: *i64) -> i64 { 269 var bad: i64 = 0 270 var i: i64 = 0 271 while i < n { 272 // ★★★SKIP `//` COMMENTS ENTIRELY. Definitions were already column-0 anchored, but CALL SITES were 273 // not, so a comment like `hash_out = aa_chain(prev, entry)` was counted as a 2-arg call against a 274 // 4-param definition. That single omission produced a plausible-looking list of "real defects" in 275 // nx_access_pep and friends, ALL FALSE. Caught by hand-verifying ONE row before reporting any. 276 // ★A FINDING YOU HAVE NOT VERIFIED IS A HYPOTHESIS WEARING A DEFECT'S CLOTHES. 277 // a string literal can contain anything that looks like a call -- skip it wholesale 278 if buf[i] == (34 as u8) { 279 i = i + 1 280 var gs: i64 = 1 281 while gs == 1 { 282 if i >= n { gs = 0 } else { 283 if buf[i] == (92 as u8) { i = i + 2 } else { 284 if buf[i] == (34 as u8) { gs = 0; i = i + 1 } else { i = i + 1 } 285 } 286 } 287 } 288 } else { 289 var isc: i64 = 0 290 if buf[i] == (47 as u8) { if i + 1 < n { if buf[i+1] == (47 as u8) { isc = 1 } } } 291 if isc == 1 { 292 var z: i64 = i 293 var g2: i64 = 1 294 while g2 == 1 { if z >= n { g2 = 0 } else { if buf[z] == (10 as u8) { g2 = 0 } else { z = z + 1 } } } 295 i = z + 1 296 } else { 297 // skip definitions -- their parens are a parameter list, not a call 298 if ac_is_def_at(buf, n, i) == 1 { 299 var q: i64 = i + 5 300 var go0: i64 = 1 301 while go0 == 1 { if q >= n { go0 = 0 } else { if buf[q] == (40 as u8) { go0 = 0 } else { q = q + 1 } } } 302 i = q + 1 303 } else { 304 if ac_isidst(buf[i] as i64) == 1 { 305 var prev_ok: i64 = 1 306 if i > 0 { if ac_isidc(buf[i-1] as i64) == 1 { prev_ok = 0 } } 307 if prev_ok == 1 { 308 var e: i64 = i 309 var go: i64 = 1 310 while go == 1 { if e >= n { go = 0 } else { if ac_isidc(buf[e] as i64) == 1 { e = e + 1 } else { go = 0 } } } 311 let nlen: i64 = e - i 312 if e < n { if buf[e] == (40 as u8) { 313 let p: *u8 = ((buf as i64) + i) as *u8 314 let idx: i64 = ac_find(names, cnt, p, nlen) 315 // judge ONLY unambiguous names; a name defined in several files in this dir 316 // belongs to several unrelated programs and this table cannot say which. 317 if idx >= 0 { if dcount[idx] > 1 { amb[0] = amb[0] + 1 } } 318 if idx >= 0 { if dcount[idx] == 1 { 319 let want: i64 = arity[idx] 320 let got: i64 = ac_args(buf, n, e) 321 if want >= 0 { if got >= 0 { if want != got { 322 bad = bad + 1 323 ac_w(" ARITY-MISMATCH " as *u8) 324 ac_w(fname) 325 ac_w(": " as *u8) 326 ac_wb(p, nlen) 327 ac_w(" called with " as *u8) 328 ac_wn(got) 329 ac_w(" arg(s), defined with " as *u8) 330 ac_wn(want) 331 ac_w("\n" as *u8) 332 } } } 333 } } 334 } } 335 i = e 336 } else { i = i + 1 } 337 } else { i = i + 1 } 338 } 339 } 340 } 341 } 342 return bad 343} 344 345func ac_ends_nx(nm: *u8) -> i64 { 346 let n: i64 = ac_slen(nm) 347 if n < 4 { return 0 } 348 if nm[n-3] != (46 as u8) { return 0 } 349 if nm[n-2] != (110 as u8) { return 0 } 350 if nm[n-1] != (120 as u8) { return 0 } 351 return 1 352} 353 354func ac_join(out: *u8, dir: *u8, name: *u8) -> i64 { 355 var i: i64 = 0 356 while dir[i] != (0 as u8) { out[i] = dir[i]; i = i + 1 } 357 out[i] = 47 as u8 358 i = i + 1 359 var j: i64 = 0 360 while name[j] != (0 as u8) { out[i+j] = name[j]; j = j + 1 } 361 out[i+j] = 0 as u8 362 return i + j 363} 364 365// TWO PASSES over one directory. Buffers are allocated ONCE and passed down -- never per file. That is the 366// v2 OOM lesson from nx_dup_source_check, whose per-file mmap leaked ~1GB/run and starved the host: a 367// helper's cost is set by its CALLER'S fan-out, so re-cost anything you amplify. 368// stats[0]=mismatches stats[1]=files stats[2]=defs stats[3]=unreadable-or-overcap 369func ac_scan_dir(dir: *u8, stats: *i64) -> i64 { 370 let names: *u8 = sys_mmap(AC_MAXDEF * AC_NAMELEN) 371 let arity: *i64 = sys_mmap(AC_MAXDEF * 8) as *i64 372 let dcount: *i64 = sys_mmap(AC_MAXDEF * 8) as *i64 373 let fseen: *i64 = sys_mmap(AC_MAXDEF * 8) as *i64 374 var fileid: i64 = 0 375 let buf: *u8 = sys_mmap(AC_FILECAP) 376 let dbuf: *u8 = sys_mmap(AC_DIRBUF) 377 let path: *u8 = sys_mmap(AC_PATHCAP) 378 var cnt: i64 = 0 379 380 // ---- pass 1: every definition visible in this directory ---- 381 var fd: i64 = sys_openat_rd(dir) 382 if fd < 0 { return 0 - 1 } 383 var go: i64 = 1 384 while go == 1 { 385 let nr: i64 = sys_getdents64(fd, dbuf, AC_DIRBUF) 386 if nr <= 0 { go = 0 } else { 387 var off: i64 = 0 388 while off < nr { 389 let rec: *u8 = (dbuf as i64 + off) as *u8 390 let nm: *u8 = dirent_name(rec) 391 if dirent_type(rec) != 4 { if ac_ends_nx(nm) == 1 { 392 ac_join(path, dir, nm) 393 let n: i64 = ac_slurp(path, buf, AC_FILECAP) 394 fileid = fileid + 1 395 if n > 0 { cnt = ac_collect(buf, n, names, arity, dcount, fseen, fileid, cnt, ((stats as i64) + 32) as *i64, nm) } 396 else { stats[3] = stats[3] + 1 } 397 } } 398 off = off + dirent_reclen(rec) 399 } 400 } 401 } 402 sys_close(fd) 403 stats[2] = cnt 404 405 // ---- pass 2: every call site, judged against pass 1 ---- 406 fd = sys_openat_rd(dir) 407 if fd < 0 { return 0 - 1 } 408 go = 1 409 while go == 1 { 410 let nr: i64 = sys_getdents64(fd, dbuf, AC_DIRBUF) 411 if nr <= 0 { go = 0 } else { 412 var off: i64 = 0 413 while off < nr { 414 let rec: *u8 = (dbuf as i64 + off) as *u8 415 let nm: *u8 = dirent_name(rec) 416 if dirent_type(rec) != 4 { if ac_ends_nx(nm) == 1 { 417 ac_join(path, dir, nm) 418 let n: i64 = ac_slurp(path, buf, AC_FILECAP) 419 if n > 0 { 420 stats[1] = stats[1] + 1 421 stats[0] = stats[0] + ac_check(buf, n, names, arity, dcount, cnt, nm, ((stats as i64) + 48) as *i64) 422 } 423 } } 424 off = off + dirent_reclen(rec) 425 } 426 } 427 } 428 sys_close(fd) 429 return stats[0] 430} 431 432func ac_wfile(path: *u8, s: *u8) -> i64 { 433 let fd: i64 = sys_openat_wr(path, 420) 434 if fd >= 0 { sys_write(fd, s, ac_slen(s)); sys_close(fd) } 435 return 0 436} 437 438// ★SELFTEST. Every tooth has its opposite, or a constant-returning checker would pass. 439func ac_selftest() -> i64 { 440 sys_mkdir("/tmp/nxarity" as *u8, 0x1ed) 441 // T1 the definition: two parameters. 442 ac_wfile("/tmp/nxarity/defs.nx" as *u8, "func two_p(a: *i64, b: i64) -> i64 { return 0 }\n" as *u8) 443 // T2 a WRONG call (1 arg) and T3 a RIGHT call (2 args) -- both must be judged, in opposite directions. 444 ac_wfile("/tmp/nxarity/bad.nx" as *u8, "func bad_caller() -> i64 { two_p(1) return 0 }\n" as *u8) 445 // ★T4 THE DEPTH TOOTH: two_p(inner(1, 9), 2) is TWO arguments. A naive comma count sees THREE and 446 // would report a false mismatch here, so this fixture is what separates a working checker from noise. 447 ac_wfile("/tmp/nxarity/good.nx" as *u8, "func inner(x: i64, y: i64) -> i64 { return x }\nfunc good_caller() -> i64 { two_p(inner(1, 9), 2) return 0 }\n" as *u8) 448 449 let st: *i64 = sys_mmap(64) as *i64 450 st[0]=0; st[1]=0; st[2]=0; st[3]=0 451 let bad: i64 = ac_scan_dir("/tmp/nxarity" as *u8, st) 452 if bad != 1 { 453 ac_w(" SELFTEST FAIL: expected EXACTLY 1 arity mismatch (bad.nx two_p(1) vs 2 params); the correct 2-arg call and the NESTED two_p(inner(1, 9), 2) must both pass. got " as *u8) 454 ac_wn(bad) 455 ac_w("\n" as *u8) 456 return 0 457 } 458 if st[2] < 3 { ac_w(" SELFTEST FAIL: expected >=3 definitions collected, got " as *u8); ac_wn(st[2]); ac_w("\n" as *u8); return 0 } 459 // ★T5 NEGATIVE CONTROL for the duplicate class: this fixture has NO conflicting redefinition, so a 460 // detector that flagged duplicates unconditionally would fail right here. 461 if st[4] != 0 { ac_w(" SELFTEST FAIL: clean fixture must report 0 conflicting duplicates, got " as *u8); ac_wn(st[4]); ac_w("\n" as *u8); return 0 } 462 // ★T6 POSITIVE: a SECOND definition of one name with a DIFFERENT arity, in its own dir so it cannot 463 // perturb the counts above. This is the 1785447657 class that nx_cc accepts with zero diagnostics. 464 sys_mkdir("/tmp/nxarity2" as *u8, 0x1ed) 465 // ★BOTH DEFINITIONS IN ONE FILE -- that is the real 1785447657 class. The earlier fixture put them in 466 // two files, which under the corrected rule is ordinary and must NOT flag; if you revert this fixture 467 // the tooth silently starts testing nothing. 468 ac_wfile("/tmp/nxarity2/one.nx" as *u8, "func dup_f(a: i64) -> i64 { return a }\nfunc dup_f(a: i64, b: i64) -> i64 { return b }\n" as *u8) 469 ac_wfile("/tmp/nxarity2/two.nx" as *u8, "func other_f(a: i64) -> i64 { return a }\n" as *u8) 470 let st2: *i64 = sys_mmap(64) as *i64 471 st2[0]=0; st2[1]=0; st2[2]=0; st2[3]=0; st2[4]=0 472 ac_scan_dir("/tmp/nxarity2" as *u8, st2) 473 if st2[4] != 1 { ac_w(" SELFTEST FAIL T6: a redefinition with a DIFFERENT arity must be reported once, got " as *u8); ac_wn(st2[4]); ac_w("\n" as *u8); return 0 } 474 ac_w(" self-test OK (arity + wrong-call caught + right-call cleared + NESTED-CALL depth tooth + dup-def conflict, both directions)\n" as *u8) 475 return 1 476} 477 478func main(argc: i64, argv: *i64) -> i64 { 479 ac_w("nx_aritycheck -- CALL-SITE ARITY CENSUS (the axis nx_cc and nx_undefscan both leave unmeasured)\n" as *u8) 480 if ac_selftest() == 0 { ac_w("verdict=RED (self-test of the checker failed)\n" as *u8); sys_exit(2); return 2 } 481 // ================================================================================================ 482 // THE CORPUS SCAN IS RETIRED. IT LIED, AND IT COULD NOT BE MADE TO STOP. 483 // 484 // Measured 2026-07-31. Scanning _hdl_build/ produced ~100 ARITY-MISMATCH rows. EVERY row that was 485 // hand-verified was FALSE, across four successive causes -- comments, directory scope, string 486 // literals, and finally CROSS-PROGRAM NAME COLLISION. The last one killed the design outright: 487 // nx_authorgen_gate.nx calls ag_build() with ZERO arguments and is CORRECT, because it imports 488 // runtime/nx_authorgen.nx:139 `func ag_build() -> i64`. This scanner compared it against an unrelated 489 // `func ag_build(parent, value, kind)` in nx_analyst_gate.nx -- a file it does not import. 490 // 491 // The "defined exactly once in this directory" rule was supposed to prevent exactly that, and did 492 // not: ag_build IS declared exactly once inside _hdl_build/, so the rule judged it and was wrong, 493 // because the declaration that actually binds lives in runtime/ -- OUTSIDE the scanned directory. 494 // No further heuristic can close this. A DIRECTORY IS NOT A SCOPE. 495 // 496 // THE CAPABILITY WAS NOT DELETED, IT MOVED TO WHERE IT CAN BE CORRECT: `nx_undefscan arity <target>` 497 // scopes every judgement to the target's IMPORT CLOSURE, reuses a real tokenizer that already skips 498 // comments / strings / numeric literals / directives as units, and inherits the PARTIAL doctrine so 499 // an incomplete closure cannot report a clean. It is proven able to FAIL (synthetic teeth T9/T10), 500 // and on 14 diverse targets -- including a 70-file, 847-function closure -- it reports ZERO. That is 501 // the real answer this scanner was never able to give. 502 // 503 // WHAT SURVIVES HERE: the self-test above, which is SOUND because its fixtures are whole programs 504 // with a known closure. It still proves the arity tooth and the dup-def-in-one-file tooth, and it is 505 // still the only place the latter is checked. A gate that verifies a real property on fixtures it 506 // fully controls is worth keeping; one that reports on a corpus it cannot scope is not. 507 // ================================================================================================ 508 ac_w(" corpus scan RETIRED -- a directory is not a scope; it produced ~100 false rows (see header)\n" as *u8) 509 ac_w(" the corpus axis now lives at: nx_undefscan arity <target> (scoped to the import closure)\n" as *u8) 510 ac_w("verdict=GREEN (self-test passed: arity tooth + nested-call depth + dup-def conflict both ways)\n" as *u8) 511 sys_exit(0) 512 return 0 513} 514 515func ac_retired_corpus_scan(argc: i64, argv: *i64) -> i64 { 516 var dir: *u8 = "buildroot/runtime/_hdl_build" as *u8 517 if argc >= 2 { dir = argv[1] as *u8 } 518 ac_w(" scanning "); ac_w(dir); ac_w("\n" as *u8) 519 let st: *i64 = sys_mmap(64) as *i64 520 st[0]=0; st[1]=0; st[2]=0; st[3]=0 521 let bad: i64 = ac_scan_dir(dir, st) 522 if bad < 0 { ac_w("verdict=RED (cannot open dir)\n" as *u8); sys_exit(3); return 3 } 523 ac_w(" files="); ac_wn(st[1]) 524 ac_w(" definitions="); ac_wn(st[2]) 525 ac_w(" unreadable_or_overcap="); ac_wn(st[3]) 526 ac_w(" DUP-DEF-IN-ONE-FILE="); ac_wn(st[4]) 527 ac_w(" NAMES-OVER-CAP="); ac_wn(st[5]) 528 ac_w(" AMBIGUOUS-SKIPPED="); ac_wn(st[6]) 529 ac_w(" ARITY-MISMATCHES="); ac_wn(st[0]) 530 ac_w("\n" as *u8) 531 ac_w(" COVERAGE: a call is judged ONLY when its name is defined EXACTLY ONCE in this directory. A directory is NOT a namespace -- every .nx is a separate program, so names like main/g_w/chk exist in many unrelated files and CANNOT be attributed from here. Those calls are counted above as AMBIGUOUS-SKIPPED, never guessed. This organ is SOUND, not COMPLETE; the complete version needs the per-file IMPORT CLOSURE (nx_undefscan already computes it)\n" as *u8) 532 if st[5] > 0 { ac_w(" TRUNCATED: the definition table hit AC_MAXDEF, so calls to the dropped names were NOT judged. The mismatch count is an UNDER-count and this run is NOT a clean -- raise AC_MAXDEF and re-run\n" as *u8) } 533 if st[3] > 0 { ac_w(" PARTIAL: some sources were unreadable or over the 1MiB cap and were NOT judged -- this is not a smaller clean\n" as *u8) } 534 if st[4] > 0 { ac_w(" NOTE: a conflicting duplicate definition makes the mismatch count above an UNDER-count -- ac_find resolves the FIRST, so calls written against the second are judged wrong or missed. Fix the duplicates first, then re-run\n" as *u8) } 535 // GREEN requires a COMPLETE census, not merely a quiet one: no mismatches, no conflicting 536 // duplicates, and no truncation. A run that could not see everything must never read as clean. 537 if st[0] == 0 { if st[4] == 0 { if st[5] == 0 { ac_w("verdict=GREEN (every resolved call site matches its definition's arity, census complete)\n" as *u8); sys_exit(0); return 0 } } } 538 ac_w("verdict=RED (call sites disagree with the definition their own directory resolves; nx_cc will NOT report these)\n" as *u8) 539 sys_exit(6) 540 return 6 541} 542