nx_inc_compile.nx
buildroot/runtime/nx_inc_compile.nx
about
nx_inc_compile.nx -- LN10: THE PER-MODULE INCREMENTAL BUILD CACHE.
CONTRACT (buildroot/knowledge/compare/lang.plan, rung LN10, verbatim):
"Per-module cache keyed by the closure sha the build door already computes (the B2 cache keys whole
targets); gate proves a one-file edit rebuilds only its dependents and the output equals a full build"
WHAT ALREADY EXISTED, AND WHAT WAS MISSING
------------------------------------------
B2 (nx_sov_build_run's bld_cache_cas) is a WHOLE-TARGET content-addressed cache: key = sha256 over the
EXPANDED closure text plus the compiler and assembler shas, value = the finished elf. It is excellent
and it is not this rung. Its unit is ONE target and its key costs a full closure EXPANSION (a 32 MiB
buffer, every byte of every dependency) paid once PER TARGET -- so it can answer "rebuild this target?"
only after doing most of the work of deciding. The question a multi-target session actually asks is the
DEPENDENTS question: "I edited one file -- which targets must rebuild, and which must not?" Nothing in
the estate could answer that without re-walking every closure.
THE KEY WAS ALREADY SOLVED AND IS NOT RE-INVENTED HERE.
nx_closurehash is the estate's ONE closure ruler; /api/build forks it on every build and records
`closure_sha256` into <target>.provenance. This organ NEVER computes a closure sha. It parses the
ruler's own per-file listing -- which is already a per-MODULE manifest, one `<sha256> <bytes> <name>`
row per source -- stores those rows as the per-module cache, and REPLAYS the ruler's own key on a hit.
There is exactly one closure-sha implementation in the estate and this is not a second one.
WHY PER-MODULE SHAS ARE SUFFICIENT -- stated because a cache that returns a stale object is a silent
miscompile, the worst class this estate tracks:
closure_sha = sha256 over the rows "<name> <sha>" in discovery order. It therefore changes iff the
member SET changes or some member's SHA changes. The member set can change only because some module's
import list changed, or because a name started resolving to a different file -- and BOTH of those
change that module's bytes, hence its sha. So:
every recorded module still hashes to its recorded sha <=> closure_sha unchanged.
The gate proves the forward direction against the LIVE ruler on every subject it touches, and the
neg-control proves the converse (a one-byte edit MISSES).
The one assumption is that resolution is the SAME resolution the ruler performed -- which is why the
resolver lives in nx_incclosure_lib and both organs call it. A private copy here would be the exact
defect that produces a false HIT.
THE INVARIANT THAT MAKES A SHARED MODULE TABLE SOUND (and the dependents mechanism itself):
for every target record T and every module M in T's member list, icm_rsha[M] is the sha that was used
when T's key was recorded. It is maintained by ic_record(): writing a NEW sha for M INVALIDATES every
other target whose member list contains M (announced, one INC-INVALIDATE line each). Without that,
dependencies 6 imports · 0 importers
imports: nx_syscalls.nxnx_sha256.nxnx_itoa_lib.nxnx_lineconf_lib.nxnx_tool_run.nxnx_incclosure_lib.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 89 | const IC_TAB_INIT: i64 = 256 |
| 90 | const IC_MEM_INIT: i64 = 4096 |
| 94 | const IC_ROWW: i64 = 320 |
| 95 | const IC_SLACK: i64 = 4096 |
| 96 | const IC_SHOWSHA: i64 = 12 // digest prefix shown in a dirty-module line (full shas stay in the index) |
| 97 | const IC_BUDGET_BOOTSTRAP: i64 = 1048576 |
| 98 | const IC_RULER_TMO_BOOT: i64 = 120000 |
| 99 | const IC_BUILD_TMO_BOOT: i64 = 900000 |
| 100 | const IC_PIPE: i64 = 124 |
| 101 | const IC_COMMA: i64 = 44 |
| 102 | const IC_NL: i64 = 10 |
| 103 | const IC_SP: i64 = 32 |
| 104 | const IC_HASH: i64 = 35 |
| 105 | const IC_DIRMODE: i64 = 0x1ed // 0755 |
| 106 | const IC_FILEMODE: i64 = 0x1a4 // 0644 |
| 107 | const IC_OK: i64 = 0 |
| 108 | const IC_BUILD_FAIL: i64 = 1 |
| 109 | const IC_USAGE: i64 = 2 |
| 110 | const IC_NO_TOOL: i64 = 4 |
| 111 | const IC_HIT: i64 = 1 |
| 112 | const IC_MISS: i64 = 0 |
| 113 | const IC_NEW: i64 = 0 - 1 |
functions
| 115 | func ic_ruler_cap() -> i64 { return ICL_MAXF * IC_ROWW + IC_SLACK } |
| 116 | func ic_shaw() -> i64 { return ICL_SHA_HEX + 1 } |
| 118 | func ic_p(s: *u8) -> i64 { sys_write(1, s, icl_len(s)); return 0 } |
| 119 | func ic_e(s: *u8) -> i64 { sys_write(2, s, icl_len(s)); return 0 } |
| 120 | func ic_n(v: i64) -> i64 { nxi_out(v); return 0 } |
| 121 | func 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 } |
| 122 | func ic_hexeq(a: *u8, b: *u8) -> i64 |
| 127 | func ic_starts(s: *u8, pfx: *u8) -> i64 |
| 132 | func ic_ishex64(b: *u8, off: i64, n: i64) -> i64 called by 1: ic_record |
| 176 | func ic_guard_name() -> *u8 { return ic_guard_p as *u8 } called by 1: ic_record |
| 177 | func ic_cc_name() -> *u8 { return ic_cc_p as *u8 } called by 1: ic_record |
| 178 | func ic_asm_name() -> *u8 { return ic_asm_p as *u8 } called by 1: ic_record |
| 180 | func ic_u8slot(base: i64, i: i64, stride: i64) -> *u8 { return (base + i*stride) as *u8 } |
| 181 | func ic_i64at(base: i64, i: i64) -> *i64 { return (base + i*8) as *i64 } |
| 182 | func ic_copy_u8(dstp: i64, srcp: i64, n: i64) -> i64 |
| 192 | func ic_mod_init() -> i64 |
| 202 | func ic_mod_grow() -> i64 |
| 225 | func ic_intern(name: *u8) -> i64 called by 3: ic_index_loadic_add_extraic_record calls 6: icl_lenicl_seqic_u8slotic_mod_growic_shawic_i64at |
| 246 | func ic_tgt_init() -> i64 |
| 262 | func ic_tgt_grow() -> i64 |
| 285 | func ic_mem_push(v: i64) -> i64 |
| 299 | func ic_tgt_find(name: *u8) -> i64 |
| 311 | func ic_tgt_slot(name: *u8) -> i64 |
| 333 | func ic_tgt_has_module(ti: i64, mi: i64) -> i64 |
| 352 | func ic_observe(root: *u8, mi: i64) -> i64 called by 2: inc_cache_lookupic_add_extra calls 12: ic_i64atsys_mmapic_u8slotic_startsicl_caticl_exists+6 |
| 388 | func inc_cache_lookup(root: *u8, target: *u8, dirty: *i64, dirtycap: i64, dirtyn: *i64, reason: *i64) -> i64 |
| 418 | func ic_artifact_path(root: *u8, target: *u8, out: *u8) -> i64 |
| 426 | func ic_sha_file(path: *u8, out: *u8) -> i64 |
| 439 | func ic_dir(root: *u8, out: *u8) -> i64 |
| 445 | func ic_index_path(root: *u8, out: *u8) -> i64 |
| 451 | func ic_upto(b: *u8, i: i64, end: i64, sep: i64, out: *u8, cap: i64) -> i64 called by 1: ic_index_load |
| 467 | func ic_dec(b: *u8, i: i64, end: i64, outv: *i64) -> i64 |
| 484 | func ic_eol(b: *u8, from: i64, n: i64) -> i64 |
| 489 | func ic_setsha(dst: *u8, src: *u8) -> i64 |
| 497 | func ic_index_load(root: *u8) -> i64 |
| 586 | func ic_catn(d: *u8, o: i64, v: i64) -> i64 |
| 602 | func ic_skipsp(b: *u8, from: i64, end: i64) -> i64 called by 1: ic_record |
| 614 | func ic_serialize(buf: *u8, cap: i64, remap: *i64) -> i64 |
| 694 | func ic_index_save(root: *u8, budget: i64) -> i64 |
| 762 | func ic_find(b: *u8, from: i64, end: i64, needle: *u8) -> i64 |
| 780 | func ic_add_extra(root: *u8, ti: i64, name: *u8, ms_start: i64, count: i64) -> i64 |
| 799 | func ic_invalidate_others(ti: i64, mi: i64) -> i64 |
| 821 | func ic_record(root: *u8, target: *u8, ruler: *u8, tmo: i64, now: i64) -> i64 |
| 924 | func ic_try(root: *u8, tail: *u8, out: *u8) -> i64 |
| 937 | func ic_abs(p: *u8) -> i64 |
| 953 | func ic_pick(root: *u8, conf: *u8, key: *u8, t1: *u8, t2: *u8, t3: *u8, out: *u8) -> i64 |
| 963 | func ic_build_one(root: *u8, builder: *u8, target: *u8, tmo: i64, out: *u8, cap: i64, ol: *i64) -> i64 |
| 971 | func ic_reason(code: i64) -> i64 |
| 981 | func ic_usage() -> i64 |
| 990 | func main(argc: i64, argv: *i64) -> i64 |