code wiki / _hdl_build / nx_undefscan.nx

nx_undefscan.nx source

↩ module page · 1774 lines · 74070 B

1// nx_undefscan.nx -- THE MISSING-SYMBOL CENSUS: report EVERY function a target calls that nobody in its 2// import closure defines, WITHOUT compiling it. 3// 4// WHY THIS EXISTS. The compiler reports undefined symbols ONE AT A TIME, and only AFTER expand_imports 5// succeeds -- so a single mis-layered import hides an unknown number of missing functions behind it, and 6// the outer explanation (bad import) is always true enough to stop the investigation. Measured 7// 2026-07-31: un-darkening 14 libs exposed SEVEN undefined functions no build could ever have reported -- 8// nx_pctl_kill_by_cmdline (2 callers), the whole nx_fft decomposed API (4 consumers), nx_bm25_score 9// (5 callers). Each fix revealed the next, so the count was never knowable in advance. This organ makes 10// it knowable in ONE pass: walk the closure, collect every definition, collect every call site, print 11// the DIFFERENCE AS A LIST rather than as the first error. 12// 13// WHAT IT IS NOT. A textual resolver, not the compiler. Two known ways it can be wrong, stated here so a 14// caller never has to guess: 15// - a call THROUGH A FUNCTION-POINTER VARIABLE looks like an undefined call (false positive) 16// - a symbol the compiler itself provides, if any exist, has no .nx definition (false positive) 17// Both are bounded by the POSITIVE CONTROL: a target that BUILDS must report ZERO. Any residue on a 18// building target IS that false-positive set, and must be fixed here rather than explained away. 19// 20// PARTIAL IS NOT A SMALLER CLEAN (the 07-31 layerscan law, built in as a tooth). If any import in the 21// closure resolves to no file, or the queue overflows, the definition set is INCOMPLETE -- so every 22// undefined it prints may merely be defined in the file it could not read. In that state the verdict is 23// PARTIAL and a ZERO count exits NONZERO, because a clean report from a truncated scan is the exact lie 24// this class of instrument keeps telling. 25// 26// nx_undefscan scan <target> -- target = basename WITHOUT .nx, resolved runtime/ then _hdl_build/ 27// EXIT: 0 CLEAN+COMPLETE - 1 UNDEFINED FOUND - 2 usage - 3 target not found - 5 PARTIAL (cannot certify) 28// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 29import "nx_syscalls.nx" 30 31const US_MAXF: i64 = 512 32const US_NAMELEN: i64 = 64 33const US_SLOTS: i64 = 32768 34const US_PATHBUF: i64 = 1024 35const US_RTDIR: *u8 = "buildroot/runtime" as *u8 36const US_HDDIR: *u8 = "buildroot/runtime/_hdl_build" as *u8 37const US_HUBDIR: *u8 = "buildroot/runtime/hub" as *u8 38const US_WIKIDIR: *u8 = "buildroot/runtime/wiki" as *u8 39 40// RESOLVE AN IMPORT THE WAY nx_cc DOES, not the way I assumed. Measured 2026-07-31: I searched only 41// runtime/ and _hdl_build/ and reported MISSING-IMPORT nx_search_query_parser for a file that lives in 42// runtime/hub/. The compiler resolved it fine -- proven because nx_search_snippet_extract failed with 43// `UNDEFINED label: main` (a no-main library built standalone) rather than an expand_imports error, and 44// expand_imports runs FIRST. An under-searching resolver manufactures both false MISSING-IMPORTs and 45// false NOWHEREs, and a false NOWHERE is the one that gets someone to WRITE CODE THAT ALREADY EXISTS. 46// _retired/ is deliberately excluded: code there is not meant to be importable. 47func us_resolve(path: *u8, nm: *u8) -> i64 { 48 us_path(path, US_RTDIR, nm) 49 if us_exists(path) == 1 { return 1 } 50 us_path(path, US_HDDIR, nm) 51 if us_exists(path) == 1 { return 1 } 52 us_path(path, US_HUBDIR, nm) 53 if us_exists(path) == 1 { return 1 } 54 us_path(path, US_WIKIDIR, nm) 55 if us_exists(path) == 1 { return 1 } 56 return 0 57} 58 59// PER-IMPORTER RESOLUTION -- THE RULE THE COMPILER ACTUALLY USES. 60// us_resolve above takes only a NAME and probes ONE fixed global order, so for a name that exists in more 61// than one probe directory it always returns the same copy no matter who imported it. nx_cc does not work 62// that way: AN IMPORT BINDS THE IMPORTER'S OWN DIRECTORY FIRST. 63// Measured 2026-07-31: nx_m2d_engine lives in _hdl_build/ and is written against 64// _hdl_build/nx_input_abstract.nx (ia_init/2, ia_bind/4). The fixed order handed it 65// runtime/nx_input_abstract.nx (ia_init/1, ia_bind/3) -- a DIFFERENT API wearing the same names -- and the 66// arity pass reported EIGHTEEN mismatches that were not defects. The closure was also one file too big: 67// it had pulled in a file the compiler never binds. 68// THE DAMAGE IS NOT LIMITED TO ARITY. A wrong binding is a wrong DEFINITION SET, so undefined-symbol 69// verdicts go wrong in BOTH directions -- a false CLEAN when the wrong file happens to declare the name, 70// and a false UNDEFINED when it does not. That is why this is fixed at the resolver rather than per-pass. 71// Fallback order is BUILD-TARGET probe rank (_hdl_build first): that is the copy /api/build compiles, so 72// a closure read in that order is the closure the artifact was actually produced from. 73func us_dirp(id: i64) -> *u8 { 74 if id == 0 { return US_HDDIR } 75 if id == 1 { return US_RTDIR } 76 if id == 2 { return US_HUBDIR } 77 if id == 3 { return US_WIKIDIR } 78 return "" as *u8 79} 80 81// Returns the dir id that won, or 0-1 if no probe directory has the file. prefid<0 means "no importer", 82// which is the ROOT target -- it takes probe rank alone. 83func us_resolve_dir(path: *u8, nm: *u8, prefid: i64) -> i64 { 84 if prefid >= 0 { 85 us_path(path, us_dirp(prefid), nm) 86 if us_exists(path) == 1 { return prefid } 87 } 88 var i: i64 = 0 89 while i < 4 { 90 us_path(path, us_dirp(i), nm) 91 if us_exists(path) == 1 { return i } 92 i = i + 1 93 } 94 return 0 - 1 95} 96const US_FNV_OFF: i64 = 1469598103934665603 97const US_FNV_PRIME: i64 = 1099511628211 98const US_EXIT_UNDEF: i64 = 1 99const US_EXIT_USAGE: i64 = 2 100const US_EXIT_NOTFOUND: i64 = 3 101const US_EXIT_PARTIAL: i64 = 5 102const US_EXIT_SELFTEST: i64 = 4 103const US_BICONF: *u8 = "knowledge/undefscan_builtins.conf" as *u8 104const US_UNDEFCAP: i64 = 256 105const US_DIRBUF: i64 = 1048576 106 107// UNDEFINED IN THIS CLOSURE IS NOT THE SAME CLAIM AS DOES NOT EXIST, and the two have OPPOSITE remedies: 108// a symbol defined elsewhere in the corpus is a MISSING IMPORT (one line); a symbol defined nowhere is 109// UNWRITTEN CODE (a module). Measured 2026-07-31: nx_search_neutrality_gate reported 7 undefined names, 110// and I read that as one absent inverted-index module and filed it sev-8. SIX of the seven were already 111// in runtime/nx_search_inverted.nx with ~25 other consumers -- pure wiring -- and only nx_bm25_tf was 112// genuinely absent. A careful reader got it wrong from the bare list, so the LIST IS NOT THE ANSWER. 113// `corpus` mode resolves each undefined name against both source trees and names the defining file. 114 115func us_puts(s: *u8) { 116 var n: i64 = 0 117 while s[n] != (0 as u8) { n = n + 1 } 118 sys_write(1, s, n) 119} 120 121func us_putn(p: *u8, n: i64) { sys_write(1, p, n) } 122 123func us_puti(x: i64) { 124 let buf: *u8 = sys_mmap(64) 125 var v: i64 = x 126 var neg: i64 = 0 127 if v < 0 { neg = 1; v = 0 - v } 128 var i: i64 = 40 129 if v == 0 { i = i - 1; buf[i] = 48 as u8 } 130 while v > 0 { 131 let d: i64 = v - (v / 10) * 10 132 i = i - 1 133 buf[i] = (d + 48) as u8 134 v = v / 10 135 } 136 if neg == 1 { i = i - 1; buf[i] = 45 as u8 } 137 sys_write(1, ((buf as i64) + i) as *u8, 40 - i) 138} 139 140func us_isidst(c: i64) -> i64 { 141 if c == 95 { return 1 } 142 if c >= 97 { if c <= 122 { return 1 } } 143 if c >= 65 { if c <= 90 { return 1 } } 144 return 0 145} 146 147func us_isidc(c: i64) -> i64 { 148 if us_isidst(c) == 1 { return 1 } 149 if c >= 48 { if c <= 57 { return 1 } } 150 return 0 151} 152 153func us_eqlit(p: *u8, L: i64, lit: *u8, litlen: i64) -> i64 { 154 if L != litlen { return 0 } 155 var i: i64 = 0 156 while i < L { 157 if p[i] != lit[i] { return 0 } 158 i = i + 1 159 } 160 return 1 161} 162 163// Idents that can legally sit immediately before ( without being a call. Kept explicit rather than 164// inferred: a keyword silently treated as a call would be reported as a missing function forever. 165func us_iskw(p: *u8, L: i64) -> i64 { 166 if us_eqlit(p, L, "if" as *u8, 2) == 1 { return 1 } 167 if us_eqlit(p, L, "while" as *u8, 5) == 1 { return 1 } 168 if us_eqlit(p, L, "return" as *u8, 6) == 1 { return 1 } 169 if us_eqlit(p, L, "else" as *u8, 4) == 1 { return 1 } 170 if us_eqlit(p, L, "let" as *u8, 3) == 1 { return 1 } 171 if us_eqlit(p, L, "var" as *u8, 3) == 1 { return 1 } 172 if us_eqlit(p, L, "const" as *u8, 5) == 1 { return 1 } 173 if us_eqlit(p, L, "func" as *u8, 4) == 1 { return 1 } 174 if us_eqlit(p, L, "import" as *u8, 6) == 1 { return 1 } 175 if us_eqlit(p, L, "struct" as *u8, 6) == 1 { return 1 } 176 if us_eqlit(p, L, "for" as *u8, 3) == 1 { return 1 } 177 if us_eqlit(p, L, "break" as *u8, 5) == 1 { return 1 } 178 if us_eqlit(p, L, "continue" as *u8, 8) == 1 { return 1 } 179 if us_eqlit(p, L, "as" as *u8, 2) == 1 { return 1 } 180 if us_eqlit(p, L, "static" as *u8, 6) == 1 { return 1 } 181 return 0 182} 183 184// A `__`-prefixed identifier that nothing declares is a COMPILER INTRINSIC by naming convention. 185// 186// WHY A RULE AND NOT A LIST: I appended to undefscan_builtins.conf three separate times -- __syscall, 187// then six bit intrinsics, then seven x86 crypto ones -- and the sweep immediately turned up seven more 188// (atomics, __thread_clone, __i16x16_madd) plus __f32_mul/__f32_add. Each batch was verified the same 189// way and each time the list was still incomplete. The corpus convention is absolute: NO .nx file in 190// 19,981 sources declares a `__` name, so the prefix IS the classifier. A rule nothing has to remember 191// beats a list somebody must maintain. 192// 193// THIS DOES NOT HIDE ANYTHING. Presumed intrinsics are counted and printed as their own number, never 194// folded into `undefined` and never silently dropped. And a `__` name that IS declared somewhere in the 195// closure never reaches here -- it resolves as a normal definition. 196func us_is_dunder(p: *u8) -> i64 { 197 if p[0] != (95 as u8) { return 0 } 198 if p[1] != (95 as u8) { return 0 } 199 return 1 200} 201 202func us_hash(s: *u8, n: i64) -> i64 { 203 var h: i64 = US_FNV_OFF 204 var i: i64 = 0 205 while i < n { 206 h = h ^ (s[i] as i64) 207 h = h * US_FNV_PRIME 208 i = i + 1 209 } 210 if h < 0 { h = 0 - h } 211 if h < 0 { h = 0 } 212 return h 213} 214 215func us_slot(tab: *u8, i: i64) -> *u8 { return ((tab as i64) + i * US_NAMELEN) as *u8 } 216 217// Open-addressed insert. Returns the slot index, or 0-1 if the name does not fit / the table is full. 218func us_put(tab: *u8, s: *u8, n: i64) -> i64 { 219 if n <= 0 { return 0 - 1 } 220 if n >= US_NAMELEN { return 0 - 1 } 221 let h: i64 = us_hash(s, n) 222 var idx: i64 = h - (h / US_SLOTS) * US_SLOTS 223 var probes: i64 = 0 224 while probes < US_SLOTS { 225 let p: *u8 = us_slot(tab, idx) 226 if p[0] == (0 as u8) { 227 var i: i64 = 0 228 while i < n { p[i] = s[i]; i = i + 1 } 229 p[n] = 0 as u8 230 return idx 231 } 232 var eq: i64 = 1 233 if p[n] != (0 as u8) { eq = 0 } 234 var k: i64 = 0 235 while k < n { 236 if p[k] != s[k] { eq = 0; k = n } else { k = k + 1 } 237 } 238 if eq == 1 { return idx } 239 idx = idx + 1 240 if idx >= US_SLOTS { idx = 0 } 241 probes = probes + 1 242 } 243 return 0 - 1 244} 245 246func us_has(tab: *u8, s: *u8, n: i64) -> i64 { 247 if n <= 0 { return 0 } 248 if n >= US_NAMELEN { return 0 } 249 let h: i64 = us_hash(s, n) 250 var idx: i64 = h - (h / US_SLOTS) * US_SLOTS 251 var probes: i64 = 0 252 while probes < US_SLOTS { 253 let p: *u8 = us_slot(tab, idx) 254 if p[0] == (0 as u8) { return 0 } 255 var eq: i64 = 1 256 if p[n] != (0 as u8) { eq = 0 } 257 var k: i64 = 0 258 while k < n { 259 if p[k] != s[k] { eq = 0; k = n } else { k = k + 1 } 260 } 261 if eq == 1 { return 1 } 262 idx = idx + 1 263 if idx >= US_SLOTS { idx = 0 } 264 probes = probes + 1 265 } 266 return 0 267} 268 269// Queue of closure member basenames. Dedupes against 0..cnt-1 using slot cnt as scratch. 270func us_qpush(q: *u8, cnt: i64, src: *u8, n: i64) -> i64 { 271 if n <= 0 { return cnt } 272 if n >= US_NAMELEN { return cnt } 273 if cnt >= US_MAXF { return cnt } 274 let d: *u8 = us_slot(q, cnt) 275 var i: i64 = 0 276 while i < n { d[i] = src[i]; i = i + 1 } 277 d[n] = 0 as u8 278 var k: i64 = 0 279 while k < cnt { 280 if us_eqlit(us_slot(q, k), n, d, n) == 1 { 281 let e: *u8 = us_slot(q, k) 282 if e[n] == (0 as u8) { return cnt } 283 } 284 k = k + 1 285 } 286 return cnt + 1 287} 288 289// out = <dir>/<name>.nx 290func us_path(out: *u8, dir: *u8, name: *u8) { 291 var w: i64 = 0 292 var i: i64 = 0 293 while dir[i] != (0 as u8) { out[w] = dir[i]; w = w + 1; i = i + 1 } 294 out[w] = 47 as u8 295 w = w + 1 296 i = 0 297 while name[i] != (0 as u8) { out[w] = name[i]; w = w + 1; i = i + 1 } 298 out[w] = 46 as u8 299 out[w + 1] = 110 as u8 300 out[w + 2] = 120 as u8 301 out[w + 3] = 0 as u8 302} 303 304func us_exists(path: *u8) -> i64 { 305 let fd: i64 = sys_openat_rd(path) 306 if fd < 0 { return 0 } 307 sys_close(fd) 308 return 1 309} 310 311// Load compiler-provided symbols from the conf. An unreadable/absent conf yields ZERO exclusions, which 312// makes every intrinsic show up as UNDEFINED -- loud and obviously wrong. That direction is deliberate: 313// an empty exclusion set can only add false positives, never hide a real missing function. 314func us_load_builtins(tab: *u8) -> i64 { 315 let lenbox: *i64 = sys_mmap(16) as *i64 316 lenbox[0] = 0 317 let buf: *u8 = sys_read_file(US_BICONF, lenbox) 318 let n: i64 = lenbox[0] 319 if n <= 0 { return 0 } 320 var cnt: i64 = 0 321 var i: i64 = 0 322 while i < n { 323 var e: i64 = i 324 var st: i64 = 0 325 while st == 0 { 326 if e >= n { st = 1 } else { 327 if buf[e] == (10 as u8) { st = 1 } else { e = e + 1 } 328 } 329 } 330 var L: i64 = e - i 331 var trimming: i64 = 1 332 while trimming == 1 { 333 if L <= 0 { trimming = 0 } else { 334 let ch: i64 = buf[i + L - 1] as i64 335 if ch == 13 { L = L - 1 } else { 336 if ch == 32 { L = L - 1 } else { 337 if ch == 9 { L = L - 1 } else { trimming = 0 } } } 338 } 339 } 340 if L > 0 { 341 if buf[i] != (35 as u8) { 342 us_put(tab, ((buf as i64) + i) as *u8, L) 343 cnt = cnt + 1 344 } 345 } 346 i = e + 1 347 } 348 return cnt 349} 350 351// After a `func`, `struct`, or `type` keyword the next identifier is a DECLARATION. All three spell it 352// the same way, so one helper serves all three (rule 15) -- and MISSING any of them is expensive: not 353// handling `type` alone accounted for ~32 phantom undefined identifiers, because nx_tier.nx:62 declares 354// `type nx_int = i64` and thirty-odd sibling aliases that the whole corpus then uses as types. 355// Returns the index just past the declared name, or 0-1 if no identifier follows. 356func us_declare_next(buf: *u8, n: i64, from: i64, defs: *u8) -> i64 { 357 var k: i64 = from 358 var st: i64 = 0 359 while st == 0 { 360 if k >= n { st = 1 } else { 361 if us_isidst(buf[k] as i64) == 1 { st = 1 } else { k = k + 1 } 362 } 363 } 364 if k >= n { return 0 - 1 } 365 var e: i64 = k 366 var s2: i64 = 0 367 while s2 == 0 { 368 if e >= n { s2 = 1 } else { 369 if us_isidc(buf[e] as i64) == 1 { e = e + 1 } else { s2 = 1 } 370 } 371 } 372 us_put(defs, ((buf as i64) + k) as *u8, e - k) 373 return e 374} 375 376func us_isdig(c: i64) -> i64 { 377 if c >= 48 { if c <= 57 { return 1 } } 378 return 0 379} 380 381// THE SCANNER. One left-to-right pass; comments and string literals are skipped as units so a function 382// name mentioned in prose or in a message string never becomes a phantom call. 383func us_scan(buf: *u8, n: i64, defs: *u8, calls: *u8, refs: *u8, q: *u8, qcnt: i64) -> i64 { 384 var qc: i64 = qcnt 385 var i: i64 = 0 386 while i < n { 387 let c: i64 = buf[i] as i64 388 var adv: i64 = 1 389 390 var iscmt: i64 = 0 391 if c == 47 { if i + 1 < n { if buf[i + 1] == (47 as u8) { iscmt = 1 } } } 392 if iscmt == 1 { 393 var j: i64 = i + 2 394 var st: i64 = 0 395 while st == 0 { 396 if j >= n { st = 1 } else { 397 if buf[j] == (10 as u8) { st = 1 } else { j = j + 1 } 398 } 399 } 400 i = j 401 adv = 0 402 } 403 404 if adv == 1 { 405 if c == 34 { 406 var j: i64 = i + 1 407 var st: i64 = 0 408 while st == 0 { 409 if j >= n { st = 1 } else { 410 if buf[j] == (92 as u8) { j = j + 2 } else { 411 if buf[j] == (34 as u8) { j = j + 1; st = 1 } else { j = j + 1 } 412 } 413 } 414 } 415 i = j 416 adv = 0 417 } } 418 419 // A NUMERIC LITERAL is consumed WHOLE. Without this, 0x00004000 scans as the digit 0 followed by 420 // an identifier x00004000 -- ten phantom undefined names in the first real run, every one a hex 421 // constant. A tokenizer that splits a literal invents symbols that were never written. 422 if adv == 1 { 423 if us_isdig(c) == 1 { 424 var j: i64 = i 425 var st: i64 = 0 426 while st == 0 { 427 if j >= n { st = 1 } else { 428 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 429 } 430 } 431 i = j 432 adv = 0 433 } } 434 435 // A PREPROCESSOR DIRECTIVE (@ifdef / @ifndef / @endif). '@' is not an identifier character, so 436 // without this the directive NAME leaks through as a bare reference. The condition after it does 437 // NOT leak -- TARGET_X86_64 is a genuine compiler-provided flag and is declared in the conf. 438 if adv == 1 { 439 if c == 64 { 440 var j: i64 = i + 1 441 var st: i64 = 0 442 while st == 0 { 443 if j >= n { st = 1 } else { 444 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 445 } 446 } 447 i = j 448 adv = 0 449 } } 450 451 if adv == 1 { 452 if us_isidst(c) == 1 { 453 var j: i64 = i 454 var st: i64 = 0 455 while st == 0 { 456 if j >= n { st = 1 } else { 457 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 458 } 459 } 460 let L: i64 = j - i 461 let p: *u8 = ((buf as i64) + i) as *u8 462 463 var done: i64 = 0 464 465 // `let` and `var` introduce a binding whose TYPE ANNOTATION IS OPTIONAL. Classifying a 466 // declaration by a following ':' therefore misses `let _ = expr` entirely, and the name lands 467 // in the reference bucket as a phantom undefined symbol. Measured: nx_search_inverted_test 468 // uses `let _ = ...` and `let _2 = ...` as discard bindings and was reported with two 469 // undefined identifiers, one of which the corpus resolver then "found" in an unrelated file. 470 // A false positive that RESOLVES somewhere is worse than one that does not -- it invites a 471 // wrong import. Declare on the KEYWORD, not on the punctuation that may not be there. 472 var isdecl: i64 = 0 473 if us_eqlit(p, L, "func" as *u8, 4) == 1 { isdecl = 1 } 474 if us_eqlit(p, L, "struct" as *u8, 6) == 1 { isdecl = 1 } 475 if us_eqlit(p, L, "type" as *u8, 4) == 1 { isdecl = 1 } 476 if us_eqlit(p, L, "let" as *u8, 3) == 1 { isdecl = 1 } 477 if us_eqlit(p, L, "var" as *u8, 3) == 1 { isdecl = 1 } 478 if isdecl == 1 { 479 let de: i64 = us_declare_next(buf, n, j, defs) 480 if de >= 0 { i = de; done = 1 } 481 } 482 483 if done == 0 { 484 if us_eqlit(p, L, "import" as *u8, 6) == 1 { 485 var k: i64 = j 486 var qpos: i64 = 0 - 1 487 var s4: i64 = 0 488 while s4 == 0 { 489 if k >= n { s4 = 1 } else { 490 if buf[k] == (34 as u8) { qpos = k; s4 = 1 } else { 491 if buf[k] == (10 as u8) { s4 = 1 } else { k = k + 1 } 492 } 493 } 494 } 495 if qpos >= 0 { 496 var e: i64 = qpos + 1 497 var s5: i64 = 0 498 while s5 == 0 { 499 if e >= n { s5 = 1 } else { 500 if buf[e] == (34 as u8) { s5 = 1 } else { e = e + 1 } 501 } 502 } 503 if e < n { 504 let ip: *u8 = ((buf as i64) + qpos + 1) as *u8 505 var IL: i64 = e - (qpos + 1) 506 if IL > 3 { 507 if ip[IL - 3] == (46 as u8) { 508 if ip[IL - 2] == (110 as u8) { 509 if ip[IL - 1] == (120 as u8) { IL = IL - 3 } } } 510 } 511 qc = us_qpush(q, qc, ip, IL) 512 i = e + 1 513 done = 1 514 } 515 } 516 } } 517 518 if done == 0 { 519 // CLASSIFY BY THE NEXT SIGNIFICANT CHARACTER. An identifier followed by ':' is a 520 // DECLARATION -- parameter, let, var, const, or struct field -- and this language spells 521 // all five that one way, so the rule is exact rather than heuristic. Collecting them all 522 // OVER-approximates the declared set, which is the safe direction: it cannot invent a 523 // false positive from scoping, and a name declared NOWHERE in the closure still stands out. 524 var k: i64 = j 525 var t4: i64 = 0 526 while t4 == 0 { 527 if k >= n { t4 = 1 } else { 528 if buf[k] == (32 as u8) { k = k + 1 } else { 529 if buf[k] == (9 as u8) { k = k + 1 } else { t4 = 1 } 530 } 531 } 532 } 533 var cls: i64 = 0 534 if j < n { if buf[j] == (40 as u8) { cls = 1 } } 535 if cls == 0 { if k < n { if buf[k] == (58 as u8) { cls = 2 } } } 536 if cls == 1 { if us_iskw(p, L) == 0 { us_put(calls, p, L) } } 537 if cls == 2 { us_put(defs, p, L) } 538 if cls == 0 { if us_iskw(p, L) == 0 { us_put(refs, p, L) } } 539 i = j 540 } 541 adv = 0 542 } } 543 544 if adv == 1 { i = i + 1 } 545 } 546 return qc 547} 548 549// Scan ONE file for top-level declarations and, for any name in the undefined set, record the file that 550// declares it. Only the (small) undefined set is searched for, so memory is O(undefined), not O(corpus). 551func us_scan_decls(buf: *u8, n: i64, und: *u8, undcnt: i64, src: *u8, fname: *u8) -> i64 { 552 var hits: i64 = 0 553 var i: i64 = 0 554 while i < n { 555 var adv: i64 = 1 556 if us_isidst(buf[i] as i64) == 1 { 557 var j: i64 = i 558 var st: i64 = 0 559 while st == 0 { 560 if j >= n { st = 1 } else { 561 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 562 } 563 } 564 let L: i64 = j - i 565 let p: *u8 = ((buf as i64) + i) as *u8 566 var isd: i64 = 0 567 if us_eqlit(p, L, "func" as *u8, 4) == 1 { isd = 1 } 568 if us_eqlit(p, L, "struct" as *u8, 6) == 1 { isd = 1 } 569 if us_eqlit(p, L, "type" as *u8, 4) == 1 { isd = 1 } 570 if us_eqlit(p, L, "const" as *u8, 5) == 1 { isd = 1 } 571 if isd == 1 { 572 var k: i64 = j 573 var s2: i64 = 0 574 while s2 == 0 { 575 if k >= n { s2 = 1 } else { 576 if us_isidst(buf[k] as i64) == 1 { s2 = 1 } else { k = k + 1 } 577 } 578 } 579 if k < n { 580 var e: i64 = k 581 var s3: i64 = 0 582 while s3 == 0 { 583 if e >= n { s3 = 1 } else { 584 if us_isidc(buf[e] as i64) == 1 { e = e + 1 } else { s3 = 1 } 585 } 586 } 587 let dn: *u8 = ((buf as i64) + k) as *u8 588 let dl: i64 = e - k 589 var u: i64 = 0 590 while u < undcnt { 591 let up: *u8 = us_slot(und, u) 592 if us_eqlit(up, dl, dn, dl) == 1 { 593 if up[dl] == (0 as u8) { 594 let sp: *u8 = us_slot(src, u) 595 if sp[0] == (0 as u8) { 596 var z: i64 = 0 597 while fname[z] != (0 as u8) { 598 if z < US_NAMELEN - 1 { sp[z] = fname[z] } 599 z = z + 1 600 } 601 if z > US_NAMELEN - 1 { z = US_NAMELEN - 1 } 602 sp[z] = 0 as u8 603 hits = hits + 1 604 } 605 } } 606 u = u + 1 607 } 608 i = e 609 adv = 0 610 } 611 } 612 if adv == 1 { i = j; adv = 0 } 613 } 614 if adv == 1 { i = i + 1 } 615 } 616 return hits 617} 618 619// Walk ONE source tree. Deliberately the SAME two directories the import resolver searches (runtime/ then 620// _hdl_build/) and no others: a definition sitting in wiki/ or _retired/ is NOT importable, so reporting 621// it as ELSEWHERE would hand the caller a fix that cannot work. 622func us_corpus_dir(dir: *u8, und: *u8, undcnt: i64, src: *u8) -> i64 { 623 let dfd: i64 = sys_openat_rd(dir) 624 if dfd < 0 { return 0 } 625 let dbuf: *u8 = sys_mmap(US_DIRBUF) 626 let path: *u8 = sys_mmap(US_PATHBUF) 627 let lenbox: *i64 = sys_mmap(16) as *i64 628 var files: i64 = 0 629 var go: i64 = 1 630 while go == 1 { 631 let nb: i64 = sys_getdents64(dfd, dbuf, US_DIRBUF) 632 if nb <= 0 { go = 0 } else { 633 var off: i64 = 0 634 while off < nb { 635 let rec: *u8 = (dbuf as i64 + off) as *u8 636 let rl: i64 = dirent_reclen(rec) 637 if rl <= 0 { off = nb } else { 638 let nm: *u8 = dirent_name(rec) 639 var ln: i64 = 0 640 while nm[ln] != (0 as u8) { ln = ln + 1 } 641 var isnx: i64 = 0 642 if ln > 3 { 643 if nm[ln - 3] == (46 as u8) { 644 if nm[ln - 2] == (110 as u8) { 645 if nm[ln - 1] == (120 as u8) { isnx = 1 } } } 646 } 647 if isnx == 1 { 648 var po: i64 = 0 649 var di: i64 = 0 650 while dir[di] != (0 as u8) { path[po] = dir[di]; po = po + 1; di = di + 1 } 651 path[po] = 47 as u8 652 po = po + 1 653 var ni: i64 = 0 654 while nm[ni] != (0 as u8) { path[po] = nm[ni]; po = po + 1; ni = ni + 1 } 655 path[po] = 0 as u8 656 lenbox[0] = 0 657 let fb: *u8 = sys_read_file(path, lenbox) 658 if lenbox[0] > 0 { 659 us_scan_decls(fb, lenbox[0], und, undcnt, src, nm) 660 files = files + 1 661 sys_munmap(fb, lenbox[0]) 662 } 663 } 664 off = off + rl 665 } 666 } 667 } 668 } 669 sys_close(dfd) 670 return files 671} 672 673// Collect EVERY top-level declaration name (func/struct/type/const) from a file into tab. 674func us_collect_decls(buf: *u8, n: i64, tab: *u8) -> i64 { 675 var cnt: i64 = 0 676 var i: i64 = 0 677 while i < n { 678 var adv: i64 = 1 679 var iscmt: i64 = 0 680 if buf[i] == (47 as u8) { if i + 1 < n { if buf[i + 1] == (47 as u8) { iscmt = 1 } } } 681 if iscmt == 1 { 682 var j: i64 = i + 2 683 var st: i64 = 0 684 while st == 0 { 685 if j >= n { st = 1 } else { 686 if buf[j] == (10 as u8) { st = 1 } else { j = j + 1 } 687 } 688 } 689 i = j 690 adv = 0 691 } 692 if adv == 1 { if buf[i] == (34 as u8) { 693 var j: i64 = i + 1 694 var st: i64 = 0 695 while st == 0 { 696 if j >= n { st = 1 } else { 697 if buf[j] == (92 as u8) { j = j + 2 } else { 698 if buf[j] == (34 as u8) { j = j + 1; st = 1 } else { j = j + 1 } 699 } 700 } 701 } 702 i = j 703 adv = 0 704 } } 705 if adv == 1 { if us_isidst(buf[i] as i64) == 1 { 706 var j: i64 = i 707 var st: i64 = 0 708 while st == 0 { 709 if j >= n { st = 1 } else { 710 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 711 } 712 } 713 let L: i64 = j - i 714 let p: *u8 = ((buf as i64) + i) as *u8 715 var isd: i64 = 0 716 if us_eqlit(p, L, "func" as *u8, 4) == 1 { isd = 1 } 717 if us_eqlit(p, L, "struct" as *u8, 6) == 1 { isd = 1 } 718 if us_eqlit(p, L, "type" as *u8, 4) == 1 { isd = 1 } 719 if us_eqlit(p, L, "const" as *u8, 5) == 1 { isd = 1 } 720 if isd == 1 { 721 let de: i64 = us_declare_next(buf, n, j, tab) 722 if de >= 0 { i = de; cnt = cnt + 1; adv = 0 } 723 } 724 if adv == 1 { i = j; adv = 0 } 725 } } 726 if adv == 1 { i = i + 1 } 727 } 728 return cnt 729} 730 731// Scan ONE target's closure end to end. Returns undefined count (funcs + idents, builtins excluded), or 732// 0-1 if the target resolves to no file. flag[0] = closure_complete, flag[1] = has_main. 733// Allocates and RELEASES its own tables so a sweep over thousands of targets stays bounded -- the memfloor 734// lesson: an allocator that grows per item is a freeze waiting for a big enough input. 735func us_scan_one(name: *u8, bi: *u8, flag: *i64) -> i64 { 736 let q: *u8 = sys_mmap(US_MAXF * US_NAMELEN) 737 let defs: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 738 let calls: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 739 let refs: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 740 let path: *u8 = sys_mmap(US_PATHBUF) 741 let lb: *i64 = sys_mmap(16) as *i64 742 flag[0] = 1 743 flag[1] = 0 744 var nl: i64 = 0 745 while name[nl] != (0 as u8) { nl = nl + 1 } 746 var qcnt: i64 = us_qpush(q, 0, name, nl) 747 var head: i64 = 0 748 var rc: i64 = 0 749 let qdir: *i64 = sys_mmap(US_MAXF * 8) as *i64 750 var qd: i64 = 0 751 while qd < US_MAXF { qdir[qd] = 0 - 1; qd = qd + 1 } 752 while head < qcnt { 753 let nm: *u8 = us_slot(q, head) 754 let did: i64 = us_resolve_dir(path, nm, qdir[head]) 755 if did < 0 { 756 if head == 0 { rc = 0 - 1 } 757 flag[0] = 0 758 } else { 759 lb[0] = 0 760 let buf: *u8 = sys_read_file(path, lb) 761 if lb[0] <= 0 { flag[0] = 0 } else { 762 let before: i64 = qcnt 763 qcnt = us_scan(buf, lb[0], defs, calls, refs, q, qcnt) 764 // Every entry pushed while scanning THIS file was imported BY this file, so its binding 765 // preference is the directory this file actually resolved to -- not the root's. 766 var kid: i64 = before 767 while kid < qcnt { qdir[kid] = did; kid = kid + 1 } 768 if head == 0 { flag[1] = us_has(defs, "main" as *u8, 4) } 769 sys_munmap(buf, lb[0]) 770 } 771 } 772 head = head + 1 773 if qcnt >= US_MAXF { flag[0] = 0 } 774 } 775 sys_munmap(qdir, US_MAXF * 8) 776 var undef: i64 = 0 777 if rc == 0 { 778 var s: i64 = 0 779 while s < US_SLOTS { 780 let p: *u8 = us_slot(calls, s) 781 if p[0] != (0 as u8) { 782 var L: i64 = 0 783 while p[L] != (0 as u8) { L = L + 1 } 784 if us_has(defs, p, L) == 0 { if us_has(bi, p, L) == 0 { if us_is_dunder(p) == 0 { undef = undef + 1 } } } 785 } 786 let r: *u8 = us_slot(refs, s) 787 if r[0] != (0 as u8) { 788 var L2: i64 = 0 789 while r[L2] != (0 as u8) { L2 = L2 + 1 } 790 if us_has(defs, r, L2) == 0 { if us_has(bi, r, L2) == 0 { if us_is_dunder(r) == 0 { undef = undef + 1 } } } 791 } 792 s = s + 1 793 } 794 } 795 sys_munmap(q, US_MAXF * US_NAMELEN) 796 sys_munmap(defs, US_SLOTS * US_NAMELEN) 797 sys_munmap(calls, US_SLOTS * US_NAMELEN) 798 sys_munmap(refs, US_SLOTS * US_NAMELEN) 799 if rc < 0 { return 0 - 1 } 800 return undef 801} 802 803// PAGED sweep over a directory. off/lim make this a bounded operation instead of one long run that a 804// 12s deadline would truncate silently -- and a truncated sweep reporting zero is exactly the PARTIAL lie 805// this organ exists to prevent. Prints ONLY targets with findings, plus a tail line naming what was 806// covered so the caller can page the rest and can never mistake a page for the whole corpus. 807func us_sweep(dir: *u8, off0: i64, lim: i64, bi: *u8) -> i64 { 808 let dfd: i64 = sys_openat_rd(dir) 809 if dfd < 0 { return 0 } 810 let dbuf: *u8 = sys_mmap(US_DIRBUF) 811 let base: *u8 = sys_mmap(US_PATHBUF) 812 let flag: *i64 = sys_mmap(32) as *i64 813 var idx: i64 = 0 814 var done: i64 = 0 815 var hits: i64 = 0 816 var go: i64 = 1 817 while go == 1 { 818 let nb: i64 = sys_getdents64(dfd, dbuf, US_DIRBUF) 819 if nb <= 0 { go = 0 } else { 820 var off: i64 = 0 821 while off < nb { 822 let rec: *u8 = (dbuf as i64 + off) as *u8 823 let rl: i64 = dirent_reclen(rec) 824 if rl <= 0 { off = nb } else { 825 let nm: *u8 = dirent_name(rec) 826 var ln: i64 = 0 827 while nm[ln] != (0 as u8) { ln = ln + 1 } 828 var isnx: i64 = 0 829 if ln > 3 { 830 if nm[ln - 3] == (46 as u8) { 831 if nm[ln - 2] == (110 as u8) { 832 if nm[ln - 1] == (120 as u8) { isnx = 1 } } } 833 } 834 if isnx == 1 { 835 if idx >= off0 { if done < lim { 836 var b: i64 = 0 837 while b < ln - 3 { base[b] = nm[b]; b = b + 1 } 838 base[ln - 3] = 0 as u8 839 let u: i64 = us_scan_one(base, bi, flag) 840 if u > 0 { 841 us_puts("TARGET " as *u8) 842 us_puts(base) 843 us_puts(" undefined=" as *u8) 844 us_puti(u) 845 us_puts(" has_main=" as *u8) 846 us_puti(flag[1]) 847 if flag[0] == 0 { us_puts(" PARTIAL" as *u8) } 848 us_puts("\n" as *u8) 849 hits = hits + 1 850 } 851 done = done + 1 852 } } 853 idx = idx + 1 854 } 855 off = off + rl 856 } 857 } 858 } 859 } 860 sys_close(dfd) 861 us_puts("sweep_dir_total_nx=" as *u8) 862 us_puti(idx) 863 us_puts(" scanned=" as *u8) 864 us_puti(done) 865 us_puts(" offset=" as *u8) 866 us_puti(off0) 867 us_puts(" with_findings=" as *u8) 868 us_puti(hits) 869 us_puts("\n" as *u8) 870 return hits 871} 872 873func us_join(out: *u8, dir: *u8, name: *u8) { 874 var w: i64 = 0 875 var i: i64 = 0 876 while dir[i] != (0 as u8) { out[w] = dir[i]; w = w + 1; i = i + 1 } 877 out[w] = 47 as u8 878 w = w + 1 879 i = 0 880 while name[i] != (0 as u8) { out[w] = name[i]; w = w + 1; i = i + 1 } 881 out[w] = 0 as u8 882} 883 884// THE DEDUPE DISCRIMINATOR. For an artifact X.nx.<suffix> the survivor is X.nx. The artifact is a COPY if 885// the survivor declares every symbol it does; it is a COLLISION -- a DELETED CAPABILITY -- if it declares 886// symbols the survivor lacks. Sampling this population found one real deletion by luck and produced two 887// false positives, which is exactly why it has to be measured rather than eyeballed. 888// A nonzero `lost` is a CANDIDATE, not a verdict: confirm by checking whether anything still imports the 889// survivor's name. Nothing here deletes, restores, or rewrites anything -- it only reports. 890func us_dupescan(dir: *u8) -> i64 { 891 let dfd: i64 = sys_openat_rd(dir) 892 if dfd < 0 { return 0 } 893 let dbuf: *u8 = sys_mmap(US_DIRBUF) 894 let apath: *u8 = sys_mmap(US_PATHBUF) 895 let spath: *u8 = sys_mmap(US_PATHBUF) 896 let base: *u8 = sys_mmap(US_PATHBUF) 897 let lb: *i64 = sys_mmap(16) as *i64 898 var seen: i64 = 0 899 var go: i64 = 1 900 while go == 1 { 901 let nb: i64 = sys_getdents64(dfd, dbuf, US_DIRBUF) 902 if nb <= 0 { go = 0 } else { 903 var off: i64 = 0 904 while off < nb { 905 let rec: *u8 = (dbuf as i64 + off) as *u8 906 let rl: i64 = dirent_reclen(rec) 907 if rl <= 0 { off = nb } else { 908 let nm: *u8 = dirent_name(rec) 909 var ln: i64 = 0 910 while nm[ln] != (0 as u8) { ln = ln + 1 } 911 var cut: i64 = 0 - 1 912 var k: i64 = 0 913 while k + 3 < ln { 914 if nm[k] == (46 as u8) { 915 if nm[k + 1] == (110 as u8) { 916 if nm[k + 2] == (120 as u8) { 917 if nm[k + 3] == (46 as u8) { cut = k + 3 } } } } 918 k = k + 1 919 } 920 if cut > 0 { 921 seen = seen + 1 922 var b: i64 = 0 923 while b < cut { base[b] = nm[b]; b = b + 1 } 924 base[cut] = 0 as u8 925 us_join(apath, dir, nm) 926 us_join(spath, US_RTDIR, base) 927 var sfound: i64 = us_exists(spath) 928 if sfound == 0 { 929 us_join(spath, US_HDDIR, base) 930 sfound = us_exists(spath) 931 } 932 us_puts("ARTIFACT " as *u8) 933 us_puts(nm) 934 if sfound == 0 { 935 us_puts(" survivor=NONE -- nothing reclaimed the name\n" as *u8) 936 } else { 937 let ta: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 938 let tb: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 939 lb[0] = 0 940 let ab: *u8 = sys_read_file(apath, lb) 941 let an: i64 = lb[0] 942 var adecl: i64 = 0 943 if an > 0 { adecl = us_collect_decls(ab, an, ta) } 944 lb[0] = 0 945 let sb: *u8 = sys_read_file(spath, lb) 946 let sn: i64 = lb[0] 947 if sn > 0 { us_collect_decls(sb, sn, tb) } 948 var lost: i64 = 0 949 var s: i64 = 0 950 while s < US_SLOTS { 951 let p: *u8 = us_slot(ta, s) 952 if p[0] != (0 as u8) { 953 var L: i64 = 0 954 while p[L] != (0 as u8) { L = L + 1 } 955 if us_has(tb, p, L) == 0 { lost = lost + 1 } 956 } 957 s = s + 1 958 } 959 us_puts(" decls=" as *u8) 960 us_puti(adecl) 961 us_puts(" lost=" as *u8) 962 us_puti(lost) 963 if lost > 0 { 964 us_puts(" CANDIDATE-COLLISION -- survivor lacks symbols this file declared\n" as *u8) 965 } else { 966 us_puts(" COPY -- survivor declares everything\n" as *u8) 967 } 968 if an > 0 { sys_munmap(ab, an) } 969 if sn > 0 { sys_munmap(sb, sn) } 970 sys_munmap(ta, US_SLOTS * US_NAMELEN) 971 sys_munmap(tb, US_SLOTS * US_NAMELEN) 972 } 973 } 974 off = off + rl 975 } 976 } 977 } 978 } 979 sys_close(dfd) 980 return seen 981} 982 983// =================================================================================================== 984// CALL-SITE ARITY, ON THE IMPORT CLOSURE. 985// 986// WHY IT LIVES HERE AND NOT IN A SCANNER OF ITS OWN. A directory-scoped arity checker 987// (nx_aritycheck_gate) was built first and produced FOUR successive classes of false positive on the 988// real corpus, each one discovered only by hand-verifying a finding before reporting it: 989// 1. COMMENTS -- definitions were column-0 anchored but call sites were not, so a call written 990// inside a `//` line counted. aa_chain is defined once with 4 params and called 991// with 4; the "2-arg call" was a comment. 992// 2. SCOPE -- definitions were pooled per DIRECTORY, so every organ's own `main` collided. 993// 3. STRING LITERALS -- commas inside a string counted as argument separators, so 994// as_append(out, o, "system-ui,-apple-system,Segoe UI,sans-serif") read as 6 args 995// against a 3-param definition. 996// 4. CROSS-PROGRAM NAME COLLISION -- and this one is NOT patchable. nx_authorgen_gate.nx calls 997// ag_build() with ZERO arguments and is CORRECT: it imports runtime/nx_authorgen.nx 998// which declares `func ag_build() -> i64`. The directory scanner compared it against 999// an unrelated `func ag_build(parent, value, kind)` in nx_analyst_gate.nx -- a file 1000// nx_authorgen_gate.nx does not import and never links against. 1001// 1002// THE LAW THAT MOVED THE CODE: THE ONLY SOUND SCOPE FOR A CALL-SITE CHECK IS THE IMPORT CLOSURE. 1003// A directory is not a scope. Counting definitions to disambiguate does not rescue it -- ag_build is 1004// declared exactly ONCE inside _hdl_build/, so a uniqueness rule still judged it, and still got it wrong, 1005// because the definition that actually binds lives OUTSIDE the directory. Every further heuristic would 1006// have bought one more class and lost the next. This organ already walks the closure, already skips 1007// comments, strings, numeric literals and directives AS UNITS, and already refuses to certify a PARTIAL 1008// closure. Arity belongs where those three properties already hold, and nowhere else. 1009// 1010// WHY THE COMPILER DOES NOT CATCH IT: nx_cc FAILS OPEN on argument count -- a wrong-arity call compiles 1011// green and silent. Same fail-open family as the duplicate definition it now rejects (1785447657) and the 1012// undefined identifier this organ already censuses (seq1012). 1013// 1014// KNOWN FALSE-POSITIVE CLASS, STATED SO NO CALLER HAS TO GUESS: a call THROUGH A FUNCTION-POINTER 1015// VARIABLE that happens to share a name with a declared function is compared against that declaration. 1016// This is the same bounded class the undefined-symbol pass already documents, and it is bounded the same 1017// way -- by the positive control below. 1018// =================================================================================================== 1019 1020const US_ARUNK: i64 = 0 - 2 1021const US_ARAMB: i64 = 0 - 3 1022 1023func us_alen(s: *u8) -> i64 { 1024 var n: i64 = 0 1025 while s[n] != (0 as u8) { n = n + 1 } 1026 return n 1027} 1028 1029// Slot index of an EXISTING name, or 0-1. us_has answers yes/no; arity needs the identity of the slot so 1030// the parameter count can never be read for a different name than the one that was matched. 1031func us_index(tab: *u8, s: *u8, n: i64) -> i64 { 1032 if n <= 0 { return 0 - 1 } 1033 if n >= US_NAMELEN { return 0 - 1 } 1034 let h: i64 = us_hash(s, n) 1035 var idx: i64 = h - (h / US_SLOTS) * US_SLOTS 1036 var probes: i64 = 0 1037 while probes < US_SLOTS { 1038 let p: *u8 = us_slot(tab, idx) 1039 if p[0] == (0 as u8) { return 0 - 1 } 1040 var eq: i64 = 1 1041 if p[n] != (0 as u8) { eq = 0 } 1042 var k: i64 = 0 1043 while k < n { 1044 if p[k] != s[k] { eq = 0; k = n } else { k = k + 1 } 1045 } 1046 if eq == 1 { return idx } 1047 idx = idx + 1 1048 if idx >= US_SLOTS { idx = 0 } 1049 probes = probes + 1 1050 } 1051 return 0 - 1 1052} 1053 1054// Count top-level comma-separated items between the parens beginning at `op` (which must index '('). 1055// ONE helper serves BOTH a declaration's parameter list and a call's argument list -- they are the same 1056// shape, and this file already uses one helper for func/struct/type on the same reasoning (rule 15). 1057// Nested parens, string literals and // comments are skipped as units, so a comma inside any of them is 1058// never counted; a fn-pointer parameter type like `f: func(i64, i64) -> i64` therefore reads as ONE item. 1059// Returns 0 for "()", the item count otherwise, or 0-1 if the list never closes (caller must not judge). 1060func us_commas(buf: *u8, n: i64, op: i64) -> i64 { 1061 var i: i64 = op + 1 1062 var depth: i64 = 1 1063 var commas: i64 = 0 1064 var any: i64 = 0 1065 while i < n { 1066 let c: i64 = buf[i] as i64 1067 var adv: i64 = 1 1068 1069 if c == 47 { 1070 if i + 1 < n { 1071 if buf[i + 1] == (47 as u8) { 1072 var j: i64 = i + 2 1073 var st: i64 = 0 1074 while st == 0 { 1075 if j >= n { st = 1 } else { 1076 if buf[j] == (10 as u8) { st = 1 } else { j = j + 1 } 1077 } 1078 } 1079 i = j 1080 adv = 0 1081 } 1082 } 1083 } 1084 1085 if adv == 1 { 1086 if c == 34 { 1087 any = 1 1088 var j: i64 = i + 1 1089 var st: i64 = 0 1090 while st == 0 { 1091 if j >= n { st = 1 } else { 1092 if buf[j] == (92 as u8) { j = j + 2 } else { 1093 if buf[j] == (34 as u8) { j = j + 1; st = 1 } else { j = j + 1 } 1094 } 1095 } 1096 } 1097 i = j 1098 adv = 0 1099 } } 1100 1101 if adv == 1 { 1102 if c == 40 { depth = depth + 1; any = 1 } 1103 if c == 41 { 1104 depth = depth - 1 1105 if depth == 0 { 1106 if any == 0 { return 0 } 1107 return commas + 1 1108 } 1109 any = 1 1110 } 1111 if depth == 1 { if c == 44 { commas = commas + 1 } } 1112 if c != 32 { if c != 10 { if c != 9 { if c != 13 { any = 1 } } } } 1113 i = i + 1 1114 } 1115 } 1116 return 0 - 1 1117} 1118 1119// Record the parameter count of every `func NAME(...)` in this file, keyed by the SAME slot index the 1120// definition table uses -- so a later lookup can never read the arity of a different name than the one 1121// it matched. 1122// A name that appears TWICE in one closure with DIFFERENT arities is marked AMBIGUOUS and never judged 1123// again. Guessing which declaration a call meant would manufacture exactly the false positive this 1124// rewrite exists to eliminate; and two definitions of one name is a separate defect that nx_cc now 1125// rejects on its own. 1126func us_collect_arity(buf: *u8, n: i64, defs: *u8, defar: *i64) -> i64 { 1127 var cnt: i64 = 0 1128 var i: i64 = 0 1129 while i < n { 1130 var adv: i64 = 1 1131 1132 if buf[i] == (47 as u8) { 1133 if i + 1 < n { 1134 if buf[i + 1] == (47 as u8) { 1135 var j: i64 = i + 2 1136 var st: i64 = 0 1137 while st == 0 { 1138 if j >= n { st = 1 } else { 1139 if buf[j] == (10 as u8) { st = 1 } else { j = j + 1 } 1140 } 1141 } 1142 i = j 1143 adv = 0 1144 } 1145 } 1146 } 1147 1148 if adv == 1 { 1149 if buf[i] == (34 as u8) { 1150 var j: i64 = i + 1 1151 var st: i64 = 0 1152 while st == 0 { 1153 if j >= n { st = 1 } else { 1154 if buf[j] == (92 as u8) { j = j + 2 } else { 1155 if buf[j] == (34 as u8) { j = j + 1; st = 1 } else { j = j + 1 } 1156 } 1157 } 1158 } 1159 i = j 1160 adv = 0 1161 } } 1162 1163 if adv == 1 { 1164 if us_isidst(buf[i] as i64) == 1 { 1165 var j: i64 = i 1166 var st: i64 = 0 1167 while st == 0 { 1168 if j >= n { st = 1 } else { 1169 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 1170 } 1171 } 1172 let L: i64 = j - i 1173 let p: *u8 = ((buf as i64) + i) as *u8 1174 var handled: i64 = 0 1175 if us_eqlit(p, L, "func" as *u8, 4) == 1 { 1176 var k: i64 = j 1177 var s2: i64 = 0 1178 while s2 == 0 { 1179 if k >= n { s2 = 1 } else { 1180 if us_isidst(buf[k] as i64) == 1 { s2 = 1 } else { k = k + 1 } 1181 } 1182 } 1183 if k < n { 1184 var e: i64 = k 1185 var s3: i64 = 0 1186 while s3 == 0 { 1187 if e >= n { s3 = 1 } else { 1188 if us_isidc(buf[e] as i64) == 1 { e = e + 1 } else { s3 = 1 } 1189 } 1190 } 1191 let np: *u8 = ((buf as i64) + k) as *u8 1192 let NL: i64 = e - k 1193 if e < n { 1194 if buf[e] == (40 as u8) { 1195 let a: i64 = us_commas(buf, n, e) 1196 if a >= 0 { 1197 let slot: i64 = us_put(defs, np, NL) 1198 if slot >= 0 { 1199 if defar[slot] == US_ARUNK { 1200 defar[slot] = a 1201 cnt = cnt + 1 1202 } else { 1203 if defar[slot] != a { defar[slot] = US_ARAMB } 1204 } 1205 } 1206 } 1207 } 1208 } 1209 i = e 1210 handled = 1 1211 } 1212 } 1213 if handled == 0 { i = j } 1214 adv = 0 1215 } } 1216 1217 if adv == 1 { i = i + 1 } 1218 } 1219 return cnt 1220} 1221 1222// Compare every call site in this file against the arity recorded from the closure. A name with no 1223// recorded arity (US_ARUNK) is NOT a finding here -- that is the undefined-symbol pass's job, and 1224// double-reporting it would inflate one defect into two. AMBIGUOUS is likewise never judged. 1225func us_check_arity(buf: *u8, n: i64, defs: *u8, defar: *i64, fname: *u8) -> i64 { 1226 var bad: i64 = 0 1227 var i: i64 = 0 1228 while i < n { 1229 var adv: i64 = 1 1230 1231 if buf[i] == (47 as u8) { 1232 if i + 1 < n { 1233 if buf[i + 1] == (47 as u8) { 1234 var j: i64 = i + 2 1235 var st: i64 = 0 1236 while st == 0 { 1237 if j >= n { st = 1 } else { 1238 if buf[j] == (10 as u8) { st = 1 } else { j = j + 1 } 1239 } 1240 } 1241 i = j 1242 adv = 0 1243 } 1244 } 1245 } 1246 1247 if adv == 1 { 1248 if buf[i] == (34 as u8) { 1249 var j: i64 = i + 1 1250 var st: i64 = 0 1251 while st == 0 { 1252 if j >= n { st = 1 } else { 1253 if buf[j] == (92 as u8) { j = j + 2 } else { 1254 if buf[j] == (34 as u8) { j = j + 1; st = 1 } else { j = j + 1 } 1255 } 1256 } 1257 } 1258 i = j 1259 adv = 0 1260 } } 1261 1262 if adv == 1 { 1263 if us_isidst(buf[i] as i64) == 1 { 1264 var j: i64 = i 1265 var st: i64 = 0 1266 while st == 0 { 1267 if j >= n { st = 1 } else { 1268 if us_isidc(buf[j] as i64) == 1 { j = j + 1 } else { st = 1 } 1269 } 1270 } 1271 let L: i64 = j - i 1272 let p: *u8 = ((buf as i64) + i) as *u8 1273 if us_iskw(p, L) == 0 { 1274 if j < n { 1275 if buf[j] == (40 as u8) { 1276 let slot: i64 = us_index(defs, p, L) 1277 if slot >= 0 { 1278 let want: i64 = defar[slot] 1279 if want >= 0 { 1280 let got: i64 = us_commas(buf, n, j) 1281 if got >= 0 { 1282 if got != want { 1283 us_puts(" ARITY-MISMATCH " as *u8) 1284 us_puts(fname) 1285 us_puts(": " as *u8) 1286 us_putn(p, L) 1287 us_puts(" called with " as *u8) 1288 us_puti(got) 1289 us_puts(" arg(s), declared with " as *u8) 1290 us_puti(want) 1291 us_puts("\n" as *u8) 1292 bad = bad + 1 1293 } 1294 } 1295 } 1296 } 1297 } 1298 } 1299 } 1300 i = j 1301 adv = 0 1302 } } 1303 1304 if adv == 1 { i = i + 1 } 1305 } 1306 return bad 1307} 1308 1309// PROVE THE COUNTER CAN FAIL BEFORE TRUSTING IT ON THE CORPUS. Every assertion here is a case that a 1310// previous implementation of this axis got WRONG on real source -- nesting, strings, comments, empty 1311// lists -- plus the unterminated case, which must refuse rather than answer. A tooth that only tests the 1312// cases the author already handled has tested nothing. 1313func us_arity_selftest() -> i64 { 1314 var fail: i64 = 0 1315 let a: *u8 = "(a, b, c)" as *u8 1316 if us_commas(a, us_alen(a), 0) != 3 { us_puts(" T1 FAIL plain\n" as *u8); fail = fail + 1 } 1317 let b: *u8 = "()" as *u8 1318 if us_commas(b, us_alen(b), 0) != 0 { us_puts(" T2 FAIL empty\n" as *u8); fail = fail + 1 } 1319 let c: *u8 = "(x)" as *u8 1320 if us_commas(c, us_alen(c), 0) != 1 { us_puts(" T3 FAIL single\n" as *u8); fail = fail + 1 } 1321 let d: *u8 = "(f(x, y), z)" as *u8 1322 if us_commas(d, us_alen(d), 0) != 2 { us_puts(" T4 FAIL nested\n" as *u8); fail = fail + 1 } 1323 // T5 STRING LITERAL -- built BYTE-WISE, not written as an escaped literal. An escaped literal would 1324 // also be testing whatever transport carried this source onto the NAS; bytes test only the counter. 1325 // Spells: ("a,b",z) -- two arguments, two commas, one of them inside the string. 1326 let e: *u8 = sys_mmap(32) 1327 e[0] = 40 as u8 1328 e[1] = 34 as u8 1329 e[2] = 97 as u8 1330 e[3] = 44 as u8 1331 e[4] = 98 as u8 1332 e[5] = 34 as u8 1333 e[6] = 44 as u8 1334 e[7] = 122 as u8 1335 e[8] = 41 as u8 1336 e[9] = 0 as u8 1337 if us_commas(e, 9, 0) != 2 { us_puts(" T5 FAIL string-literal comma counted\n" as *u8); fail = fail + 1 } 1338 let f: *u8 = "( )" as *u8 1339 if us_commas(f, us_alen(f), 0) != 0 { us_puts(" T6 FAIL whitespace-only\n" as *u8); fail = fail + 1 } 1340 let g: *u8 = "(a, b" as *u8 1341 if us_commas(g, us_alen(g), 0) != 0 - 1 { us_puts(" T7 FAIL unterminated must refuse\n" as *u8); fail = fail + 1 } 1342 let h: *u8 = "(g(1, 2), h(3, 4), 5)" as *u8 1343 if us_commas(h, us_alen(h), 0) != 3 { us_puts(" T8 FAIL two nested\n" as *u8); fail = fail + 1 } 1344 // T9/T10 NON-VACUITY. The counter passing its own arithmetic teeth does NOT prove the REPORTING PATH 1345 // ever fires -- and 14 corpus targets in a row returned zero, which a broken checker and a clean 1346 // corpus produce identically. So drive collect AND check over a synthetic closure whose answer is 1347 // known, and REQUIRE the mismatch to be seen. Without this, "0 mismatches" is an unfalsifiable claim. 1348 let td: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1349 let ta: *i64 = sys_mmap(US_SLOTS * 8) as *i64 1350 var zz: i64 = 0 1351 while zz < US_SLOTS { ta[zz] = US_ARUNK; zz = zz + 1 } 1352 let bad9: *u8 = "func zq_a(p: i64, q: i64) -> i64 { return p }\nfunc zq_b() -> i64 { return zq_a(1) }\n" as *u8 1353 let n9: i64 = us_alen(bad9) 1354 us_collect_arity(bad9, n9, td, ta) 1355 us_puts(" (T9 expects exactly ONE synthetic mismatch on the next line -- it IS the tooth)\n" as *u8) 1356 let m9: i64 = us_check_arity(bad9, n9, td, ta, "T9-SYNTHETIC" as *u8) 1357 if m9 != 1 { us_puts(" T9 FAIL -- the reporting path never fired; every corpus zero is VACUOUS\n" as *u8); fail = fail + 1 } 1358 let ok10: *u8 = "func zr_a(p: i64, q: i64) -> i64 { return p }\nfunc zr_b() -> i64 { return zr_a(1, 2) }\n" as *u8 1359 let n10: i64 = us_alen(ok10) 1360 us_collect_arity(ok10, n10, td, ta) 1361 let m10: i64 = us_check_arity(ok10, n10, td, ta, "T10-SYNTHETIC" as *u8) 1362 if m10 != 0 { us_puts(" T10 FAIL -- a CORRECT call was reported as a mismatch\n" as *u8); fail = fail + 1 } 1363 sys_munmap(td, US_SLOTS * US_NAMELEN) 1364 sys_munmap(ta, US_SLOTS * 8) 1365 1366 // NEGATIVE CONTROL: a counter that always returned 3 would pass T1 and T8 and nothing else. T2/T3/T7 1367 // are the ones that can only pass if it really counts, and T7 only if it really refuses. 1368 if fail == 0 { us_puts(" arity self-test OK (nesting + strings + empty + unterminated-refuses)\n" as *u8) } 1369 return fail 1370} 1371 1372// Walk ONE target's closure and check every call site in it. Returns the mismatch count, or 0-1 if the 1373// target resolves to no file. flag[0] = closure_complete. 1374// THREE passes, deliberately: the definition set must be COMPLETE before any call site is judged, or a 1375// call to a function defined in a not-yet-read import would be compared against nothing (silently clean) 1376// or against a stale entry. Reading each closure file twice more is cheap; judging early is not. 1377// RESOLVE THE WAY THE COMPILER BINDS, NOT IN A FIXED GLOBAL ORDER. Measured 2026-07-31: us_resolve takes 1378// only a NAME, so for a name that exists in BOTH runtime/ and _hdl_build/ it always returns the runtime/ 1379// copy. nx_m2d_engine lives in _hdl_build/ and is written against _hdl_build/nx_input_abstract.nx 1380// (ia_init with 2 params, ia_bind with 4); the fixed order handed it runtime/nx_input_abstract.nx 1381// (ia_init with 1, ia_bind with 3) and manufactured EIGHTEEN arity mismatches that are not defects. 1382// nx_cc binds the IMPORTER'S OWN DIRECTORY FIRST, so the answer it gives is the only one that matters. 1383// This preference is applied to the ARITY path only; the existing scan path is deliberately left alone 1384// (rule 19) and carries the same latent flaw -- filed separately, with this reproducer. 1385func us_resolve_pref(path: *u8, nm: *u8, pref: *u8) -> i64 { 1386 if pref[0] != (0 as u8) { 1387 us_path(path, pref, nm) 1388 if us_exists(path) == 1 { return 1 } 1389 } 1390 return us_resolve(path, nm) 1391} 1392 1393// Which directory does this target itself live in? Probed in BUILD-TARGET rank order (_hdl_build first), 1394// because that is the copy /api/build compiles, and the closure must be read against the same tree the 1395// artifact was produced from. 1396func us_rootdir(nm: *u8, path: *u8) -> *u8 { 1397 us_path(path, US_HDDIR, nm) 1398 if us_exists(path) == 1 { return US_HDDIR } 1399 us_path(path, US_RTDIR, nm) 1400 if us_exists(path) == 1 { return US_RTDIR } 1401 us_path(path, US_HUBDIR, nm) 1402 if us_exists(path) == 1 { return US_HUBDIR } 1403 us_path(path, US_WIKIDIR, nm) 1404 if us_exists(path) == 1 { return US_WIKIDIR } 1405 return "" as *u8 1406} 1407 1408func us_arity_one(name: *u8, flag: *i64) -> i64 { 1409 let q: *u8 = sys_mmap(US_MAXF * US_NAMELEN) 1410 let defs: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1411 let calls: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1412 let refs: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1413 let defar: *i64 = sys_mmap(US_SLOTS * 8) as *i64 1414 let path: *u8 = sys_mmap(US_PATHBUF) 1415 let lb: *i64 = sys_mmap(16) as *i64 1416 1417 // mmap gives ZEROED memory and zero is a LEGITIMATE arity, so the unknown state must be written 1418 // explicitly. Leaving it zeroed would silently claim every never-declared name takes no arguments. 1419 var z: i64 = 0 1420 while z < US_SLOTS { defar[z] = US_ARUNK; z = z + 1 } 1421 1422 flag[0] = 1 1423 // ONE resolution rule for the whole organ: per-importer, exactly as us_resolve_dir documents. The 1424 // root-directory approximation this path used first was closer than the fixed global order but still 1425 // wrong for a mixed closure (a runtime/ file importing runtime/hub/ importing a shadowed name). 1426 let qdir: *i64 = sys_mmap(US_MAXF * 8) as *i64 1427 var qd: i64 = 0 1428 while qd < US_MAXF { qdir[qd] = 0 - 1; qd = qd + 1 } 1429 var nl: i64 = 0 1430 while name[nl] != (0 as u8) { nl = nl + 1 } 1431 var qcnt: i64 = us_qpush(q, 0, name, nl) 1432 var head: i64 = 0 1433 var rc: i64 = 0 1434 1435 while head < qcnt { 1436 let nm: *u8 = us_slot(q, head) 1437 let did: i64 = us_resolve_dir(path, nm, qdir[head]) 1438 if did < 0 { 1439 if head == 0 { rc = 0 - 1 } 1440 flag[0] = 0 1441 } else { 1442 lb[0] = 0 1443 let buf: *u8 = sys_read_file(path, lb) 1444 if lb[0] <= 0 { flag[0] = 0 } else { 1445 let before: i64 = qcnt 1446 qcnt = us_scan(buf, lb[0], defs, calls, refs, q, qcnt) 1447 var kid: i64 = before 1448 while kid < qcnt { qdir[kid] = did; kid = kid + 1 } 1449 sys_munmap(buf, lb[0]) 1450 } 1451 } 1452 head = head + 1 1453 if qcnt >= US_MAXF { flag[0] = 0 } 1454 } 1455 1456 var decls: i64 = 0 1457 var bad: i64 = 0 1458 if rc == 0 { 1459 var h2: i64 = 0 1460 while h2 < qcnt { 1461 let nm2: *u8 = us_slot(q, h2) 1462 if us_resolve_dir(path, nm2, qdir[h2]) >= 0 { 1463 lb[0] = 0 1464 let b2: *u8 = sys_read_file(path, lb) 1465 if lb[0] > 0 { 1466 decls = decls + us_collect_arity(b2, lb[0], defs, defar) 1467 sys_munmap(b2, lb[0]) 1468 } 1469 } 1470 h2 = h2 + 1 1471 } 1472 var h3: i64 = 0 1473 while h3 < qcnt { 1474 let nm3: *u8 = us_slot(q, h3) 1475 if us_resolve_dir(path, nm3, qdir[h3]) >= 0 { 1476 lb[0] = 0 1477 let b3: *u8 = sys_read_file(path, lb) 1478 if lb[0] > 0 { 1479 bad = bad + us_check_arity(b3, lb[0], defs, defar, nm3) 1480 sys_munmap(b3, lb[0]) 1481 } 1482 } 1483 h3 = h3 + 1 1484 } 1485 } 1486 1487 flag[1] = decls 1488 flag[2] = qcnt 1489 sys_munmap(q, US_MAXF * US_NAMELEN) 1490 sys_munmap(defs, US_SLOTS * US_NAMELEN) 1491 sys_munmap(calls, US_SLOTS * US_NAMELEN) 1492 sys_munmap(refs, US_SLOTS * US_NAMELEN) 1493 sys_munmap(defar, US_SLOTS * 8) 1494 sys_munmap(qdir, US_MAXF * 8) 1495 if rc < 0 { return 0 - 1 } 1496 return bad 1497} 1498 1499 1500func main(argc: i64, argv: *i64) -> i64 { 1501 if argc < 3 { 1502 us_puts("usage: nx_undefscan scan <target> (basename WITHOUT .nx)\n" as *u8) 1503 us_puts(" Walks the import closure and prints every called-but-undefined function.\n" as *u8) 1504 us_puts(" exit 0 CLEAN+COMPLETE | 1 UNDEFINED | 3 not found | 5 PARTIAL (cannot certify)\n" as *u8) 1505 return US_EXIT_USAGE 1506 } 1507 let verb: *u8 = argv[1] as *u8 1508 var vn: i64 = 0 1509 while verb[vn] != (0 as u8) { vn = vn + 1 } 1510 if us_eqlit(verb, vn, "sweep" as *u8, 5) == 1 { 1511 if argc < 5 { 1512 us_puts("usage: nx_undefscan sweep <dir> <offset> <limit>\n" as *u8) 1513 return US_EXIT_USAGE 1514 } 1515 let sbi: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1516 us_load_builtins(sbi) 1517 var o0: i64 = 0 1518 let os: *u8 = argv[3] as *u8 1519 var oi: i64 = 0 1520 while os[oi] != (0 as u8) { o0 = o0 * 10 + ((os[oi] as i64) - 48); oi = oi + 1 } 1521 var lm: i64 = 0 1522 let ls: *u8 = argv[4] as *u8 1523 var li: i64 = 0 1524 while ls[li] != (0 as u8) { lm = lm * 10 + ((ls[li] as i64) - 48); li = li + 1 } 1525 us_sweep(argv[2] as *u8, o0, lm, sbi) 1526 return 0 1527 } 1528 1529 if us_eqlit(verb, vn, "arity" as *u8, 5) == 1 { 1530 us_puts("nx_undefscan arity -- CALL-SITE ARITY, SCOPED TO THE IMPORT CLOSURE\n" as *u8) 1531 let sf: i64 = us_arity_selftest() 1532 if sf > 0 { 1533 us_puts("REFUSED: the counter failed its own self-test -- a corpus number from it would be noise\n" as *u8) 1534 return US_EXIT_SELFTEST 1535 } 1536 let aflag: *i64 = sys_mmap(32) as *i64 1537 let bad: i64 = us_arity_one(argv[2] as *u8, aflag) 1538 if bad < 0 { 1539 us_puts("NOT-FOUND: no such target in runtime/ or _hdl_build/: " as *u8) 1540 us_puts(argv[2] as *u8) 1541 us_puts("\n" as *u8) 1542 return US_EXIT_NOTFOUND 1543 } 1544 us_puts("target=" as *u8) 1545 us_puts(argv[2] as *u8) 1546 us_puts(" closure_files=" as *u8) 1547 us_puti(aflag[2]) 1548 us_puts(" funcs_with_arity=" as *u8) 1549 us_puti(aflag[1]) 1550 us_puts(" arity_mismatches=" as *u8) 1551 us_puti(bad) 1552 us_puts("\n" as *u8) 1553 if aflag[0] == 0 { 1554 us_puts("PARTIAL: an import in the closure could not be read, so the declaration set is INCOMPLETE.\n" as *u8) 1555 us_puts(" A zero here would be a claim this scan cannot support. Fix the closure, then re-run.\n" as *u8) 1556 return US_EXIT_PARTIAL 1557 } 1558 if bad > 0 { return US_EXIT_UNDEF } 1559 return 0 1560 } 1561 1562 if us_eqlit(verb, vn, "dupescan" as *u8, 8) == 1 { 1563 let n: i64 = us_dupescan(argv[2] as *u8) 1564 us_puts("dupescan_artifacts=" as *u8) 1565 us_puti(n) 1566 us_puts("\n" as *u8) 1567 return 0 1568 } 1569 1570 let target: *u8 = argv[2] as *u8 1571 var corpus: i64 = 0 1572 if argc > 3 { 1573 let a3: *u8 = argv[3] as *u8 1574 var a3n: i64 = 0 1575 while a3[a3n] != (0 as u8) { a3n = a3n + 1 } 1576 if us_eqlit(a3, a3n, "corpus" as *u8, 6) == 1 { corpus = 1 } 1577 } 1578 1579 let q: *u8 = sys_mmap(US_MAXF * US_NAMELEN) 1580 let defs: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1581 let calls: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1582 let refs: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1583 let und: *u8 = sys_mmap(US_MAXF * US_NAMELEN) 1584 let usrc: *u8 = sys_mmap(US_MAXF * US_NAMELEN) 1585 let path: *u8 = sys_mmap(US_PATHBUF) 1586 let lenbox: *i64 = sys_mmap(16) as *i64 1587 let bi: *u8 = sys_mmap(US_SLOTS * US_NAMELEN) 1588 let nbi: i64 = us_load_builtins(bi) 1589 1590 var tn: i64 = 0 1591 while target[tn] != (0 as u8) { tn = tn + 1 } 1592 var qcnt: i64 = us_qpush(q, 0, target, tn) 1593 if qcnt == 0 { 1594 us_puts("REFUSED: target name empty or longer than the name buffer\n" as *u8) 1595 return US_EXIT_USAGE 1596 } 1597 1598 var missing: i64 = 0 1599 var overflow: i64 = 0 1600 var hasmain: i64 = 0 1601 var head: i64 = 0 1602 var files: i64 = 0 1603 let qdir: *i64 = sys_mmap(US_MAXF * 8) as *i64 1604 var qd: i64 = 0 1605 while qd < US_MAXF { qdir[qd] = 0 - 1; qd = qd + 1 } 1606 1607 while head < qcnt { 1608 let nm: *u8 = us_slot(q, head) 1609 let did: i64 = us_resolve_dir(path, nm, qdir[head]) 1610 if did < 0 { 1611 if head == 0 { 1612 us_puts("NOT-FOUND: no such target in runtime/ or _hdl_build/: " as *u8) 1613 us_puts(nm) 1614 us_puts("\n" as *u8) 1615 return US_EXIT_NOTFOUND 1616 } 1617 us_puts("MISSING-IMPORT " as *u8) 1618 us_puts(nm) 1619 us_puts("\n" as *u8) 1620 missing = missing + 1 1621 } else { 1622 lenbox[0] = 0 1623 let buf: *u8 = sys_read_file(path, lenbox) 1624 if lenbox[0] <= 0 { 1625 us_puts("UNREADABLE " as *u8) 1626 us_puts(nm) 1627 us_puts("\n" as *u8) 1628 missing = missing + 1 1629 } else { 1630 let before: i64 = qcnt 1631 qcnt = us_scan(buf, lenbox[0], defs, calls, refs, q, qcnt) 1632 // Children inherit THIS file's directory as their binding preference (see us_resolve_dir). 1633 var kid: i64 = before 1634 while kid < qcnt { qdir[kid] = did; kid = kid + 1 } 1635 files = files + 1 1636 // Checked right after the ROOT file and before any import is scanned, so defs holds only 1637 // this file's declarations and the answer is exact. CLEAN means every symbol resolves -- 1638 // NOT that the target builds. A no-main LIBRARY is legitimately CLEAN and still fails to 1639 // compile standalone with 'UNDEFINED label: main'. Measured on nx_denoise_spectral, where 1640 // reading CLEAN as should-build would have sent the next reader hunting a phantom defect. 1641 if head == 0 { hasmain = us_has(defs, "main" as *u8, 4) } 1642 } 1643 } 1644 head = head + 1 1645 if qcnt >= US_MAXF { overflow = 1 } 1646 } 1647 1648 var undef: i64 = 0 1649 var bicount: i64 = 0 1650 var undcnt: i64 = 0 1651 var intrin: i64 = 0 1652 var s: i64 = 0 1653 while s < US_SLOTS { 1654 let p: *u8 = us_slot(calls, s) 1655 if p[0] != (0 as u8) { 1656 var L: i64 = 0 1657 while p[L] != (0 as u8) { L = L + 1 } 1658 if us_has(defs, p, L) == 0 { 1659 if us_has(bi, p, L) == 1 { bicount = bicount + 1 } else { 1660 if us_is_dunder(p) == 1 { intrin = intrin + 1 } else { 1661 undcnt = us_qpush(und, undcnt, p, L) 1662 undef = undef + 1 1663 } } 1664 } 1665 } 1666 s = s + 1 1667 } 1668 1669 // SECOND CLASS: an identifier that is never called, never declared, and provided by nothing. This is 1670 // the class that made an earlier build of this organ report CLEAN on nx_fnet_mix while nx_cc failed 1671 // with UNRESOLVED identifier NX_FFT_Q -- a false clean, the exact lie this instrument exists to stop. 1672 var undefid: i64 = 0 1673 var r: i64 = 0 1674 while r < US_SLOTS { 1675 let p: *u8 = us_slot(refs, r) 1676 if p[0] != (0 as u8) { 1677 var L: i64 = 0 1678 while p[L] != (0 as u8) { L = L + 1 } 1679 if us_has(defs, p, L) == 0 { 1680 if us_has(bi, p, L) == 1 { bicount = bicount + 1 } else { 1681 if us_is_dunder(p) == 1 { intrin = intrin + 1 } else { 1682 undcnt = us_qpush(und, undcnt, p, L) 1683 undefid = undefid + 1 1684 } } 1685 } 1686 } 1687 r = r + 1 1688 } 1689 1690 // CLASSIFY, then print. A bare list of names invites the exact misreading that produced a wrong sev-8 1691 // filing today: 6 of 7 names were already importable and only 1 was unwritten. 1692 var cfiles: i64 = 0 1693 if corpus == 1 { if undcnt > 0 { 1694 cfiles = us_corpus_dir(US_RTDIR, und, undcnt, usrc) 1695 cfiles = cfiles + us_corpus_dir(US_HDDIR, und, undcnt, usrc) 1696 cfiles = cfiles + us_corpus_dir(US_HUBDIR, und, undcnt, usrc) 1697 cfiles = cfiles + us_corpus_dir(US_WIKIDIR, und, undcnt, usrc) 1698 } } 1699 var wiring: i64 = 0 1700 var unwritten: i64 = 0 1701 var u: i64 = 0 1702 while u < undcnt { 1703 let p: *u8 = us_slot(und, u) 1704 var L: i64 = 0 1705 while p[L] != (0 as u8) { L = L + 1 } 1706 if us_has(calls, p, L) == 1 { us_puts("UNDEFINED " as *u8) } else { us_puts("UNDEFINED-IDENT " as *u8) } 1707 us_putn(p, L) 1708 if corpus == 1 { 1709 let sp: *u8 = us_slot(usrc, u) 1710 if sp[0] == (0 as u8) { 1711 us_puts(" NOWHERE -- unwritten code\n" as *u8) 1712 unwritten = unwritten + 1 1713 } else { 1714 us_puts(" ELSEWHERE-IN " as *u8) 1715 us_puts(sp) 1716 us_puts(" -- MISSING IMPORT, not missing code\n" as *u8) 1717 wiring = wiring + 1 1718 } 1719 } else { us_puts("\n" as *u8) } 1720 u = u + 1 1721 } 1722 if corpus == 1 { 1723 us_puts("corpus_files=" as *u8) 1724 us_puti(cfiles) 1725 us_puts(" wiring_fixable=" as *u8) 1726 us_puti(wiring) 1727 us_puts(" unwritten=" as *u8) 1728 us_puti(unwritten) 1729 us_puts("\n" as *u8) 1730 } 1731 if undef + undefid > undcnt { 1732 us_puts("WARN undefined-list overflowed US_MAXF; listing is TRUNCATED\n" as *u8) 1733 } 1734 1735 us_puts("target=" as *u8) 1736 us_puts(target) 1737 us_puts(" closure_files=" as *u8) 1738 us_puti(files) 1739 us_puts(" missing_imports=" as *u8) 1740 us_puti(missing) 1741 us_puts(" undefined_funcs=" as *u8) 1742 us_puti(undef) 1743 us_puts(" undefined_idents=" as *u8) 1744 us_puti(undefid) 1745 us_puts(" intrinsic_presumed=" as *u8) 1746 us_puti(intrin) 1747 us_puts(" has_main=" as *u8) 1748 us_puti(hasmain) 1749 us_puts(" builtin_excluded=" as *u8) 1750 us_puti(bicount) 1751 us_puts(" builtins_loaded=" as *u8) 1752 us_puti(nbi) 1753 us_puts("\n" as *u8) 1754 1755 // THE TOOTH: a zero from an incomplete closure is not a clean bill. Report PARTIAL and exit nonzero 1756 // even when undefined==0, because the definitions that would have resolved these calls may live in 1757 // exactly the file that could not be read. 1758 if missing > 0 { 1759 us_puts("VERDICT=PARTIAL closure_complete=0 -- count is a FLOOR, not an answer\n" as *u8) 1760 if undef + undefid > 0 { return US_EXIT_UNDEF } 1761 return US_EXIT_PARTIAL 1762 } 1763 if overflow == 1 { 1764 us_puts("VERDICT=PARTIAL closure_complete=0 -- queue hit US_MAXF, closure truncated\n" as *u8) 1765 if undef + undefid > 0 { return US_EXIT_UNDEF } 1766 return US_EXIT_PARTIAL 1767 } 1768 if undef + undefid > 0 { 1769 us_puts("VERDICT=UNDEFINED-SYMBOLS closure_complete=1\n" as *u8) 1770 return US_EXIT_UNDEF 1771 } 1772 us_puts("VERDICT=CLEAN closure_complete=1\n" as *u8) 1773 return 0 1774}