nx_incclosure_lib.nx source
↩ module page · 129 lines · 6412 B
1// nx_incclosure_lib.nx -- ONE OWNER for import-closure NAME->PATH resolution (2026-08-25, rung LN10).
2//
3// WHY THIS IS A LIB AND NOT A SECOND COPY.
4// nx_inc_compile (the per-module incremental build cache) must answer, for each module named in a
5// recorded closure, "is the file the compiler would read still the same bytes?" -- and it can only ask
6// that of a PATH. nx_closurehash already owns that resolution and the order is load-bearing:
7// runtime/_hdl_build/<name> FIRST (buildrun compiles that twin when both exist)
8// runtime/<name> SECOND
9// runtime/<subdir>/<name> a getdents64 sweep, looped until the kernel says stop
10// A SECOND RESOLVER THAT DISAGREES BY ONE PATH HASHES THE WRONG FILE, REPORTS A CACHE HIT, AND SERVES A
11// STALE ARTIFACT. That is a silent miscompile -- the worst class this estate tracks, and the one a cache
12// exists to risk. So the resolver is EXTRACTED here and BOTH organs call it: disagreement is impossible
13// by construction rather than by discipline, which is the estate's own standing answer whenever two
14// organs must agree. nx_closurehash keeps its function names and DELEGATES, so its callers are untouched
15// and its neutrality is provable by nx_behaveprobe (live vs staged) rather than asserted.
16//
17// license_tier: ORIGINAL No hw writes (Rule 26).
18import "nx_syscalls.nx"
19
20// Widths and bounds published HERE so a consumer inherits the ruler's calibration instead of inventing
21// its own. ICL_MAXF is the closure-entry bound nx_closurehash REFUSES past rather than publishing a
22// partial hash; a consumer that stores closure rows inherits the same bound for the same reason.
23const ICL_MAXF: i64 = 1024
24const ICL_NAMEW: i64 = 192
25const ICL_SHA_HEX: i64 = 64
26const ICL_PATHW: i64 = 1024
27const ICL_DIRBUF: i64 = 262144 // getdents64 window; the sweep LOOPS, so this is a window not a cap
28const ICL_SWEEPW: i64 = 512 // "<root>/runtime" scratch
29const ICL_DOT: i64 = 46
30const ICL_DT_DIR: i64 = 4
31const ICL_DENT_RECLEN_OFF: i64 = 16
32const ICL_DENT_NAME_OFF: i64 = 19
33const ICL_DENT_TYPE_OFF: i64 = 18
34
35func icl_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
36func icl_cat(d: *u8, o: i64, s: *u8) -> i64 { var x: i64 = o; var i: i64 = 0; while s[i] != (0 as u8) { d[x] = s[i]; x = x + 1; i = i + 1 } return x }
37func icl_seq(a: *u8, b: *u8) -> i64 {
38 var i: i64 = 0
39 var go: i64 = 1
40 while go == 1 {
41 let x: i64 = a[i] as i64
42 let y: i64 = b[i] as i64
43 if x != y { return 0 }
44 if x == 0 { go = 0 }
45 i = i + 1
46 }
47 return 1
48}
49func icl_exists(p: *u8) -> i64 { let fd: i64 = sys_openat_rd(p); if fd < 0 { return 0 } sys_close(fd); return 1 }
50
51// 32-byte digest -> 64 lowercase hex chars at out, NUL-terminated. ONE encoder for both consumers.
52func icl_hex(dig: *u8, out: *u8) -> i64 {
53 var i: i64 = 0
54 while i < 32 {
55 let v: i64 = dig[i] as i64
56 let hi: i64 = (v >> 4) & 15
57 let lo: i64 = v & 15
58 if hi < 10 { out[i*2] = (48+hi) as u8 } else { out[i*2] = (87+hi) as u8 }
59 if lo < 10 { out[i*2+1] = (48+lo) as u8 } else { out[i*2+1] = (87+lo) as u8 }
60 i = i + 1
61 }
62 out[ICL_SHA_HEX] = 0 as u8
63 return ICL_SHA_HEX
64}
65
66// Resolve a bare source name (e.g. "nx_syscalls.nx") to a path under root. Returns 1 and fills `out`,
67// or 0 when it resolves nowhere. EXTRACTED VERBATIM from nx_closurehash.chresolve 2026-08-25 -- the
68// comments below are its own, kept because they are the evidence for the order:
69//
70// MEASURED 2026-08-07: a two-path resolver (_hdl_build, runtime) reported unresolved=2 on nx_mgmt_api --
71// nx_h2c_p256.nx and nx_voprf.nx -- while that exact target COMPILES CLEAN. They live in `runtime/hub/`,
72// a THIRD search path I did not know about. *AN INCOMPLETE RESOLVER REPORTS A HEALTHY TREE AS BROKEN*.
73// Hardcoding the directory list would drift from nx_cc the first time a lane adds a folder, so after the
74// two ORDERED paths this ENUMERATES the subdirectories of runtime/ and tries each. The order still
75// matters and is preserved: _hdl_build first, then runtime, then any subdir -- so a name present in two
76// places still resolves to the one the compiler would read.
77func icl_resolve(root: *u8, name: *u8, out: *u8) -> i64 {
78 var o: i64 = icl_cat(out, 0, root)
79 o = icl_cat(out, o, "/runtime/_hdl_build/" as *u8)
80 o = icl_cat(out, o, name)
81 out[o] = 0 as u8
82 if icl_exists(out) == 1 { return 1 }
83 var o2: i64 = icl_cat(out, 0, root)
84 o2 = icl_cat(out, o2, "/runtime/" as *u8)
85 o2 = icl_cat(out, o2, name)
86 out[o2] = 0 as u8
87 if icl_exists(out) == 1 { return 1 }
88 // subdirectory sweep -- getdents64 looped until the kernel says stop, because one call returns only
89 // what fits the buffer and a partial listing here would silently under-resolve again.
90 let rp: *u8 = sys_mmap(ICL_SWEEPW)
91 var ro: i64 = icl_cat(rp, 0, root)
92 ro = icl_cat(rp, ro, "/runtime" as *u8)
93 rp[ro] = 0 as u8
94 let dfd: i64 = sys_openat_rd(rp)
95 if dfd < 0 { return 0 }
96 let db: *u8 = sys_mmap(ICL_DIRBUF)
97 var found: i64 = 0
98 var go: i64 = 1
99 while go == 1 {
100 let n: i64 = sys_getdents64(dfd, db, ICL_DIRBUF)
101 if n <= 0 { go = 0 } else {
102 var p: i64 = 0
103 while p < n {
104 let reclen: i64 = (db[p+ICL_DENT_RECLEN_OFF] as i64) | ((db[p+ICL_DENT_RECLEN_OFF+1] as i64) << 8)
105 if reclen <= 0 { p = n } else {
106 let dtype: i64 = db[p+ICL_DENT_TYPE_OFF] as i64
107 let nm: *u8 = ((db as i64) + p + ICL_DENT_NAME_OFF) as *u8
108 var ok: i64 = 0
109 if dtype == ICL_DT_DIR { ok = 1 }
110 if (nm[0] as i64) == ICL_DOT { ok = 0 }
111 if found == 1 { ok = 0 }
112 if ok == 1 {
113 var o3: i64 = icl_cat(out, 0, root)
114 o3 = icl_cat(out, o3, "/runtime/" as *u8)
115 o3 = icl_cat(out, o3, nm)
116 o3 = icl_cat(out, o3, "/" as *u8)
117 o3 = icl_cat(out, o3, name)
118 out[o3] = 0 as u8
119 if icl_exists(out) == 1 { found = 1 }
120 }
121 p = p + reclen
122 }
123 }
124 }
125 }
126 sys_close(dfd)
127 if found == 1 { return 1 }
128 return 0
129}