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}