nx_inc_compile.nx source
↩ module page · 1250 lines · 56944 B
1// nx_inc_compile.nx -- LN10: THE PER-MODULE INCREMENTAL BUILD CACHE.
2//
3// CONTRACT (buildroot/knowledge/compare/lang.plan, rung LN10, verbatim):
4// "Per-module cache keyed by the closure sha the build door already computes (the B2 cache keys whole
5// targets); gate proves a one-file edit rebuilds only its dependents and the output equals a full build"
6//
7// WHAT ALREADY EXISTED, AND WHAT WAS MISSING
8// ------------------------------------------
9// B2 (nx_sov_build_run's bld_cache_cas) is a WHOLE-TARGET content-addressed cache: key = sha256 over the
10// EXPANDED closure text plus the compiler and assembler shas, value = the finished elf. It is excellent
11// and it is not this rung. Its unit is ONE target and its key costs a full closure EXPANSION (a 32 MiB
12// buffer, every byte of every dependency) paid once PER TARGET -- so it can answer "rebuild this target?"
13// only after doing most of the work of deciding. The question a multi-target session actually asks is the
14// DEPENDENTS question: "I edited one file -- which targets must rebuild, and which must not?" Nothing in
15// the estate could answer that without re-walking every closure.
16//
17// THE KEY WAS ALREADY SOLVED AND IS NOT RE-INVENTED HERE.
18// nx_closurehash is the estate's ONE closure ruler; /api/build forks it on every build and records
19// `closure_sha256` into <target>.provenance. This organ NEVER computes a closure sha. It parses the
20// ruler's own per-file listing -- which is already a per-MODULE manifest, one `<sha256> <bytes> <name>`
21// row per source -- stores those rows as the per-module cache, and REPLAYS the ruler's own key on a hit.
22// There is exactly one closure-sha implementation in the estate and this is not a second one.
23//
24// WHY PER-MODULE SHAS ARE SUFFICIENT -- stated because a cache that returns a stale object is a silent
25// miscompile, the worst class this estate tracks:
26// closure_sha = sha256 over the rows "<name> <sha>" in discovery order. It therefore changes iff the
27// member SET changes or some member's SHA changes. The member set can change only because some module's
28// import list changed, or because a name started resolving to a different file -- and BOTH of those
29// change that module's bytes, hence its sha. So:
30// every recorded module still hashes to its recorded sha <=> closure_sha unchanged.
31// The gate proves the forward direction against the LIVE ruler on every subject it touches, and the
32// neg-control proves the converse (a one-byte edit MISSES).
33// The one assumption is that resolution is the SAME resolution the ruler performed -- which is why the
34// resolver lives in nx_incclosure_lib and both organs call it. A private copy here would be the exact
35// defect that produces a false HIT.
36//
37// THE INVARIANT THAT MAKES A SHARED MODULE TABLE SOUND (and the dependents mechanism itself):
38// for every target record T and every module M in T's member list, icm_rsha[M] is the sha that was used
39// when T's key was recorded. It is maintained by ic_record(): writing a NEW sha for M INVALIDATES every
40// other target whose member list contains M (announced, one INC-INVALIDATE line each). Without that,
41// recording target B would silently re-bless target A's stale key -- a false HIT by construction.
42// THAT invalidation IS "a one-file edit rebuilds only its dependents": the dependents are exactly the
43// targets holding the edited module, and no others are touched.
44//
45// RESOURCE ENVELOPE (a change that hammers the box is a bug even when its feature works):
46// ALLOCATES one growable module table + one growable target table + one flat member pool, all doubling
47// from a start WINDOW (never a ceiling), plus one capture buffer per ruler/builder fork.
48// FREES at process exit; this is a one-shot organ, not a daemon.
49// ON DISK ONE file, <root>/_build/inccache/index, rewritten atomically (tmp + renameat). Its budget
50// is knowledge/inccache.conf cache_max_bytes; over budget it EVICTS oldest-used targets
51// first, then garbage-collects modules no surviving target references, and ANNOUNCES both.
52// It never grows without bound and never truncates silently.
53// COSTS the warm path reads and hashes each distinct module ONCE per run no matter how many
54// targets share it (INC-SUMMARY prints modules_hashed and modules_reused, so the sharing is
55// measured, not claimed). It forks NOTHING on a hit.
56//
57// HOW THIS LAYERS WITH B2 -- three costs, all measured 2026-08-25 on the live NAS, same machine:
58// LN10 HIT ~1 ms per target, ZERO forks. The decision is per-module sha equality; nothing is
59// expanded, nothing is compiled, the builder is never started.
60// B2 HIT ~150 ms per target. LN10 missed, so the builder WAS forked; it expanded the closure,
61// hashed it, found its content-addressed entry and copied the artifact back.
62// real compile seconds. Both caches missed and nx_cc + nxasm actually ran.
63// They compose in exactly that order and neither replaces the other: B2 answers "must this target be
64// compiled?" after paying for a full closure expansion, LN10 answers "must this target be handed to the
65// builder at all?" before paying for anything. Measured across 5 real targets sharing 12 modules, a warm
66// decision for the whole set cost 2 ms against 9,274 ms to record them cold.
67//
68// nx_inc_compile plan <target...> [--root=<dir>] [--no-cache] decide only: zero forks, zero writes
69// nx_inc_compile build <target...> [--root=<dir>] [--no-cache] plan, then build ONLY the non-hits
70// nx_inc_compile seed <target...> [--root=<dir>] record the current closure, no build
71// nx_inc_compile stat [--root=<dir>] cache census + budget
72// exit 0 ok | 1 a build failed | 2 usage | 4 the ruler or builder could not be located
73//
74// RESOLVED 2026-08-26 -- THIS WARNING IS OBSOLETE AND IS KEPT ONLY SO THE NEXT READER IS NOT SURPRISED
75// BY ITS ABSENCE. It used to read: the `build` verb forks nx_sov_build_run, which refresh-installs the
76// artifact into the serving root EVEN UNDER --build-only. That is no longer true. nx_sov_build_run now
77// delegates the question to nx_builddeploy_lib, and --build-only installs NOTHING, anywhere -- it prints
78// SERVING-ROOT TWIN HELD and names the staged artifact instead. This organ still prints the builder path
79// it forked, and it still adds no deploy of its own.
80// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0
81import "nx_syscalls.nx"
82import "nx_sha256.nx"
83import "nx_itoa_lib.nx"
84import "nx_lineconf_lib.nx"
85import "nx_tool_run.nx"
86import "nx_incclosure_lib.nx"
87
88// ---- growth WINDOWS, not bounds: every table doubles (ic_mod_grow / ic_tgt_grow / ic_mem_grow) --------
89const IC_TAB_INIT: i64 = 256
90const IC_MEM_INIT: i64 = 4096
91// widest ruler row: 64 sha + 2 spaces + 20 decimal digits + 2 spaces + ICL_NAMEW name + newline = 280.
92// 320 rounds that up; the capture is ICL_MAXF rows of it plus a slack for the header and summary lines,
93// and a TRUNCATED capture is REFUSED (never parsed), because a short member list is a false HIT waiting.
94const IC_ROWW: i64 = 320
95const IC_SLACK: i64 = 4096
96const IC_SHOWSHA: i64 = 12 // digest prefix shown in a dirty-module line (full shas stay in the index)
97const IC_BUDGET_BOOTSTRAP: i64 = 1048576
98const IC_RULER_TMO_BOOT: i64 = 120000
99const IC_BUILD_TMO_BOOT: i64 = 900000
100const IC_PIPE: i64 = 124
101const IC_COMMA: i64 = 44
102const IC_NL: i64 = 10
103const IC_SP: i64 = 32
104const IC_HASH: i64 = 35
105const IC_DIRMODE: i64 = 0x1ed // 0755
106const IC_FILEMODE: i64 = 0x1a4 // 0644
107const IC_OK: i64 = 0
108const IC_BUILD_FAIL: i64 = 1
109const IC_USAGE: i64 = 2
110const IC_NO_TOOL: i64 = 4
111const IC_HIT: i64 = 1
112const IC_MISS: i64 = 0
113const IC_NEW: i64 = 0 - 1
114
115func ic_ruler_cap() -> i64 { return ICL_MAXF * IC_ROWW + IC_SLACK }
116func ic_shaw() -> i64 { return ICL_SHA_HEX + 1 }
117
118func ic_p(s: *u8) -> i64 { sys_write(1, s, icl_len(s)); return 0 }
119func ic_e(s: *u8) -> i64 { sys_write(2, s, icl_len(s)); return 0 }
120func ic_n(v: i64) -> i64 { nxi_out(v); return 0 }
121func ic_nl() -> i64 { let b: *u8 = sys_mmap(2); b[0] = IC_NL as u8; sys_write(1, b, 1); sys_munmap(b, 2); return 0 }
122func ic_hexeq(a: *u8, b: *u8) -> i64 {
123 var i: i64 = 0
124 while i < ICL_SHA_HEX { if a[i] != b[i] { return 0 } i = i + 1 }
125 return 1
126}
127func ic_starts(s: *u8, pfx: *u8) -> i64 {
128 var i: i64 = 0
129 while pfx[i] != (0 as u8) { if s[i] != pfx[i] { return 0 } i = i + 1 }
130 return 1
131}
132func ic_ishex64(b: *u8, off: i64, n: i64) -> i64 {
133 if off + ICL_SHA_HEX > n { return 0 }
134 var i: i64 = 0
135 while i < ICL_SHA_HEX {
136 let c: i64 = b[off+i] as i64
137 var ok: i64 = 0
138 if c >= 48 { if c <= 57 { ok = 1 } }
139 if c >= 97 { if c <= 102 { ok = 1 } }
140 if ok == 0 { return 0 }
141 i = i + 1
142 }
143 return 1
144}
145
146// ---- growable parallel tables ------------------------------------------------------------------------
147static icm_cap: i64
148static icm_n: i64
149static icm_name: i64
150static icm_rsha: i64
151static icm_osha: i64
152static icm_bytes: i64
153static icm_ost: i64 // 0 = not observed this run | 1 = hashed ok | 2 = unreadable/absent
154static ict_cap: i64
155static ict_n: i64
156static ict_name: i64
157static ict_key: i64
158static ict_asha: i64
159static ict_unres: i64
160static ict_used: i64
161static ict_ms: i64
162static ict_mc: i64
163static ict_live: i64 // 1 = record valid | 0 = evicted or invalidated this run
164static icx_cap: i64
165static icx_n: i64
166static icx_mem: i64
167static ic_hashed: i64
168static ic_reused: i64
169// The three inputs the ruler cannot see. DATA, not literals in the decision path: knowledge/inccache.conf
170// rows crash_guard / compiler_path / assembler_path override them, and the defaults are the names
171// nx_sov_build_run itself uses, adopted rather than re-chosen.
172static ic_guard_p: i64
173static ic_cc_p: i64
174static ic_asm_p: i64
175static ic_fsync_on: i64
176func ic_guard_name() -> *u8 { return ic_guard_p as *u8 }
177func ic_cc_name() -> *u8 { return ic_cc_p as *u8 }
178func ic_asm_name() -> *u8 { return ic_asm_p as *u8 }
179
180func ic_u8slot(base: i64, i: i64, stride: i64) -> *u8 { return (base + i*stride) as *u8 }
181func ic_i64at(base: i64, i: i64) -> *i64 { return (base + i*8) as *i64 }
182func ic_copy_u8(dstp: i64, srcp: i64, n: i64) -> i64 {
183 var i: i64 = 0
184 while i < n {
185 let s: *u8 = (srcp + i) as *u8
186 let d: *u8 = (dstp + i) as *u8
187 d[0] = s[0]
188 i = i + 1
189 }
190 return 0
191}
192func ic_mod_init() -> i64 {
193 icm_cap = IC_TAB_INIT
194 icm_n = 0
195 icm_name = sys_mmap(icm_cap * ICL_NAMEW) as i64
196 icm_rsha = sys_mmap(icm_cap * ic_shaw()) as i64
197 icm_osha = sys_mmap(icm_cap * ic_shaw()) as i64
198 icm_bytes = sys_mmap(icm_cap * 8) as i64
199 icm_ost = sys_mmap(icm_cap * 8) as i64
200 return 0
201}
202func ic_mod_grow() -> i64 {
203 let nc: i64 = icm_cap * 2
204 let nn: i64 = sys_mmap(nc * ICL_NAMEW) as i64
205 let nr: i64 = sys_mmap(nc * ic_shaw()) as i64
206 let no: i64 = sys_mmap(nc * ic_shaw()) as i64
207 let nb: i64 = sys_mmap(nc * 8) as i64
208 let ns: i64 = sys_mmap(nc * 8) as i64
209 ic_copy_u8(nn, icm_name, icm_n * ICL_NAMEW)
210 ic_copy_u8(nr, icm_rsha, icm_n * ic_shaw())
211 ic_copy_u8(no, icm_osha, icm_n * ic_shaw())
212 ic_copy_u8(nb, icm_bytes, icm_n * 8)
213 ic_copy_u8(ns, icm_ost, icm_n * 8)
214 sys_munmap(icm_name as *u8, icm_cap * ICL_NAMEW)
215 sys_munmap(icm_rsha as *u8, icm_cap * ic_shaw())
216 sys_munmap(icm_osha as *u8, icm_cap * ic_shaw())
217 sys_munmap(icm_bytes as *u8, icm_cap * 8)
218 sys_munmap(icm_ost as *u8, icm_cap * 8)
219 icm_name = nn; icm_rsha = nr; icm_osha = no; icm_bytes = nb; icm_ost = ns
220 icm_cap = nc
221 return 0
222}
223// find-or-create. -1 only when the NAME does not fit ICL_NAMEW, which is REFUSED and announced rather
224// than truncated: two different modules truncating to one name would share a cache row.
225func ic_intern(name: *u8) -> i64 {
226 if icl_len(name) >= ICL_NAMEW - 1 { return 0 - 1 }
227 var i: i64 = 0
228 while i < icm_n {
229 if icl_seq(ic_u8slot(icm_name, i, ICL_NAMEW), name) == 1 { return i }
230 i = i + 1
231 }
232 if icm_n >= icm_cap { ic_mod_grow() }
233 let dst: *u8 = ic_u8slot(icm_name, icm_n, ICL_NAMEW)
234 var z: i64 = 0
235 while name[z] != (0 as u8) { dst[z] = name[z]; z = z + 1 }
236 dst[z] = 0 as u8
237 let r: *u8 = ic_u8slot(icm_rsha, icm_n, ic_shaw())
238 r[0] = 0 as u8
239 let o: *u8 = ic_u8slot(icm_osha, icm_n, ic_shaw())
240 o[0] = 0 as u8
241 let bp: *i64 = ic_i64at(icm_bytes, icm_n); bp[0] = 0
242 let sp: *i64 = ic_i64at(icm_ost, icm_n); sp[0] = 0
243 icm_n = icm_n + 1
244 return icm_n - 1
245}
246func ic_tgt_init() -> i64 {
247 ict_cap = IC_TAB_INIT
248 ict_n = 0
249 ict_name = sys_mmap(ict_cap * ICL_NAMEW) as i64
250 ict_key = sys_mmap(ict_cap * ic_shaw()) as i64
251 ict_asha = sys_mmap(ict_cap * ic_shaw()) as i64
252 ict_unres = sys_mmap(ict_cap * 8) as i64
253 ict_used = sys_mmap(ict_cap * 8) as i64
254 ict_ms = sys_mmap(ict_cap * 8) as i64
255 ict_mc = sys_mmap(ict_cap * 8) as i64
256 ict_live = sys_mmap(ict_cap * 8) as i64
257 icx_cap = IC_MEM_INIT
258 icx_n = 0
259 icx_mem = sys_mmap(icx_cap * 8) as i64
260 return 0
261}
262func ic_tgt_grow() -> i64 {
263 let nc: i64 = ict_cap * 2
264 let a1: i64 = sys_mmap(nc * ICL_NAMEW) as i64
265 let a2: i64 = sys_mmap(nc * ic_shaw()) as i64
266 let a3: i64 = sys_mmap(nc * ic_shaw()) as i64
267 let a4: i64 = sys_mmap(nc * 8) as i64
268 let a5: i64 = sys_mmap(nc * 8) as i64
269 let a6: i64 = sys_mmap(nc * 8) as i64
270 let a7: i64 = sys_mmap(nc * 8) as i64
271 let a8: i64 = sys_mmap(nc * 8) as i64
272 ic_copy_u8(a1, ict_name, ict_n * ICL_NAMEW)
273 ic_copy_u8(a2, ict_key, ict_n * ic_shaw())
274 ic_copy_u8(a3, ict_asha, ict_n * ic_shaw())
275 ic_copy_u8(a4, ict_unres, ict_n * 8)
276 ic_copy_u8(a5, ict_used, ict_n * 8)
277 ic_copy_u8(a6, ict_ms, ict_n * 8)
278 ic_copy_u8(a7, ict_mc, ict_n * 8)
279 ic_copy_u8(a8, ict_live, ict_n * 8)
280 ict_name = a1; ict_key = a2; ict_asha = a3; ict_unres = a4
281 ict_used = a5; ict_ms = a6; ict_mc = a7; ict_live = a8
282 ict_cap = nc
283 return 0
284}
285func ic_mem_push(v: i64) -> i64 {
286 if icx_n >= icx_cap {
287 let nc: i64 = icx_cap * 2
288 let nb: i64 = sys_mmap(nc * 8) as i64
289 ic_copy_u8(nb, icx_mem, icx_n * 8)
290 sys_munmap(icx_mem as *u8, icx_cap * 8)
291 icx_mem = nb
292 icx_cap = nc
293 }
294 let p: *i64 = ic_i64at(icx_mem, icx_n)
295 p[0] = v
296 icx_n = icx_n + 1
297 return icx_n - 1
298}
299func ic_tgt_find(name: *u8) -> i64 {
300 var i: i64 = 0
301 while i < ict_n {
302 if icl_seq(ic_u8slot(ict_name, i, ICL_NAMEW), name) == 1 {
303 let lv: *i64 = ic_i64at(ict_live, i)
304 if lv[0] == 1 { return i }
305 return 0 - 1
306 }
307 i = i + 1
308 }
309 return 0 - 1
310}
311func ic_tgt_slot(name: *u8) -> i64 {
312 var i: i64 = 0
313 while i < ict_n {
314 if icl_seq(ic_u8slot(ict_name, i, ICL_NAMEW), name) == 1 { return i }
315 i = i + 1
316 }
317 if icl_len(name) >= ICL_NAMEW - 1 { return 0 - 1 }
318 if ict_n >= ict_cap { ic_tgt_grow() }
319 let dst: *u8 = ic_u8slot(ict_name, ict_n, ICL_NAMEW)
320 var z: i64 = 0
321 while name[z] != (0 as u8) { dst[z] = name[z]; z = z + 1 }
322 dst[z] = 0 as u8
323 let k: *u8 = ic_u8slot(ict_key, ict_n, ic_shaw()); k[0] = 0 as u8
324 let a: *u8 = ic_u8slot(ict_asha, ict_n, ic_shaw()); a[0] = 0 as u8
325 let u: *i64 = ic_i64at(ict_unres, ict_n); u[0] = 0
326 let w: *i64 = ic_i64at(ict_used, ict_n); w[0] = 0
327 let s: *i64 = ic_i64at(ict_ms, ict_n); s[0] = 0
328 let c: *i64 = ic_i64at(ict_mc, ict_n); c[0] = 0
329 let l: *i64 = ic_i64at(ict_live, ict_n); l[0] = 1
330 ict_n = ict_n + 1
331 return ict_n - 1
332}
333func ic_tgt_has_module(ti: i64, mi: i64) -> i64 {
334 let s: *i64 = ic_i64at(ict_ms, ti)
335 let c: *i64 = ic_i64at(ict_mc, ti)
336 var j: i64 = 0
337 while j < c[0] {
338 let m: *i64 = ic_i64at(icx_mem, s[0] + j)
339 if m[0] == mi { return 1 }
340 j = j + 1
341 }
342 return 0
343}
344
345// ---- observe: read + sha a module ONCE per run, memoised across every target that names it -----------
346// TWO KINDS OF MEMBER, ONE TABLE. A bare name ("nx_syscalls.nx") is a SOURCE and is resolved by the one
347// shared resolver. A name beginning "_offc/" is a raw root-relative PATH, which is how the compiler and
348// the assembler enter the dependency set (see ic_record): they are inputs to the artifact exactly as much
349// as any source, the ruler cannot see them, and without them a toolchain change would leave a HIT
350// standing over an artifact a full build would no longer reproduce. The ruler never emits a name with a
351// slash, so the two namespaces cannot collide.
352func ic_observe(root: *u8, mi: i64) -> i64 {
353 let st: *i64 = ic_i64at(icm_ost, mi)
354 if st[0] != 0 { ic_reused = ic_reused + 1; return st[0] }
355 let path: *u8 = sys_mmap(ICL_PATHW)
356 let nm: *u8 = ic_u8slot(icm_name, mi, ICL_NAMEW)
357 var got: i64 = 0
358 if ic_starts(nm, "_offc/" as *u8) == 1 {
359 var po: i64 = icl_cat(path, 0, root)
360 path[po] = 47 as u8; po = po + 1
361 po = icl_cat(path, po, nm)
362 path[po] = 0 as u8
363 got = icl_exists(path)
364 } else { got = icl_resolve(root, nm, path) }
365 if got == 0 { st[0] = 2; return 2 }
366 let lp: *i64 = sys_mmap(16) as *i64
367 lp[0] = 0
368 let buf: *u8 = sys_read_file(path, lp)
369 if (buf as i64) == 0 { st[0] = 2; return 2 }
370 let dig: *u8 = sys_mmap(48)
371 sha256_digest(buf, lp[0], dig)
372 icl_hex(dig, ic_u8slot(icm_osha, mi, ic_shaw()))
373 sys_free_file(buf, lp[0])
374 ic_hashed = ic_hashed + 1
375 st[0] = 1
376 return 1
377}
378
379// ---- inc_cache_lookup: THE WATCH SYMBOL (lang.matrix LN10). ------------------------------------------
380// Decides WITHOUT forking anything and WITHOUT computing a closure sha:
381// IC_NEW no record for this target
382// IC_MISS at least one recorded module no longer hashes to its recorded sha (or is gone), or the
383// recorded closure had unresolved imports (never cacheable), or the artifact moved
384// IC_HIT every recorded module is byte-identical to what the ruler read, so the ruler's own key still
385// describes this tree -- and the artifact still hashes to what the recorded build produced
386// `dirty` (an i64 array of module indices) and its count receive the modules that moved, so the caller
387// can NAME them instead of reporting a bare verdict.
388func inc_cache_lookup(root: *u8, target: *u8, dirty: *i64, dirtycap: i64, dirtyn: *i64, reason: *i64) -> i64 {
389 dirtyn[0] = 0
390 reason[0] = 0
391 let ti: i64 = ic_tgt_find(target)
392 if ti < 0 { return IC_NEW }
393 let up: *i64 = ic_i64at(ict_unres, ti)
394 if up[0] > 0 { reason[0] = 1; return IC_MISS }
395 let s: *i64 = ic_i64at(ict_ms, ti)
396 let c: *i64 = ic_i64at(ict_mc, ti)
397 if c[0] <= 0 { reason[0] = 2; return IC_MISS }
398 var j: i64 = 0
399 while j < c[0] {
400 let mp: *i64 = ic_i64at(icx_mem, s[0] + j)
401 let mi: i64 = mp[0]
402 let ok: i64 = ic_observe(root, mi)
403 var moved: i64 = 0
404 if ok != 1 { moved = 1 }
405 if ok == 1 { if ic_hexeq(ic_u8slot(icm_osha, mi, ic_shaw()), ic_u8slot(icm_rsha, mi, ic_shaw())) == 0 { moved = 1 } }
406 if moved == 1 {
407 if dirtyn[0] < dirtycap { let d: *i64 = ic_i64at(dirty as i64, dirtyn[0]); d[0] = mi }
408 dirtyn[0] = dirtyn[0] + 1
409 }
410 j = j + 1
411 }
412 if dirtyn[0] > 0 { reason[0] = 3; return IC_MISS }
413 return IC_HIT
414}
415
416// artifact verification: a HIT promises the recorded bytes are still on disk. B2's law -- a hit is
417// byte-verified before use -- applies to the artifact this cache decides NOT to rebuild.
418func ic_artifact_path(root: *u8, target: *u8, out: *u8) -> i64 {
419 var o: i64 = icl_cat(out, 0, root)
420 o = icl_cat(out, o, "/_build/" as *u8)
421 o = icl_cat(out, o, target)
422 o = icl_cat(out, o, ".sov.elf" as *u8)
423 out[o] = 0 as u8
424 return o
425}
426func ic_sha_file(path: *u8, out: *u8) -> i64 {
427 let lp: *i64 = sys_mmap(16) as *i64
428 lp[0] = 0
429 let buf: *u8 = sys_read_file(path, lp)
430 if (buf as i64) == 0 { return 0 }
431 let dig: *u8 = sys_mmap(48)
432 sha256_digest(buf, lp[0], dig)
433 icl_hex(dig, out)
434 sys_free_file(buf, lp[0])
435 return 1
436}
437
438// ---- the index: ONE file, rewritten atomically, budgeted, garbage-collected on every save ------------
439func ic_dir(root: *u8, out: *u8) -> i64 {
440 var o: i64 = icl_cat(out, 0, root)
441 o = icl_cat(out, o, "/_build/inccache" as *u8)
442 out[o] = 0 as u8
443 return o
444}
445func ic_index_path(root: *u8, out: *u8) -> i64 {
446 var o: i64 = ic_dir(root, out)
447 o = icl_cat(out, o, "/index" as *u8)
448 out[o] = 0 as u8
449 return o
450}
451func ic_upto(b: *u8, i: i64, end: i64, sep: i64, out: *u8, cap: i64) -> i64 {
452 var p: i64 = i
453 var o: i64 = 0
454 var stop: i64 = 0
455 while stop == 0 {
456 if p >= end { stop = 1 } else {
457 let c: i64 = b[p] as i64
458 if c == sep { stop = 1 } else {
459 if o >= cap - 1 { out[0] = 0 as u8; return 0 - 1 }
460 out[o] = b[p]; o = o + 1; p = p + 1
461 }
462 }
463 }
464 out[o] = 0 as u8
465 return p
466}
467func ic_dec(b: *u8, i: i64, end: i64, outv: *i64) -> i64 {
468 var p: i64 = i
469 var v: i64 = 0
470 var any: i64 = 0
471 var stop: i64 = 0
472 while stop == 0 {
473 if p >= end { stop = 1 } else {
474 let c: i64 = b[p] as i64
475 var dig: i64 = 0
476 if c >= 48 { if c <= 57 { dig = 1 } }
477 if dig == 0 { stop = 1 } else { v = v*10 + (c - 48); any = any + 1; p = p + 1 }
478 }
479 }
480 outv[0] = v
481 if any == 0 { return 0 - 1 }
482 return p
483}
484func ic_eol(b: *u8, from: i64, n: i64) -> i64 {
485 var i: i64 = from
486 while i < n { if (b[i] as i64) == IC_NL { return i } i = i + 1 }
487 return n
488}
489func ic_setsha(dst: *u8, src: *u8) -> i64 {
490 var i: i64 = 0
491 while i < ICL_SHA_HEX { dst[i] = src[i]; i = i + 1 }
492 dst[ICL_SHA_HEX] = 0 as u8
493 return 0
494}
495// Returns 1 loaded, 0 absent (a cold cache is not an error), -1 REFUSED as corrupt. A corrupt index is
496// refused whole rather than half-trusted: a half-parsed member list is a false HIT waiting to happen.
497func ic_index_load(root: *u8) -> i64 {
498 let p: *u8 = sys_mmap(ICL_PATHW)
499 ic_index_path(root, p)
500 let lp: *i64 = sys_mmap(16) as *i64
501 lp[0] = 0
502 let b: *u8 = sys_read_file(p, lp)
503 if (b as i64) == 0 { return 0 }
504 let n: i64 = lp[0]
505 if n <= 0 { sys_free_file(b, n); return 0 }
506 let fld: *u8 = sys_mmap(ICL_PATHW)
507 let vp: *i64 = sys_mmap(16) as *i64
508 var i: i64 = 0
509 var bad: i64 = 0
510 while i < n {
511 let e: i64 = ic_eol(b, i, n)
512 if bad == 0 {
513 let k: i64 = b[i] as i64
514 if k == 109 {
515 var q: i64 = ic_upto(b, i+2, e, IC_PIPE, fld, ICL_PATHW)
516 if q < 0 { bad = 1 } else {
517 let mi: i64 = ic_intern(fld)
518 if mi < 0 { bad = 1 }
519 if mi >= 0 { if mi != icm_n - 1 { bad = 1 } }
520 if bad == 0 {
521 q = ic_upto(b, q+1, e, IC_PIPE, fld, ICL_PATHW)
522 if q < 0 { bad = 1 } else {
523 if icl_len(fld) != ICL_SHA_HEX { bad = 1 } else {
524 ic_setsha(ic_u8slot(icm_rsha, mi, ic_shaw()), fld)
525 if ic_dec(b, q+1, e, vp) < 0 { bad = 1 } else {
526 let bp: *i64 = ic_i64at(icm_bytes, mi)
527 bp[0] = vp[0]
528 }
529 }
530 }
531 }
532 }
533 }
534 if k == 116 {
535 var q2: i64 = ic_upto(b, i+2, e, IC_PIPE, fld, ICL_PATHW)
536 if q2 < 0 { bad = 1 } else {
537 let ti: i64 = ic_tgt_slot(fld)
538 if ti < 0 { bad = 1 }
539 if bad == 0 {
540 q2 = ic_upto(b, q2+1, e, IC_PIPE, fld, ICL_PATHW)
541 if q2 < 0 { bad = 1 } else { if icl_len(fld) != ICL_SHA_HEX { bad = 1 } else { ic_setsha(ic_u8slot(ict_key, ti, ic_shaw()), fld) } }
542 }
543 if bad == 0 {
544 q2 = ic_upto(b, q2+1, e, IC_PIPE, fld, ICL_PATHW)
545 if q2 < 0 { bad = 1 } else {
546 let ap: *u8 = ic_u8slot(ict_asha, ti, ic_shaw())
547 if icl_len(fld) == ICL_SHA_HEX { ic_setsha(ap, fld) } else { ap[0] = 0 as u8 }
548 }
549 }
550 if bad == 0 {
551 q2 = ic_dec(b, q2+1, e, vp)
552 if q2 < 0 { bad = 1 } else { let u: *i64 = ic_i64at(ict_unres, ti); u[0] = vp[0] }
553 }
554 if bad == 0 {
555 q2 = ic_dec(b, q2+1, e, vp)
556 if q2 < 0 { bad = 1 } else { let w: *i64 = ic_i64at(ict_used, ti); w[0] = vp[0] }
557 }
558 if bad == 0 {
559 let sp: *i64 = ic_i64at(ict_ms, ti)
560 let cp: *i64 = ic_i64at(ict_mc, ti)
561 sp[0] = icx_n
562 cp[0] = 0
563 var r: i64 = q2 + 1
564 var go: i64 = 1
565 if r >= e { go = 0 }
566 while go == 1 {
567 let r2: i64 = ic_dec(b, r, e, vp)
568 if r2 < 0 { bad = 1; go = 0 } else {
569 if vp[0] >= icm_n { bad = 1; go = 0 } else {
570 ic_mem_push(vp[0])
571 cp[0] = cp[0] + 1
572 if r2 >= e { go = 0 } else { r = r2 + 1 }
573 }
574 }
575 }
576 }
577 }
578 }
579 }
580 i = e + 1
581 }
582 sys_free_file(b, n)
583 if bad == 1 { return 0 - 1 }
584 return 1
585}
586func ic_catn(d: *u8, o: i64, v: i64) -> i64 {
587 var m: i64 = v
588 var p: i64 = o
589 if m < 0 { d[p] = 45 as u8; p = p + 1; m = 0 - m }
590 let t: *u8 = sys_mmap(32)
591 var k: i64 = 0
592 if m == 0 { t[0] = 48 as u8; k = 1 }
593 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
594 var i: i64 = 0
595 while i < k { d[p+i] = t[k-1-i]; i = i + 1 }
596 sys_munmap(t, 32)
597 return p + k
598}
599// SKIP SPACES WITH A FLAG, NEVER BY WRITING THE EXIT SENTINEL INTO THE CURSOR. `p = end + 1` to leave a
600// loop destroys the very position the caller needs -- the estate has measured that idiom erasing an
601// answer four times in one day, so it is written once, here, and called.
602func ic_skipsp(b: *u8, from: i64, end: i64) -> i64 {
603 var p: i64 = from
604 var go: i64 = 1
605 while go == 1 {
606 if p >= end { go = 0 } else {
607 if (b[p] as i64) == IC_SP { p = p + 1 } else { go = 0 }
608 }
609 }
610 return p
611}
612// Serialize live targets + the modules they reference (unreferenced modules are GARBAGE COLLECTED here,
613// with indices remapped). Returns bytes written into buf, or -1 if buf was too small (never truncates).
614func ic_serialize(buf: *u8, cap: i64, remap: *i64) -> i64 {
615 var i: i64 = 0
616 while i < icm_n { let r: *i64 = ic_i64at(remap as i64, i); r[0] = 0 - 1; i = i + 1 }
617 var nm: i64 = 0
618 var t: i64 = 0
619 while t < ict_n {
620 let lv: *i64 = ic_i64at(ict_live, t)
621 if lv[0] == 1 {
622 let s: *i64 = ic_i64at(ict_ms, t)
623 let c: *i64 = ic_i64at(ict_mc, t)
624 var j: i64 = 0
625 while j < c[0] {
626 let mp: *i64 = ic_i64at(icx_mem, s[0] + j)
627 let r2: *i64 = ic_i64at(remap as i64, mp[0])
628 if r2[0] < 0 { r2[0] = nm; nm = nm + 1 }
629 j = j + 1
630 }
631 }
632 t = t + 1
633 }
634 var o: i64 = 0
635 o = icl_cat(buf, o, "v|1\n" as *u8)
636 // m-rows are emitted in REMAPPED order, so a member index is the row's own position -- the loader
637 // asserts exactly that and REFUSES the file otherwise.
638 var k: i64 = 0
639 while k < nm {
640 var src: i64 = 0 - 1
641 var z: i64 = 0
642 while z < icm_n { let r3: *i64 = ic_i64at(remap as i64, z); if r3[0] == k { src = z; z = icm_n } else { z = z + 1 } }
643 if src < 0 { return 0 - 1 }
644 if o + ICL_NAMEW + ICL_SHA_HEX + 48 >= cap { return 0 - 1 }
645 o = icl_cat(buf, o, "m|" as *u8)
646 o = icl_cat(buf, o, ic_u8slot(icm_name, src, ICL_NAMEW))
647 buf[o] = IC_PIPE as u8; o = o + 1
648 o = icl_cat(buf, o, ic_u8slot(icm_rsha, src, ic_shaw()))
649 buf[o] = IC_PIPE as u8; o = o + 1
650 let bp: *i64 = ic_i64at(icm_bytes, src)
651 o = ic_catn(buf, o, bp[0])
652 buf[o] = IC_NL as u8; o = o + 1
653 k = k + 1
654 }
655 t = 0
656 while t < ict_n {
657 let lv2: *i64 = ic_i64at(ict_live, t)
658 if lv2[0] == 1 {
659 let s2: *i64 = ic_i64at(ict_ms, t)
660 let c2: *i64 = ic_i64at(ict_mc, t)
661 if o + ICL_NAMEW + 2*ICL_SHA_HEX + 96 + c2[0]*12 >= cap { return 0 - 1 }
662 o = icl_cat(buf, o, "t|" as *u8)
663 o = icl_cat(buf, o, ic_u8slot(ict_name, t, ICL_NAMEW))
664 buf[o] = IC_PIPE as u8; o = o + 1
665 o = icl_cat(buf, o, ic_u8slot(ict_key, t, ic_shaw()))
666 buf[o] = IC_PIPE as u8; o = o + 1
667 let ap2: *u8 = ic_u8slot(ict_asha, t, ic_shaw())
668 if ap2[0] == (0 as u8) { o = icl_cat(buf, o, "-" as *u8) } else { o = icl_cat(buf, o, ap2) }
669 buf[o] = IC_PIPE as u8; o = o + 1
670 let u2: *i64 = ic_i64at(ict_unres, t)
671 o = ic_catn(buf, o, u2[0])
672 buf[o] = IC_PIPE as u8; o = o + 1
673 let w2: *i64 = ic_i64at(ict_used, t)
674 o = ic_catn(buf, o, w2[0])
675 buf[o] = IC_PIPE as u8; o = o + 1
676 var j2: i64 = 0
677 while j2 < c2[0] {
678 let mp2: *i64 = ic_i64at(icx_mem, s2[0] + j2)
679 let r4: *i64 = ic_i64at(remap as i64, mp2[0])
680 if j2 > 0 { buf[o] = IC_COMMA as u8; o = o + 1 }
681 o = ic_catn(buf, o, r4[0])
682 j2 = j2 + 1
683 }
684 buf[o] = IC_NL as u8; o = o + 1
685 }
686 t = t + 1
687 }
688 buf[o] = 0 as u8
689 return o
690}
691// Write the index atomically (tmp + fsync + renameat). Over budget it EVICTS oldest-used targets first
692// and ANNOUNCES each one -- an unbounded store does not ship, and a silent eviction is a cache that lies
693// about why it missed. Returns bytes written, or -1.
694func ic_index_save(root: *u8, budget: i64) -> i64 {
695 let cap: i64 = IC_SLACK + icm_n*(ICL_NAMEW + ICL_SHA_HEX + 48) + ict_n*(ICL_NAMEW + 2*ICL_SHA_HEX + 96) + icx_n*12
696 let buf: *u8 = sys_mmap(cap + 16)
697 let remap: *i64 = sys_mmap(icm_n*8 + 16) as *i64
698 var nb: i64 = ic_serialize(buf, cap, remap)
699 if nb < 0 { ic_p("INC-CACHE SAVE-REFUSED reason=serialize-overflow (index not written; nothing was truncated)" as *u8); ic_nl(); return 0 - 1 }
700 var evicted: i64 = 0
701 var go: i64 = 1
702 while go == 1 {
703 if nb <= budget { go = 0 } else {
704 var best: i64 = 0 - 1
705 var bestu: i64 = 0
706 var t: i64 = 0
707 while t < ict_n {
708 let lv: *i64 = ic_i64at(ict_live, t)
709 if lv[0] == 1 {
710 let u: *i64 = ic_i64at(ict_used, t)
711 var take: i64 = 0
712 if best < 0 { take = 1 }
713 if best >= 0 { if u[0] < bestu { take = 1 } }
714 if take == 1 { best = t; bestu = u[0] }
715 }
716 t = t + 1
717 }
718 if best < 0 { go = 0 } else {
719 let lb: *i64 = ic_i64at(ict_live, best)
720 lb[0] = 0
721 evicted = evicted + 1
722 ic_p("INC-EVICT " as *u8); ic_p(ic_u8slot(ict_name, best, ICL_NAMEW))
723 ic_p(" reason=budget last_used=" as *u8); ic_n(bestu)
724 ic_p(" index_bytes=" as *u8); ic_n(nb); ic_p(" budget=" as *u8); ic_n(budget); ic_nl()
725 nb = ic_serialize(buf, cap, remap)
726 if nb < 0 { go = 0 }
727 }
728 }
729 }
730 if nb < 0 { return 0 - 1 }
731 let dirp: *u8 = sys_mmap(ICL_PATHW)
732 ic_dir(root, dirp)
733 sys_mkdir(dirp, IC_DIRMODE)
734 let tmp: *u8 = sys_mmap(ICL_PATHW)
735 var to: i64 = ic_index_path(root, tmp)
736 to = icl_cat(tmp, to, ".tmp" as *u8)
737 tmp[to] = 0 as u8
738 let fd: i64 = sys_openat_wr(tmp, IC_FILEMODE)
739 if fd < 0 { ic_p("INC-CACHE SAVE-REFUSED reason=cannot-open-tmp" as *u8); ic_nl(); return 0 - 1 }
740 var w: i64 = 0
741 while w < nb {
742 let r: i64 = sys_write(fd, ((buf as i64) + w) as *u8, nb - w)
743 if r <= 0 { sys_close(fd); ic_p("INC-CACHE SAVE-REFUSED reason=short-write" as *u8); ic_nl(); return 0 - 1 }
744 w = w + r
745 }
746 // FSYNC IS OFF BY DEFAULT AND THAT IS A DECISION, NOT AN OMISSION. MEASURED 2026-08-25 on a loaded
747 // NAS: a warm 3-target run decided in 4 ms and then spent 27,107 ms in this one call writing a
748 // 2,548-byte index -- the cache would have cost more than the builds it saved. Durability is not a
749 // cache's job: the index is fully reconstructible by re-running `seed`, the tmp+renameat is atomic
750 // regardless of fsync so no reader ever sees a half-file, and the ONE residual risk -- a power loss
751 // exposing a renamed file whose blocks never reached the platter -- lands on a loader that REFUSES a
752 // corrupt index whole and restarts cold. Set fsync_index=1 in inccache.conf to pay for durability we
753 // do not need.
754 if ic_fsync_on == 1 { sys_fsync(fd) }
755 sys_close(fd)
756 let fin: *u8 = sys_mmap(ICL_PATHW)
757 ic_index_path(root, fin)
758 if sys_renameat(tmp, fin) != 0 { ic_p("INC-CACHE SAVE-REFUSED reason=rename-failed" as *u8); ic_nl(); return 0 - 1 }
759 return nb
760}
761
762func ic_find(b: *u8, from: i64, end: i64, needle: *u8) -> i64 {
763 let nl: i64 = icl_len(needle)
764 if nl <= 0 { return 0 - 1 }
765 var i: i64 = from
766 while i + nl <= end {
767 var m: i64 = 0
768 var hit: i64 = 1
769 while m < nl { if b[i+m] != needle[m] { hit = 0; m = nl } else { m = m + 1 } }
770 if hit == 1 { return i }
771 i = i + 1
772 }
773 return 0 - 1
774}
775// Add a member the RULER CANNOT SEE but the compiler certainly reads: the implicitly-appended crash
776// guard, the compiler binary and the assembler binary. Its recorded sha comes from a fresh hash rather
777// than from a ruler row. The member list is therefore a SUPERSET of the ruler's closure -- deliberately,
778// and in the only safe direction: a superset can only ever cause an EXTRA miss, never a false hit, and
779// this is the axis on which nx_closurehash's closure is genuinely narrower than what a build consumes.
780func ic_add_extra(root: *u8, ti: i64, name: *u8, ms_start: i64, count: i64) -> i64 {
781 let mi: i64 = ic_intern(name)
782 if mi < 0 { return count }
783 var j: i64 = 0
784 while j < count {
785 let p: *i64 = ic_i64at(icx_mem, ms_start + j)
786 if p[0] == mi { return count }
787 j = j + 1
788 }
789 if ic_observe(root, mi) != 1 { return count }
790 let rp: *u8 = ic_u8slot(icm_rsha, mi, ic_shaw())
791 let op: *u8 = ic_u8slot(icm_osha, mi, ic_shaw())
792 var changed: i64 = 0
793 if rp[0] != (0 as u8) { if ic_hexeq(rp, op) == 0 { changed = 1 } }
794 if changed == 1 { ic_invalidate_others(ti, mi) }
795 ic_setsha(rp, op)
796 ic_mem_push(mi)
797 return count + 1
798}
799func ic_invalidate_others(ti: i64, mi: i64) -> i64 {
800 var t: i64 = 0
801 var n: i64 = 0
802 while t < ict_n {
803 if t != ti {
804 let lv: *i64 = ic_i64at(ict_live, t)
805 if lv[0] == 1 {
806 if ic_tgt_has_module(t, mi) == 1 {
807 lv[0] = 0
808 n = n + 1
809 ic_p("INC-INVALIDATE " as *u8); ic_p(ic_u8slot(ict_name, t, ICL_NAMEW))
810 ic_p(" by=" as *u8); ic_p(ic_u8slot(icm_name, mi, ICL_NAMEW))
811 ic_p(" (a dependent of a module whose bytes moved -- its recorded key no longer describes this tree)" as *u8); ic_nl()
812 }
813 }
814 }
815 t = t + 1
816 }
817 return n
818}
819
820// Fork the ONE closure ruler and store its per-module manifest. Returns 1 recorded, 0 refused.
821func ic_record(root: *u8, target: *u8, ruler: *u8, tmo: i64, now: i64) -> i64 {
822 let cap: i64 = ic_ruler_cap()
823 let out: *u8 = sys_mmap(cap + 16)
824 let ol: *i64 = sys_mmap(16) as *i64
825 ol[0] = 0
826 let av: *i64 = sys_mmap(8*4) as *i64
827 av[0] = ruler as i64
828 av[1] = target as i64
829 av[2] = root as i64
830 av[3] = 0
831 let rc: i64 = tr_run_capture_to(ruler, av, out, cap, ol, tmo)
832 let n: i64 = ol[0]
833 if rc < 0 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=ruler-harness-error rc=" as *u8); ic_n(rc); ic_nl(); return 0 }
834 if rc == 2 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=target-source-not-found" as *u8); ic_nl(); return 0 }
835 if n >= cap { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=ruler-output-filled-the-capture (a short member list is a false HIT; nothing recorded)" as *u8); ic_nl(); return 0 }
836 let sump: i64 = ic_find(out, 0, n, "closure_sha=" as *u8)
837 if sump < 0 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=no-closure_sha-line (the ruler did not finish)" as *u8); ic_nl(); return 0 }
838 let keyoff: i64 = sump + icl_len("closure_sha=" as *u8)
839 if ic_ishex64(out, keyoff, n) == 0 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=closure_sha-not-64-hex" as *u8); ic_nl(); return 0 }
840 let ti: i64 = ic_tgt_slot(target)
841 if ti < 0 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=target-name-too-long-for-ICL_NAMEW (refused, never truncated)" as *u8); ic_nl(); return 0 }
842 let ms_start: i64 = icx_n
843 var count: i64 = 0
844 var unres: i64 = 0
845 var moved: i64 = 0
846 let nmbuf: *u8 = sys_mmap(ICL_PATHW)
847 let shabuf: *u8 = sys_mmap(ic_shaw() + 8)
848 let vp: *i64 = sys_mmap(16) as *i64
849 var i: i64 = 0
850 var bad: i64 = 0
851 while i < n {
852 let e: i64 = ic_eol(out, i, n)
853 if bad == 0 {
854 var handled: i64 = 0
855 if (out[i] as i64) == IC_HASH { handled = 1 }
856 if handled == 0 { if i == sump { handled = 1 } }
857 if handled == 0 {
858 if ic_ishex64(out, i, e) == 1 {
859 var z: i64 = 0
860 while z < ICL_SHA_HEX { shabuf[z] = out[i+z]; z = z + 1 }
861 shabuf[ICL_SHA_HEX] = 0 as u8
862 let p: i64 = ic_skipsp(out, i + ICL_SHA_HEX, e)
863 let p2: i64 = ic_dec(out, p, e, vp)
864 var q: i64 = p
865 if p2 > 0 { q = p2 }
866 q = ic_skipsp(out, q, e)
867 var o2: i64 = 0
868 while q < e { if o2 < ICL_PATHW - 1 { nmbuf[o2] = out[q]; o2 = o2 + 1 } q = q + 1 }
869 nmbuf[o2] = 0 as u8
870 let mi: i64 = ic_intern(nmbuf)
871 if mi < 0 { bad = 1 } else {
872 let rp: *u8 = ic_u8slot(icm_rsha, mi, ic_shaw())
873 var changed: i64 = 0
874 if rp[0] == (0 as u8) { changed = 0 } else { if ic_hexeq(rp, shabuf) == 0 { changed = 1 } }
875 if changed == 1 { moved = moved + ic_invalidate_others(ti, mi) }
876 ic_setsha(rp, shabuf)
877 let bp: *i64 = ic_i64at(icm_bytes, mi)
878 bp[0] = vp[0]
879 let op: *i64 = ic_i64at(icm_ost, mi)
880 op[0] = 1
881 ic_setsha(ic_u8slot(icm_osha, mi, ic_shaw()), shabuf)
882 ic_mem_push(mi)
883 count = count + 1
884 handled = 1
885 }
886 }
887 }
888 if handled == 0 { if e > i { unres = unres + 1 } }
889 }
890 i = e + 1
891 }
892 if bad == 1 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=module-name-too-long-for-ICL_NAMEW" as *u8); ic_nl(); return 0 }
893 if count <= 0 { ic_p("INC-RECORD-REFUSED " as *u8); ic_p(target); ic_p(" reason=zero-module-rows (a record over the empty set is not a record)" as *u8); ic_nl(); return 0 }
894 let ruler_files: i64 = count
895 count = ic_add_extra(root, ti, ic_guard_name(), ms_start, count)
896 count = ic_add_extra(root, ti, ic_cc_name(), ms_start, count)
897 count = ic_add_extra(root, ti, ic_asm_name(), ms_start, count)
898 var kb: i64 = 0
899 let kp: *u8 = ic_u8slot(ict_key, ti, ic_shaw())
900 while kb < ICL_SHA_HEX { kp[kb] = out[keyoff+kb]; kb = kb + 1 }
901 kp[ICL_SHA_HEX] = 0 as u8
902 let sp: *i64 = ic_i64at(ict_ms, ti); sp[0] = ms_start
903 let cp: *i64 = ic_i64at(ict_mc, ti); cp[0] = count
904 let up: *i64 = ic_i64at(ict_unres, ti); up[0] = unres
905 let wp: *i64 = ic_i64at(ict_used, ti); wp[0] = now
906 let lp2: *i64 = ic_i64at(ict_live, ti); lp2[0] = 1
907 let apath: *u8 = sys_mmap(ICL_PATHW)
908 ic_artifact_path(root, target, apath)
909 let ap: *u8 = ic_u8slot(ict_asha, ti, ic_shaw())
910 if ic_sha_file(apath, ap) == 0 { ap[0] = 0 as u8 }
911 ic_p("INC-RECORD " as *u8); ic_p(target)
912 ic_p(" key=" as *u8); ic_p(kp)
913 ic_p(" ruler_files=" as *u8); ic_n(ruler_files)
914 ic_p(" members=" as *u8); ic_n(count)
915 ic_p(" unresolved=" as *u8); ic_n(unres)
916 ic_p(" dependents_invalidated=" as *u8); ic_n(moved)
917 ic_p(" artifact=" as *u8)
918 if ap[0] == (0 as u8) { ic_p("ABSENT" as *u8) } else { ic_p(ap) }
919 ic_nl()
920 return 1
921}
922
923// ---- tool location: PROBED, and the chosen path is PRINTED, never assumed -----------------------------
924func ic_try(root: *u8, tail: *u8, out: *u8) -> i64 {
925 var o: i64 = icl_cat(out, 0, root)
926 o = icl_cat(out, o, tail)
927 out[o] = 0 as u8
928 if icl_exists(out) == 1 { return 1 }
929 return 0
930}
931// A RELATIVE TOOL PATH CANNOT SURVIVE THE CHILD'S chdir. MEASURED 2026-08-25 on the first live run of
932// this organ: the builder resolved to "buildroot/../nx_sov_build_run.elf", which EXISTS from here, and
933// tr_run_capture_cwd then chdir'd the child into <root> before execve -- where that same relative path
934// resolves to buildroot/buildroot/../... and the child died 127. An execve 127 reads as "the builder is
935// missing", so the error names the wrong subject entirely. Absolutise at PICK time, once, for every
936// candidate: the path is then correct from any working directory a consumer chooses.
937func ic_abs(p: *u8) -> i64 {
938 if p[0] == (47 as u8) { return 0 }
939 let here: *u8 = sys_mmap(ICL_PATHW)
940 if sys_getcwd(here, ICL_PATHW) < 0 { return 0 }
941 let tmp: *u8 = sys_mmap(ICL_PATHW)
942 var o: i64 = icl_cat(tmp, 0, here)
943 tmp[o] = 47 as u8; o = o + 1
944 o = icl_cat(tmp, o, p)
945 tmp[o] = 0 as u8
946 var i: i64 = 0
947 while tmp[i] != (0 as u8) { p[i] = tmp[i]; i = i + 1 }
948 p[i] = 0 as u8
949 sys_munmap(tmp, ICL_PATHW)
950 sys_munmap(here, ICL_PATHW)
951 return 1
952}
953func ic_pick(root: *u8, conf: *u8, key: *u8, t1: *u8, t2: *u8, t3: *u8, out: *u8) -> i64 {
954 if conf[0] != (0 as u8) {
955 if lcf_str_of(conf, key, out, ICL_PATHW) > 0 { if icl_exists(out) == 1 { ic_abs(out); return 1 } }
956 }
957 if ic_try(root, t1, out) == 1 { ic_abs(out); return 1 }
958 if ic_try(root, t2, out) == 1 { ic_abs(out); return 1 }
959 if ic_try(root, t3, out) == 1 { ic_abs(out); return 1 }
960 out[0] = 0 as u8
961 return 0
962}
963func ic_build_one(root: *u8, builder: *u8, target: *u8, tmo: i64, out: *u8, cap: i64, ol: *i64) -> i64 {
964 let av: *i64 = sys_mmap(8*4) as *i64
965 av[0] = builder as i64
966 av[1] = target as i64
967 av[2] = "--build-only" as *u8 as i64
968 av[3] = 0
969 return tr_run_capture_cwd(builder, av, out, cap, ol, tmo, root)
970}
971func ic_reason(code: i64) -> i64 {
972 if code == 1 { ic_p("recorded-closure-has-unresolved-imports (never cacheable: the ruler could not see every source)" as *u8) }
973 if code == 2 { ic_p("recorded-member-list-empty" as *u8) }
974 if code == 3 { ic_p("module-bytes-moved" as *u8) }
975 if code == 4 { ic_p("disabled-by-flag-or-conf" as *u8) }
976 if code == 5 { ic_p("artifact-sha-never-recorded" as *u8) }
977 if code == 6 { ic_p("artifact-missing-or-changed-since-record" as *u8) }
978 if code == 0 { ic_p("none" as *u8) }
979 return 0
980}
981func ic_usage() -> i64 {
982 ic_e("usage: nx_inc_compile plan|build|seed <target...> [--root=<dir>] [--no-cache]\n" as *u8)
983 ic_e(" nx_inc_compile stat [--root=<dir>]\n" as *u8)
984 ic_e(" plan decide only -- zero forks, zero writes; names the modules that moved\n" as *u8)
985 ic_e(" build plan, then fork nx_sov_build_run for the NON-HITS only, then record\n" as *u8)
986 ic_e(" seed record the current closure for each target without building\n" as *u8)
987 return 0
988}
989
990func main(argc: i64, argv: *i64) -> i64 {
991 if argc < 2 { ic_usage(); sys_exit(IC_USAGE); return IC_USAGE }
992 let verb: *u8 = argv[1] as *u8
993 var root: *u8 = "." as *u8
994 var no_cache: i64 = 0
995 let tg: *i64 = sys_mmap(argc*8 + 16) as *i64
996 var ntg: i64 = 0
997 // ARGV IS SCANNED TO THE END. nx_compile_x86 shipped a loop that set `i = argc` on the first non-flag
998 // argument, so every flag AFTER the path was silently eaten -- a whole Windows lane ran on stale asm
999 // because of it. The cursor here is only ever advanced.
1000 var i: i64 = 2
1001 while i < argc {
1002 let a: *u8 = argv[i] as *u8
1003 var handled: i64 = 0
1004 if ic_starts(a, "--root=" as *u8) == 1 { root = ((a as i64) + icl_len("--root=" as *u8)) as *u8; handled = 1 }
1005 if handled == 0 { if icl_seq(a, "--no-cache" as *u8) == 1 { no_cache = 1; handled = 1 } }
1006 if handled == 0 { let s: *i64 = ic_i64at(tg as i64, ntg); s[0] = a as i64; ntg = ntg + 1 }
1007 i = i + 1
1008 }
1009
1010 // TWO knowledge trees exist in this estate and organs disagree about which one they mean, so BOTH are
1011 // probed and the one actually read is PRINTED. An absent conf is announced as NONE with the derived
1012 // defaults, never silently assumed.
1013 let conf: *u8 = sys_mmap(ICL_PATHW)
1014 var o: i64 = icl_cat(conf, 0, root)
1015 o = icl_cat(conf, o, "/../knowledge/inccache.conf" as *u8)
1016 conf[o] = 0 as u8
1017 var have_conf: i64 = 0
1018 if icl_exists(conf) == 1 { have_conf = 1 }
1019 if have_conf == 0 {
1020 o = icl_cat(conf, 0, root)
1021 o = icl_cat(conf, o, "/knowledge/inccache.conf" as *u8)
1022 conf[o] = 0 as u8
1023 if icl_exists(conf) == 1 { have_conf = 1 }
1024 }
1025 if have_conf == 0 { conf[0] = 0 as u8 }
1026 var budget: i64 = IC_BUDGET_BOOTSTRAP
1027 var enabled: i64 = 1
1028 var rtmo: i64 = IC_RULER_TMO_BOOT
1029 var btmo: i64 = IC_BUILD_TMO_BOOT
1030 if have_conf == 1 {
1031 let v1: i64 = lcf_int_of(conf, "cache_max_bytes" as *u8)
1032 if v1 != LCF_MISS { budget = v1 }
1033 let v2: i64 = lcf_int_of(conf, "cache_enabled" as *u8)
1034 if v2 != LCF_MISS { enabled = v2 }
1035 let v3: i64 = lcf_int_of(conf, "ruler_timeout_ms" as *u8)
1036 if v3 != LCF_MISS { rtmo = v3 }
1037 let v4: i64 = lcf_int_of(conf, "build_timeout_ms" as *u8)
1038 if v4 != LCF_MISS { btmo = v4 }
1039 }
1040 ic_fsync_on = 0
1041 if have_conf == 1 {
1042 let v5: i64 = lcf_int_of(conf, "fsync_index" as *u8)
1043 if v5 != LCF_MISS { ic_fsync_on = v5 }
1044 }
1045 if no_cache == 1 { enabled = 0 }
1046 let gname: *u8 = sys_mmap(ICL_PATHW)
1047 let ccname: *u8 = sys_mmap(ICL_PATHW)
1048 let asname: *u8 = sys_mmap(ICL_PATHW)
1049 icl_cat(gname, 0, "nx_crash.nx" as *u8); gname[icl_len("nx_crash.nx" as *u8)] = 0 as u8
1050 icl_cat(ccname, 0, "_offc/nx_cc_sovereign.elf" as *u8); ccname[icl_len("_offc/nx_cc_sovereign.elf" as *u8)] = 0 as u8
1051 icl_cat(asname, 0, "_offc/nxasm_x86_main.elf" as *u8); asname[icl_len("_offc/nxasm_x86_main.elf" as *u8)] = 0 as u8
1052 if have_conf == 1 {
1053 lcf_str_of(conf, "crash_guard" as *u8, gname, ICL_PATHW)
1054 lcf_str_of(conf, "compiler_path" as *u8, ccname, ICL_PATHW)
1055 lcf_str_of(conf, "assembler_path" as *u8, asname, ICL_PATHW)
1056 if gname[0] == (0 as u8) { icl_cat(gname, 0, "nx_crash.nx" as *u8); gname[icl_len("nx_crash.nx" as *u8)] = 0 as u8 }
1057 if ccname[0] == (0 as u8) { icl_cat(ccname, 0, "_offc/nx_cc_sovereign.elf" as *u8); ccname[icl_len("_offc/nx_cc_sovereign.elf" as *u8)] = 0 as u8 }
1058 if asname[0] == (0 as u8) { icl_cat(asname, 0, "_offc/nxasm_x86_main.elf" as *u8); asname[icl_len("_offc/nxasm_x86_main.elf" as *u8)] = 0 as u8 }
1059 }
1060 ic_guard_p = gname as i64
1061 ic_cc_p = ccname as i64
1062 ic_asm_p = asname as i64
1063 ic_p("INC-CACHE extra_members=" as *u8); ic_p(gname); ic_p("," as *u8); ic_p(ccname); ic_p("," as *u8); ic_p(asname)
1064 ic_p(" (inputs the ruler cannot see; a SUPERSET of its closure, deliberately)" as *u8); ic_nl()
1065 ic_p("INC-CACHE conf=" as *u8)
1066 if have_conf == 1 { ic_p(conf) } else { ic_p("NONE (bootstrap defaults)" as *u8) }
1067 ic_p(" root=" as *u8); ic_p(root)
1068 ic_p(" enabled=" as *u8); ic_n(enabled)
1069 ic_p(" budget_bytes=" as *u8); ic_n(budget)
1070 ic_p(" ruler_timeout_ms=" as *u8); ic_n(rtmo)
1071 ic_p(" build_timeout_ms=" as *u8); ic_n(btmo)
1072 ic_nl()
1073
1074 ic_mod_init()
1075 ic_tgt_init()
1076 ic_hashed = 0
1077 ic_reused = 0
1078 let lrc: i64 = ic_index_load(root)
1079 if lrc < 0 {
1080 ic_p("INC-CACHE index REFUSED as corrupt -- starting cold (a half-parsed member list is a false HIT waiting)" as *u8); ic_nl()
1081 ic_mod_init()
1082 ic_tgt_init()
1083 }
1084 ic_p("INC-CACHE index=" as *u8)
1085 if lrc == 1 { ic_p("loaded" as *u8) }
1086 if lrc == 0 { ic_p("cold" as *u8) }
1087 if lrc < 0 { ic_p("refused" as *u8) }
1088 ic_p(" modules=" as *u8); ic_n(icm_n)
1089 ic_p(" targets=" as *u8); ic_n(ict_n)
1090 ic_nl()
1091
1092 if icl_seq(verb, "stat" as *u8) == 1 {
1093 var live: i64 = 0
1094 var t: i64 = 0
1095 while t < ict_n { let lv: *i64 = ic_i64at(ict_live, t); if lv[0] == 1 { live = live + 1 } t = t + 1 }
1096 let ip: *u8 = sys_mmap(ICL_PATHW)
1097 ic_index_path(root, ip)
1098 let lp: *i64 = sys_mmap(16) as *i64
1099 lp[0] = 0
1100 let b: *u8 = sys_read_file(ip, lp)
1101 var ib: i64 = 0
1102 if (b as i64) != 0 { ib = lp[0]; sys_free_file(b, ib) }
1103 ic_p("INC-STAT modules=" as *u8); ic_n(icm_n)
1104 ic_p(" targets_live=" as *u8); ic_n(live)
1105 ic_p(" targets_rows=" as *u8); ic_n(ict_n)
1106 ic_p(" members=" as *u8); ic_n(icx_n)
1107 ic_p(" index_bytes=" as *u8); ic_n(ib)
1108 ic_p(" budget_bytes=" as *u8); ic_n(budget)
1109 ic_p(" index=" as *u8); ic_p(ip)
1110 ic_p(" corpus_complete=1" as *u8)
1111 ic_nl()
1112 return IC_OK
1113 }
1114
1115 var want_build: i64 = 0
1116 var want_seed: i64 = 0
1117 var known: i64 = 0
1118 if icl_seq(verb, "plan" as *u8) == 1 { known = 1 }
1119 if icl_seq(verb, "build" as *u8) == 1 { known = 1; want_build = 1 }
1120 if icl_seq(verb, "seed" as *u8) == 1 { known = 1; want_seed = 1 }
1121 if known == 0 { ic_usage(); sys_exit(IC_USAGE); return IC_USAGE }
1122 if ntg <= 0 { ic_usage(); sys_exit(IC_USAGE); return IC_USAGE }
1123
1124 let ruler: *u8 = sys_mmap(ICL_PATHW)
1125 let builder: *u8 = sys_mmap(ICL_PATHW)
1126 let have_ruler: i64 = ic_pick(root, conf, "closurehash_elf" as *u8, "/../nx_closurehash.elf" as *u8, "/_offc/nx_closurehash.elf" as *u8, "/_build/nx_closurehash.sov.elf" as *u8, ruler)
1127 let have_builder: i64 = ic_pick(root, conf, "builder_elf" as *u8, "/../nx_sov_build_run.elf" as *u8, "/_offc/nx_sov_build_run.elf" as *u8, "/_build/nx_sov_build_run.sov.elf" as *u8, builder)
1128 ic_p("INC-CACHE ruler=" as *u8)
1129 if have_ruler == 1 { ic_p(ruler) } else { ic_p("NOT-FOUND" as *u8) }
1130 ic_p(" builder=" as *u8)
1131 if have_builder == 1 { ic_p(builder) } else { ic_p("NOT-FOUND" as *u8) }
1132 ic_nl()
1133 if want_build == 1 { if have_builder == 0 { ic_e("nx_inc_compile: builder not found; nothing built\n" as *u8); sys_exit(IC_NO_TOOL); return IC_NO_TOOL } }
1134 var need_ruler: i64 = want_seed
1135 if want_build == 1 { need_ruler = 1 }
1136 if need_ruler == 1 { if have_ruler == 0 { ic_e("nx_inc_compile: nx_closurehash not found; refusing to invent a second key\n" as *u8); sys_exit(IC_NO_TOOL); return IC_NO_TOOL } }
1137
1138 let dirty: *i64 = sys_mmap(ICL_MAXF*8 + 16) as *i64
1139 let dn: *i64 = sys_mmap(16) as *i64
1140 let rsn: *i64 = sys_mmap(16) as *i64
1141 let bcap: i64 = ic_ruler_cap()
1142 let bout: *u8 = sys_mmap(bcap + 16)
1143 let bol: *i64 = sys_mmap(16) as *i64
1144 let apath: *u8 = sys_mmap(ICL_PATHW)
1145 let cursha: *u8 = sys_mmap(ic_shaw() + 8)
1146 let now: i64 = sys_now_realtime_sec()
1147 let t_all: i64 = sys_now_ms()
1148 var nhit: i64 = 0
1149 var nmiss: i64 = 0
1150 var nnew: i64 = 0
1151 var nbuilt: i64 = 0
1152 var nskip: i64 = 0
1153 var nfail: i64 = 0
1154 var k: i64 = 0
1155 while k < ntg {
1156 let sp2: *i64 = ic_i64at(tg as i64, k)
1157 let tname: *u8 = sp2[0] as *u8
1158 let t0: i64 = sys_now_ms()
1159 var verdict: i64 = IC_MISS
1160 dn[0] = 0
1161 rsn[0] = 4
1162 if enabled == 1 { verdict = inc_cache_lookup(root, tname, dirty, ICL_MAXF, dn, rsn) }
1163 // A HIT PROMISES THE RECORDED ARTIFACT IS STILL THERE, SO VERIFY IT BEFORE SKIPPING A BUILD.
1164 if verdict == IC_HIT {
1165 let ti: i64 = ic_tgt_find(tname)
1166 let rec: *u8 = ic_u8slot(ict_asha, ti, ic_shaw())
1167 if rec[0] == (0 as u8) { verdict = IC_MISS; rsn[0] = 5 } else {
1168 ic_artifact_path(root, tname, apath)
1169 if ic_sha_file(apath, cursha) == 0 { verdict = IC_MISS; rsn[0] = 6 } else {
1170 if ic_hexeq(rec, cursha) == 0 { verdict = IC_MISS; rsn[0] = 6 }
1171 }
1172 }
1173 }
1174 let t1: i64 = sys_now_ms()
1175 ic_p("INC " as *u8); ic_p(tname); ic_p(" " as *u8)
1176 if verdict == IC_HIT {
1177 let ti2: i64 = ic_tgt_find(tname)
1178 let cp2: *i64 = ic_i64at(ict_mc, ti2)
1179 ic_p("HIT key=" as *u8); ic_p(ic_u8slot(ict_key, ti2, ic_shaw()))
1180 ic_p(" files=" as *u8); ic_n(cp2[0])
1181 nhit = nhit + 1
1182 }
1183 if verdict == IC_NEW { ic_p("NEW reason=no-record" as *u8); nnew = nnew + 1 }
1184 if verdict == IC_MISS {
1185 if enabled == 1 { nmiss = nmiss + 1 } else { nmiss = nmiss + 1 }
1186 ic_p("MISS reason=" as *u8); ic_reason(rsn[0])
1187 ic_p(" dirty=" as *u8); ic_n(dn[0])
1188 }
1189 ic_p(" decide_ms=" as *u8); ic_n(t1 - t0)
1190 ic_nl()
1191 var d: i64 = 0
1192 while d < dn[0] {
1193 if d < ICL_MAXF {
1194 let dp: *i64 = ic_i64at(dirty as i64, d)
1195 let mi: i64 = dp[0]
1196 ic_p("INC-DIRTY " as *u8); ic_p(tname); ic_p(" " as *u8); ic_p(ic_u8slot(icm_name, mi, ICL_NAMEW))
1197 ic_p(" recorded=" as *u8)
1198 let rr: *u8 = ic_u8slot(icm_rsha, mi, ic_shaw())
1199 sys_write(1, rr, IC_SHOWSHA)
1200 ic_p(" observed=" as *u8)
1201 let os: *i64 = ic_i64at(icm_ost, mi)
1202 if os[0] == 1 { sys_write(1, ic_u8slot(icm_osha, mi, ic_shaw()), IC_SHOWSHA) } else { ic_p("UNREADABLE" as *u8) }
1203 ic_nl()
1204 }
1205 d = d + 1
1206 }
1207 if want_build == 1 {
1208 if verdict == IC_HIT {
1209 nskip = nskip + 1
1210 ic_p("INC-SKIP " as *u8); ic_p(tname); ic_p(" (cache hit -- the builder was NOT forked)" as *u8); ic_nl()
1211 } else {
1212 let b0: i64 = sys_now_ms()
1213 bol[0] = 0
1214 let rc: i64 = ic_build_one(root, builder, tname, btmo, bout, bcap, bol)
1215 let b1: i64 = sys_now_ms()
1216 ic_p("INC-BUILD " as *u8); ic_p(tname); ic_p(" rc=" as *u8); ic_n(rc)
1217 ic_p(" build_ms=" as *u8); ic_n(b1 - b0)
1218 ic_p(" out_bytes=" as *u8); ic_n(bol[0]); ic_nl()
1219 if rc == 0 { nbuilt = nbuilt + 1; ic_record(root, tname, ruler, rtmo, now) } else {
1220 nfail = nfail + 1
1221 sys_write(1, bout, bol[0])
1222 }
1223 }
1224 }
1225 if want_seed == 1 { ic_record(root, tname, ruler, rtmo, now) }
1226 k = k + 1
1227 }
1228 var ib2: i64 = 0
1229 if enabled == 1 {
1230 var write_it: i64 = want_seed
1231 if want_build == 1 { write_it = 1 }
1232 if write_it == 1 { ib2 = ic_index_save(root, budget) }
1233 }
1234 let t_end: i64 = sys_now_ms()
1235 ic_p("INC-SUMMARY targets=" as *u8); ic_n(ntg)
1236 ic_p(" hit=" as *u8); ic_n(nhit)
1237 ic_p(" miss=" as *u8); ic_n(nmiss)
1238 ic_p(" new=" as *u8); ic_n(nnew)
1239 ic_p(" built=" as *u8); ic_n(nbuilt)
1240 ic_p(" skipped=" as *u8); ic_n(nskip)
1241 ic_p(" failed=" as *u8); ic_n(nfail)
1242 ic_p(" modules_hashed=" as *u8); ic_n(ic_hashed)
1243 ic_p(" modules_reused=" as *u8); ic_n(ic_reused)
1244 ic_p(" index_bytes=" as *u8); ic_n(ib2)
1245 ic_p(" ms_total=" as *u8); ic_n(t_end - t_all)
1246 ic_p(" corpus_complete=1" as *u8)
1247 ic_nl()
1248 if nfail > 0 { sys_exit(IC_BUILD_FAIL); return IC_BUILD_FAIL }
1249 return IC_OK
1250}