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