code wiki / (root) / nx_normdiff.nx

nx_normdiff.nx source

↩ module page · 1551 lines · 72638 B

1// nx_normdiff.nx -- IS THIS DIFFERENCE MADE OF CODE, OR OF COSMETICS? The escalation ruler. 2// 3// WHY IT EXISTS. nx_srcdiff answers "is a direction merge safe?" on the RAW line multiset, and that 4// strictness is correct as a GUARD -- it is what caught the escaped nx_media_extract mutant that a 5// symbol-set ruler waved through. But as a CENSUS it overstates the fork: it reports BIDIRECTIONAL 6// for two files that differ only because a NAS cron lifted `4096` into `FOO_MAGIC_4096`, or because 7// one side carries a trailing `// why` comment the other lacks. Measured on the standing residual: 8// the raw ruler could decide 3.6% of 3,073 forked files; normalising first decides 8.6%. The other 9// 91% is REAL CODE -- surplus shapes censused as control-flow 1408, bindings 1042, func-decls 278 -- 10// which is the finding that matters most: NO FURTHER NORMALISER WILL CRACK IT, so stop building them. 11// => ★A RULER TUNED TO BE A SAFE GUARD IS THE WRONG RULER FOR A CENSUS. Report BOTH verdicts and 12// let the caller pick: the guard decides whether to ACT, the census decides what to LOOK AT. 13// 14// WHAT IT NORMALISES, and nothing else: 15// 1. a trailing `//` comment -- ONLY when outside a string literal (backslash-escape aware, so 16// `"http://x"` and `"a\"b // c"` are not mistaken for comments) 17// 2. runs of whitespace -> one space; leading/trailing stripped 18// 3. a const identifier -> ITS DECLARED VALUE TEXT, read out of the file that declares it. 19// !!THE OBVIOUS SHORTCUT IS UNSOUND, measured: reading the digits out of `FOO_MAGIC_4096` breaks 20// on nx_lz4_header's `const LZ4_MAGIC_0: i64 = 0x04` and nx_nxtask_store's `NXT_MAGIC_0 = 0x4E`. 21// There the trailing digit is a FIELD INDEX ("magic byte 0"), and the value is an unrelated hex 22// byte. A name-digit un-lift equates 0 with 4 and calls two different files identical. 23// ⇒ ★A NAMING CONVENTION IS A COMMENT: IT IS EVIDENCE ABOUT INTENT, NEVER ABOUT VALUE. 24// The value is substituted as its DECLARED TEXT, verbatim and unparsed. That is deliberate: a 25// parser that "helpfully" normalised 0x04 to 4 would need to be right about every literal form 26// in the language, and being wrong about one is how the line above got mis-measured TWICE (a 27// decimal regex silently matched the `0` out of `0x04` and reported the const as declared zero). 28// ⇒ ★DON'T PARSE WHAT YOU ONLY NEED TO COMPARE. 29// 30// WHERE THE TABLE COMES FROM, and why there is no tree walk here: NishiLang requires consts above 31// use, and the NAS magicsweep lifts a literal into a const IN THE SAME FILE. Measured across both 32// trees: 3,728 of 3,730 lifted-const uses (99.946%) are declared in the file that uses them -- the 33// only two exceptions tree-wide are nx_archive_server's K_MAGIC_8080 and nx_web_crawl_step_gate's 34// WC_MAGIC_2048. So each side's table is built FROM THAT SIDE'S OWN SOURCE, which is also the 35// semantically correct scope: a const means what ITS file declares it to mean. Two files that 36// declare the same name differently therefore normalise DIFFERENTLY and stay visible as surplus, 37// which a single shared table would have silently equated. 38// ⇒ ★A SYMBOL'S MEANING IS SCOPED; A LOOKUP TABLE THAT FORGETS THE SCOPE INVENTS AGREEMENT. 39// [consttab] remains as an optional FALLBACK for the cross-file residual; file-local always wins. 40// 41// !!NORMALISING IS LOSSY BY DESIGN, SO IT MUST NEVER SILENTLY DECIDE A MERGE. Adopting A on a 42// COSMETIC-ONLY verdict is code-safe but can still DROP B'S COMMENTS -- the "why" that Rule 23 exists 43// to protect. So this organ also counts, and prints, every B-only trailing comment that adopting A 44// would destroy. Measured on the 237 cosmetic-only files: only 3 such comments existed, but all 3 45// were load-bearing (a syscall-translation footnote naming its debt id, a 1<<40 ranking rationale, 46// and a "EXISTS but unreadable -> destructive, REFUSE" safety note). 47// => ★A LOSSY COMPARISON MUST REPORT WHAT IT DISCARDED, OR IT IS LAUNDERING THE LOSS. 48// 49// THE SAFETY PROPERTY, and it is tested not asserted: a single-operator mutation MUST still read as 50// BIDIRECTIONAL. `if hit == 0` and `if hit != 0` normalise to DIFFERENT strings, so the escaped 51// mutant that motivated nx_srcdiff survives this ruler too. A RULER MUST BE TESTED AGAINST THE WORST 52// DEFECT YOU ALREADY FOUND -- see nx_normdiff_gate. 53// 54// DIALECT: plain-if, <=6 params, consts above use. Tables are mmap'd through static POINTERS -- 55// a BSS static ARRAY silently crashes the module at startup (banked gotcha). 56// license_tier: ORIGINAL expect_exit: 0 No hw writes (Rule 26). 57import "nx_syscalls.nx" 58const ND_MAGIC_8192: i64 = 8192 59 60const ND_SLOTS: i64 = 131072 // power of two; a 1000-line file loads at <1% -- headroom is cheap 61const ND_MASK: i64 = 131071 62const ND_OUTBUF: i64 = 4194304 63const ND_NUMBUF: i64 = 64 64const ND_TOK: i64 = 512 // longest identifier we will try to resolve 65const ND_ARENA: i64 = 8388608 // fixed per-side arena for census mode; a source over this is REFUSED, never truncated 66const ND_SURP_CAP: i64 = 16384 // surplus lines tracked per side for the supersede match 67const ND_MAXIMP: i64 = 128 // direct imports followed; a .nx file today declares <20 68const ND_BCL_CAP: i64 = 65536 // B comment-sites tracked; a 1000-line file uses <2% 69const ND_SLACK: i64 = 65536 // normalise can only shrink a line, but never trust that alone 70const ND_FNV_OFF: i64 = 1469598103934665603 71const ND_FNV_PRIME: i64 = 1099511628211 72const ND_SAMPLE_CAP: i64 = 25 // stdout sample; the FULL surplus list always goes to [outfile] 73const ND_LF: i64 = 10 74const ND_CR: i64 = 13 75const ND_TAB: i64 = 9 76const ND_SP: i64 = 32 77const ND_SLASH: i64 = 47 78const ND_QUOTE: i64 = 34 79const ND_BSLASH: i64 = 92 80const ND_USCORE: i64 = 95 81 82// raw multisets (the GUARD ruler -- byte-for-byte lines, nx_srcdiff semantics) 83static nd_rka: *i64 84static nd_rca: *i64 85static nd_rkb: *i64 86static nd_rcb: *i64 87// normalised multisets (the CENSUS ruler) 88static nd_nka: *i64 89static nd_nca: *i64 90static nd_nkb: *i64 91static nd_ncb: *i64 92// A's comments, keyed by normalised line -> used to find B-only comments we would destroy 93static nd_ack: *i64 94static nd_acv: *i64 95// const name -> declared value TEXT, ONE TABLE PER SIDE (a const is scoped to its file) 96static nd_ctka: *i64 97static nd_ctva: *i64 98static nd_ctkb: *i64 99static nd_ctvb: *i64 100static nd_carena: *u8 // arena holding copied const names/values 101static nd_carena_n: *i64 102 103static nd_n: *i64 // [0]lA [1]lB [2]rsA [3]rsB [4]crA [5]crB [6]shown [7]nsA [8]nsB [9]lostcmt [10]consts 104static nd_out: *u8 105static nd_out_n: *i64 106static nd_num: *u8 107static nd_rev: *u8 108static nd_tok: *u8 109static nd_na: *u8 // normalised-line arena, side A 110static nd_na_n: *i64 111static nd_nb: *u8 112static nd_nb_n: *i64 113static nd_ca_buf: *u8 // comment arena, SHARED by both sides 114static nd_ca_n: *i64 115static nd_bcl: *i64 // B comment sites: [norm-line ptr, comment ptr] pairs 116static nd_bcl_n: *i64 117// A's COMPILATION UNIT = A's own lines + the lines of every module A directly imports. 118static nd_uka: *i64 119static nd_uca: *i64 120static nd_ua: *u8 121static nd_ua_n: *i64 122static nd_imp: *i64 // resolved import path strings 123static nd_imp_n: *i64 124// surplus lines, collected so each B line can be matched against the A lines 125static nd_asl: *i64 126static nd_asl_n: *i64 127static nd_bsl: *i64 128static nd_bsl_n: *i64 129// GENERATION-STAMPED SLOTS. Census mode compares thousands of pairs in ONE process, so every hash 130// table must be emptied between pairs. Zeroing them is 12 MB of memset per pair (33 GB over the 131// residual); instead each slot carries the generation it was written in and a slot counts as EMPTY 132// unless its stamp equals the current generation. Clearing all twelve tables is then `nd_gen + 1`. 133// => *AN O(1) RESET IS WHAT MAKES A BATCH MODE POSSIBLE AT ALL -- WITHOUT IT THE ORGAN IS FORCED 134// BACK OUT INTO A SHELL LOOP, ONE PROCESS PER PAIR, AND THE SHELL STARTS DOING THE WORK AGAIN. 135static nd_g_rka: *i64 136static nd_g_rkb: *i64 137static nd_g_nka: *i64 138static nd_g_nkb: *i64 139static nd_g_uka: *i64 140static nd_g_ack: *i64 141static nd_g_ctka: *i64 142static nd_g_ctkb: *i64 143static nd_gen: *i64 144static nd_ipath: *u8 145static nd_used: *u8 146static nd_la: *i64 147static nd_lb: *i64 148 149func nd_puts(s: *u8) -> i64 { 150 var n: i64 = 0 151 while s[n] != (0 as u8) { n = n + 1 } 152 sys_write(1, s, n) 153 return 0 154} 155func nd_putn(v: i64) -> i64 { 156 var m: i64 = v 157 if m == 0 { nd_num[0] = 48 as u8; sys_write(1, nd_num, 1); return 0 } 158 if m < 0 { nd_puts("-" as *u8); m = 0 - m } 159 var k: i64 = 0 160 while m > 0 { nd_rev[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 161 var i: i64 = 0 162 while i < k { nd_num[i] = nd_rev[k - 1 - i]; i = i + 1 } 163 sys_write(1, nd_num, k) 164 return 0 165} 166func nd_streq(a: *u8, b: *u8) -> i64 { 167 var i: i64 = 0 168 var go: i64 = 1 169 var eq: i64 = 1 170 while go == 1 { 171 if a[i] != b[i] { eq = 0; go = 0 } else { 172 if a[i] == (0 as u8) { go = 0 } else { i = i + 1 } 173 } 174 } 175 return eq 176} 177func nd_hash(s: *u8) -> i64 { 178 var h: i64 = ND_FNV_OFF 179 var i: i64 = 0 180 while s[i] != (0 as u8) { 181 h = h ^ (s[i] as i64) 182 h = h * ND_FNV_PRIME 183 i = i + 1 184 } 185 if h < 0 { h = 0 - h } 186 return h 187} 188func nd_add(keys: *i64, counts: *i64, gen: *i64, line: *u8) -> i64 { 189 var s: i64 = nd_hash(line) & ND_MASK 190 var go: i64 = 1 191 while go == 1 { 192 if gen[s] != nd_gen[0] { gen[s] = nd_gen[0]; keys[s] = 0 } 193 if keys[s] == 0 { 194 keys[s] = line as i64 195 counts[s] = 1 196 go = 0 197 } else { 198 if nd_streq(keys[s] as *u8, line) == 1 { counts[s] = counts[s] + 1; go = 0 } else { s = (s + 1) & ND_MASK } 199 } 200 } 201 return 0 202} 203func nd_get(keys: *i64, counts: *i64, gen: *i64, line: *u8) -> i64 { 204 var s: i64 = nd_hash(line) & ND_MASK 205 var go: i64 = 1 206 while go == 1 { 207 if gen[s] != nd_gen[0] { return 0 } 208 if keys[s] == 0 { return 0 } 209 if nd_streq(keys[s] as *u8, line) == 1 { return counts[s] } 210 s = (s + 1) & ND_MASK 211 } 212 return 0 213} 214// key -> string value map (const table, and A's comment map). First writer wins: a duplicate 215// declaration is reported by the caller's census, not silently overwritten here. 216func nd_put_kv(keys: *i64, vals: *i64, gen: *i64, k: *u8, v: *u8) -> i64 { 217 var s: i64 = nd_hash(k) & ND_MASK 218 var go: i64 = 1 219 while go == 1 { 220 if gen[s] != nd_gen[0] { gen[s] = nd_gen[0]; keys[s] = 0 } 221 if keys[s] == 0 { keys[s] = k as i64; vals[s] = v as i64; go = 0 } else { 222 if nd_streq(keys[s] as *u8, k) == 1 { go = 0 } else { s = (s + 1) & ND_MASK } 223 } 224 } 225 return 0 226} 227func nd_get_kv(keys: *i64, vals: *i64, gen: *i64, k: *u8) -> i64 { 228 var s: i64 = nd_hash(k) & ND_MASK 229 var go: i64 = 1 230 while go == 1 { 231 if gen[s] != nd_gen[0] { return 0 } 232 if keys[s] == 0 { return 0 } 233 if nd_streq(keys[s] as *u8, k) == 1 { return vals[s] } 234 s = (s + 1) & ND_MASK 235 } 236 return 0 237} 238func nd_is_id_char(c: i64) -> i64 { 239 if c >= 97 { if c <= 122 { return 1 } } 240 if c >= 65 { if c <= 90 { return 1 } } 241 if c >= 48 { if c <= 57 { return 1 } } 242 if c == ND_USCORE { return 1 } 243 return 0 244} 245func nd_is_id_start(c: i64) -> i64 { 246 if c >= 97 { if c <= 122 { return 1 } } 247 if c >= 65 { if c <= 90 { return 1 } } 248 if c == ND_USCORE { return 1 } 249 return 0 250} 251// Index of the `//` that starts a trailing comment, or -1. STRING-AWARE and ESCAPE-AWARE: without 252// this, every `"http://host"` in the tree reads as a comment and the two sides "agree" on a truncated 253// line. Returns the cut index; the caller uses it for BOTH the body and the comment text. 254func nd_comment_at(src: *u8) -> i64 { 255 var i: i64 = 0 256 var ins: i64 = 0 257 var esc: i64 = 0 258 var go: i64 = 1 259 var cut: i64 = 0 - 1 260 while go == 1 { 261 let c: i64 = src[i] as i64 262 if c == 0 { go = 0 } else { 263 if esc == 1 { esc = 0 } else { 264 if c == ND_BSLASH { esc = 1 } else { 265 if c == ND_QUOTE { 266 if ins == 0 { ins = 1 } else { ins = 0 } 267 } else { 268 if ins == 0 { 269 if c == ND_SLASH { 270 if (src[i + 1] as i64) == ND_SLASH { cut = i; go = 0 } 271 } 272 } 273 } 274 } 275 } 276 if go == 1 { i = i + 1 } 277 } 278 } 279 return cut 280} 281func nd_len(s: *u8) -> i64 { 282 var n: i64 = 0 283 while s[n] != (0 as u8) { n = n + 1 } 284 return n 285} 286// Normalise src into dst (NUL-terminated). Returns bytes written. 287// One pass: drop the comment, collapse whitespace, and resolve any identifier the const table knows 288// to its DECLARED value. Identifiers absent from the table are copied verbatim. 289func nd_norm(src: *u8, dst: *u8, ctk: *i64, ctv: *i64, ctg: *i64) -> i64 { 290 let cut: i64 = nd_comment_at(src) 291 var end: i64 = cut 292 if end < 0 { end = nd_len(src) } 293 var o: i64 = 0 294 var p: i64 = 0 295 var pend: i64 = 0 296 while p < end { 297 let c: i64 = src[p] as i64 298 var ws: i64 = 0 299 if c == ND_SP { ws = 1 } 300 if c == ND_TAB { ws = 1 } 301 if c == ND_CR { ws = 1 } 302 if ws == 1 { pend = 1; p = p + 1 } else { 303 if pend == 1 { 304 if o > 0 { dst[o] = ND_SP as u8; o = o + 1 } 305 pend = 0 306 } 307 if nd_is_id_start(c) == 1 { 308 var q: i64 = p 309 var gi: i64 = 1 310 while gi == 1 { 311 if q >= end { gi = 0 } else { 312 if nd_is_id_char(src[q] as i64) == 1 { q = q + 1 } else { gi = 0 } 313 } 314 } 315 var tl: i64 = q - p 316 if tl >= ND_TOK { tl = ND_TOK - 1 } 317 var t: i64 = 0 318 while t < tl { nd_tok[t] = src[p + t]; t = t + 1 } 319 nd_tok[tl] = 0 as u8 320 let hit: i64 = nd_get_kv(ctk, ctv, ctg, nd_tok) 321 if hit == 0 { 322 t = 0 323 while t < tl { dst[o] = nd_tok[t]; o = o + 1; t = t + 1 } 324 } else { 325 let vs: *u8 = hit as *u8 326 var k: i64 = 0 327 while vs[k] != (0 as u8) { dst[o] = vs[k]; o = o + 1; k = k + 1 } 328 } 329 p = q 330 } else { 331 dst[o] = src[p] as u8 332 o = o + 1 333 p = p + 1 334 } 335 } 336 } 337 dst[o] = 0 as u8 338 return o 339} 340// Harvest `const NAME: TYPE = <literal>` out of one side's source into that side's table. 341// READ-ONLY over buf: names and values are COPIED into an arena, because nd_load later rewrites this 342// same buffer in place to NUL-terminate its lines, and a table of pointers into a buffer someone else 343// mutates is a use-after-write waiting to happen. 344// ⇒ ★IF TWO PASSES SHARE A BUFFER AND ONE OF THEM WRITES, THE OTHER MUST OWN ITS COPY. 345// A value that does not START with a digit or '-' is SKIPPED, not chased: `const A = B` is an alias, 346// and resolving it needs transitive lookup this organ deliberately does not do. Skipping leaves the 347// identifier in place, which can only ever cost a missed match -- never a false one. 348func nd_scan_consts(buf: *u8, n: i64, ctk: *i64, ctv: *i64, ctg: *i64) -> i64 { 349 var p: i64 = 0 350 var found: i64 = 0 351 while p < n { 352 var e: i64 = p 353 var go: i64 = 1 354 while go == 1 { 355 if e >= n { go = 0 } else { 356 if buf[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 357 } 358 } 359 // skip leading whitespace 360 var i: i64 = p 361 var gw: i64 = 1 362 while gw == 1 { 363 if i >= e { gw = 0 } else { 364 var w: i64 = 0 365 if buf[i] == (ND_SP as u8) { w = 1 } 366 if buf[i] == (ND_TAB as u8) { w = 1 } 367 if w == 1 { i = i + 1 } else { gw = 0 } 368 } 369 } 370 // literal "const" followed by a space 371 var isc: i64 = 0 372 if i + 6 <= e { 373 if buf[i] == (99 as u8) { if buf[i+1] == (111 as u8) { if buf[i+2] == (110 as u8) { 374 if buf[i+3] == (115 as u8) { if buf[i+4] == (116 as u8) { if buf[i+5] == (ND_SP as u8) { 375 isc = 1 376 } } } } } } 377 } 378 if isc == 1 { 379 i = i + 6 380 var gs: i64 = 1 381 while gs == 1 { 382 if i >= e { gs = 0 } else { 383 if buf[i] == (ND_SP as u8) { i = i + 1 } else { gs = 0 } 384 } 385 } 386 let ns: i64 = i 387 var gn: i64 = 1 388 while gn == 1 { 389 if i >= e { gn = 0 } else { 390 if nd_is_id_char(buf[i] as i64) == 1 { i = i + 1 } else { gn = 0 } 391 } 392 } 393 let ne: i64 = i 394 // find '=' on this line 395 var eq: i64 = 0 - 1 396 var j: i64 = i 397 while j < e { 398 if buf[j] == (61 as u8) { if eq < 0 { eq = j } } 399 j = j + 1 400 } 401 if ne > ns { if eq > 0 { 402 var v: i64 = eq + 1 403 var gv: i64 = 1 404 while gv == 1 { 405 if v >= e { gv = 0 } else { 406 var w2: i64 = 0 407 if buf[v] == (ND_SP as u8) { w2 = 1 } 408 if buf[v] == (ND_TAB as u8) { w2 = 1 } 409 if w2 == 1 { v = v + 1 } else { gv = 0 } 410 } 411 } 412 let vs: i64 = v 413 var gt: i64 = 1 414 while gt == 1 { 415 if v >= e { gt = 0 } else { 416 var stop: i64 = 0 417 if buf[v] == (ND_SP as u8) { stop = 1 } 418 if buf[v] == (ND_TAB as u8) { stop = 1 } 419 if buf[v] == (ND_CR as u8) { stop = 1 } 420 if buf[v] == (ND_SLASH as u8) { stop = 1 } 421 if stop == 1 { gt = 0 } else { v = v + 1 } 422 } 423 } 424 let ve: i64 = v 425 var lit: i64 = 0 426 if ve > vs { 427 let c0: i64 = buf[vs] as i64 428 if c0 >= 48 { if c0 <= 57 { lit = 1 } } 429 if c0 == 45 { lit = 1 } 430 } 431 if lit == 1 { 432 let kd: *u8 = ((nd_carena as i64) + nd_carena_n[0]) as *u8 433 var k: i64 = 0 434 while k < (ne - ns) { kd[k] = buf[ns + k]; k = k + 1 } 435 kd[k] = 0 as u8 436 nd_carena_n[0] = nd_carena_n[0] + k + 1 437 let vd: *u8 = ((nd_carena as i64) + nd_carena_n[0]) as *u8 438 var m: i64 = 0 439 while m < (ve - vs) { vd[m] = buf[vs + m]; m = m + 1 } 440 vd[m] = 0 as u8 441 nd_carena_n[0] = nd_carena_n[0] + m + 1 442 nd_put_kv(ctk, ctv, ctg, kd, vd) 443 found = found + 1 444 } 445 } } 446 } 447 p = e + 1 448 } 449 return found 450} 451// Collect `import "X"` paths out of a PRISTINE buffer (before nd_load terminates its lines in place). 452// Paths are copied into the const arena because the source buffer is about to be rewritten. 453func nd_scan_imports(buf: *u8, n: i64) -> i64 { 454 var p: i64 = 0 455 while p < n { 456 var e: i64 = p 457 var go: i64 = 1 458 while go == 1 { 459 if e >= n { go = 0 } else { 460 if buf[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 461 } 462 } 463 var i: i64 = p 464 var gw2: i64 = 1 465 while gw2 == 1 { 466 if i >= e { gw2 = 0 } else { 467 var w: i64 = 0 468 if buf[i] == (ND_SP as u8) { w = 1 } 469 if buf[i] == (ND_TAB as u8) { w = 1 } 470 if w == 1 { i = i + 1 } else { gw2 = 0 } 471 } 472 } 473 var isi: i64 = 0 474 if i + 7 <= e { 475 if buf[i] == (105 as u8) { if buf[i+1] == (109 as u8) { if buf[i+2] == (112 as u8) { 476 if buf[i+3] == (111 as u8) { if buf[i+4] == (114 as u8) { if buf[i+5] == (116 as u8) { 477 isi = 1 478 } } } } } } 479 } 480 if isi == 1 { 481 var q: i64 = i + 6 482 var gq: i64 = 1 483 var s0: i64 = 0 - 1 484 while gq == 1 { 485 if q >= e { gq = 0 } else { 486 if buf[q] == (ND_QUOTE as u8) { s0 = q + 1; gq = 0 } else { q = q + 1 } 487 } 488 } 489 if s0 > 0 { 490 var q2: i64 = s0 491 var g2: i64 = 1 492 var s1: i64 = 0 - 1 493 while g2 == 1 { 494 if q2 >= e { g2 = 0 } else { 495 if buf[q2] == (ND_QUOTE as u8) { s1 = q2; g2 = 0 } else { q2 = q2 + 1 } 496 } 497 } 498 if s1 > s0 { 499 if nd_imp_n[0] < ND_MAXIMP { 500 let d: *u8 = ((nd_carena as i64) + nd_carena_n[0]) as *u8 501 var k: i64 = 0 502 while k < (s1 - s0) { d[k] = buf[s0 + k]; k = k + 1 } 503 d[k] = 0 as u8 504 nd_carena_n[0] = nd_carena_n[0] + k + 1 505 nd_imp[nd_imp_n[0]] = d as i64 506 nd_imp_n[0] = nd_imp_n[0] + 1 507 } 508 } 509 } 510 } 511 p = e + 1 512 } 513 return nd_imp_n[0] 514} 515// "-" in a positional slot means "not supplied". Without it, a caller who wants [importroot] 516// (arg 5) but no [outfile]/[consttab] cannot say so, and the organ tries to OPEN a file called "-" -- 517// turning an optional argument into a hard failure. 518// => *POSITIONAL OPTIONALS NEED A SKIP TOKEN, OR ONLY THE LAST ONE IS REALLY OPTIONAL. 519func nd_is_dash(s: *u8) -> i64 { 520 if s[0] != (45 as u8) { return 0 } 521 if s[1] != (0 as u8) { return 0 } 522 return 1 523} 524// root + "/" + name -> out (the fallback resolution root). 525func nd_join_root(root: *u8, name: *u8, out: *u8) -> i64 { 526 var o: i64 = 0 527 while root[o] != (0 as u8) { out[o] = root[o]; o = o + 1 } 528 if o > 0 { if out[o-1] != (47 as u8) { out[o] = 47 as u8; o = o + 1 } } 529 var k: i64 = 0 530 while name[k] != (0 as u8) { out[o] = name[k]; o = o + 1; k = k + 1 } 531 out[o] = 0 as u8 532 return o 533} 534// dir(path) + "/" + name -> out. Imports resolve RELATIVE TO THE IMPORTING FILE, which is how the 535// compiler resolves them; `../nxasm/x.nx` is left to the kernel to flatten. 536func nd_join_dir(path: *u8, name: *u8, out: *u8) -> i64 { 537 var last: i64 = 0 - 1 538 var i: i64 = 0 539 while path[i] != (0 as u8) { 540 if path[i] == (47 as u8) { last = i } 541 i = i + 1 542 } 543 var o: i64 = 0 544 if last >= 0 { 545 while o <= last { out[o] = path[o]; o = o + 1 } 546 } 547 var k: i64 = 0 548 while name[k] != (0 as u8) { out[o] = name[k]; o = o + 1; k = k + 1 } 549 out[o] = 0 as u8 550 return o 551} 552// Fold one imported module's lines into A's UNIT multiset. The module is normalised under ITS OWN 553// const table -- a const means what its own file declares it to mean (same scoping law as the sides). 554func nd_fold_module(path: *u8) -> i64 { 555 let ln: *i64 = sys_mmap(16) as *i64 556 ln[0] = 0 557 let buf: *u8 = sys_read_file(path, ln) 558 if (buf as i64) == 0 { return 0 } 559 let n: i64 = ln[0] 560 let mk: *i64 = sys_mmap(ND_SLOTS * 8) as *i64 561 let mv: *i64 = sys_mmap(ND_SLOTS * 8) as *i64 562 // a FRESH mmap is zero-filled, and nd_gen starts at 1, so every slot reads as empty. No memset. 563 let mg: *i64 = sys_mmap(ND_SLOTS * 8) as *i64 564 nd_scan_consts(buf, n, mk, mv, mg) 565 var p: i64 = 0 566 var added: i64 = 0 567 while p < n { 568 let base: i64 = buf as i64 569 var e: i64 = p 570 var go: i64 = 1 571 while go == 1 { 572 if e >= n { go = 0 } else { 573 if buf[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 574 } 575 } 576 var t: i64 = e 577 if t > p { if buf[t-1] == (ND_CR as u8) { t = t - 1 } } 578 buf[t] = 0 as u8 579 let line: *u8 = (base + p) as *u8 580 let dst: *u8 = ((nd_ua as i64) + nd_ua_n[0]) as *u8 581 let w: i64 = nd_norm(line, dst, mk, mv, mg) 582 if w > 0 { 583 nd_add(nd_uka, nd_uca, nd_g_uka, dst) 584 nd_ua_n[0] = nd_ua_n[0] + w + 1 585 added = added + 1 586 } 587 p = e + 1 588 } 589 // !!FREE THE PER-MODULE TABLES. Single-pair mode leaked ~15 MB and nobody noticed; CENSUS mode runs 590 // thousands of pairs in ONE process, so the same leak becomes 3 MB x imports x pairs -- tens of GB of 591 // mapping, and the run went from an expected ~1 minute to a 10-minute timeout THROUGH THRASHING, not 592 // through doing more work. 593 // => *A LEAK IS A CONSTANT COST UNTIL YOU PUT THE CODE IN A LOOP, AND THEN IT IS THE WHOLE COST. 594 // => *A BATCH MODE DOES NOT JUST RUN THE SAME CODE MORE TIMES -- IT RE-PRICES EVERY ALLOCATION 595 // THAT THE SINGLE-SHOT PATH WAS QUIETLY GETTING AWAY WITH. 596 sys_munmap(mk as *u8, ND_SLOTS * 8) 597 sys_munmap(mv as *u8, ND_SLOTS * 8) 598 sys_munmap(mg as *u8, ND_SLOTS * 8) 599 if n > 0 { sys_munmap(buf, n + 16) } 600 sys_munmap(ln as *u8, 16) 601 return added 602} 603// IS `b` THE SAME LINE AS `a` WITH ONE CONTIGUOUS RUN DELETED? 604// If so, every character of b survives in a, in order, and a only ADDS -- so b is the PRE-EDIT version 605// and adopting a loses nothing. This is the shape a bulk sweep leaves behind: on 2026-08-07 another 606// seat fixed a sign-printing bug across 2,308 gates, each gate carrying its own copy of the helper 607// under a different local name (49 names / 151 files measured), and every delta was exactly 608// `;sys_write(fd,"-" as *u8,1)` inserted into one line. 609// => ★A BULK SWEEP IS ONE DECISION REPLICATED N TIMES; WITHOUT THIS TEST THE LANE REFUSES ALL N. 610// 611// !!IT CANNOT EXPRESS THE ESCAPED MUTANT. `if hit == 0` -> `if hit != 0` is a SUBSTITUTION: same 612// length, so the `blen >= alen` guard rejects it before the shape test runs. The ruler that motivated 613// this whole lane still refuses. A RULER MUST BE TESTED AGAINST THE WORST DEFECT YOU ALREADY FOUND -- 614// see nd_selftest, which asserts that case and 12 others BEFORE any verdict is printed. 615// 1 if the line is an `import "..."` declaration. An import path is a LAYOUT fact, never a version 616// fact, and the supersede predicate must never be applied to one. 617// MEASURED 2026-08-07, and it was about to propose breaking a build: extending the supersede check to 618// the LAPTOP-AHEAD direction flagged 157 files, and 132 of the 133 clean pairs were exactly 619// NAS : import "nxasm_v2.nx" 620// laptop : import "../nxasm/nxasm_v2.nx" 621// The NAS line IS the laptop line minus the contiguous run `../nxasm/`, so the shape test says 622// "laptop ahead" -- but the two trees have DIFFERENT LAYOUTS by design (this is the standing 623// structural-exclusion class), and pushing the laptop form to the NAS makes the import unresolvable 624// and the build fail. The token-boundary guard does not catch it: every boundary is a quote or a 625// slash, not a word character. 626// => ★★★★★★A PATH IS NOT CODE. A TEXTUAL "SUPERSEDE" ON AN IMPORT LINE PROPOSES BREAKING THE OTHER 627// TREE'S BUILD, AND IT LOOKS EXACTLY LIKE PROGRESS. 628// => ★★★★★A PREDICATE THAT IS SOUND IN ONE DIRECTION IS NOT THEREBY SOUND IN THE MIRROR -- I HAD 629// RUN IT 151/151 SAFELY ONE WAY BEFORE THE OTHER WAY EXPOSED THE CLASS. 630func nd_is_import(l: *u8) -> i64 { 631 var i: i64 = 0 632 while l[i] == (ND_SP as u8) { i = i + 1 } 633 if l[i] != (105 as u8) { return 0 } 634 if l[i+1] != (109 as u8) { return 0 } 635 if l[i+2] != (112 as u8) { return 0 } 636 if l[i+3] != (111 as u8) { return 0 } 637 if l[i+4] != (114 as u8) { return 0 } 638 if l[i+5] != (116 as u8) { return 0 } 639 return 1 640} 641func nd_supersedes(a: *u8, b: *u8) -> i64 { 642 if nd_is_import(a) == 1 { return 0 } 643 if nd_is_import(b) == 1 { return 0 } 644 let alen: i64 = nd_len(a) 645 let blen: i64 = nd_len(b) 646 if blen >= alen { return 0 } 647 var pfx: i64 = 0 648 var g1: i64 = 1 649 while g1 == 1 { 650 if pfx >= blen { g1 = 0 } else { 651 if a[pfx] == b[pfx] { pfx = pfx + 1 } else { g1 = 0 } 652 } 653 } 654 var sfx: i64 = 0 655 var g2: i64 = 1 656 while g2 == 1 { 657 if sfx >= (blen - pfx) { g2 = 0 } else { 658 if a[alen - 1 - sfx] == b[blen - 1 - sfx] { sfx = sfx + 1 } else { g2 = 0 } 659 } 660 } 661 if pfx + sfx != blen { return 0 } 662 // !!THE DELETION MUST NOT SPLIT A TOKEN. Found by this organ's OWN hex-trap fixture, AFTER the 663 // predicate had self-tested 8/8 and classified 151 real files: normalised, A is 664 // `let b: i64 = 0x04` and B is `let b: i64 = 0`, and B *is* textually A minus the contiguous run 665 // "x04" -- so the raw shape test said SUPERSEDED and would have adopted a file where a numeric 666 // LITERAL was truncated from 4 to 0. A purely textual predicate cannot see that it cut a token in 667 // half. 668 // => ★★★★★★A TEXTUAL TEST THAT LANDS INSIDE A TOKEN IS NOT A CODE TEST. Require both deletion 669 // boundaries to fall between a word character and a non-word one. 670 // => ★★★★★A PREDICATE THAT PASSED ITS OWN SUITE AND A 151-FILE POPULATION WAS STILL UNSOUND -- 671 // THE COUNTEREXAMPLE CAME FROM A FIXTURE BUILT FOR A DIFFERENT DEFECT ENTIRELY. Keep old 672 // fixtures running against new logic; they are the cheapest adversary you have. 673 // The real sweep is unaffected: its deletions begin at ';' (a non-word char), so no token splits. 674 if pfx > 0 { if nd_is_id_char(a[pfx-1] as i64) == 1 { if nd_is_id_char(a[pfx] as i64) == 1 { return 0 } } } 675 if sfx > 0 { 676 let lastdel: i64 = alen - sfx - 1 677 if lastdel >= 0 { if nd_is_id_char(a[lastdel] as i64) == 1 { if nd_is_id_char(a[alen - sfx] as i64) == 1 { return 0 } } } 678 } 679 return 1 680} 681// SELF-TEST THE PREDICATE BEFORE TRUSTING IT WITH A MERGE DECISION. Runs on EVERY invocation (8 682// string compares, unmeasurable) and its result gates whether a supersede verdict is printed at all. 683// ★A CLASSIFIER WHOSE OWN CASES ARE UNPROVEN CLASSIFIES NOTHING · ★A PREDICATE THAT SILENTLY REGRESSED 684// MUST NOT BE TRUSTED WITH A MERGE -- so the failure mode is "no verdict", never "wrong verdict". 685// Case 2 is THE ESCAPED MUTANT that motivated this entire lane; it must read 0 forever. 686func nd_selftest() -> i64 { 687 var bad: i64 = 0 688 if nd_supersedes("func f(a){x;EXTRA}" as *u8, "func f(a){x}" as *u8) != 1 { bad = bad + 1 } 689 if nd_supersedes("if hit == 0 { h = 1 }" as *u8, "if hit != 0 { h = 1 }" as *u8) != 0 { bad = bad + 1 } 690 if nd_supersedes("abc" as *u8, "abc" as *u8) != 0 { bad = bad + 1 } 691 if nd_supersedes("let x = 1" as *u8, "let y = 1" as *u8) != 0 { bad = bad + 1 } 692 if nd_supersedes("a;b;c" as *u8, "a;c" as *u8) != 1 { bad = bad + 1 } 693 if nd_supersedes("short" as *u8, "muchlongerline" as *u8) != 0 { bad = bad + 1 } 694 if nd_supersedes("f(1,2,3)" as *u8, "f(1,3)" as *u8) != 1 { bad = bad + 1 } 695 if nd_supersedes("xAyBz" as *u8, "xyz" as *u8) != 0 { bad = bad + 1 } 696 // THE HEX TRAP: a deletion inside a numeric literal changes the VALUE, not just the text. 697 if nd_supersedes("let b: i64 = 0x04" as *u8, "let b: i64 = 0" as *u8) != 0 { bad = bad + 1 } 698 // and inside an identifier 699 if nd_supersedes("call_widget(x)" as *u8, "call(x)" as *u8) != 0 { bad = bad + 1 } 700 // the REAL sweep shape still passes -- deletion starts at ';', a token boundary 701 if nd_supersedes("if m<0{m=0-m;sys_write(fd,1)}" as *u8, "if m<0{m=0-m}" as *u8) != 1 { bad = bad + 1 } 702 // THE LAYOUT TRAP: the NAS form IS the laptop form minus "../nxasm/", and adopting either way 703 // breaks the other tree's build. An import line is never a supersede candidate. 704 if nd_supersedes("import \"../nxasm/nxasm_v2.nx\"" as *u8, "import \"nxasm_v2.nx\"" as *u8) != 0 { bad = bad + 1 } 705 if nd_supersedes("import \"nxasm_v2.nx\"" as *u8, "import \"../nxasm/nxasm_v2.nx\"" as *u8) != 0 { bad = bad + 1 } 706 return bad 707} 708func nd_emit(tag: *u8, n: i64, line: *u8) -> i64 { 709 var o: i64 = nd_out_n[0] 710 let ll: i64 = nd_len(line) 711 if o + ll + 32 >= ND_OUTBUF { return 0 } 712 var i: i64 = 0 713 while tag[i] != (0 as u8) { nd_out[o] = tag[i]; o = o + 1; i = i + 1 } 714 nd_out[o] = ND_SP as u8; o = o + 1 715 nd_out[o] = 120 as u8; o = o + 1 // 'x' 716 var k: i64 = 0 717 var m: i64 = n 718 if m == 0 { nd_num[k] = 48 as u8; k = k + 1 } 719 var j: i64 = 0 720 while m > 0 { nd_rev[j] = (48 + (m % 10)) as u8; m = m / 10; j = j + 1 } 721 while j > 0 { nd_num[k] = nd_rev[j - 1]; k = k + 1; j = j - 1 } 722 i = 0 723 while i < k { nd_out[o] = nd_num[i]; o = o + 1; i = i + 1 } 724 nd_out[o] = ND_SP as u8; o = o + 1 725 i = 0 726 while i < ll { nd_out[o] = line[i]; o = o + 1; i = i + 1 } 727 nd_out[o] = ND_LF as u8; o = o + 1 728 nd_out_n[0] = o 729 return 0 730} 731func nd_sample(tag: *u8, n: i64, line: *u8) -> i64 { 732 if nd_n[6] >= ND_SAMPLE_CAP { return 0 } 733 nd_puts(" " as *u8); nd_puts(tag); nd_puts(" x" as *u8); nd_putn(n) 734 nd_puts(" " as *u8); nd_puts(line); nd_puts("\n" as *u8) 735 nd_n[6] = nd_n[6] + 1 736 return 0 737} 738// Split buf into NUL-terminated lines in place; load the RAW line into (rk,rc) and the NORMALISED 739// line into (nk,nc). A trailing CR is stripped from the line but COUNTED -- it is the classic delta a 740// text diff hides and a byte gate reports. 741func nd_load(buf: *u8, n: i64, rk: *i64, rc: *i64, rg: *i64, side: i64) -> i64 { 742 var p: i64 = 0 743 while p < n { 744 let base: i64 = buf as i64 745 var e: i64 = p 746 var go: i64 = 1 747 while go == 1 { 748 if e >= n { go = 0 } else { 749 if buf[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 750 } 751 } 752 var t: i64 = e 753 var cslot: i64 = 4 754 var lslot: i64 = 0 755 if side == 1 { cslot = 5; lslot = 1 } 756 if t > p { if buf[t-1] == (ND_CR as u8) { t = t - 1; nd_n[cslot] = nd_n[cslot] + 1 } } 757 buf[t] = 0 as u8 758 let line: *u8 = (base + p) as *u8 759 nd_add(rk, rc, rg, line) 760 nd_n[lslot] = nd_n[lslot] + 1 761 // normalised copy into this side's arena 762 if side == 0 { 763 let dst: *u8 = ((nd_na as i64) + nd_na_n[0]) as *u8 764 let w: i64 = nd_norm(line, dst, nd_ctka, nd_ctva, nd_g_ctka) 765 if w > 0 { 766 nd_add(nd_nka, nd_nca, nd_g_nka, dst) 767 nd_add(nd_uka, nd_uca, nd_g_uka, dst) // A's own lines are part of A's unit 768 nd_na_n[0] = nd_na_n[0] + w + 1 769 // remember A's comment for this normalised line, so we can spot B-only ones later 770 let cut: i64 = nd_comment_at(line) 771 if cut >= 0 { 772 let cdst: *u8 = ((nd_ca_buf as i64) + nd_ca_n[0]) as *u8 773 var ci: i64 = 0 774 while line[cut + ci] != (0 as u8) { cdst[ci] = line[cut + ci]; ci = ci + 1 } 775 cdst[ci] = 0 as u8 776 nd_ca_n[0] = nd_ca_n[0] + ci + 1 777 nd_put_kv(nd_ack, nd_acv, nd_g_ack, dst, cdst) 778 } 779 } 780 } else { 781 let dst: *u8 = ((nd_nb as i64) + nd_nb_n[0]) as *u8 782 let w: i64 = nd_norm(line, dst, nd_ctkb, nd_ctvb, nd_g_ctkb) 783 if w > 0 { 784 nd_add(nd_nkb, nd_ncb, nd_g_nkb, dst) 785 nd_nb_n[0] = nd_nb_n[0] + w + 1 786 // capture B's comments HERE, while the buffer still has its line structure -- 787 // nd_lost_comments cannot recover it later (see that function's header). 788 let cut2: i64 = nd_comment_at(line) 789 if cut2 >= 0 { 790 if nd_bcl_n[0] < ND_BCL_CAP { 791 let cd: *u8 = ((nd_ca_buf as i64) + nd_ca_n[0]) as *u8 792 var ci: i64 = 0 793 while line[cut2 + ci] != (0 as u8) { cd[ci] = line[cut2 + ci]; ci = ci + 1 } 794 cd[ci] = 0 as u8 795 nd_ca_n[0] = nd_ca_n[0] + ci + 1 796 let sl: i64 = nd_bcl_n[0] 797 nd_bcl[sl * 2] = dst as i64 798 nd_bcl[sl * 2 + 1] = cd as i64 799 nd_bcl_n[0] = sl + 1 800 } 801 } 802 } 803 } 804 p = e + 1 805 } 806 return 0 807} 808// Every B comment whose normalised line carries NO comment on A's side. These are exactly the "why" 809// notes that adopting A would destroy. Printed in full -- never summarised to a count -- because a 810// comment you cannot read is a comment you cannot decide to keep. 811// 812// !!THIS WALKS A LIST CAPTURED DURING nd_load, IT DOES NOT RE-READ THE SOURCE BUFFER. The first 813// version re-scanned bb looking for '\n' -- but nd_load has already overwritten every separator with 814// NUL to terminate its lines in place, so the rescan saw the ENTIRE FILE as one line, found no 815// trailing comment on it, and reported lostComments=0 on a file that plainly had one. It failed 816// SILENTLY and in the SAFE-LOOKING direction: "nothing would be lost" is exactly the answer that 817// invites the destructive adopt. Caught only because a fixture asserted the count, not the verdict. 818// ⇒ ★A SECOND PASS OVER A BUFFER THE FIRST PASS MUTATED IS READING A DIFFERENT FILE. 819// ⇒ ★TEST THE NUMBER A GUARD REPORTS, NOT JUST THE VERDICT IT PRINTS -- a guard that under-counts 820// losses agrees with you about the verdict right up until it is wrong. 821func nd_lost_comments() -> i64 { 822 var i: i64 = 0 823 let n: i64 = nd_bcl_n[0] 824 while i < n { 825 let normp: *u8 = nd_bcl[i * 2] as *u8 826 let cmtp: *u8 = nd_bcl[i * 2 + 1] as *u8 827 if nd_get_kv(nd_ack, nd_acv, nd_g_ack, normp) == 0 { 828 nd_n[9] = nd_n[9] + 1 829 nd_puts(" LOST-COMMENT " as *u8); nd_puts(cmtp) 830 nd_puts("\n on line: " as *u8); nd_puts(normp); nd_puts("\n" as *u8) 831 nd_emit("LOST-COMMENT" as *u8, 1, cmtp) 832 } 833 i = i + 1 834 } 835 return 0 836} 837// consttab format, one per line: NAME <TAB> VALUE. Anything else is skipped, not guessed at. 838func nd_load_consttab(path: *u8) -> i64 { 839 let ln: *i64 = sys_mmap(16) as *i64 840 ln[0] = 0 841 let buf: *u8 = sys_read_file(path, ln) 842 if (buf as i64) == 0 { return 0 - 1 } 843 let n: i64 = ln[0] 844 var p: i64 = 0 845 while p < n { 846 let base: i64 = buf as i64 847 var e: i64 = p 848 var go: i64 = 1 849 while go == 1 { 850 if e >= n { go = 0 } else { 851 if buf[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 852 } 853 } 854 var t: i64 = e 855 if t > p { if buf[t-1] == (ND_CR as u8) { t = t - 1 } } 856 buf[t] = 0 as u8 857 // split on the first TAB 858 var s: i64 = p 859 var tabat: i64 = 0 - 1 860 while s < t { 861 if buf[s] == (ND_TAB as u8) { if tabat < 0 { tabat = s } } 862 s = s + 1 863 } 864 if tabat > p { 865 buf[tabat] = 0 as u8 866 let k: *u8 = (base + p) as *u8 867 let v: *u8 = (base + tabat + 1) as *u8 868 if v[0] != (0 as u8) { 869 // FALLBACK ONLY: file-local consts are already in place and win, because nd_put_kv 870 // is first-writer-wins. An external table must never override what the file itself 871 // says -- that is the scope rule this organ is built on. 872 nd_put_kv(nd_ctka, nd_ctva, nd_g_ctka, k, v) 873 nd_put_kv(nd_ctkb, nd_ctvb, nd_g_ctkb, k, v) 874 nd_n[10] = nd_n[10] + 1 875 } 876 } 877 p = e + 1 878 } 879 return 0 880} 881 882func nd_run_pair(apath: *u8, bpath: *u8, iroot: *u8, quiet: i64, argc: i64, argv: *i64) -> i64 { 883 // O(1) CLEAR OF EVERY TABLE: one generation bump invalidates all twelve at once. The arenas are 884 // reused by resetting their offsets -- safe precisely BECAUSE the tables were invalidated, so no 885 // pointer into the old arena bytes can still be reached. 886 // => *AN ARENA RESET IS ONLY SAFE IF EVERY INDEX INTO IT DIED IN THE SAME BREATH. 887 nd_gen[0] = nd_gen[0] + 1 888 var zz: i64 = 0 889 while zz < 16 { nd_n[zz] = 0; zz = zz + 1 } 890 nd_out_n[0] = 0 891 nd_na_n[0] = 0 892 nd_nb_n[0] = 0 893 nd_ca_n[0] = 0 894 nd_carena_n[0] = 0 895 nd_bcl_n[0] = 0 896 nd_imp_n[0] = 0 897 nd_asl_n[0] = 0 898 nd_bsl_n[0] = 0 899 // !!THE ONE I MISSED. I hand-enumerated nine arena offsets here and left out the UNIT arena, so 900 // nd_ua grew monotonically across pairs and eventually walked off its 8 MB mapping. It crashed at 901 // EXACTLY pair 1,241 every time -- not the 1,241st FILE (that file alone is fine, and rows 902 // 1200-1400 as their own run are fine) but the 1,241st ACCUMULATION. Bisecting by content would 903 // never have found it; only 'n=1200 passes, n=1241 dies, the suffix alone passes' separates a 904 // cumulative fault from an input fault. 905 // => *A HAND-ENUMERATED RESET LIST IS A LIST YOU WILL MISS ONE ITEM FROM, AND THE ITEM YOU MISS 906 // STAYS INVISIBLE UNTIL THE LOOP IS LONG ENOUGH. 907 // => *A FAILURE THAT IS DETERMINISTIC AT AN ODD COUNT IS AN ACCUMULATOR, NOT AN INPUT. 908 nd_ua_n[0] = 0 909 let la: *i64 = nd_la 910 let ba: *u8 = sys_read_file(apath, la) 911 if (ba as i64) == 0 { 912 nd_puts("# NORMDIFF RED -- cannot read fileA\n" as *u8) 913 sys_exit(3); return 3 914 } 915 let lb: *i64 = nd_lb 916 let bb: *u8 = sys_read_file(bpath, lb) 917 if (bb as i64) == 0 { 918 nd_puts("# NORMDIFF RED -- cannot read fileB\n" as *u8) 919 sys_exit(3); return 3 920 } 921 // !!ARENAS ARE ALLOCATED ONCE IN main AND REUSED, NOT MAPPED PER PAIR. Nine sys_mmap calls used to 922 // live in this function with ZERO munmaps -- invisible in single-pair mode (one process, ~10 MB, 923 // then exit) and fatal in census mode: 200 pairs alone exceeded a 10-minute budget through 924 // THRASHING, not through work. Fixing the module-table leak was not enough because the arenas 925 // leaked too; the same defect had five more sites. 926 // => *AN ALLOCATION WITHOUT A FREE IS NOT A LEAK UNTIL THE CODE ENTERS A LOOP -- AND THEN EVERY 927 // SITE MATTERS, NOT THE FIRST ONE YOU FOUND. 928 // => *WHEN A BATCH MODE IS SLOWER THAN N SINGLE RUNS, SUSPECT MEMORY, NOT ALGORITHM: the 929 // per-pair CPU was measured at 19 ms and never changed. 930 // A source too large for the fixed arena is REFUSED, never silently truncated. 931 if la[0] * 2 + ND_SLACK > ND_ARENA { nd_puts("# NORMDIFF RED -- fileA exceeds the arena; refusing rather than truncating\n" as *u8); return 0 } 932 if lb[0] * 2 + ND_SLACK > ND_ARENA { nd_puts("# NORMDIFF RED -- fileB exceeds the arena; refusing rather than truncating\n" as *u8); return 0 } 933 934 nd_puts("=== nx_normdiff -- code difference vs cosmetic difference ===\n" as *u8) 935 // ORDER IS LOad-BEARING: harvest each side's consts while its buffer is still pristine, BEFORE 936 // nd_load rewrites that same buffer in place to terminate its lines. Then the optional external 937 // table fills only what neither file declared. 938 let ca: i64 = nd_scan_consts(ba, la[0], nd_ctka, nd_ctva, nd_g_ctka) 939 nd_scan_imports(ba, la[0]) // MUST precede nd_load: it rewrites this buffer in place 940 let cb: i64 = nd_scan_consts(bb, lb[0], nd_ctkb, nd_ctvb, nd_g_ctkb) 941 nd_n[10] = ca + cb 942 if argc > 4 { if nd_is_dash(argv[4] as *u8) == 0 { 943 if nd_load_consttab(argv[4] as *u8) < 0 { 944 nd_puts("# NORMDIFF RED -- cannot read consttab\n" as *u8) 945 sys_exit(3); return 3 946 } 947 } } 948 nd_load(ba, la[0], nd_rka, nd_rca, nd_g_rka, 0) 949 nd_load(bb, lb[0], nd_rkb, nd_rcb, nd_g_rkb, 1) 950 // ---- A'S COMPILATION UNIT ---- 951 // ★★★★★★A FILE IS NOT THE COMPILATION UNIT. COMPARING FILES WHILE THE LANGUAGE COMPARES UNITS 952 // MANUFACTURES DIFFERENCES THAT DO NOT EXIST. Measured 2026-08-07: 267 gates on the laptop each 953 // carry an inline `func gw(s: *u8)`, and the NAS copies carry NONE -- because the NAS refactored 954 // them to `import "nx_gate_base.nx"`, whose definition is BYTE-IDENTICAL to the inline one. A 955 // file-vs-file ruler calls that 267 files of lost work; it is one DRY refactor (Rule 15). 956 // Import-aware, 550 of 2,590 laptop-only lines (21.2%) turn out to be present in a module the NAS 957 // side imports, and 221 files become fully covered. 958 // DEPTH IS 1 BY MEASUREMENT, NOT BY LAZINESS: transitive resolution to depth 4 was measured at 959 // 568/2,590 and 231 files -- +10 files for a fixpoint walk and a cycle guard. ★FILE THE CEILING 960 // WITH THE REMEDY so the next reader does not re-derive it. 961 // This NEVER relaxes verdict_raw or verdict_norm; it is reported as its own third verdict. 962 let uarena: i64 = la[0] * 4 + ND_SLACK 963 // uarena bound is enforced by the ND_ARENA guard above 964 var ii: i64 = 0 965 var folded: i64 = 0 966 let ipath: *u8 = nd_ipath 967 // TWO RESOLUTION SITES, and the second is not optional. NishiLang resolves a bare 968 // `import "nx_syscalls.nx"` from a file in _hdl_build/ against the RUNTIME ROOT, not the file's own 969 // directory -- so dirname-only resolution silently folds ~1 of 5 imports and presents a nearly 970 // EMPTY unit. That fails in the SAFE direction (an empty unit cannot cover B, so it refuses), but 971 // it is a refusal for a FALSE REASON: measured, dirname-only found 1 adoptable file where full 972 // resolution finds hundreds. ⇒ ★★★★★A RESOLVER THAT SILENTLY MISSES IS NOT CONSERVATIVE, IT IS 973 // BLIND -- AND A BLIND INSTRUMENT THAT FAILS SAFE STILL REPORTS THE WRONG NUMBER. 974 // [importroot] (argv[5]) is the fallback; without it only same-directory imports resolve. 975 while ii < nd_imp_n[0] { 976 nd_join_dir(apath, nd_imp[ii] as *u8, ipath) 977 var got: i64 = nd_fold_module(ipath) 978 if got == 0 { 979 nd_join_root(iroot, nd_imp[ii] as *u8, ipath) 980 got = nd_fold_module(ipath) 981 } 982 if got > 0 { folded = folded + 1 } 983 ii = ii + 1 984 } 985 nd_n[11] = folded 986 // ★ASSERT THE MACHINE RAN: an unresolved import is REPORTED, never absorbed into a clean number. 987 if folded < nd_imp_n[0] { 988 nd_puts(" !! UNRESOLVED IMPORTS: " as *u8); nd_putn(nd_imp_n[0] - folded) 989 nd_puts(" of " as *u8); nd_putn(nd_imp_n[0]) 990 nd_puts(" -- A unit INCOMPLETE; unitSurplusB may overstate. Pass [importroot].\n" as *u8) 991 } 992 993 // raw surplus (the guard ruler) 994 var s: i64 = 0 995 while s < ND_SLOTS { 996 if nd_g_rka[s] == nd_gen[0] { if nd_rka[s] != 0 { 997 let line: *u8 = nd_rka[s] as *u8 998 let d: i64 = nd_rca[s] - nd_get(nd_rkb, nd_rcb, nd_g_rkb, line) 999 if d > 0 { nd_n[2] = nd_n[2] + d } 1000 } } 1001 s = s + 1 1002 } 1003 s = 0 1004 while s < ND_SLOTS { 1005 if nd_g_rkb[s] == nd_gen[0] { if nd_rkb[s] != 0 { 1006 let line: *u8 = nd_rkb[s] as *u8 1007 let d: i64 = nd_rcb[s] - nd_get(nd_rka, nd_rca, nd_g_rka, line) 1008 if d > 0 { nd_n[3] = nd_n[3] + d } 1009 } } 1010 s = s + 1 1011 } 1012 // normalised surplus (the census ruler) -- these are the lines that are REALLY different 1013 if quiet == 0 { nd_puts(" A-SURPLUS after normalisation (real code A holds and B lacks):\n" as *u8) } 1014 s = 0 1015 while s < ND_SLOTS { 1016 if nd_g_nka[s] == nd_gen[0] { if nd_nka[s] != 0 { 1017 let line: *u8 = nd_nka[s] as *u8 1018 let d: i64 = nd_nca[s] - nd_get(nd_nkb, nd_ncb, nd_g_nkb, line) 1019 if d > 0 { 1020 nd_n[7] = nd_n[7] + d 1021 if nd_asl_n[0] < ND_SURP_CAP { nd_asl[nd_asl_n[0]] = line as i64; nd_asl_n[0] = nd_asl_n[0] + 1 } 1022 nd_emit("A-SURPLUS-NORM" as *u8, d, line) 1023 if quiet == 0 { nd_sample("A" as *u8, d, line) } 1024 } 1025 } } 1026 s = s + 1 1027 } 1028 nd_n[6] = 0 1029 if quiet == 0 { nd_puts(" B-SURPLUS after normalisation (real code B holds and A lacks):\n" as *u8) } 1030 s = 0 1031 while s < ND_SLOTS { 1032 if nd_g_nkb[s] == nd_gen[0] { if nd_nkb[s] != 0 { 1033 let line: *u8 = nd_nkb[s] as *u8 1034 let d: i64 = nd_ncb[s] - nd_get(nd_nka, nd_nca, nd_g_nka, line) 1035 if d > 0 { 1036 nd_n[8] = nd_n[8] + d 1037 if nd_bsl_n[0] < ND_SURP_CAP { nd_bsl[nd_bsl_n[0]] = line as i64; nd_bsl_n[0] = nd_bsl_n[0] + 1 } 1038 nd_emit("B-SURPLUS-NORM" as *u8, d, line) 1039 if quiet == 0 { nd_sample("B" as *u8, d, line) } 1040 } 1041 } } 1042 s = s + 1 1043 } 1044 // What would adopting A destroy? Only meaningful when A actually covers B's code. 1045 // !!THE WALK MUST RUN EVEN WHEN QUIET -- it is what SETS nd_n[9]. Gating the whole block instead 1046 // of only its printing would make census mode report lostComments=0 for every file and adopt 1047 // straight through the one veto that protects a "why". 1048 // => *GATE THE OUTPUT, NEVER THE MEASUREMENT. 1049 if nd_n[8] == 0 { 1050 if quiet == 0 { nd_puts(" B-ONLY COMMENTS that adopting A would DESTROY:\n" as *u8) } 1051 nd_lost_comments() 1052 if quiet == 0 { if nd_n[9] == 0 { nd_puts(" (none)\n" as *u8) } } 1053 } 1054 1055 if quiet == 0 { if argc > 3 { if nd_is_dash(argv[3] as *u8) == 0 { 1056 let fd: i64 = sys_openat_wr(argv[3] as *u8, 0x1a4) 1057 if fd < 0 { 1058 nd_puts("# NORMDIFF RED -- cannot open outfile\n" as *u8) 1059 sys_exit(3) 1060 } 1061 sys_write(fd, nd_out, nd_out_n[0]) 1062 sys_close(fd) 1063 nd_puts("# rows written: " as *u8); nd_puts(argv[3] as *u8) 1064 nd_puts(" bytes=" as *u8); nd_putn(nd_out_n[0]); nd_puts("\n" as *u8) 1065 } } } 1066 1067 // unit surplus: lines B holds that A's WHOLE UNIT (own lines + directly imported modules) lacks. 1068 // !!THIS COMPUTATION WAS DELETED BY A REGION REWRITE THAT WAS ONLY MEANT TO GATE THE PRINTING -- 1069 // the same error the comment three blocks up warns about, committed in the very edit that added 1070 // the warning. The build caught it as "usb is not defined", which is the good outcome; had the 1071 // variable still existed with a stale value it would have shipped a silently wrong verdict. 1072 // => *WHEN YOU REWRITE A REGION TO CHANGE ITS OUTPUT, DIFF WHAT COMPUTATION LEFT WITH IT. 1073 var usb: i64 = 0 1074 var s2: i64 = 0 1075 while s2 < ND_SLOTS { 1076 if nd_g_nkb[s2] == nd_gen[0] { if nd_nkb[s2] != 0 { 1077 let line: *u8 = nd_nkb[s2] as *u8 1078 let d: i64 = nd_ncb[s2] - nd_get(nd_uka, nd_uca, nd_g_uka, line) 1079 if d > 0 { usb = usb + d } 1080 } } 1081 s2 = s2 + 1 1082 } 1083 nd_n[13] = usb // census reads it from here; a local cannot be reported by the caller 1084 if quiet == 0 { 1085 nd_puts("# NORMDIFF linesA=" as *u8); nd_putn(nd_n[0]) 1086 nd_puts(" linesB=" as *u8); nd_putn(nd_n[1]) 1087 nd_puts(" rawSurplusA=" as *u8); nd_putn(nd_n[2]) 1088 nd_puts(" rawSurplusB=" as *u8); nd_putn(nd_n[3]) 1089 nd_puts(" normSurplusA=" as *u8); nd_putn(nd_n[7]) 1090 nd_puts(" normSurplusB=" as *u8); nd_putn(nd_n[8]) 1091 nd_puts(" crA=" as *u8); nd_putn(nd_n[4]) 1092 nd_puts(" crB=" as *u8); nd_putn(nd_n[5]) 1093 nd_puts(" unitSurplusB=" as *u8); nd_putn(usb) 1094 nd_puts(" importsFolded=" as *u8); nd_putn(nd_n[11]) 1095 nd_puts(" lostComments=" as *u8); nd_putn(nd_n[9]) 1096 nd_puts(" constsLoaded=" as *u8); nd_putn(nd_n[10]) 1097 nd_puts("\n" as *u8) 1098 } 1099 1100 // verdict_raw -- nx_srcdiff semantics, unchanged. THIS is the one a guard may act on. 1101 nd_puts("# verdict_raw=" as *u8) 1102 if nd_n[2] == 0 { if nd_n[3] == 0 { nd_puts("IDENTICAL" as *u8) } } 1103 if nd_n[2] > 0 { if nd_n[3] == 0 { nd_puts("A-SUPERSET" as *u8) } } 1104 if nd_n[2] == 0 { if nd_n[3] > 0 { nd_puts("B-SUPERSET" as *u8) } } 1105 if nd_n[2] > 0 { if nd_n[3] > 0 { nd_puts("BIDIRECTIONAL" as *u8) } } 1106 nd_puts("\n" as *u8) 1107 1108 nd_puts("# verdict_norm=" as *u8) 1109 if nd_n[7] == 0 { if nd_n[8] == 0 { nd_puts("IDENTICAL" as *u8) } } 1110 if nd_n[7] > 0 { if nd_n[8] == 0 { nd_puts("A-SUPERSET" as *u8) } } 1111 if nd_n[7] == 0 { if nd_n[8] > 0 { nd_puts("B-SUPERSET" as *u8) } } 1112 if nd_n[7] > 0 { if nd_n[8] > 0 { nd_puts("BIDIRECTIONAL" as *u8) } } 1113 nd_puts("\n" as *u8) 1114 1115 nd_puts("# verdict_unit=" as *u8) 1116 if usb == 0 { nd_puts("A-UNIT-COVERS-B -- every line B holds is present in A or in a module A imports\n" as *u8) } 1117 if usb > 0 { nd_puts("B-HAS-UNIQUE -- B holds lines absent from A AND from every module A imports\n" as *u8) } 1118 1119 // ---- SUPERSEDE MATCH ---- 1120 // Every B-surplus line must be some A-surplus line minus one contiguous run, each A line used at 1121 // most once. That is the bulk-sweep shape: the laptop holds the PRE-EDIT copy of a line the NAS 1122 // edited in place, so adopting A loses nothing but the older text. 1123 var supall: i64 = 0 1124 let stbad: i64 = nd_selftest() 1125 nd_puts("# supersedeSelfTest=" as *u8) 1126 if stbad == 0 { nd_puts("13/13" as *u8) } else { nd_puts("FAILED" as *u8); nd_putn(stbad) } 1127 nd_puts("\n" as *u8) 1128 if stbad == 0 { 1129 let used: *u8 = nd_used 1130 var u: i64 = 0 1131 while u < nd_asl_n[0] { used[u] = 0 as u8; u = u + 1 } 1132 var matched: i64 = 0 1133 var bi: i64 = 0 1134 while bi < nd_bsl_n[0] { 1135 let bl: *u8 = nd_bsl[bi] as *u8 1136 var ai: i64 = 0 1137 var hit: i64 = 0 1138 while ai < nd_asl_n[0] { 1139 if hit == 0 { if used[ai] == (0 as u8) { 1140 if nd_supersedes(nd_asl[ai] as *u8, bl) == 1 { 1141 used[ai] = 1 as u8 1142 matched = matched + 1 1143 hit = 1 1144 } 1145 } } 1146 ai = ai + 1 1147 } 1148 bi = bi + 1 1149 } 1150 nd_puts("# verdict_supersede=" as *u8) 1151 if nd_bsl_n[0] == 0 { nd_puts("N/A (B holds no surplus lines)" as *u8) } 1152 if nd_bsl_n[0] > 0 { 1153 if matched == nd_bsl_n[0] { 1154 supall = 1 1155 nd_n[14] = 1 1156 nd_puts("SUPERSEDED-BY-A -- every laptop-only line is a NAS line minus one contiguous insertion; the laptop holds the PRE-EDIT copy" as *u8) 1157 } else { 1158 nd_puts("NO (" as *u8); nd_putn(matched) 1159 nd_puts(" of " as *u8); nd_putn(nd_bsl_n[0]) 1160 nd_puts(" laptop-only lines superseded)" as *u8) 1161 } 1162 } 1163 nd_puts("\n" as *u8) 1164 } 1165 1166 // ---- THE DECISION ---- 1167 // THE ESCALATION CHAIN LIVES HERE, NOT IN A LAUNCHER. It was briefly implemented in 1168 // sync_nas_ahead.ps1 -- four verdicts and their precedence, expressed in PowerShell regexes -- which 1169 // is exactly what the standing doctrine forbids: "never write a .ps1 that judges anything". A shell 1170 // script may LAUNCH a sovereign ELF and move laptop<->NAS bytes (irreducibly laptop-side, since the 1171 // MCP transport is remote http); it may not decide. Emitting one machine-readable DECISION line 1172 // leaves the launcher with nothing to do but copy bytes. 1173 // => ★A JUDGMENT EXPRESSED IN THE GLUE IS A RULER NO OTHER SEAT CAN RUN, GATE, OR BITE-PROVE. 1174 // Precedence is strongest-proof-first, and EVERY adopting branch additionally requires 1175 // lostComments=0 -- code safety never buys the right to destroy a "why" (Rule 23). 1176 var dec: i64 = 0 1177 if quiet == 0 { nd_puts("# DECISION=" as *u8) } 1178 if nd_n[3] == 0 { 1179 if quiet == 0 { nd_puts("ADOPT-A raw-safe (verdict_raw covers B; no escalation needed)" as *u8) }; dec = 1 1180 } 1181 if dec == 0 { if nd_n[9] == 0 { if nd_n[8] == 0 { 1182 if quiet == 0 { nd_puts("ADOPT-A cosmetic-only (normalised A covers B; 0 comments lost)" as *u8) }; dec = 1 1183 } } } 1184 if dec == 0 { if nd_n[9] == 0 { if usb == 0 { if nd_n[11] > 0 { 1185 if quiet == 0 { nd_puts("ADOPT-A unit-covered (A's imports supply every line B holds; 0 comments lost)" as *u8) }; dec = 1 1186 } } } } 1187 if dec == 0 { if nd_n[9] == 0 { if stbad == 0 { if supall == 1 { 1188 if quiet == 0 { nd_puts("ADOPT-A superseded (every B line is an A line minus one insertion; selftest 13/13)" as *u8) }; dec = 1 1189 } } } } 1190 if dec == 0 { 1191 if quiet == 0 { nd_puts("REFUSE -- B holds work A does not, or adopting would destroy a comment. Merge by hand." as *u8) } 1192 } 1193 nd_puts("\n" as *u8) 1194 1195 // The escalation the whole organ exists for. 1196 var cosmetic: i64 = 0 1197 if nd_n[8] == 0 { 1198 if nd_n[3] > 0 { cosmetic = 1 } 1199 } 1200 if cosmetic == 1 { 1201 nd_puts("# COSMETIC-ONLY=yes -- the raw ruler refuses this file, but every line B holds is\n" as *u8) 1202 nd_puts("# present in A once comments/whitespace/const-lifting are normalised away.\n" as *u8) 1203 nd_puts("# Adopting A is CODE-safe. lostComments=" as *u8); nd_putn(nd_n[9]) 1204 nd_puts(" -- carry those into A FIRST if non-zero.\n" as *u8) 1205 } else { 1206 nd_puts("# COSMETIC-ONLY=no\n" as *u8) 1207 } 1208 // the two source buffers are the last per-pair mappings; release them or census leaks the corpus 1209 if la[0] > 0 { sys_munmap(ba, la[0] + 16) } 1210 if lb[0] > 0 { sys_munmap(bb, lb[0] + 16) } 1211 nd_n[12] = dec 1212 return dec 1213} 1214// ATOMIC ADOPT: write to <dst>.ndtmp, fsync, then rename over <dst>. A partial write left in place 1215// is worse than no write at all -- the authoring tree would hold a truncated source that still parses 1216// far enough to mislead. rename(2) within a directory is the only step that is atomic for a reader. 1217// => *A CAMPAIGN THAT CAN BE INTERRUPTED MUST LEAVE EITHER THE OLD FILE OR THE NEW ONE, NEVER HALF. 1218func nd_copy_atomic(src: *u8, dst: *u8) -> i64 { 1219 let ln: *i64 = sys_mmap(16) as *i64 1220 ln[0] = 0 1221 let buf: *u8 = sys_read_file(src, ln) 1222 if (buf as i64) == 0 { sys_munmap(ln as *u8, 16); return 0 - 1 } 1223 let n: i64 = ln[0] 1224 let tmp: *u8 = sys_mmap(ND_MAGIC_8192) 1225 var o: i64 = 0 1226 while dst[o] != (0 as u8) { tmp[o] = dst[o]; o = o + 1 } 1227 let sfx: *u8 = ".ndtmp" as *u8 1228 var k: i64 = 0 1229 while sfx[k] != (0 as u8) { tmp[o] = sfx[k]; o = o + 1; k = k + 1 } 1230 tmp[o] = 0 as u8 1231 let fd: i64 = sys_openat_wr(tmp, 0x1a4) 1232 if fd < 0 { 1233 if n > 0 { sys_munmap(buf, n + 16) } 1234 sys_munmap(ln as *u8, 16); sys_munmap(tmp, ND_MAGIC_8192); return 0 - 2 1235 } 1236 let w: i64 = sys_write(fd, buf, n) 1237 sys_fsync(fd) 1238 sys_close(fd) 1239 if w != n { 1240 if n > 0 { sys_munmap(buf, n + 16) } 1241 sys_munmap(ln as *u8, 16); sys_munmap(tmp, ND_MAGIC_8192); return 0 - 3 1242 } 1243 let r: i64 = sys_renameat(tmp, dst) 1244 if n > 0 { sys_munmap(buf, n + 16) } 1245 sys_munmap(ln as *u8, 16) 1246 sys_munmap(tmp, ND_MAGIC_8192) 1247 if r != 0 { return 0 - 4 } 1248 return n 1249} 1250func nd_is_adopt(s: *u8) -> i64 { 1251 if s[0] != (97 as u8) { return 0 } 1252 if s[1] != (100 as u8) { return 0 } 1253 if s[2] != (111 as u8) { return 0 } 1254 if s[3] != (112 as u8) { return 0 } 1255 if s[4] != (116 as u8) { return 0 } 1256 if s[5] != (0 as u8) { return 0 } 1257 return 1 1258} 1259func nd_is_apply(s: *u8) -> i64 { 1260 let a: *u8 = "--apply" as *u8 1261 var i: i64 = 0 1262 while i < 7 { if s[i] != a[i] { return 0 } i = i + 1 } 1263 if s[7] != (0 as u8) { return 0 } 1264 return 1 1265} 1266func nd_is_census(s: *u8) -> i64 { 1267 if s[0] != (99 as u8) { return 0 } 1268 if s[1] != (101 as u8) { return 0 } 1269 if s[2] != (110 as u8) { return 0 } 1270 if s[3] != (115 as u8) { return 0 } 1271 if s[4] != (117 as u8) { return 0 } 1272 if s[5] != (115 as u8) { return 0 } 1273 if s[6] != (0 as u8) { return 0 } 1274 return 1 1275} 1276// THE WHOLE CENSUS, IN THE ORGAN. Reads a relpath list, compares each pair under both roots, and 1277// writes one machine row per pair to stdout. No shell loop, no sed, no second implementation of any 1278// ruler -- every emitted field is read straight out of nd_n[] after the SAME nd_run_pair the 1279// single-pair mode calls. 1280// => *THE CURE FOR TWO IMPLEMENTATIONS OF ONE RULER IS NOT TO SYNC THEM, IT IS TO DELETE ONE. 1281// A row that cannot be read emits decision=UNREADABLE rather than being skipped: a census that 1282// silently drops rows reports a denominator it did not measure. 1283// JUDGE AND ADOPT IN ONE ORGAN. The converge_*.sh scripts this replaces each looped over a list, 1284// invoked the organ, parsed `unitSurplusB` and `lostComments` back out with sed, re-implemented the 1285// precedence in shell, and then copied. Three of the four steps were the organ's job. 1286// Dry by default: `adopt` reports, `adopt ... --apply` writes. A campaign whose default is to mutate 1287// is a campaign that will mutate by accident. 1288func nd_adopt(listp: *u8, aroot: *u8, broot: *u8, iroot: *u8, apply: i64) -> i64 { 1289 let ln: *i64 = sys_mmap(16) as *i64 1290 ln[0] = 0 1291 let lb: *u8 = sys_read_file(listp, ln) 1292 if (lb as i64) == 0 { 1293 nd_puts("# ADOPT RED -- cannot read the relpath list\n" as *u8) 1294 sys_exit(3) 1295 return 3 1296 } 1297 let n: i64 = ln[0] 1298 let pa: *u8 = sys_mmap(ND_MAGIC_8192) 1299 let pb: *u8 = sys_mmap(ND_MAGIC_8192) 1300 // THE CAMPAIGN HANDS ITS VERIFIER A POPULATION ONLY IT COULD HAVE WRITTEN. <list>.adopted is 1301 // truncated here and appended to ONLY on a successful write, so nx_campaign_verify's denominator 1302 // is exactly what this run changed -- not what a dry run listed, and not what a previous run left 1303 // behind. That is not hypothetical: a shell version of this campaign appended from its DRY RUN 1304 // too, and a later proof reported rows=89 (54 distinct, 35 duplicates) for a run that had adopted 1305 // 40. It PASSED, on a population that was not the one reported. 1306 // => *A VERIFIER IS ONLY AS HONEST AS THE LIST IT IS HANDED. 1307 let alp: *u8 = sys_mmap(ND_MAGIC_8192) 1308 var ao: i64 = 0 1309 while listp[ao] != (0 as u8) { alp[ao] = listp[ao]; ao = ao + 1 } 1310 let asfx: *u8 = ".adopted" as *u8 1311 var ak: i64 = 0 1312 while asfx[ak] != (0 as u8) { alp[ao] = asfx[ak]; ao = ao + 1; ak = ak + 1 } 1313 alp[ao] = 0 as u8 1314 var afd: i64 = 0 - 1 1315 if apply == 1 { afd = sys_openat_wr(alp, 0x1a4) } 1316 var rows: i64 = 0 1317 var ok: i64 = 0 1318 var wrote: i64 = 0 1319 var failed: i64 = 0 1320 var p: i64 = 0 1321 while p < n { 1322 let base: i64 = lb as i64 1323 var e: i64 = p 1324 var go: i64 = 1 1325 while go == 1 { 1326 if e >= n { go = 0 } else { 1327 if lb[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 1328 } 1329 } 1330 var t: i64 = e 1331 if t > p { if lb[t-1] == (ND_CR as u8) { t = t - 1 } } 1332 lb[t] = 0 as u8 1333 let rel: *u8 = (base + p) as *u8 1334 if rel[0] != (0 as u8) { 1335 nd_join_root(aroot, rel, pa) 1336 nd_join_root(broot, rel, pb) 1337 let d: i64 = nd_run_pair(pa, pb, iroot, 1, 0, 0 as *i64) 1338 rows = rows + 1 1339 if d == 1 { 1340 ok = ok + 1 1341 if apply == 1 { 1342 let rc: i64 = nd_copy_atomic(pa, pb) 1343 if rc >= 0 { 1344 wrote = wrote + 1 1345 if afd >= 0 { 1346 var rl: i64 = 0 1347 while rel[rl] != (0 as u8) { rl = rl + 1 } 1348 sys_write(afd, rel, rl) 1349 sys_write(afd, "\n" as *u8, 1) 1350 } 1351 } else { 1352 failed = failed + 1 1353 nd_puts(" WRITE-FAILED rc=" as *u8); nd_putn(rc) 1354 nd_puts(" " as *u8); nd_puts(rel); nd_puts("\n" as *u8) 1355 } 1356 } 1357 } 1358 } 1359 p = e + 1 1360 } 1361 if afd >= 0 { sys_fsync(afd); sys_close(afd) } 1362 if apply == 1 { nd_puts("# adopted list: " as *u8); nd_puts(alp); nd_puts("\n" as *u8) } 1363 nd_puts("# ADOPT rows=" as *u8); nd_putn(rows) 1364 nd_puts(" adoptable=" as *u8); nd_putn(ok) 1365 nd_puts(" written=" as *u8); nd_putn(wrote) 1366 nd_puts(" writeFailed=" as *u8); nd_putn(failed) 1367 if apply == 0 { nd_puts(" (DRY RUN -- pass --apply to write)" as *u8) } 1368 nd_puts("\n" as *u8) 1369 // A write that failed must not be reported as a clean run: the caller's next step is a proof over 1370 // the adopted list, and a silent shortfall would make that proof measure a smaller population. 1371 if failed > 0 { sys_exit(1); return 1 } 1372 sys_exit(0) 1373 return 0 1374} 1375func nd_census(listp: *u8, aroot: *u8, broot: *u8, iroot: *u8) -> i64 { 1376 let ln: *i64 = sys_mmap(16) as *i64 1377 ln[0] = 0 1378 let lb: *u8 = sys_read_file(listp, ln) 1379 if (lb as i64) == 0 { 1380 nd_puts("# CENSUS RED -- cannot read the relpath list\n" as *u8) 1381 sys_exit(3) 1382 return 3 1383 } 1384 let n: i64 = ln[0] 1385 let pa: *u8 = sys_mmap(ND_MAGIC_8192) 1386 let pb: *u8 = sys_mmap(ND_MAGIC_8192) 1387 var rows: i64 = 0 1388 var adopt: i64 = 0 1389 var p: i64 = 0 1390 while p < n { 1391 let base: i64 = lb as i64 1392 var e: i64 = p 1393 var go: i64 = 1 1394 while go == 1 { 1395 if e >= n { go = 0 } else { 1396 if lb[e] == (ND_LF as u8) { go = 0 } else { e = e + 1 } 1397 } 1398 } 1399 var t: i64 = e 1400 if t > p { if lb[t-1] == (ND_CR as u8) { t = t - 1 } } 1401 lb[t] = 0 as u8 1402 let rel: *u8 = (base + p) as *u8 1403 if rel[0] != (0 as u8) { 1404 nd_join_root(aroot, rel, pa) 1405 nd_join_root(broot, rel, pb) 1406 let d: i64 = nd_run_pair(pa, pb, iroot, 1, 0, 0 as *i64) 1407 rows = rows + 1 1408 if d == 1 { adopt = adopt + 1 } 1409 nd_puts(rel); nd_puts(" " as *u8) 1410 if d == 1 { nd_puts("ADOPT-A" as *u8) } else { nd_puts("REFUSE" as *u8) } 1411 nd_puts(" " as *u8); nd_putn(nd_n[7]) 1412 nd_puts(" " as *u8); nd_putn(nd_n[8]) 1413 nd_puts(" " as *u8); nd_putn(nd_n[13]) 1414 nd_puts(" " as *u8); nd_putn(nd_n[9]) 1415 nd_puts(" " as *u8); nd_putn(nd_n[14]) 1416 nd_puts("\n" as *u8) 1417 } 1418 p = e + 1 1419 } 1420 nd_puts("# CENSUS rows=" as *u8); nd_putn(rows) 1421 nd_puts(" adoptable=" as *u8); nd_putn(adopt) 1422 nd_puts(" refused=" as *u8); nd_putn(rows - adopt) 1423 nd_puts("\n" as *u8) 1424 sys_exit(0) 1425 return 0 1426} 1427func main(argc: i64, argv: *i64) -> i64 { 1428 nd_num = sys_mmap(ND_NUMBUF) 1429 nd_rev = sys_mmap(ND_NUMBUF) 1430 nd_tok = sys_mmap(ND_TOK) 1431 if argc < 3 { 1432 nd_puts("usage: nx_normdiff <fileA> <fileB> [outfile] [consttab]\n" as *u8) 1433 nd_puts(" Answers: is this difference made of CODE, or of COSMETICS?\n" as *u8) 1434 nd_puts(" verdict_raw = nx_srcdiff semantics (the GUARD -- what may safely be ACTED on)\n" as *u8) 1435 nd_puts(" verdict_norm = after dropping comments, collapsing whitespace, and resolving\n" as *u8) 1436 nd_puts(" consts from [consttab] to their DECLARED values (the CENSUS).\n" as *u8) 1437 nd_puts(" COSMETIC-ONLY = raw says BIDIRECTIONAL but normalised says A covers B: adopting A\n" as *u8) 1438 nd_puts(" is CODE-safe. Any B-only comment it would destroy is printed.\n" as *u8) 1439 nd_puts(" Const values come from EACH FILE'S OWN declarations (99.946% are file-local).\n" as *u8) 1440 nd_puts(" [consttab] is an optional NAME<TAB>VALUE fallback; file-local always wins.\n" as *u8) 1441 nd_puts(" [importroot] resolves A imports not in A own directory (e.g. a bare\n" as *u8) 1442 nd_puts(" nx_syscalls.nx imported from _hdl_build/). verdict_unit needs it.\n" as *u8) 1443 nd_puts(" Values are READ, never inferred: LZ4_MAGIC_0 is declared 0x04, so a name-digit\n" as *u8) 1444 nd_puts(" un-lift would equate 0 with 4 and call two different files identical.\n" as *u8) 1445 sys_exit(2) 1446 return 2 1447 } 1448 nd_rka = sys_mmap(ND_SLOTS * 8) as *i64 1449 nd_rca = sys_mmap(ND_SLOTS * 8) as *i64 1450 nd_rkb = sys_mmap(ND_SLOTS * 8) as *i64 1451 nd_rcb = sys_mmap(ND_SLOTS * 8) as *i64 1452 nd_nka = sys_mmap(ND_SLOTS * 8) as *i64 1453 nd_nca = sys_mmap(ND_SLOTS * 8) as *i64 1454 nd_nkb = sys_mmap(ND_SLOTS * 8) as *i64 1455 nd_ncb = sys_mmap(ND_SLOTS * 8) as *i64 1456 nd_ack = sys_mmap(ND_SLOTS * 8) as *i64 1457 nd_acv = sys_mmap(ND_SLOTS * 8) as *i64 1458 nd_ctka = sys_mmap(ND_SLOTS * 8) as *i64 1459 nd_ctva = sys_mmap(ND_SLOTS * 8) as *i64 1460 nd_ctkb = sys_mmap(ND_SLOTS * 8) as *i64 1461 nd_ctvb = sys_mmap(ND_SLOTS * 8) as *i64 1462 nd_carena_n = sys_mmap(16) as *i64 1463 nd_bcl = sys_mmap(ND_BCL_CAP * 16) as *i64 1464 nd_bcl_n = sys_mmap(16) as *i64 1465 nd_uka = sys_mmap(ND_SLOTS * 8) as *i64 1466 nd_uca = sys_mmap(ND_SLOTS * 8) as *i64 1467 nd_ua_n = sys_mmap(16) as *i64 1468 nd_imp = sys_mmap(ND_MAXIMP * 8) as *i64 1469 nd_imp_n = sys_mmap(16) as *i64 1470 nd_asl = sys_mmap(ND_SURP_CAP * 8) as *i64 1471 nd_asl_n = sys_mmap(16) as *i64 1472 nd_bsl = sys_mmap(ND_SURP_CAP * 8) as *i64 1473 nd_bsl_n = sys_mmap(16) as *i64 1474 nd_gen = sys_mmap(16) as *i64 1475 nd_gen[0] = 1 1476 nd_la = sys_mmap(16) as *i64 1477 nd_lb = sys_mmap(16) as *i64 1478 nd_ipath = sys_mmap(ND_MAGIC_8192) 1479 nd_used = sys_mmap(ND_SURP_CAP) 1480 nd_na = sys_mmap(ND_ARENA) 1481 nd_nb = sys_mmap(ND_ARENA) 1482 nd_ca_buf = sys_mmap(ND_ARENA) 1483 nd_carena = sys_mmap(ND_ARENA) 1484 nd_ua = sys_mmap(ND_ARENA) 1485 nd_g_rka = sys_mmap(ND_SLOTS * 8) as *i64 1486 nd_g_rkb = sys_mmap(ND_SLOTS * 8) as *i64 1487 nd_g_nka = sys_mmap(ND_SLOTS * 8) as *i64 1488 nd_g_nkb = sys_mmap(ND_SLOTS * 8) as *i64 1489 nd_g_uka = sys_mmap(ND_SLOTS * 8) as *i64 1490 nd_g_ack = sys_mmap(ND_SLOTS * 8) as *i64 1491 nd_g_ctka = sys_mmap(ND_SLOTS * 8) as *i64 1492 nd_g_ctkb = sys_mmap(ND_SLOTS * 8) as *i64 1493 nd_n = sys_mmap(128) as *i64 1494 nd_out_n = sys_mmap(16) as *i64 1495 nd_na_n = sys_mmap(16) as *i64 1496 nd_nb_n = sys_mmap(16) as *i64 1497 nd_ca_n = sys_mmap(16) as *i64 1498 nd_out = sys_mmap(ND_OUTBUF) 1499 nd_out_n[0] = 0 1500 nd_na_n[0] = 0 1501 nd_nb_n[0] = 0 1502 nd_ca_n[0] = 0 1503 nd_carena_n[0] = 0 1504 nd_bcl_n[0] = 0 1505 nd_ua_n[0] = 0 1506 nd_imp_n[0] = 0 1507 nd_asl_n[0] = 0 1508 nd_bsl_n[0] = 0 1509 var z: i64 = 0 1510 while z < 12 { nd_n[z] = 0; z = z + 1 } 1511 1512// ONE PAIR, ONE ANSWER -- extracted from main so a BATCH can call it without a shell loop. 1513// The operator directive is the estate doctrine: NishiLang for everything, shell is break-glass 1514// only, and "every step that computes, decides, filters, counts or formats is an organ". The 1515// census driver I had written used `sed` to parse this organ's own stdout and a `while read` 1516// loop to iterate -- shell doing the filtering and the formatting, one process per pair. 1517// => *IF THE ANSWER HAS TO BE PARSED BACK OUT OF STDOUT BY A SHELL, THE ORGAN STOPPED ONE 1518// STEP SHORT. Emit the row yourself. 1519// `quiet` suppresses the human report so census mode emits ONLY machine rows; every verdict is 1520// left in nd_n[] for the caller to format. Returns 1 if the DECISION was ADOPT-A, else 0. 1521 // ---- DISPATCH ---- 1522 // census: iterate a relpath list IN THIS PROCESS. The shell driver it replaces launched one 1523 // process per pair (measured: 79 ms of bare `wsl` launch against 19 ms of actual work) and then 1524 // parsed the verdicts back out with `sed`. Both halves were the shell doing the organ's job. 1525 if argc > 1 { if nd_is_adopt(argv[1] as *u8) == 1 { 1526 if argc < 6 { 1527 nd_puts("usage: nx_normdiff adopt <relpath-list> <A-root> <B-root> <import-root> [--apply]\n" as *u8) 1528 nd_puts(" Judges every row and, with --apply, ATOMICALLY copies A over B where the DECISION\n" as *u8) 1529 nd_puts(" is ADOPT-A. Dry by default. Verify afterwards with nx_campaign_verify.\n" as *u8) 1530 sys_exit(2) 1531 return 2 1532 } 1533 var ap: i64 = 0 1534 if argc > 6 { if nd_is_apply(argv[6] as *u8) == 1 { ap = 1 } } 1535 return nd_adopt(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, ap) 1536 } } 1537 if argc > 1 { if nd_is_census(argv[1] as *u8) == 1 { 1538 if argc < 6 { 1539 nd_puts("usage: nx_normdiff census <relpath-list> <A-root> <B-root> <import-root>\n" as *u8) 1540 nd_puts(" Emits one TSV row per pair: relpath, decision, verdict_supersede, normSurplusA,\n" as *u8) 1541 nd_puts(" normSurplusB, unitSurplusB, lostComments -- every field COPIED from the same code\n" as *u8) 1542 nd_puts(" path the single-pair mode runs, so there is no second ruler to drift.\n" as *u8) 1543 sys_exit(2) 1544 return 2 1545 } 1546 return nd_census(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8) 1547 } } 1548 nd_run_pair(argv[1] as *u8, argv[2] as *u8, argv[5] as *u8, 0, argc, argv) 1549 sys_exit(0) 1550 return 0 1551}