code wiki / (root) / nx_inc_compile.nx

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}