code wiki / (root) / nx_seg_store.nx

nx_seg_store.nx source

↩ module page · 5113 lines · 252513 B

1// nx_seg_store.nx -- PART A of the sovereign storage substrate rung 1 2// (knowledge/specs/2026-06-09-tutoring-storage-substrate-rung1.md). 3// 4// Append-only IMMUTABLE segment store, pure NishiLang, NO SQL. Model: 5// - a segment is written once (seg-<id>.docs) and never mutated 6// - new data => a NEW segment; deletes => tombstone entries (ADDITIVE law: 7// history is never destroyed, old versions stay readable) 8// - THE COMMIT POINT: segment temp file -> rename(2), then manifest temp -> 9// rename(2). Readers only see segments listed in manifest.txt, so a 10// half-written segment (crash before commit) is INVISIBLE by construction. 11// That is the crash-safety claim the gate proves by fault injection. 12// 13// Entry format inside .docs: u8 kind (1=put 2=tombstone) | u32be klen | key 14// | u32be vlen | value-bytes 15// Values are nx_canon_cid canonical record bytes (content-addressed upstream). 16// "Directory" = a path PREFIX string (e.g. "/tmp/im123-"), so no mkdir is 17// needed (sys_mkdir landmine avoided); a durable store uses a knowledge/ prefix. 18// 19// Rung-2 additions 2026-06-10: sys_fsync (probe-proven x86 74) on every file 20// write + directory fsync after the commit renames = the IM3 power-loss debt 21// CLOSED at the code level; per-segment sorted key index seg-<id>.keys with 22// binary-search reads (ss_get_idx), scan path kept as the ORACLE the gate 23// races it against. Honest remaining scope (flagged, NOT silent caps): 24// term-postings for text search (IM2 full: delta+bitpack), manifest cap 256 25// segments + 4096 keys/segment (compaction/merge = IM4). 26// license_tier: ORIGINAL 27 28import "nx_syscalls.nx" 29const SS_MAGIC_16777619: i64 = 16777619 30const SS_MAGIC_65536: i64 = 65536 31const SS_MAGIC_1048576: i64 = 1048576 32const SS_MAGIC_4096: i64 = 4096 33// MADV_WILLNEED, named for exactly ONE purpose -- the index-blob prefetch in ss_open2 -- so it can 34// never drift into doubling as some other advice value. The six existing WILLNEED sites one file away 35// in nx_docportal_search_seg pass a bare 3; this is that same constant given a name at the layer that 36// now depends on it. 37const SS_MADV_WILLNEED: i64 = 3 38const SS_MAGIC_1024: i64 = 1024 39const SS_MAGIC_1039: i64 = 1039 40const SS_MAGIC_1040: i64 = 1040 41const SS_MAGIC_1071: i64 = 1071 42const SS_MAGIC_1072: i64 = 1072 43const SS_MAGIC_1279: i64 = 1279 44const SS_MAGIC_12352: i64 = 12352 45const SS_MAGIC_12543: i64 = 12543 46const SS_MAGIC_3040: i64 = 3040 47const SS_MAGIC_13312: i64 = 13312 48const SS_MAGIC_19903: i64 = 19903 49const SS_MAGIC_3400: i64 = 3400 50const SS_MAGIC_19968: i64 = 19968 51const SS_MAGIC_40959: i64 = 40959 52const SS_MAGIC_44032: i64 = 44032 53const SS_MAGIC_55215: i64 = 55215 54const SS_MAGIC_63744: i64 = 63744 55const SS_MAGIC_64255: i64 = 64255 56const SS_MAGIC_1469598103934665603: i64 = 1469598103934665603 57const SS_MAGIC_1099511628211: i64 = 1099511628211 58 59func ss_len(s: *u8) -> i64 { 60 var n: i64 = 0 61 while s[n] != (0 as u8) { n = n + 1 } 62 return n 63} 64 65func ss_cat(dst: *u8, off: i64, s: *u8) -> i64 { 66 var i: i64 = 0 67 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 } 68 return off + i 69} 70 71// append decimal of v 72func ss_catn(dst: *u8, off: i64, v: i64) -> i64 { 73 var m: i64 = v 74 var o: i64 = off 75 if m < 0 { m = 0 - m; dst[o] = 45 as u8; o = o + 1 } 76 let t: *u8 = sys_mmap(28) 77 var k: i64 = 0 78 if m == 0 { t[0] = 48 as u8; k = 1 } 79 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 80 var i: i64 = 0 81 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 82 return o + k 83} 84 85func ss_w32(p: *u8, off: i64, v: i64) -> i64 { 86 p[off] = ((v >> 24) & 0xff) as u8 87 p[off + 1] = ((v >> 16) & 0xff) as u8 88 p[off + 2] = ((v >> 8) & 0xff) as u8 89 p[off + 3] = (v & 0xff) as u8 90 return off + 4 91} 92 93func ss_r32(p: *u8, off: i64) -> i64 { 94 let a: i64 = p[off] 95 let b: i64 = p[off + 1] 96 let c: i64 = p[off + 2] 97 let d: i64 = p[off + 3] 98 return (((((a << 8) | b) << 8) | c) << 8) | d 99} 100 101// read whole file into fresh mmap; szout[0] = size (-1 if absent) 102// SS_READALL_PAD: the slack ss_readall maps beyond the file size. It lives HERE, beside the map it pads, and 103// ss_freeall (below) re-derives the mapping from it -- a caller forced to know its allocator's padding is a 104// coupling that drifts (the sys_read_file / sys_free_file pair is the precedent, 2026-08-17). 105const SS_READALL_PAD: i64 = 64 106func ss_readall(path: *u8, szout: *i64) -> *u8 { 107 let fd: i64 = sys_openat_rd(path) 108 if fd < 0 { szout[0] = 0 - 1; return 0 as *u8 } 109 let sz: i64 = sys_lseek(fd, 0, 2) 110 sys_lseek(fd, 0, 0) 111 let buf: *u8 = sys_mmap(sz + SS_READALL_PAD) 112 var got: i64 = 0 113 var n: i64 = 1 114 while n > 0 { 115 n = sys_read(fd, (buf as i64 + got) as *u8, SS_MAGIC_65536) 116 if n > 0 { got = got + n } 117 } 118 sys_close(fd) 119 szout[0] = got 120 return buf 121} 122// PAIRED FREE FOR ss_readall (2026-08-19): pass the length ss_readall reported through szout; null-safe and 123// negative-safe (an absent file returns 0 / -1), so callers need no guard. Every per-request caller that reads an 124// index through ss_readall and never released it was the per-request mmap-leak class (nx_opaque_login, 177 GB). 125func ss_freeall(buf: *u8, len: i64) -> i64 { 126 if (buf as i64) == 0 { return 0 } 127 if len < 0 { return 0 } 128 return sys_munmap(buf, len + SS_READALL_PAD) 129} 130 131// SEGMENT LOADER (2026-07-23, the mmap-serve rung): read a segment file EITHER by reading it fully into 132// anon RAM (usemmap=0, the historical default -- every consumer keeps this) OR by a read-only FILE-BACKED 133// mmap (usemmap=1). Same (ptr, *szout) contract as ss_readall, so it is a drop-in swap. mmap mode means: 134// load time ~0 (nothing copied), only TOUCHED pages become resident (a query reads its hits, not the whole 135// 1.6GB shard), the mapping is SHARED across forked children (read-only, no COW) AND backed by the OS page 136// cache that survives daemon restarts. This is what turns the RAM-bound serve ceiling into a disk-bound one 137// (research: "memory-mapped immutable segments give fast read-heavy retrieval"). Segments are append-only 138// immutable and readers never mutate segment bytes, so PROT_READ is safe by construction. 139func ss_loadfile(path: *u8, szout: *i64, usemmap: i64) -> *u8 { 140 if usemmap == 1 { 141 let m: *u8 = sys_map_file(path, szout) 142 if (m as i64) != 0 { return m } 143 // mmap failed (missing/empty) -> szout already 0; fall through to a normal read so callers that 144 // treat "size 0" as absent behave identically to ss_readall's -1/empty on a missing file. 145 } 146 return ss_readall(path, szout) 147} 148 149const SS_ERR_DURABILITY: i64 = -10 150 151func ss_writefile(path: *u8, bytes: *u8, n: i64) -> i64 { 152 let fd: i64 = sys_openat_wr(path, 0x1a4) 153 if fd < 0 { return 0 - 1 } 154 var off: i64 = 0 155 while off < n { 156 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off) 157 if wr <= 0 { sys_close(fd); return 0 - 2 } 158 off = off + wr 159 } 160 let sync_rc: i64 = sys_fsync(fd) 161 let close_rc: i64 = sys_close(fd) 162 if sync_rc != 0 || close_rc != 0 { return SS_ERR_DURABILITY } 163 return 0 164} 165 166// fsync the directory holding the store files so the rename(2) commit point 167// itself reaches stable storage (power-loss closure of the IM3 debt; without 168// this only process-crash safety is proven). prefix = ".../name-" => dir is 169// everything up to the last "/"; no slash => current directory. 170func ss_syncdir(prefix: *u8) -> i64 { 171 let d: *u8 = sys_mmap(512) 172 var last: i64 = 0 - 1 173 var i: i64 = 0 174 while prefix[i] != (0 as u8) { 175 if prefix[i] == (47 as u8) { last = i } 176 i = i + 1 177 } 178 if last < 0 { 179 d[0] = 46 as u8 180 d[1] = 0 as u8 181 } 182 if last >= 0 { 183 var t: i64 = 0 184 while t <= last { d[t] = prefix[t]; t = t + 1 } 185 d[t] = 0 as u8 186 } 187 let fd: i64 = sys_openat_rd(d) 188 if fd < 0 { return 0 - 1 } 189 let rc: i64 = sys_fsync(fd) 190 sys_close(fd) 191 return rc 192} 193 194// segment writer state: w[0]=buf ptr, w[1]=len, w[2]=cap (heap-mmap idiom, no stack structs) 195// writer with a caller-sized buffer (compaction sizes it from the merged total -- no fixed cap). 196// ---- SCRATCH RELEASE LIST (2026-08-18, lane F box health) --------------------------------------- 197// The commit path (ss_build_keys -> ss_build_terms -> ss_write_seg) maps ~55x the writer size in 198// index scratch PER COMMIT and never released a byte of it: MEASURED on nx_web_crawl_step as 264-456 199// MB/min of address growth (nx_leak_check fleet, full population), and paid identically by every 200// seg-store writer in the estate. Every one of those mappings is consumed IN-CALL -- the builders emit 201// into caller-owned blobs (kout / tblob / pblob / qblob) or straight to disk -- so releasing them at 202// function exit is sound by construction. 203// SHAPE, chosen so it cannot drift: each builder owns a small release list; ss_sr_map records each 204// mapping AS IT IS MADE (pointer + the exact length passed to sys_mmap), and ONE ss_sr_release call on 205// EVERY exit path -- early returns included -- frees the lot. A future allocation routed through 206// ss_sr_map is freed automatically; nothing has to mirror sizes across eight return sites by hand. 207// Sub-256 B requests are arena-class where sys_munmap is a documented no-op; they pass through 208// unchanged (recorded, released as no-ops), so the list needs no size branch. 209// The list itself is arena-class (SS_SR_SLOTS x 16 B) -- unfreed by design, ~0.5 KB per commit, 210// which the arena chunk amortises; naming it here is the whole cost. 211const SS_SR_SLOTS: i64 = 40 // max mappings one builder records (build_terms makes 25; head-room named) 212const SS_SR_STRIDE: i64 = 2 // (ptr, len) per slot 213func ss_sr_new() -> *i64 { let l: *i64 = sys_mmap(8 * (1 + SS_SR_SLOTS * SS_SR_STRIDE)) as *i64; l[0] = 0; return l } 214func ss_sr_map(l: *i64, n: i64) -> *u8 { 215 let p: *u8 = sys_mmap(n) 216 let k: i64 = l[0] 217 if k < SS_SR_SLOTS { l[1 + k * SS_SR_STRIDE] = p as i64; l[2 + k * SS_SR_STRIDE] = n; l[0] = k + 1 } 218 else { 219 // FULL LIST ANNOUNCES rather than silently leaking the overflow: a cap reached in silence 220 // becomes a measurement nobody knows is partial. 221 sys_write(2, "SS-SR-LIST-FULL: a builder mapped more scratch than SS_SR_SLOTS records; the overflow is NOT released -- raise SS_SR_SLOTS\n" as *u8, 129) 222 } 223 return p 224} 225func ss_sr_release(l: *i64) -> i64 { 226 var i: i64 = 0 227 let k: i64 = l[0] 228 while i < k { sys_munmap(l[1 + i * SS_SR_STRIDE] as *u8, l[2 + i * SS_SR_STRIDE]); i = i + 1 } 229 l[0] = 0 230 return k 231} 232 233func ss_begin_cap(cap: i64) -> *i64 { 234 let w: *i64 = sys_mmap(32) as *i64 235 w[0] = sys_mmap(cap) as i64 236 w[1] = 0 237 w[2] = cap 238 return w 239} 240// default single-write buffer (a document is ~KB; ss_add2 fail-closes if exceeded, never truncates). 241func ss_begin() -> *i64 { 242 return ss_begin_cap(SS_MAGIC_1048576) 243} 244 245func ss_add(w: *i64, kind: i64, key: *u8, val: *u8, vlen: i64) -> i64 { 246 return ss_add2(w, kind, key, ss_len(key), val, vlen) 247} 248 249// explicit key length (compaction feeds keys straight from .docs bytes, not null-terminated) 250func ss_add2(w: *i64, kind: i64, key: *u8, kl: i64, val: *u8, vlen: i64) -> i64 { 251 let buf: *u8 = w[0] as *u8 252 var o: i64 = w[1] 253 if o + kl + vlen + 16 > w[2] { return 0 - 1 } 254 buf[o] = kind as u8 255 o = o + 1 256 o = ss_w32(buf, o, kl) 257 var t: i64 = 0 258 while t < kl { buf[o] = key[t]; o = o + 1; t = t + 1 } 259 o = ss_w32(buf, o, vlen) 260 t = 0 261 while t < vlen { buf[o] = val[t]; o = o + 1; t = t + 1 } 262 w[1] = o 263 return 0 264} 265 266// build "<prefix>seg-<id>.docs" (+ ".tmp" if tmp==1) 267func ss_segname(prefix: *u8, segid: i64, tmp: i64, out: *u8) -> i64 { 268 var o: i64 = 0 269 o = ss_cat(out, o, prefix) 270 o = ss_cat(out, o, "seg-" as *u8) 271 o = ss_catn(out, o, segid) 272 o = ss_cat(out, o, ".docs" as *u8) 273 if tmp == 1 { o = ss_cat(out, o, ".tmp" as *u8) } 274 out[o] = 0 as u8 275 return o 276} 277 278// length-aware byte-lex compare: <0, 0, >0 (shorter strict-prefix sorts first) 279// Key hash for the compactor's dedup INDEX (2026-08-06). Uses the same 131 multiplier as 280// sts_pfxhash so the estate carries ONE hashing idiom rather than two. Masked to stay positive; 281// wrap-around in the multiply is fine and intended for a hash. 282// THIS IS AN INDEX ONLY -- it never decides equality. Every probe still confirms with ss_kcmp, so a 283// collision costs one extra comparison and can never merge two distinct keys. A hash that is trusted 284// as an identity is a data-loss bug; a hash that is trusted as a HINT is just a speedup. 285func ss_khash(k: *u8, kl: i64) -> i64 { 286 var h: i64 = 2166136261 287 var i: i64 = 0 288 while i < kl { 289 h = h * 131 + (k[i] as i64) 290 h = h & 0x3FFFFFFFFFFFFFFF 291 i = i + 1 292 } 293 return h 294} 295func ss_kcmp(a: *u8, al: i64, b: *u8, bl: i64) -> i64 { 296 var n: i64 = al 297 if bl < n { n = bl } 298 var i: i64 = 0 299 while i < n { 300 let ca: i64 = a[i] 301 let cb: i64 = b[i] 302 if ca != cb { return ca - cb } 303 i = i + 1 304 } 305 return al - bl 306} 307 308// Build the sorted key index blob for a writer buffer (IM2 rung). Within a 309// segment the LAST entry for a key wins (identical to scan semantics), so 310// duplicates are dropped keeping the later one. Layout: 311// "NXK1" | u32be N | N x u32be entry-rel-offset | entries: 312// u8 kind | u32be klen | key | u32be voff(into .docs) | u32be vlen 313// Returns index byte length written into kout. 314// ONE derivation of the key-blob capacity for a writer, shared by ss_commit_body and the store gate: every emitted 315// index entry is smaller than three times its record, plus a page of headroom. 316func ss_keyblob_cap(w: *i64) -> i64 { return w[1] * 3 + SS_MAGIC_65536 } 317// ss_build_keys refusal codes, NAMED so the store gate plants and asserts them by name rather than by a second copy 318const SS_KEYS_REFUSE_KEY: i64 = 0 - 2 // a record's key length runs past the writer buffer 319const SS_KEYS_REFUSE_VALUE: i64 = 0 - 3 // a record's value length runs past the writer buffer 320func ss_build_keys(w: *i64, kout: *u8) -> i64 { 321 let buf: *u8 = w[0] as *u8 322 let blen: i64 = w[1] 323 // capacity DATA-DRIVEN from the writer size (every record >= 9 bytes) 324 let maxn: i64 = blen / 9 + 16 325 let srl: *i64 = ss_sr_new() 326 let kls: *i64 = ss_sr_map(srl, 8 * maxn) as *i64 327 let kps: *i64 = ss_sr_map(srl, 8 * maxn) as *i64 328 let vos: *i64 = ss_sr_map(srl, 8 * maxn) as *i64 329 let vls: *i64 = ss_sr_map(srl, 8 * maxn) as *i64 330 let kinds: *i64 = ss_sr_map(srl, 8 * maxn) as *i64 331 var n: i64 = 0 332 var i: i64 = 0 333 while i + 9 <= blen { 334 let kind: i64 = buf[i] 335 let kl: i64 = ss_r32(buf, i + 1) 336 let koff: i64 = i + 5 337 // A MALFORMED RECORD IS REFUSED, NEVER WALKED: a key or value length that runs past the writer buffer would 338 // read garbage as the next header and the emit below would then touch memory outside the buffer (measured 339 // 2026-09-14 as a SIGSEGV at the key-emit loop during an estate ingest). -2 = key past the buffer, 340 // -3 = value past the buffer; ss_commit_body already turns any negative into its commit refusal. 341 if kl < 0 { ss_sr_release(srl); return SS_KEYS_REFUSE_KEY } 342 if koff + kl + 4 > blen { ss_sr_release(srl); return SS_KEYS_REFUSE_KEY } 343 let vl: i64 = ss_r32(buf, koff + kl) 344 let voff: i64 = koff + kl + 4 345 if vl < 0 { ss_sr_release(srl); return SS_KEYS_REFUSE_VALUE } 346 if voff + vl > blen { ss_sr_release(srl); return SS_KEYS_REFUSE_VALUE } 347 if n >= maxn { ss_sr_release(srl); return 0 - 1 } 348 kinds[n] = kind 349 kls[n] = kl 350 kps[n] = (buf as i64) + koff 351 vos[n] = voff 352 vls[n] = vl 353 n = n + 1 354 i = voff + vl 355 } 356 // sort by (key bytes, original index) -- bottom-up merge, stable; equal 357 // keys end up chronological, so the LAST of a run is the shadowing entry 358 let sidx: *i64 = ss_sr_map(srl, 8 * (n + 16)) as *i64 359 let stmp: *i64 = ss_sr_map(srl, 8 * (n + 16)) as *i64 360 i = 0 361 while i < n { sidx[i] = i; i = i + 1 } 362 // RESEARCH-DRIVEN FAST-PATH (Knuth TAOCP vol.3 5.2.5): when keys are UNIFORM length, a stable LSD radix 363 // sort is O(width*n) and produces a result BIT-IDENTICAL to the stable merge sort (a stable sort is 364 // unique). Variable-length keys fall through to the merge sort. Proven 5.1x on the index in nx_radix_index_bench. 365 var ss_uniform: i64 = 1 366 let ss_ul: i64 = kls[0] 367 i = 1 368 while i < n { if kls[i] != ss_ul { ss_uniform = 0 } i = i + 1 } 369 var ss_radixed: i64 = 0 370 if ss_uniform == 1 { if n > 64 { if ss_ul > 0 { 371 let rcnt: *i64 = ss_sr_map(srl, 256 * 8) as *i64 372 var pos: i64 = ss_ul - 1 373 while pos >= 0 { 374 var c: i64 = 0 375 while c < 256 { rcnt[c] = 0; c = c + 1 } 376 i = 0 377 while i < n { let kp: *u8 = kps[sidx[i]] as *u8; let kb: i64 = kp[pos] as i64; rcnt[kb] = rcnt[kb] + 1; i = i + 1 } 378 var sm: i64 = 0 379 c = 0 380 while c < 256 { let tc: i64 = rcnt[c]; rcnt[c] = sm; sm = sm + tc; c = c + 1 } 381 i = 0 382 while i < n { let kp: *u8 = kps[sidx[i]] as *u8; let kb: i64 = kp[pos] as i64; stmp[rcnt[kb]] = sidx[i]; rcnt[kb] = rcnt[kb] + 1; i = i + 1 } 383 i = 0 384 while i < n { sidx[i] = stmp[i]; i = i + 1 } 385 pos = pos - 1 386 } 387 ss_radixed = 1 388 } } } 389 if ss_radixed == 0 { 390 var width: i64 = 1 391 while width < n { 392 var lo: i64 = 0 393 while lo < n { 394 var mid: i64 = lo + width 395 if mid > n { mid = n } 396 var hi: i64 = lo + 2 * width 397 if hi > n { hi = n } 398 var a2: i64 = lo 399 var b2: i64 = mid 400 var o2: i64 = lo 401 while o2 < hi { 402 var takea: i64 = 0 403 if a2 < mid { 404 if b2 >= hi { takea = 1 } else { 405 if ss_kcmp(kps[sidx[a2]] as *u8, kls[sidx[a2]], kps[sidx[b2]] as *u8, kls[sidx[b2]]) <= 0 { takea = 1 } 406 } 407 } 408 if takea == 1 { stmp[o2] = sidx[a2]; a2 = a2 + 1 } else { stmp[o2] = sidx[b2]; b2 = b2 + 1 } 409 o2 = o2 + 1 410 } 411 o2 = lo 412 while o2 < hi { sidx[o2] = stmp[o2]; o2 = o2 + 1 } 413 lo = lo + 2 * width 414 } 415 width = width * 2 416 } 417 } 418 // collapse equal-key runs to their LAST (latest) entry 419 let idx: *i64 = ss_sr_map(srl, 8 * (n + 16)) as *i64 420 var m: i64 = 0 421 i = 0 422 while i < n { 423 var j: i64 = i + 1 424 var run: i64 = 1 425 while run == 1 { 426 if j >= n { run = 0 } 427 if run == 1 { 428 if ss_kcmp(kps[sidx[j]] as *u8, kls[sidx[j]], kps[sidx[i]] as *u8, kls[sidx[i]]) == 0 { j = j + 1 } else { run = 0 } 429 } 430 } 431 idx[m] = sidx[j - 1] 432 m = m + 1 433 i = j 434 } 435 // emit 436 kout[0] = 78 as u8 // N 437 kout[1] = 88 as u8 // X 438 kout[2] = 75 as u8 // K 439 kout[3] = 49 as u8 // 1 440 ss_w32(kout, 4, m) 441 let tbl: i64 = 8 442 let base: i64 = 8 + 4 * m 443 var o: i64 = base 444 i = 0 445 while i < m { 446 let e: i64 = idx[i] 447 ss_w32(kout, tbl + 4 * i, o - base) 448 kout[o] = kinds[e] as u8 449 o = o + 1 450 o = ss_w32(kout, o, kls[e]) 451 let kp: *u8 = kps[e] as *u8 452 var t: i64 = 0 453 while t < kls[e] { kout[o] = kp[t]; o = o + 1; t = t + 1 } 454 o = ss_w32(kout, o, vos[e]) 455 o = ss_w32(kout, o, vls[e]) 456 i = i + 1 457 } 458 ss_sr_release(srl) // kout (caller-owned) holds the result; every scratch mapping above is dead here 459 return o 460} 461 462// FAULT INJECTION: write ONLY the temp file = death before the commit point. 463// The gate uses this to prove a half-written segment is invisible. 464func ss_crashwrite(prefix: *u8, w: *i64, segid: i64) -> i64 { 465 let p: *u8 = sys_mmap(512) 466 ss_segname(prefix, segid, 1, p) 467 return ss_writefile(p, w[0] as *u8, w[1]) 468} 469 470// ---- IM2b: term postings (text search on the store) ---- 471// Tokenizer: lowercased alphanumeric runs, length 2..32 (longer runs 472// truncated at 32). IDENTICAL function serves index build, query, and the 473// gate's brute-force oracle -- consistency by construction. 474// .terms layout: "NXT1" | u32be nterms | nterms x u32be entry-off | entries: 475// u32be tlen | term | u32be postoff | u32be postlen | u32be dcount 476// .post layout: "NXP1" | u32be ndocs | ndocs x u32be docs-entry-offset | 477// per-term varint-delta docid streams (ascending; <=128 docs 478// per segment block = exactly the spec's varint tail block; 479// bitpacked 128-blocks = flagged optimization rung) 480 481// varint append (LE 7-bit, high bit = continue); returns new offset 482func ss_vw(p: *u8, off: i64, v: i64) -> i64 { 483 var m: i64 = v 484 var o: i64 = off 485 var go: i64 = 1 486 while go == 1 { 487 let b7: i64 = m % 128 488 m = m / 128 489 if m > 0 { p[o] = (b7 + 128) as u8 } else { p[o] = b7 as u8; go = 0 } 490 o = o + 1 491 } 492 return o 493} 494 495// varint read at pos[0]; advances pos 496func ss_vr(p: *u8, pos: *i64) -> i64 { 497 var v: i64 = 0 498 var sh: i64 = 0 499 var go: i64 = 1 500 while go == 1 { 501 let b: i64 = p[pos[0]] 502 pos[0] = pos[0] + 1 503 v = v + ((b % 128) << sh) 504 sh = sh + 7 505 if b < 128 { go = 0 } 506 } 507 return v 508} 509 510// 256-entry token class table: tbl[c] = 0 for separators, else the 511// lowercased byte. THE one tokenizer definition -- build, query and oracle 512// all tokenize through this table (semantic consistency by construction). 513func ss_tok_table(tbl: *u8) -> i64 { 514 var c: i64 = 0 515 while c < 256 { 516 var v: i64 = 0 517 if c >= 97 { if c <= 122 { v = c } } 518 if c >= 65 { if c <= 90 { v = c + 32 } } 519 if c >= 48 { if c <= 57 { v = c } } 520 tbl[c] = v as u8 521 c = c + 1 522 } 523 return 0 524} 525 526// IDENTIFIER ANCHORS (search rung E1, 2026-09-14). The one tokenizer above keeps letters and digits and treats every 527// other byte -- the underscore included -- as a separator, so `sx_agent_decide` indexes as three common words and the 528// file that DEFINES it ranks behind any page that says agent and decide. For a source corpus the identifier IS the 529// anchor a reader searches by. ss_ident_anchors appends, for every run of identifier bytes that carries at least one 530// underscore, its COLLAPSED form (underscores removed, so the tokenizer keeps it whole), space separated; the query 531// side collapses a query term through ss_ident_collapse -- ONE derivation on both sides, so they cannot drift. 532// Returns bytes written and stops when out is full; the caller sizes out from its input (an anchor is never longer 533// than the run it came from) so a full buffer is a caller defect, not a silent truncation. 534const SS_LOWER_A: i64 = 97 535const SS_LOWER_Z: i64 = 122 536const SS_UPPER_A: i64 = 65 537const SS_UPPER_Z: i64 = 90 538const SS_DIG_0: i64 = 48 539const SS_DIG_9: i64 = 57 540const SS_UNDERSCORE: i64 = 95 541const SS_SP: i64 = 32 542const SS_IDENT_MIN: i64 = 2 // a collapsed anchor shorter than this is noise (an underscore beside one letter) 543func ss_is_ident_byte(c: i64) -> i64 { 544 if c >= SS_LOWER_A { if c <= SS_LOWER_Z { return 1 } } 545 if c >= SS_UPPER_A { if c <= SS_UPPER_Z { return 1 } } 546 if c >= SS_DIG_0 { if c <= SS_DIG_9 { return 1 } } 547 if c == SS_UNDERSCORE { return 1 } 548 return 0 549} 550func ss_ident_anchors(src: *u8, n: i64, out: *u8, cap: i64) -> i64 { 551 var o: i64 = 0 552 var i: i64 = 0 553 while i < n { 554 if ss_is_ident_byte(src[i] as i64) == 0 { i = i + 1 } else { 555 var e: i64 = i 556 var us: i64 = 0 557 var go: i64 = 1 558 while go == 1 { if e >= n { go = 0 } else { let c: i64 = src[e] as i64; if ss_is_ident_byte(c) == 1 { if c == SS_UNDERSCORE { us = us + 1 } e = e + 1 } else { go = 0 } } } 559 if us > 0 { 560 let len: i64 = e - i - us 561 if len >= SS_IDENT_MIN { if o + len + 1 < cap { 562 var k: i64 = i 563 while k < e { let c2: i64 = src[k] as i64; if c2 != SS_UNDERSCORE { out[o] = c2 as u8; o = o + 1 } k = k + 1 } 564 out[o] = SS_SP as u8; o = o + 1 565 } } 566 } 567 i = e 568 } 569 } 570 return o 571} 572// the query-side twin: collapse ONE NUL-terminated term when it carries an underscore. Returns the collapsed length 573// written to out (NUL-terminated), 0 when the term has no underscore -- the caller then adds nothing. 574func ss_ident_collapse(term: *u8, out: *u8, cap: i64) -> i64 { 575 var us: i64 = 0 576 var i: i64 = 0 577 while term[i] != (0 as u8) { if (term[i] as i64) == SS_UNDERSCORE { us = us + 1 } i = i + 1 } 578 if us == 0 { return 0 } 579 var o: i64 = 0 580 i = 0 581 while term[i] != (0 as u8) { if (term[i] as i64) != SS_UNDERSCORE { if o < cap - 1 { out[o] = term[i]; o = o + 1 } } i = i + 1 } 582 out[o] = 0 as u8 583 if o < SS_IDENT_MIN { return 0 } 584 return o 585} 586 587// E1b DEFINITION TOKEN (2026-09-14): the def prefix glued to the collapsed name -- the term a source file carries 588// only when it DEFINES the symbol (the ingest emits one per function head, the query asks for one per typed term). 589// ONE derivation for both sides: a prefix spelled in two organs would agree the day it was written and drift on 590// the next edit. Returns the token length written to out (NUL-terminated). 591const SS_DEF_PREFIX: *u8 = "def" 592func ss_def_token(name: *u8, nlen: i64, out: *u8, cap: i64) -> i64 { 593 let pfx: *u8 = SS_DEF_PREFIX 594 var o: i64 = 0 595 var p: i64 = 0 596 while pfx[p] != (0 as u8) { if o < cap - 1 { out[o] = pfx[p]; o = o + 1 } p = p + 1 } 597 var j: i64 = 0 598 while j < nlen { let c: i64 = name[j] as i64; if c != SS_UNDERSCORE { if o < cap - 1 { out[o] = c as u8; o = o + 1 } } j = j + 1 } 599 out[o] = 0 as u8 600 return o 601} 602 603// ACCENT FOLD (2026-07-03, the Unicode rung): fold a decoded codepoint to its ASCII base letter -- 604// Latin-1 Supplement (U+00C0-U+00FF) + Latin Extended-A (U+0100-U+017F) via 64/128-entry base-letter 605// tables ('.' = stays a separator: multiplication/division signs). "Kyōka" tokenizes as "kyoka" on BOTH 606// the index and query sides (same ss_tok_next2), so accented pages match plain-ASCII queries; compaction 607// re-tokenizes shards through ss_build_terms = the live upgrade path. Non-Latin (CJK etc.) folds to 0 = 608// separator -- CJK SEGMENTATION is a separate named rung, honestly not claimed here. 609func ss_fold_cp(cp: i64) -> i64 { 610 if cp >= 192 { if cp <= 255 { 611 let t: *u8 = "aaaaaaaceeeeiiiidnooooo.ouuuuytsaaaaaaaceeeeiiiidnooooo.ouuuuyty" as *u8 612 let ch: i64 = t[cp - 192] as i64 613 if ch == 46 { return 0 } 614 return ch 615 } } 616 if cp >= 256 { if cp <= 383 { 617 let t2: *u8 = "aaaaaaccccccccddddeeeeeeeeeegggggggghhhhiiiiiiiiiiiijjkkkllllllllllnnnnnnnnnoooooooorrrrrrssssssssttttttuuuuuuuuuuuuwwyyyzzzzzzs" as *u8 618 return t2[cp - 256] as i64 619 } } 620 return 0 621} 622 623// MULTILINGUAL rung (2026-07-23, operator: index ru/eu/zh/ja/ko not just English): 624// decode ONE multibyte UTF-8 char at i -> packed (cp<<3)|adv; cp=0 when not a valid multibyte lead 625// (caller's ASCII fast path owns bytes <194). No allocations -- lives under the tf-scan hot loop. 626func ss_u8cp(b: *u8, sz: i64, i: i64) -> i64 { 627 if i >= sz { return 1 } 628 let c0: i64 = b[i] as i64 629 if c0 < 194 { return 1 } 630 if c0 < 224 { 631 if i + 1 < sz { let c1: i64 = b[i+1] as i64; if c1 >= 128 { if c1 < 192 { 632 return ((c0 - 192) * 64 + (c1 - 128)) * 8 + 2 633 } } } 634 return 1 635 } 636 if c0 < 240 { 637 if i + 2 < sz { 638 let c1: i64 = b[i+1] as i64 639 let c2: i64 = b[i+2] as i64 640 if c1 >= 128 { if c1 < 192 { if c2 >= 128 { if c2 < 192 { 641 return ((c0 - 224) * SS_MAGIC_4096 + (c1 - 128) * 64 + (c2 - 128)) * 8 + 3 642 } } } } 643 } 644 return 1 645 } 646 if i + 3 < sz { return 4 } 647 return 1 648} 649// case-fold a Cyrillic/Greek codepoint to its lowercase WORD form (kept as UTF-8 in the token, 650// unlike ss_fold_cp which folds TO ASCII). 0 = not a word char in these scripts. 651func ss_fold_word_cp(cp: i64) -> i64 { 652 if cp >= SS_MAGIC_1024 { if cp <= SS_MAGIC_1039 { return cp + 80 } } // U+0400-040F upper -> U+0450-045F 653 if cp >= SS_MAGIC_1040 { if cp <= SS_MAGIC_1071 { return cp + 32 } } // А-Я -> а-я 654 if cp >= SS_MAGIC_1072 { if cp <= SS_MAGIC_1279 { return cp } } // а-я + Cyrillic ext: identity 655 if cp >= 913 { if cp <= 937 { if cp != 930 { return cp + 32 } } } // Greek Α-Ω -> α-ω (03A2 hole) 656 if cp == 962 { return 963 } // final sigma -> sigma 657 if cp >= 945 { if cp <= 969 { return cp } } // α-ω identity 658 return 0 659} 660// CJK char class for BIGRAM tokenization (no-space scripts + Hangul): Han + Ext-A + compat, kana, Hangul 661// syllables. Standard CJK IR practice: overlapping character bigrams, symmetric index+query. 662func ss_is_cjk(cp: i64) -> i64 { 663 if cp >= SS_MAGIC_12352 { if cp <= SS_MAGIC_12543 { return 1 } } // Hiragana U+SS_MAGIC_3040-309F + Katakana U+30A0-30FF 664 if cp >= SS_MAGIC_13312 { if cp <= SS_MAGIC_19903 { return 1 } } // CJK Ext-A U+SS_MAGIC_3400-4DBF 665 if cp >= SS_MAGIC_19968 { if cp <= SS_MAGIC_40959 { return 1 } } // CJK Unified U+4E00-9FFF 666 if cp >= SS_MAGIC_44032 { if cp <= SS_MAGIC_55215 { return 1 } } // Hangul syllables U+AC00-D7AF 667 if cp >= SS_MAGIC_63744 { if cp <= SS_MAGIC_64255 { return 1 } } // CJK Compat U+F900-FAFF 668 return 0 669} 670 671// next token from b[pos[0]..sz): lowercased alnum run len>=2 (cap 32) into 672// tout (null-terminated); returns token length, or -1 when exhausted. 673// UTF-8 aware: Latin accents FOLD to ASCII (ss_fold_cp); Cyrillic/Greek case-fold and stay UTF-8 674// word chars (ss_fold_word_cp); CJK/kana/Hangul emit overlapping character BIGRAMS (isolated char = 675// unigram); everything else separates, whole sequences consumed so continuations never mangle a token. 676func ss_tok_next2(b: *u8, sz: i64, pos: *i64, tout: *u8, tbl: *u8) -> i64 { 677 var i: i64 = pos[0] 678 var l: i64 = 0 679 while i < sz { 680 // ASCII FAST PATH FIRST (one byte load, one compare added vs the pre-fold loop -- the tf scan 681 // over 32KB x 128 candidates lives here; the reversed branch order cost p95 169->211ms) 682 let c0: i64 = b[i] as i64 683 if c0 < 194 { 684 let m: i64 = tbl[c0] 685 if m != 0 { 686 if l < 32 { tout[l] = m as u8; l = l + 1 } 687 } 688 if m == 0 { 689 if l >= 2 { pos[0] = i + 1; tout[l] = 0 as u8; return l } 690 l = 0 691 } 692 i = i + 1 693 } else { 694 // UTF-8 lead byte (0xC2..0xF4): decode + classify 695 let v1: i64 = ss_u8cp(b, sz, i) 696 let cp: i64 = v1 >> 3 697 let adv: i64 = v1 & 7 698 // 1) Latin accent fold -> ASCII base letter continues the word 699 var f: i64 = 0 700 if cp > 0 { f = ss_fold_cp(cp) } 701 if f != 0 { 702 if l < 32 { tout[l] = f as u8; l = l + 1 } 703 i = i + adv 704 } else { 705 // 2) Cyrillic/Greek word char: case-fold, keep as UTF-8 (2 bytes) in the token 706 var wcp: i64 = 0 707 if cp > 0 { wcp = ss_fold_word_cp(cp) } 708 if wcp != 0 { 709 if l + 2 <= 32 { 710 tout[l] = (192 + wcp / 64) as u8 711 tout[l + 1] = (128 + (wcp % 64)) as u8 712 l = l + 2 713 } 714 i = i + adv 715 } else { 716 // 3) CJK/kana/Hangul: overlapping bigrams (run>=2) / unigram (isolated char) 717 var cjk: i64 = 0 718 if cp > 0 { cjk = ss_is_cjk(cp) } 719 if cjk == 1 { 720 if l >= 2 { pos[0] = i; tout[l] = 0 as u8; return l } // flush word; re-read this char next call 721 l = 0 722 let j: i64 = i + adv 723 let v2: i64 = ss_u8cp(b, sz, j) 724 let cp2: i64 = v2 >> 3 725 let adv2: i64 = v2 & 7 726 var cjk2: i64 = 0 727 if cp2 > 0 { cjk2 = ss_is_cjk(cp2) } 728 if cjk2 == 1 { 729 var x: i64 = 0 730 while x < adv { tout[x] = b[i + x]; x = x + 1 } 731 var y: i64 = 0 732 while y < adv2 { tout[adv + y] = b[j + y]; y = y + 1 } 733 tout[adv + adv2] = 0 as u8 734 // resume AT the second char while the run continues (overlap); past it at run end 735 let k: i64 = j + adv2 736 let v3: i64 = ss_u8cp(b, sz, k) 737 let cp3: i64 = v3 >> 3 738 var cjk3: i64 = 0 739 if cp3 > 0 { cjk3 = ss_is_cjk(cp3) } 740 if cjk3 == 1 { pos[0] = j } else { pos[0] = k } 741 return adv + adv2 742 } 743 var x2: i64 = 0 744 while x2 < adv { tout[x2] = b[i + x2]; x2 = x2 + 1 } 745 tout[adv] = 0 as u8 746 pos[0] = i + adv 747 return adv 748 } 749 // 4) separator (symbols, unclaimed scripts, stray sequences) 750 if l >= 2 { pos[0] = i + adv; tout[l] = 0 as u8; return l } 751 l = 0 752 i = i + adv 753 } 754 } 755 } 756 } 757 pos[0] = sz 758 if l >= 2 { tout[l] = 0 as u8; return l } 759 return 0 - 1 760} 761 762// compat wrapper (one-shot callers; hot loops build the table once instead) 763func ss_tok_next(b: *u8, sz: i64, pos: *i64, tout: *u8) -> i64 { 764 let tbl: *u8 = sys_mmap(272) 765 ss_tok_table(tbl) 766 return ss_tok_next2(b, sz, pos, tout, tbl) 767} 768 769// does the value contain token `term`? (the gate's brute-force oracle) 770func ss_tok_has(b: *u8, sz: i64, term: *u8) -> i64 { 771 let pos: *i64 = sys_mmap(16) as *i64 772 pos[0] = 0 773 let t: *u8 = sys_mmap(40) 774 let tbl: *u8 = sys_mmap(272) 775 ss_tok_table(tbl) 776 var go: i64 = 1 777 while go == 1 { 778 let l: i64 = ss_tok_next2(b, sz, pos, t, tbl) 779 if l < 0 { go = 0 } 780 if go == 1 { 781 var eq: i64 = 1 782 var x: i64 = 0 783 while eq == 1 { 784 if t[x] != term[x] { eq = 0 } 785 if eq == 1 { if t[x] == (0 as u8) { return 1 } } 786 x = x + 1 787 } 788 } 789 } 790 return 0 791} 792 793// 64-bit FNV-1a over a null-terminated token 794func ss_fnv(s: *u8) -> i64 { 795 var h: i64 = SS_MAGIC_1469598103934665603 796 var i: i64 = 0 797 while s[i] != (0 as u8) { 798 h = h ^ (s[i] as i64) 799 h = h * SS_MAGIC_1099511628211 800 i = i + 1 801 } 802 if h < 0 { h = 0 - h } 803 return h 804} 805 806// Build .terms (tblob) + .post (pblob) for a writer buffer; lengths into 807// louts[0]/louts[1]. Capacities are DATA-DRIVEN, derived from the writer 808// size (every record >= 9 bytes => docs <= blen/9; every counted (term,doc) 809// pair consumes >= 3 source bytes => pairs and unique terms <= blen/3) -- 810// no fixed caps to silently or loudly hit; the construction bounds are 811// defensive-checked LOUD (-1) anyway. Term lookup = open-addressing FNV 812// hash (O(1) per token); postings emit = counting buckets (O(pairs)); 813// term dict ordering = bottom-up merge sort (O(n log n)). 814// 2026-07-03 PHRASE RUNG (additive): qblob receives the POSITIONS sidecar ("NXQ1" | u32 nterms | 815// u32 entryoff[nterms] | per-term entries), louts[2] = its length. An entry holds, for each posting doc 816// IN THE SAME (sorted-term, ascending-doc) ORDER as .post: varint(npos) + delta-varint token indexes. 817// Positions are indexes among EMITTED tokens (the same space the query tokenizer sees; 1-char words are 818// invisible to both sides, so they never break adjacency -- documented semantics, not an accident). 819func ss_build_terms(w: *i64, tblob: *u8, pblob: *u8, qblob: *u8, louts: *i64) -> i64 { 820 let buf: *u8 = w[0] as *u8 821 let blen: i64 = w[1] 822 let maxdocs: i64 = blen / 9 + 16 823 let maxterms: i64 = blen / 3 + 64 824 let maxpairs: i64 = blen / 3 + 64 825 let maxocc: i64 = blen / 2 + 64 // every emitted token consumes >= 2 bytes of source 826 let srl: *i64 = ss_sr_new() 827 let docoff: *i64 = ss_sr_map(srl, 8 * maxdocs) as *i64 828 let pool: *u8 = ss_sr_map(srl, blen + SS_MAGIC_65536) 829 var pooloff: i64 = 0 830 let terms: *i64 = ss_sr_map(srl, 8 * maxterms) as *i64 831 var nterms: i64 = 0 832 let pterm: *i64 = ss_sr_map(srl, 8 * maxpairs) as *i64 833 let pdoc: *i64 = ss_sr_map(srl, 8 * maxpairs) as *i64 834 var npairs: i64 = 0 835 let oterm: *i64 = ss_sr_map(srl, 8 * maxocc) as *i64 836 let odoc: *i64 = ss_sr_map(srl, 8 * maxocc) as *i64 837 let opos: *i64 = ss_sr_map(srl, 8 * maxocc) as *i64 838 var nocc: i64 = 0 839 var hts: i64 = 16 840 while hts < maxterms * 2 { hts = hts * 2 } 841 let ht: *i64 = ss_sr_map(srl, 8 * hts) as *i64 842 // per-term last-doc-seen (stores nd+1; 0 = never) -- O(1) per-doc dedupe 843 let lastdoc: *i64 = ss_sr_map(srl, 8 * maxterms) as *i64 844 let tlens: *i64 = ss_sr_map(srl, 8 * maxterms) as *i64 845 var nd: i64 = 0 846 var i: i64 = 0 847 let pos: *i64 = ss_sr_map(srl, 16) as *i64 848 let tok: *u8 = ss_sr_map(srl, 40) 849 let ttbl: *u8 = ss_sr_map(srl, 272) 850 ss_tok_table(ttbl) 851 while i + 9 <= blen { 852 let kind: i64 = buf[i] 853 let kl: i64 = ss_r32(buf, i + 1) 854 let vl: i64 = ss_r32(buf, i + 5 + kl) 855 let voff: i64 = i + 5 + kl + 4 856 if nd >= maxdocs { ss_sr_release(srl); return 0 - 1 } 857 docoff[nd] = i 858 if kind == 1 { 859 pos[0] = voff 860 var tokidx: i64 = 0 861 var go: i64 = 1 862 while go == 1 { 863 let tl: i64 = ss_tok_next2(buf, voff + vl, pos, tok, ttbl) 864 if tl < 0 { go = 0 } 865 if go == 1 { 866 // find/create term id via the hash table (slot holds tid+1) 867 var tid: i64 = 0 - 1 868 var slot: i64 = ss_fnv(tok) % hts 869 var probe: i64 = 1 870 while probe == 1 { 871 if ht[slot] == 0 { probe = 0 } else { 872 let cand: i64 = ht[slot] - 1 873 let tp: *u8 = terms[cand] as *u8 874 var eq: i64 = 1 875 var x: i64 = 0 876 while eq == 1 { 877 if tp[x] != tok[x] { eq = 0 } 878 if eq == 1 { if tp[x] == (0 as u8) { tid = cand; eq = 0 } } 879 x = x + 1 880 } 881 if tid >= 0 { probe = 0 } else { 882 slot = slot + 1 883 if slot >= hts { slot = 0 } 884 } 885 } 886 } 887 if tid < 0 { 888 if nterms >= maxterms { ss_sr_release(srl); return 0 - 1 } 889 if pooloff + tl + 2 >= blen + SS_MAGIC_65536 { ss_sr_release(srl); return 0 - 1 } 890 let dst: *u8 = (pool as i64 + pooloff) as *u8 891 var x2: i64 = 0 892 while x2 <= tl { dst[x2] = tok[x2]; x2 = x2 + 1 } 893 terms[nterms] = dst as i64 894 tlens[nterms] = tl 895 tid = nterms 896 nterms = nterms + 1 897 pooloff = pooloff + tl + 1 898 ht[slot] = tid + 1 899 } 900 // per-doc dedupe: O(1) via the per-term last-doc-seen mark 901 if lastdoc[tid] != nd + 1 { 902 lastdoc[tid] = nd + 1 903 if npairs >= maxpairs { ss_sr_release(srl); return 0 - 1 } 904 pterm[npairs] = tid 905 pdoc[npairs] = nd 906 npairs = npairs + 1 907 } 908 // EVERY occurrence carries its token index (the phrase rung's raw material) 909 if nocc >= maxocc { ss_sr_release(srl); return 0 - 1 } 910 oterm[nocc] = tid 911 odoc[nocc] = nd 912 opos[nocc] = tokidx 913 nocc = nocc + 1 914 tokidx = tokidx + 1 915 } 916 } 917 } 918 nd = nd + 1 919 i = voff + vl 920 } 921 // sort term ids by term bytes (bottom-up merge sort over sidx) 922 let sidx: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 923 let stmp: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 924 var t3: i64 = 0 925 while t3 < nterms { sidx[t3] = t3; t3 = t3 + 1 } 926 var width: i64 = 1 927 while width < nterms { 928 var lo: i64 = 0 929 while lo < nterms { 930 var mid: i64 = lo + width 931 if mid > nterms { mid = nterms } 932 var hi: i64 = lo + 2 * width 933 if hi > nterms { hi = nterms } 934 var a2: i64 = lo 935 var b2: i64 = mid 936 var o2: i64 = lo 937 while o2 < hi { 938 var takea: i64 = 0 939 if a2 < mid { 940 if b2 >= hi { takea = 1 } else { 941 if ss_kcmp(terms[sidx[a2]] as *u8, tlens[sidx[a2]], terms[sidx[b2]] as *u8, tlens[sidx[b2]]) <= 0 { takea = 1 } 942 } 943 } 944 if takea == 1 { stmp[o2] = sidx[a2]; a2 = a2 + 1 } else { stmp[o2] = sidx[b2]; b2 = b2 + 1 } 945 o2 = o2 + 1 946 } 947 o2 = lo 948 while o2 < hi { sidx[o2] = stmp[o2]; o2 = o2 + 1 } 949 lo = lo + 2 * width 950 } 951 width = width * 2 952 } 953 // bucket pairs by term (counting sort; docs stay ascending within a term) 954 let dcnt: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 955 var p9: i64 = 0 956 while p9 < npairs { dcnt[pterm[p9]] = dcnt[pterm[p9]] + 1; p9 = p9 + 1 } 957 let bstart: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 958 let bfill: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 959 var acc: i64 = 0 960 var t9: i64 = 0 961 while t9 < nterms { 962 bstart[t9] = acc 963 bfill[t9] = acc 964 acc = acc + dcnt[t9] 965 t9 = t9 + 1 966 } 967 let bdoc: *i64 = ss_sr_map(srl, 8 * (npairs + 16)) as *i64 968 p9 = 0 969 while p9 < npairs { 970 bdoc[bfill[pterm[p9]]] = pdoc[p9] 971 bfill[pterm[p9]] = bfill[pterm[p9]] + 1 972 p9 = p9 + 1 973 } 974 // emit .post: magic | ndocs | doc-offset table | per-term delta-varint streams 975 pblob[0] = 78 as u8 976 pblob[1] = 88 as u8 977 pblob[2] = 80 as u8 978 pblob[3] = 49 as u8 979 ss_w32(pblob, 4, nd) 980 var po: i64 = 8 + 4 * nd 981 var d2: i64 = 0 982 while d2 < nd { ss_w32(pblob, 8 + 4 * d2, docoff[d2]); d2 = d2 + 1 } 983 let postoffs: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 984 let postlens: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 985 let dcounts: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 986 t3 = 0 987 while t3 < nterms { 988 let tid2: i64 = sidx[t3] 989 postoffs[t3] = po 990 var prev: i64 = 0 991 var p4: i64 = bstart[tid2] 992 let pend: i64 = bstart[tid2] + dcnt[tid2] 993 while p4 < pend { 994 po = ss_vw(pblob, po, bdoc[p4] - prev) 995 prev = bdoc[p4] 996 p4 = p4 + 1 997 } 998 postlens[t3] = po - postoffs[t3] 999 dcounts[t3] = dcnt[tid2] 1000 t3 = t3 + 1 1001 } 1002 // emit .terms: magic | nterms | entry-off table | entries 1003 tblob[0] = 78 as u8 1004 tblob[1] = 88 as u8 1005 tblob[2] = 84 as u8 1006 tblob[3] = 49 as u8 1007 ss_w32(tblob, 4, nterms) 1008 let base: i64 = 8 + 4 * nterms 1009 var to: i64 = base 1010 t3 = 0 1011 while t3 < nterms { 1012 ss_w32(tblob, 8 + 4 * t3, to - base) 1013 let tp2: *u8 = terms[sidx[t3]] as *u8 1014 let tl2: i64 = tlens[sidx[t3]] 1015 to = ss_w32(tblob, to, tl2) 1016 var x3: i64 = 0 1017 while x3 < tl2 { tblob[to] = tp2[x3]; to = to + 1; x3 = x3 + 1 } 1018 to = ss_w32(tblob, to, postoffs[t3]) 1019 to = ss_w32(tblob, to, postlens[t3]) 1020 to = ss_w32(tblob, to, dcounts[t3]) 1021 t3 = t3 + 1 1022 } 1023 // emit the POSITIONS sidecar blob (NXQ1): bucket occurrences by term (counting sort preserves the 1024 // (doc asc, position asc) collection order), then per SORTED term emit doc-runs aligned with .post: 1025 // varint(npos) + delta-varint token indexes. NOTE the magic: .post already uses "NXP1" -- NXQ1 here. 1026 let ocnt: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 1027 var o9: i64 = 0 1028 while o9 < nocc { ocnt[oterm[o9]] = ocnt[oterm[o9]] + 1; o9 = o9 + 1 } 1029 let ostart: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 1030 let ofill: *i64 = ss_sr_map(srl, 8 * (nterms + 16)) as *i64 1031 var oacc: i64 = 0 1032 var t8: i64 = 0 1033 while t8 < nterms { 1034 ostart[t8] = oacc 1035 ofill[t8] = oacc 1036 oacc = oacc + ocnt[t8] 1037 t8 = t8 + 1 1038 } 1039 let obdoc: *i64 = ss_sr_map(srl, 8 * (nocc + 16)) as *i64 1040 let obpos: *i64 = ss_sr_map(srl, 8 * (nocc + 16)) as *i64 1041 o9 = 0 1042 while o9 < nocc { 1043 let f9: i64 = ofill[oterm[o9]] 1044 obdoc[f9] = odoc[o9] 1045 obpos[f9] = opos[o9] 1046 ofill[oterm[o9]] = f9 + 1 1047 o9 = o9 + 1 1048 } 1049 qblob[0] = 78 as u8 1050 qblob[1] = 88 as u8 1051 qblob[2] = 81 as u8 1052 qblob[3] = 49 as u8 1053 ss_w32(qblob, 4, nterms) 1054 let qbase: i64 = 8 + 4 * nterms 1055 var qo: i64 = qbase 1056 t3 = 0 1057 while t3 < nterms { 1058 let tid3: i64 = sidx[t3] 1059 ss_w32(qblob, 8 + 4 * t3, qo - qbase) 1060 var r0: i64 = ostart[tid3] 1061 let rend: i64 = ostart[tid3] + ocnt[tid3] 1062 while r0 < rend { 1063 // one doc-run: occurrences of THIS doc are contiguous (collection order) 1064 let dcur: i64 = obdoc[r0] 1065 var r1: i64 = r0 1066 var run9: i64 = 1 1067 while run9 == 1 { 1068 if r1 >= rend { run9 = 0 } else { 1069 if obdoc[r1] == dcur { r1 = r1 + 1 } else { run9 = 0 } 1070 } 1071 } 1072 qo = ss_vw(qblob, qo, r1 - r0) 1073 var prevp: i64 = 0 1074 var r2: i64 = r0 1075 while r2 < r1 { 1076 qo = ss_vw(qblob, qo, obpos[r2] - prevp) 1077 prevp = obpos[r2] 1078 r2 = r2 + 1 1079 } 1080 r0 = r1 1081 } 1082 t3 = t3 + 1 1083 } 1084 louts[0] = to 1085 louts[1] = po 1086 louts[2] = qo 1087 ss_sr_release(srl) // tblob/pblob/qblob (caller-owned) hold the result; every scratch mapping above is dead here 1088 return 0 1089} 1090 1091// ---- IMPACT-ORDERED POSTINGS sidecar (2026-07-25, the WAND rung, seq606) ------------------------ 1092// .imp layout: "NXW1" | u32be nterms | nterms x u32be entry-off (relative to table end) | per SORTED 1093// term (same ordinal as .terms/NXQ1): varint(k) then k x (varint docidx, varint tf) -- the k highest- 1094// tf postings of the term (k = min(dcount, SS_IMP_K); set membership is by tf, walk order within). 1095// WHY: per-term candidacy caps truncated in ASCENDING-DOC order, so a high-tf doc past the cap (an 1096// entity's PROFILE page under a common name) could never become a candidate at any score. The impact 1097// list makes the cap keep the BEST postings, and the stored tf makes candidacy need zero doc reads. 1098// OPTIONAL like .pos: absent file -> readers fall back to ss_term; ss_write_seg emits it, so both 1099// compaction AND nx_seg_imp_build (in-place, additive-only) are format-upgrade paths. 1100const SS_IMP_K: i64 = 512 1101 1102// top-k selection threshold via a tf histogram (tf clamped into 1023 buckets so the scan is O(n+1024) 1103// whatever the tf range): outs[0]=T, outs[1]=count(clamped tf > T). Emit contract used by builder and 1104// reader: every clamped-tf>T item plus (k - outs[1]) clamped-tf==T items = exactly min(n,k) items. 1105func ss_imp_thresh(tfs: *i64, n: i64, k: i64, outs: *i64) -> i64 { 1106 if n <= k { outs[0] = 0; outs[1] = n; return 0 } 1107 let hist: *i64 = sys_mmap(8 * SS_MAGIC_1024) as *i64 1108 var i: i64 = 0 1109 while i < n { 1110 var v: i64 = tfs[i] 1111 if v > 1023 { v = 1023 } 1112 if v < 0 { v = 0 } 1113 hist[v] = hist[v] + 1 1114 i = i + 1 1115 } 1116 var acc: i64 = 0 1117 var b: i64 = 1023 1118 var t: i64 = 0 1119 var going: i64 = 1 1120 while going == 1 { 1121 if b < 0 { going = 0 } else { 1122 if acc + hist[b] >= k { t = b; going = 0 } else { acc = acc + hist[b]; b = b - 1 } 1123 } 1124 } 1125 outs[0] = t 1126 outs[1] = acc 1127 return 0 1128} 1129 1130// build the .imp blob from the three emitted index blobs alone (no writer-buffer access), so a LIVE 1131// segment can be upgraded from its .idx + .pos without recompacting. Returns blob length, -1 LOUD on 1132// malformed input. tf of (term,doc) = the NXQ1 doc-run occurrence count (npos), true value stored 1133// unclamped -- the 1023 clamp exists only inside selection. 1134func ss_build_imp(tb: *u8, tsz: i64, pb: *u8, psz: i64, qb: *u8, qsz: i64, wblob: *u8) -> i64 { 1135 if tsz < 8 { return 0 - 1 } 1136 if psz < 8 { return 0 - 1 } 1137 if qsz < 8 { return 0 - 1 } 1138 if qb[0] != (78 as u8) { return 0 - 1 } 1139 if qb[2] != (81 as u8) { return 0 - 1 } 1140 let nterms: i64 = ss_r32(tb, 4) 1141 if ss_r32(qb, 4) != nterms { return 0 - 1 } 1142 let nd: i64 = ss_r32(pb, 4) 1143 let tbase: i64 = 8 + 4 * nterms 1144 let qbase: i64 = 8 + 4 * nterms 1145 let docs: *i64 = sys_mmap(8 * (nd + 16)) as *i64 1146 let tfs: *i64 = sys_mmap(8 * (nd + 16)) as *i64 1147 let pv: *i64 = sys_mmap(16) as *i64 1148 let qv: *i64 = sys_mmap(16) as *i64 1149 let touts: *i64 = sys_mmap(32) as *i64 1150 wblob[0] = 78 as u8 1151 wblob[1] = 88 as u8 1152 wblob[2] = 87 as u8 1153 wblob[3] = 49 as u8 1154 ss_w32(wblob, 4, nterms) 1155 let wbase: i64 = 8 + 4 * nterms 1156 var wo: i64 = wbase 1157 var t: i64 = 0 1158 while t < nterms { 1159 ss_w32(wblob, 8 + 4 * t, wo - wbase) 1160 let eo: i64 = tbase + ss_r32(tb, 8 + 4 * t) 1161 let tl: i64 = ss_r32(tb, eo) 1162 let postoff: i64 = ss_r32(tb, eo + 4 + tl) 1163 let dcount: i64 = ss_r32(tb, eo + 4 + tl + 8) 1164 if dcount > nd { return 0 - 1 } 1165 // lockstep walk: .post deltas give ascending docs, the NXQ1 run headers give per-doc tf 1166 pv[0] = postoff 1167 qv[0] = qbase + ss_r32(qb, 8 + 4 * t) 1168 var prev: i64 = 0 1169 var i: i64 = 0 1170 while i < dcount { 1171 if qv[0] >= qsz { return 0 - 1 } 1172 prev = prev + ss_vr(pb, pv) 1173 docs[i] = prev 1174 let np: i64 = ss_vr(qb, qv) 1175 var sk: i64 = 0 1176 while sk < np { ss_vr(qb, qv); sk = sk + 1 } 1177 tfs[i] = np 1178 i = i + 1 1179 } 1180 ss_imp_thresh(tfs, dcount, SS_IMP_K, touts) 1181 let thr: i64 = touts[0] 1182 var k2: i64 = dcount 1183 if k2 > SS_IMP_K { k2 = SS_IMP_K } 1184 var room: i64 = k2 - touts[1] 1185 wo = ss_vw(wblob, wo, k2) 1186 // pass 1: every clamped tf > T; pass 2: clamped tf == T fills the remaining room 1187 var pass: i64 = 0 1188 while pass < 2 { 1189 i = 0 1190 while i < dcount { 1191 var cv: i64 = tfs[i] 1192 if cv > 1023 { cv = 1023 } 1193 var take: i64 = 0 1194 if pass == 0 { if cv > thr { take = 1 } } 1195 if pass == 1 { if cv == thr { if room > 0 { take = 1; room = room - 1 } } } 1196 if take == 1 { wo = ss_vw(wblob, wo, docs[i]); wo = ss_vw(wblob, wo, tfs[i]) } 1197 i = i + 1 1198 } 1199 pass = pass + 1 1200 } 1201 t = t + 1 1202 } 1203 return wo 1204} 1205 1206// build "<prefix>seg-<id>.terms" / ".post" (+ ".tmp") 1207func ss_auxname(prefix: *u8, segid: i64, ext: *u8, tmp: i64, out: *u8) -> i64 { 1208 var o: i64 = 0 1209 o = ss_cat(out, o, prefix) 1210 o = ss_cat(out, o, "seg-" as *u8) 1211 o = ss_catn(out, o, segid) 1212 o = ss_cat(out, o, ext) 1213 if tmp == 1 { o = ss_cat(out, o, ".tmp" as *u8) } 1214 out[o] = 0 as u8 1215 return o 1216} 1217 1218// write one segment's .docs + .keys files (temp -> rename each); NOT yet 1219// visible to readers until a manifest names it (commit or compaction swap) 1220// seq1730 COMPACTOR HOLE CLOSED: the guard used to sit at ss_commit ONLY, but compactors 1221// (nx_web_shard_compact.nx:165, nx_shard_compact.nx) call ss_write_seg DIRECTLY and then hand-write 1222// the manifest -- they never touch ss_commit, so the old placement had a hole EXACTLY where 1223// dp-web-pub- was actually corrupted. ss_write_seg is the deepest point BOTH paths share. 1224// ss_segid_ok is defined further down this file; forward references resolve (probed in isolation 1225// via nx_fwdref_probe: build exit=0 AND run exit=0), so no definition move and no compile-break 1226// window on a lib every organ imports. 1227func ss_write_seg(prefix: *u8, w: *i64, segid: i64) -> i64 { 1228 // ONE release discipline for the whole function (see SS_SR_* above): the list is created first so 1229 // the refusal path and the pre-write name scratch are covered too, not only the blob section. 1230 let srl: *i64 = ss_sr_new() 1231 if ss_segid_ok(segid) == 0 { 1232 let eb: *u8 = ss_sr_map(srl, 512) 1233 var eo: i64 = ss_cat(eb, 0, "SS-WRITE-SEG REFUSED: segid=" as *u8) 1234 eo = ss_catn(eb, eo, segid) 1235 eo = ss_cat(eb, eo, " is an ADDRESS or negative, not a segment id -- refusing to poison plane " as *u8) 1236 eo = ss_cat(eb, eo, prefix) 1237 eo = ss_cat(eb, eo, " (seq1730). NOTHING WRITTEN. Caller is deriving its id from a manifest array that holds POINTERS: parse the digits, or call ss_next_segid.\n" as *u8) 1238 sys_write(2, eb, eo) 1239 ss_sr_release(srl) 1240 return 0 - 7 1241 } 1242 let pt: *u8 = ss_sr_map(srl, 512) 1243 let pf: *u8 = ss_sr_map(srl, 512) 1244 ss_segname(prefix, segid, 1, pt) 1245 ss_segname(prefix, segid, 0, pf) 1246 if ss_writefile(pt, w[0] as *u8, w[1]) != 0 { ss_sr_release(srl); return 0 - 1 } 1247 if sys_renameat(pt, pf) != 0 { ss_sr_release(srl); return 0 - 2 } 1248 // MERGED aux index (IM6c): keys + terms + post in ONE seg-<id>.idx file 1249 // "NXI1" | u32be klen | u32be tlen | u32be plen | keys | terms | post 1250 // -- one write + fsync + rename instead of three (the race-localized 1251 // fsync-ceremony cost), same temp -> rename discipline. Readers slice. 1252 // Every blob below is written to disk in this call and dead afterwards; w[0] (the writer buffer) 1253 // is the CALLER's and is never on the list. 1254 let kblob: *u8 = ss_sr_map(srl, ss_keyblob_cap(w)) 1255 let klen2: i64 = ss_build_keys(w, kblob) 1256 if klen2 < 0 { ss_sr_release(srl); return 0 - 5 } 1257 let tblob: *u8 = ss_sr_map(srl, w[1] * 8 + SS_MAGIC_65536) 1258 let pblob: *u8 = ss_sr_map(srl, w[1] * 8 + SS_MAGIC_65536) 1259 let qblob: *u8 = ss_sr_map(srl, w[1] * 4 + SS_MAGIC_65536) 1260 let louts: *i64 = ss_sr_map(srl, 32) as *i64 1261 if ss_build_terms(w, tblob, pblob, qblob, louts) != 0 { ss_sr_release(srl); return 0 - 11 } 1262 let isz: i64 = 16 + klen2 + louts[0] + louts[1] 1263 let iblob: *u8 = ss_sr_map(srl, isz + 64) 1264 iblob[0] = 78 as u8 1265 iblob[1] = 88 as u8 1266 iblob[2] = 73 as u8 1267 iblob[3] = 49 as u8 1268 ss_w32(iblob, 4, klen2) 1269 ss_w32(iblob, 8, louts[0]) 1270 ss_w32(iblob, 12, louts[1]) 1271 var co: i64 = 16 1272 var ci: i64 = 0 1273 while ci < klen2 { iblob[co] = kblob[ci]; co = co + 1; ci = ci + 1 } 1274 ci = 0 1275 while ci < louts[0] { iblob[co] = tblob[ci]; co = co + 1; ci = ci + 1 } 1276 ci = 0 1277 while ci < louts[1] { iblob[co] = pblob[ci]; co = co + 1; ci = ci + 1 } 1278 let it: *u8 = ss_sr_map(srl, 512) 1279 let if2: *u8 = ss_sr_map(srl, 512) 1280 ss_auxname(prefix, segid, ".idx" as *u8, 1, it) 1281 ss_auxname(prefix, segid, ".idx" as *u8, 0, if2) 1282 if ss_writefile(it, iblob, isz) != 0 { ss_sr_release(srl); return 0 - 7 } 1283 if sys_renameat(it, if2) != 0 { ss_sr_release(srl); return 0 - 8 } 1284 // POSITIONS SIDECAR (2026-07-03, phrase rung): seg-<id>.pos = the NXQ1 blob. OPTIONAL by design -- 1285 // an old segment simply lacks the file and phrase queries degrade to AND there; compaction rebuilds 1286 // through THIS function, so compacting a shard upgrades it. Same temp->rename discipline, fail LOUD. 1287 let qt: *u8 = ss_sr_map(srl, 512) 1288 let qf: *u8 = ss_sr_map(srl, 512) 1289 ss_auxname(prefix, segid, ".pos" as *u8, 1, qt) 1290 ss_auxname(prefix, segid, ".pos" as *u8, 0, qf) 1291 if ss_writefile(qt, qblob, louts[2]) != 0 { ss_sr_release(srl); return 0 - 13 } 1292 if sys_renameat(qt, qf) != 0 { ss_sr_release(srl); return 0 - 14 } 1293 // IMPACT sidecar (2026-07-25, WAND rung): seg-<id>.imp = the NXW1 blob. OPTIONAL like .pos -- 1294 // readers fall back when absent; compaction runs through here so compacting upgrades a shard. 1295 let wcap: i64 = louts[0] * 2 + louts[1] * 10 + SS_MAGIC_65536 1296 let wblob2: *u8 = ss_sr_map(srl, wcap) 1297 let wlen: i64 = ss_build_imp(tblob, louts[0], pblob, louts[1], qblob, louts[2], wblob2) 1298 if wlen < 0 { ss_sr_release(srl); return 0 - 15 } 1299 let wt: *u8 = ss_sr_map(srl, 512) 1300 let wf: *u8 = ss_sr_map(srl, 512) 1301 ss_auxname(prefix, segid, ".imp" as *u8, 1, wt) 1302 ss_auxname(prefix, segid, ".imp" as *u8, 0, wf) 1303 if ss_writefile(wt, wblob2, wlen) != 0 { ss_sr_release(srl); return 0 - 16 } 1304 if sys_renameat(wt, wf) != 0 { ss_sr_release(srl); return 0 - 17 } 1305 ss_sr_release(srl) 1306 return 0 1307} 1308 1309// load a segment's index blobs: NEW merged .idx (sliced) or LEGACY 3-file 1310// (.keys/.terms/.post) fallback -- compaction is the format-upgrade path. 1311// outs: [0]=keys ptr [1]=keys sz [2]=terms ptr [3]=terms sz [4]=post ptr [5]=post sz 1312func ss_load_aux(prefix: *u8, segname: *u8, outs: *i64) -> i64 { 1313 return ss_load_aux2(prefix, segname, outs, 0) 1314} 1315func ss_load_aux2(prefix: *u8, segname: *u8, outs: *i64, usemmap: i64) -> i64 { 1316 let path: *u8 = sys_mmap(512) 1317 var o: i64 = 0 1318 o = ss_cat(path, o, prefix) 1319 o = ss_cat(path, o, segname) 1320 o = ss_cat(path, o, ".idx" as *u8) 1321 path[o] = 0 as u8 1322 let szp: *i64 = sys_mmap(16) as *i64 1323 let ib: *u8 = ss_loadfile(path, szp, usemmap) 1324 if szp[0] >= 16 { if ib[0] == (78 as u8) { if ib[2] == (73 as u8) { 1325 let kl: i64 = ss_r32(ib, 4) 1326 let tl: i64 = ss_r32(ib, 8) 1327 let pl: i64 = ss_r32(ib, 12) 1328 if 16 + kl + tl + pl <= szp[0] { 1329 outs[0] = (ib as i64) + 16 1330 outs[1] = kl 1331 outs[2] = (ib as i64) + 16 + kl 1332 outs[3] = tl 1333 outs[4] = (ib as i64) + 16 + kl + tl 1334 outs[5] = pl 1335 // Release this call's scratch before handing back (seq997). `ib` is NOT freed -- outs[] point 1336 // INTO it and the handle owns it from here; ss_close reclaims it as ONE mapping via the 1337 // merged-.idx branch. Leaking these two cost ~2 pages x nsegs x opens, the last T8 residual. 1338 sys_munmap(path, 512) 1339 sys_munmap(szp as *u8, 16) 1340 return 1 1341 } 1342 } } } 1343 // legacy fallback: three separate files 1344 var fi: i64 = 0 1345 while fi < 3 { 1346 var ext: *u8 = ".keys" as *u8 1347 if fi == 1 { ext = ".terms" as *u8 } 1348 if fi == 2 { ext = ".post" as *u8 } 1349 o = 0 1350 o = ss_cat(path, o, prefix) 1351 o = ss_cat(path, o, segname) 1352 o = ss_cat(path, o, ext) 1353 path[o] = 0 as u8 1354 outs[fi * 2] = ss_loadfile(path, szp, usemmap) as i64 1355 outs[fi * 2 + 1] = szp[0] 1356 fi = fi + 1 1357 } 1358 return 0 1359} 1360 1361// COMMIT: seg temp -> rename into place; manifest rewritten via temp -> rename. 1362// The manifest rename IS the commit point (atomicity without a txn engine). 1363// ---- seg-id sanity (seq1730: POINTER-SHAPED SEGIDS SILENTLY CORRUPTED SEVEN PLANES) --------------- 1364// A segment id is either a small counter (1, 2, 1001) or an epoch (sec ~1.8e9, ms ~1.8e12, us ~1.8e15). 1365// It is NEVER AN ADDRESS. Seven planes under knowledge/store were poisoned by a caller passing a pointer 1366// into the segid slot, and because ss_next_segid derives max(existing)+1, ONE poisoned id PINS that plane 1367// near 1.4e14 FOREVER: every later epoch-derived write then sorts BELOW the poisoned segment in supersede 1368// order, so its rows are shadowed by older data -- silently, unboundedly, while the writer sees rc=0. 1369// THE BAND IS SEPARABLE BY CONSTRUCTION, NOT BY TASTE: x86-64 user-space mmap lives under 2^47, and the 1370// observed poison sat at 1.35e14-1.41e14. Epoch-ms (~1.8e12) is two orders BELOW the low bound; epoch-us 1371// (~1.8e15) is an order ABOVE the high bound. So no legitimate id scale can collide with this window. 1372// BOUND HERE because ss_commit is THE ONE ACT EVERY WRITER PERFORMS -- 408 call sites, one chokepoint. 1373// A fix that needs 408 authors to remember it is not a fix. 1374const SS_SEGID_PTRBAND_LO: i64 = 10000000000000 1375const SS_SEGID_PTRBAND_HI: i64 = 140737488355328 1376 1377// 1 = a usable segment id, 0 = refuse (negative, or inside the user-space pointer band). 1378func ss_segid_ok(v: i64) -> i64 { 1379 if v < 0 { return 0 } 1380 if v >= SS_SEGID_PTRBAND_LO { if v < SS_SEGID_PTRBAND_HI { return 0 } } 1381 return 1 1382} 1383 1384// ---- THE PLANE COMMIT LOCK (2026-08-06) --------------------------------------------------------- 1385// ss_commit is the one act every writer performs, and until now it took NO lock. Everything it does 1386// is a read-modify-write: the segid arrives from an unlocked ss_next_segid max+1 scan, and the 1387// manifest readall->rename below is a second RMW. The rename is atomic, so the manifest is never 1388// TORN -- and that is precisely what disguised the lost update for so long. ATOMIC IS NOT 1389// SERIALISABLE: an atomic swap makes each writer's result whole, not each writer's result present. 1390// 1391// MEASURED on the pre-fix code by nx_segrace_gate, 6 processes x 10 commits on one plane: 1392// 16 segment files on disk, a manifest naming only 7 of them, and seg-1 listed THREE times. 1393// -- duplicate ids => writers overwrote each other's segment file, bytes destroyed 1394// -- orphaned files => written and fsynced, then erased from the manifest, invisible forever 1395// 1396// ★WHY THIS LOCK IS <prefix>slock AND NOT <prefix>plock, WHICH IS THE WHOLE REASON IT IS SAFE. 1397// nx_store_seed_lib.nx twice rules out locking a shared primitive, and it is RIGHT about plock: 1398// flock is per OPEN FILE DESCRIPTION, so taking plock here would make nx_debt -- which already holds 1399// plock via db_lock and then calls sts_seed -> ss_commit -- block against ITSELF on a second fd and 1400// hang the debt board for every seat. That objection is specific to plock, not to locking at all. 1401// slock is a DIFFERENT file and a strictly INNER lock: it is acquired only inside ss_commit and 1402// released before ss_commit returns, and ss_commit never acquires plock. So the order is always 1403// plock (outer, held across a caller's whole read-modify-write) then slock (inner, held across one 1404// commit), never the reverse. A cycle is impossible BY CONSTRUCTION, so a deadlock is impossible -- 1405// and unlike a caller-held lock, no author has to remember anything. 1406// ★THE COST IS MEASURED, NOT ASSUMED: nx_regprof_test already profiled lock_cycle=35us against 1407// reg_put=129335us steady-state -- the lock is 0.03% of a write. Correctness here is nearly free. 1408const SS_LOCK_EX: i64 = 2 1409const SS_LOCK_UN: i64 = 8 1410const SS_LOCK_NB: i64 = 4 // flock LOCK_NB -- ORed with LOCK_EX to poll instead of block 1411const SS_LOCK_SLEEP_MS: i64 = 50 // backoff between attempts 1412const SS_LOCK_TRIES: i64 = 2400 // 2400 x 50ms = 120s ceiling. RAISED FROM 30s BY MEASUREMENT: nx_segrace_gate stresses 7 processes doing fsync-per-commit on one plane, and at 30s FOUR writers were refused while merely QUEUED (not wedged) on a disk at IO load ~15. A bound must exceed legitimate worst-case contention or it manufactures the failure it exists to report; 120s still bounds the hang, and the >10min stall this was built for is still caught. 1413const SS_LOCKMODE: i64 = 420 1414const SS_LOCKPATHCAP: i64 = 512 1415 1416// Returns the held fd, or -1 LOUDLY naming the path -- "cannot lock" alone has historically blamed 1417// flock when the real cause was an open failure. 1418func ss_plane_lock(prefix: *u8) -> i64 { 1419 let p: *u8 = sys_mmap(SS_LOCKPATHCAP) 1420 var o: i64 = ss_cat(p, 0, prefix) 1421 o = ss_cat(p, o, "slock" as *u8) 1422 p[o] = 0 as u8 1423 let fd: i64 = sys_openat_append(p, SS_LOCKMODE) 1424 if fd < 0 { 1425 let eb: *u8 = sys_mmap(512) 1426 var eo: i64 = ss_cat(eb, 0, "SS-PLANE-LOCK open-failed (not flock) path=" as *u8) 1427 eo = ss_cat(eb, eo, p) 1428 eo = ss_cat(eb, eo, "\n" as *u8) 1429 sys_write(2, eb, eo) 1430 return 0 - 1 1431 } 1432 // BOUNDED WAIT REVERTED 2026-08-06 -- see debt 1786070596. A LOCK_NB retry loop lived here and 1433 // it was WRONG for a reason an isolated gate could not see: flock is per OPEN FILE DESCRIPTION 1434 // and fork() COPIES fds, so a child inherits a HELD lock and then blocks acquiring it on a fresh 1435 // fd -- deadlocked against a lock it already owns. /proc/locks named it: pid 22685 HELD inode 1436 // 42096345 while sleeping in its own retry backoff, and every other writer was refused behind it. 1437 // nx_lockbound_gate passed 2/2 because it never forked while holding. 1438 // >>AN ISOLATED GATE PROVES THE MECHANISM, NOT THE INTERACTION<< -- the retry did exactly what it 1439 // promised and still broke the system, converting an instant self-deadlock into a silent stall. 1440 // Blocking flock is restored: it is what nx_segrace_gate proved 24/24 and what is live today. 1441 // Re-attempt ONLY with a fork-inheritance guard (close-after-fork / CLOEXEC, or a process-local 1442 // registry of held planes) AND a gate that forks WHILE HOLDING, which this one did not. 1443 // BOUNDED WAIT, ATTEMPT 2 (2026-08-07, debt 1786070596). Attempt 1 was reverted after a deadlock 1444 // I attributed to fork-inheritance -- but that diagnosis was CONTAMINATED: the same session had 1445 // accidentally injected an unreleased write lock into four READER functions via a non-unique 1446 // replace-all, which alone explains a process holding a lock while waiting for it. This is the 1447 // controlled re-test: the library is verified GREEN on all six gates with NO injection, and this 1448 // is now the ONLY variable. 1449 // WHY BOUND IT AT ALL: a blocking flock has no timeout, so one writer wedged in a filesystem 1450 // journal commit (observed: state D, wchan=wait_for_commit, host at IO load 15.63) blocks every 1451 // other writer on that plane forever. Fail-fast beats fail-never -- a caller told NO can retry, 1452 // alert or degrade; a caller that hangs takes its supervisor's health check down with it. 1453 // ss_commit treats a negative return as fail-closed (-8, NOTHING WRITTEN), so refusing here can 1454 // never produce a partial write. 1455 var tries: i64 = 0 1456 var got: i64 = 0 1457 while got == 0 { 1458 if sys_flock(fd, SS_LOCK_EX + SS_LOCK_NB) == 0 { 1459 got = 1 1460 } else { 1461 tries = tries + 1 1462 if tries >= SS_LOCK_TRIES { 1463 let wb: *u8 = sys_mmap(512) 1464 var wo: i64 = ss_cat(wb, 0, "SS-PLANE-LOCK BUSY -- refusing after " as *u8) 1465 wo = ss_catn(wb, wo, (SS_LOCK_TRIES * SS_LOCK_SLEEP_MS) / 1000) 1466 wo = ss_cat(wb, wo, "s waiting on " as *u8) 1467 wo = ss_cat(wb, wo, p) 1468 wo = ss_cat(wb, wo, " -- another writer holds it (check /proc/<pid>/wchan: locks_lock_inode_wait = queued, wait_for_commit = wedged on disk). NOTHING WRITTEN.\n" as *u8) 1469 sys_write(2, wb, wo) 1470 sys_close(fd) 1471 return 0 - 1 1472 } 1473 sys_sleep_ms(SS_LOCK_SLEEP_MS) 1474 } 1475 } 1476 return fd 1477} 1478 1479// 1 if two NUL-terminated segment names are identical. Used by compaction to tell a segment it 1480// merged from one that landed WHILE it was merging -- the difference between folding history and 1481// deleting a commit. 1482func ss_name_eq(a: *u8, b: *u8) -> i64 { 1483 var i: i64 = 0 1484 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 1485 if b[i] != (0 as u8) { return 0 } 1486 return 1 1487} 1488 1489func ss_plane_unlock(fd: i64) -> i64 { 1490 if fd < 0 { return 0 } 1491 sys_flock(fd, SS_LOCK_UN) 1492 sys_close(fd) 1493 return 0 1494} 1495 1496// The max-segid scan, lifted OUT of ss_next_segid so ss_commit can reach it. ss_next_segid is 1497// defined ~240 lines BELOW ss_commit and NishiLang resolves identifiers in textual order, so a 1498// forward reference would not compile; duplicating the scan would be two copies of one rule (rule 1499// 15). ss_next_segid now delegates here, so both paths can never disagree. 1500// Returns the highest usable seg-<N> in the manifest, or -1 if there is none. 1501// Poisoned pointer-band ids are SKIPPED (seq1730 repair half): a corrupted manifest heals itself on 1502// the next write instead of staying pinned near 1.4e14 forever. 1503func ss_max_segid(prefix: *u8) -> i64 { 1504 let mf: *u8 = sys_mmap(512) 1505 var o: i64 = ss_cat(mf, 0, prefix) 1506 o = ss_cat(mf, o, "manifest.txt" as *u8) 1507 mf[o] = 0 as u8 1508 let szp: *i64 = sys_mmap(16) as *i64 1509 let b: *u8 = ss_readall(mf, szp) 1510 let sz: i64 = szp[0] 1511 if sz <= 0 { return 0 - 1 } 1512 var mx: i64 = 0 - 1 1513 var i: i64 = 0 1514 while i + 4 <= sz { 1515 var m: i64 = 0 1516 if b[i] == (115 as u8) { if b[i+1] == (101 as u8) { if b[i+2] == (103 as u8) { if b[i+3] == (45 as u8) { m = 1 } } } } 1517 if m == 1 { 1518 var j: i64 = i + 4 1519 var v: i64 = 0 1520 var any: i64 = 0 1521 var go: i64 = 1 1522 while go == 1 { go = 0; if j < sz { let c: i64 = b[j] as i64; if c >= (48 as i64) { if c <= (57 as i64) { v = v * (10 as i64) + (c - (48 as i64)); any = 1; j = j + 1; go = 1 } } } } 1523 if any == 1 { if ss_segid_ok(v) == 1 { if v > mx { mx = v } } } 1524 i = j 1525 } else { i = i + 1 } 1526 } 1527 return mx 1528} 1529 1530// RAW COMMIT BODY -- byte-identical to the ss_commit that shipped before 2026-08-06, deliberately 1531// untouched so the segid-poison guard and the manifest logic are provably unchanged. It is UNSAFE 1532// on its own (it is a read-modify-write with no lock); ss_commit below is now the only way in. 1533func ss_commit_body(prefix: *u8, w: *i64, segid: i64) -> i64 { 1534 if ss_segid_ok(segid) == 0 { 1535 let eb: *u8 = sys_mmap(512) 1536 var eo: i64 = ss_cat(eb, 0, "SS-COMMIT REFUSED: segid=" as *u8) 1537 eo = ss_catn(eb, eo, segid) 1538 eo = ss_cat(eb, eo, " is an ADDRESS or negative, not a segment id -- refusing to poison plane " as *u8) 1539 eo = ss_cat(eb, eo, prefix) 1540 eo = ss_cat(eb, eo, " (seq1730). NOTHING WRITTEN. Fix the caller: it is passing a pointer into the third argument of ss_commit.\n" as *u8) 1541 sys_write(2, eb, eo) 1542 return 0 - 7 1543 } 1544 let wrc: i64 = ss_write_seg(prefix, w, segid) 1545 if wrc != 0 { return wrc } 1546 let mf: *u8 = sys_mmap(512) 1547 let mt: *u8 = sys_mmap(512) 1548 var o: i64 = 0 1549 o = ss_cat(mf, o, prefix) 1550 o = ss_cat(mf, o, "manifest.txt" as *u8) 1551 mf[o] = 0 as u8 1552 o = 0 1553 o = ss_cat(mt, o, prefix) 1554 o = ss_cat(mt, o, "manifest.tmp" as *u8) 1555 mt[o] = 0 as u8 1556 let szp: *i64 = sys_mmap(16) as *i64 1557 let old: *u8 = ss_readall(mf, szp) 1558 var osz: i64 = szp[0] 1559 if osz < 0 { osz = 0 } 1560 let nb: *u8 = sys_mmap(osz + 128) 1561 var no: i64 = 0 1562 var t: i64 = 0 1563 while t < osz { nb[no] = old[t]; no = no + 1; t = t + 1 } 1564 no = ss_cat(nb, no, "seg-" as *u8) 1565 no = ss_catn(nb, no, segid) 1566 nb[no] = 10 as u8 1567 no = no + 1 1568 if ss_writefile(mt, nb, no) != 0 { return 0 - 3 } 1569 if sys_renameat(mt, mf) != 0 { return 0 - 4 } 1570 // A visible manifest is not a durable acknowledgement if the directory barrier failed. 1571 if ss_syncdir(prefix) != 0 { return SS_ERR_DURABILITY } 1572 return 0 1573} 1574 1575// ---- ss_commit_cas: COMPARE-AND-SWAP COMMIT (2026-08-06) ---------------------------------------- 1576// expect_max = the ss_max_segid the caller's snapshot was taken at. If the plane has moved since, 1577// another writer committed inside the caller's read-modify-write and committing now would overwrite 1578// them from a stale snapshot -> refuse with SS_ERR_STALE and write NOTHING. Pass SS_CAS_ANY to skip. 1579// 1580// ★THE CHECK MUST LIVE HERE, NOT IN THE CALLER, AND THIS WAS PROVEN BY MEASUREMENT, NOT REASONING. 1581// The first version of this guard sat at the top of sts_seed. It changed NOTHING -- nx_sts_cas_gate 1582// still reported 6 writers told yes and 1 row present -- because sts_seed checks, then builds the 1583// writer and ss_add's every row, and only THEN commits. All six workers passed the check before any 1584// of them committed. A COMPARISON SEPARATED FROM THE WRITE IT GUARDS BY A WINDOW IS NOT A GUARD; it 1585// is a second race. Here the read of ss_max_segid and the commit are under ONE plane lock, so the 1586// answer cannot change between the test and the write. That is the whole difference. 1587const SS_CAS_ANY: i64 = 0 - 2 1588const SS_ERR_STALE: i64 = 0 - 9 1589func ss_commit_cas(prefix: *u8, w: *i64, segid: i64, expect_max: i64) -> i64 { 1590 if ss_segid_ok(segid) == 0 { return ss_commit_body(prefix, w, segid) } 1591 let lk: i64 = ss_plane_lock(prefix) 1592 if lk < 0 { return 0 - 8 } 1593 let mx: i64 = ss_max_segid(prefix) 1594 if expect_max != SS_CAS_ANY { if mx != expect_max { ss_plane_unlock(lk); return SS_ERR_STALE } } 1595 var eff: i64 = segid 1596 if mx >= 0 { if mx + 1 > eff { eff = mx + 1 } } 1597 let rc: i64 = ss_commit_body(prefix, w, eff) 1598 ss_plane_unlock(lk) 1599 return rc 1600} 1601 1602// ---- ss_commit: THE SERIALISED ENTRY POINT (2026-08-06) ----------------------------------------- 1603// Same name, same signature, same return codes -- so all 261 ss_ call sites and BOTH sts_ families 1604// reach this without a single edit. That is the entire point. A fix that needs 261 authors to 1605// remember it is not a fix: the one already-built safe twin, sts_append_row, was gate-proven on 1606// 2026-07-30 and a day later still had ONE consumer against 62 call sites, while new unlocked 1607// writers landed FASTER than old ones were migrated (52 -> 56 in a single day). Migration was 1608// losing a race it cannot win, so make the primitive itself safe and let every caller inherit it. 1609// NEW failure code -8 = could not take the plane lock; callers already treat nonzero as failure. 1610func ss_commit(prefix: *u8, w: *i64, segid: i64) -> i64 { 1611 // Refusals are delegated UNLOCKED and UNCHANGED: a poisoned segid writes nothing, so there is 1612 // nothing to serialise, and locking merely to reject an argument would only add a failure mode. 1613 if ss_segid_ok(segid) == 0 { return ss_commit_body(prefix, w, segid) } 1614 let lk: i64 = ss_plane_lock(prefix) 1615 if lk < 0 { return 0 - 8 } 1616 // RE-DERIVE THE ID UNDER THE LOCK. The caller computed its segid BEFORE the lock -- the universal 1617 // shape is ss_commit(p, w, ss_next_segid(p)) -- so by now another writer may already own it. This 1618 // is what stops two writers both writing seg-<N>.docs, one silently destroying the other. 1619 // Only ever move FORWARD: a caller passing a deliberately higher id keeps it, so intentional id 1620 // schemes survive and only genuine collisions are resolved. 1621 var eff: i64 = segid 1622 let mx: i64 = ss_max_segid(prefix) 1623 if mx >= 0 { if mx + 1 > eff { eff = mx + 1 } } 1624 let rc: i64 = ss_commit_body(prefix, w, eff) 1625 ss_plane_unlock(lk) 1626 return rc 1627} 1628 1629// ---- BATCHED DURABILITY (added 2026-08-01 ws=legal, PROFILE-DRIVEN, debt 1785610685) -------------- 1630// MEASURED, not guessed: nx_regprof_test decomposed reg_put on a fresh plane and found 1631// lock_cycle=35us u00b7 ss_get_absent=19us u00b7 ss_get@20seg=371us u00b7 reg_put=129335us steady-state. 1632// The lock is 0.03% of a write and the read path 0.3%, and cost FALLS as segments accumulate -- so 1633// neither the plane lock nor the full-index-per-segment shape is the cost. What remains is the 1634// per-commit DIRECTORY FSYNC on the line above: one durability barrier per appended row. 1635// 1636// u2605THE POINT: ss_commit is CORRECT and stays byte-for-byte unchanged. A single logical transaction 1637// that appends N rows does not need N barriers -- it needs ONE, at the end. Paying per row is paying 1638// for a guarantee nobody asked for at that granularity. 1639// 1640// ADDITIVE BY CONSTRUCTION: ss_commit and all 261 of its callers are untouched. A caller opts in by 1641// using ss_commit_deferred for the rows and calling ss_sync_now ONCE afterwards. 1642// 1643// u2605u2605THE HONEST BOUND, stated so nobody mistakes what this buys: after ss_commit_deferred the data IS 1644// VISIBLE -- the segment file is written and the manifest rename has already happened, so any reader 1645// sees the row immediately. ONLY DURABILITY ACROSS POWER LOSS is deferred. A crash between the last 1646// deferred commit and ss_sync_now can lose the tail of the batch. That is exactly the trade a batch 1647// wants and exactly the trade a single critical write must NOT make: u2605USE ss_commit FOR ROWS THAT MUST 1648// SURVIVE A CRASH ON THEIR OWN, ss_commit_deferred ONLY INSIDE A BATCH YOU WILL SYNC. 1649// RAW DEFERRED BODY -- byte-identical to what shipped before 2026-08-06. Same hazard as 1650// ss_commit_body: an unlocked read-modify-write. ss_commit_deferred below is now the only way in. 1651func ss_commit_deferred_body(prefix: *u8, w: *i64, segid: i64) -> i64 { 1652 if ss_segid_ok(segid) == 0 { 1653 let eb: *u8 = sys_mmap(512) 1654 var eo: i64 = ss_cat(eb, 0, "SS-COMMIT-DEFERRED REFUSED: segid=" as *u8) 1655 eo = ss_catn(eb, eo, segid) 1656 eo = ss_cat(eb, eo, " is an ADDRESS or negative, not a segment id -- refusing to poison plane " as *u8) 1657 eo = ss_cat(eb, eo, prefix) 1658 eo = ss_cat(eb, eo, ". NOTHING WRITTEN.\n" as *u8) 1659 sys_write(2, eb, eo) 1660 return 0 - 7 1661 } 1662 let wrc: i64 = ss_write_seg(prefix, w, segid) 1663 if wrc != 0 { return wrc } 1664 let mf: *u8 = sys_mmap(512) 1665 let mt: *u8 = sys_mmap(512) 1666 var o: i64 = 0 1667 o = ss_cat(mf, o, prefix) 1668 o = ss_cat(mf, o, "manifest.txt" as *u8) 1669 mf[o] = 0 as u8 1670 o = 0 1671 o = ss_cat(mt, o, prefix) 1672 o = ss_cat(mt, o, "manifest.tmp" as *u8) 1673 mt[o] = 0 as u8 1674 let szp: *i64 = sys_mmap(16) as *i64 1675 let old: *u8 = ss_readall(mf, szp) 1676 var osz: i64 = szp[0] 1677 if osz < 0 { osz = 0 } 1678 let nb: *u8 = sys_mmap(osz + 128) 1679 var no: i64 = 0 1680 var t: i64 = 0 1681 while t < osz { nb[no] = old[t]; no = no + 1; t = t + 1 } 1682 no = ss_cat(nb, no, "seg-" as *u8) 1683 no = ss_catn(nb, no, segid) 1684 nb[no] = 10 as u8 1685 no = no + 1 1686 if ss_writefile(mt, nb, no) != 0 { return 0 - 3 } 1687 if sys_renameat(mt, mf) != 0 { return 0 - 4 } 1688 // u2605deliberately NO ss_syncdir here -- that is the whole difference, and the caller owes one. 1689 return 0 1690} 1691 1692// u2605THE BARRIER THE BATCH OWES. Call once after a run of ss_commit_deferred. Idempotent and cheap to 1693// over-call: syncing a directory that is already durable is a no-op, so when in doubt, call it. 1694func ss_sync_now(prefix: *u8) -> i64 { 1695 ss_syncdir(prefix) 1696 return 0 1697} 1698 1699// ---- ss_commit_deferred: SERIALISED ENTRY POINT (2026-08-06) ------------------------------------ 1700// The batch variant needs the SAME lock for the same reason: deferring the fsync changes only WHEN 1701// the bytes become durable, never whether two writers can pick the same segid or clobber each 1702// other's manifest line. Skipping the lock here would leave a fully open race behind a door marked 1703// "performance", and every ss_commit_deferred caller is by definition a HIGH-VOLUME writer -- the 1704// most likely to collide, not the least. 1705// The lock is per COMMIT, not per batch: each row is serialised against other writers, while the 1706// batch's durability barrier stays the caller's to place via ss_sync_now. Those are independent 1707// guarantees and conflating them is what would make this wrong. 1708func ss_commit_deferred(prefix: *u8, w: *i64, segid: i64) -> i64 { 1709 if ss_segid_ok(segid) == 0 { return ss_commit_deferred_body(prefix, w, segid) } 1710 let lk: i64 = ss_plane_lock(prefix) 1711 if lk < 0 { return 0 - 8 } 1712 var eff: i64 = segid 1713 let mx: i64 = ss_max_segid(prefix) 1714 if mx >= 0 { if mx + 1 > eff { eff = mx + 1 } } 1715 let rc: i64 = ss_commit_deferred_body(prefix, w, eff) 1716 ss_plane_unlock(lk) 1717 return rc 1718} 1719 1720// -- named caps (rule-11 burn-down of the 256/260 magic-number class) --------------------------------- 1721// SS_MANIFEST_LEGACY_CAP: the byte-identical legacy API cap (ss_manifest/_file). UNCAPPED paths exist and 1722// are canonical: ss_manifest_dyn (enumeration) + ss_next_segid (writers). New code must NOT use the legacy 1723// capped pair -- past the cap, count-as-segid CLOBBERS the cap segment (the reg_put persistence bug class). 1724const SS_MANIFEST_LEGACY_CAP: i64 = 256 1725const SS_VER_SLOTS: i64 = 260 // per-key version buffer allocation (kinds/ptrs/lens/srcs slots) 1726const SS_VER_WINDOW: i64 = 256 // versions of ONE key a scan retains; on overflow the OLDEST slides out 1727 // so the LATEST is ALWAYS kept (ss_get correctness for high-churn keys 1728 // like every registry's __idx__, which gains a version per put) 1729 1730// parse a manifest-format file (one "seg-<id>" name per line) into segs[]; 1731// returns count. Serves BOTH the live manifest and the compaction archive. 1732// LEGACY-CAPPED (SS_MANIFEST_LEGACY_CAP): kept byte-identical for old callers; use ss_manifest_dyn. 1733func ss_manifest_file(prefix: *u8, fname: *u8, segs: *i64) -> i64 { 1734 let mf: *u8 = sys_mmap(512) 1735 var o: i64 = 0 1736 o = ss_cat(mf, o, prefix) 1737 o = ss_cat(mf, o, fname) 1738 mf[o] = 0 as u8 1739 let szp: *i64 = sys_mmap(16) as *i64 1740 let b: *u8 = ss_readall(mf, szp) 1741 let sz: i64 = szp[0] 1742 if sz <= 0 { return 0 } 1743 var cnt: i64 = 0 1744 var i: i64 = 0 1745 var ls: i64 = 0 1746 while i < sz { 1747 if b[i] == (10 as u8) { 1748 let name: *u8 = sys_mmap(128) 1749 var t: i64 = 0 1750 while ls + t < i { name[t] = b[ls + t]; t = t + 1 } 1751 name[t] = 0 as u8 1752 if cnt < SS_MANIFEST_LEGACY_CAP { segs[cnt] = name as i64; cnt = cnt + 1 } 1753 ls = i + 1 1754 } 1755 i = i + 1 1756 } 1757 return cnt 1758} 1759 1760// parse the LIVE manifest into segs[]; returns count 1761// LEGACY-CAPPED: writers must use ss_next_segid; enumerators ss_manifest_dyn. 1762func ss_manifest(prefix: *u8, segs: *i64) -> i64 { 1763 return ss_manifest_file(prefix, "manifest.txt" as *u8, segs) 1764} 1765 1766// SOTA data-driven manifest read (NO hardcoded cap -- the 256/260 magic-number cap is the debt this eats): 1767// read the live manifest ONCE, size the segs[] buffer from the ACTUAL entry count, and return the WHOLE 1768// corpus's live segment list. *out_segs receives the freshly-allocated, exactly-sized buffer; returns count. 1769// Sentinel guarding the stashed manifest-buffer slots in the segs slack. ss_manifest_free only trusts 1770// those slots when it sees this value, so a short read (cnt < mc, leaving a NAME pointer at segs[n]) 1771// degrades to leaking rather than munmapping an address that was never a manifest buffer. 1772// 1773// DECLARED HERE, ABOVE ITS FIRST READER, 2026-07-26. It used to sit BELOW ss_manifest_file_dyn, which 1774// writes it at `segs[mc] = SS_MF_MAGIC`. A module const read before its declaration silently evaluated 1775// to 0, so the writer stamped 0 while ss_manifest_free compared against the real 0x53534D46 -- the 1776// sentinel COULD NEVER MATCH and the stashed manifest buffer (segs[n+1], size segs[n+2]) was NEVER 1777// munmapped. A real leak on every manifest read, in the most-shared primitive in the ecosystem, in the 1778// same memory class as seq905/seq975 that this file's own comments already reference. It stayed 1779// invisible until the sovereign compiler learned to REFUSE use-before-declaration instead of silently 1780// reading 0. KEEP THIS ABOVE ss_manifest_file_dyn. 1781const SS_MF_MAGIC: i64 = 0x53534D46 1782func ss_manifest_file_dyn(prefix: *u8, fname: *u8, out_segs: *i64) -> i64 { 1783 let mf: *u8 = sys_mmap(512) 1784 var o: i64 = 0 1785 o = ss_cat(mf, o, prefix) 1786 o = ss_cat(mf, o, fname) 1787 mf[o] = 0 as u8 1788 let szp: *i64 = sys_mmap(16) as *i64 1789 let b: *u8 = ss_readall(mf, szp) 1790 let sz: i64 = szp[0] 1791 if sz <= 0 { out_segs[0] = sys_mmap(64) as i64; return 0 } 1792 var mc: i64 = 0 1793 var i: i64 = 0 1794 while i < sz { if b[i] == (10 as u8) { mc = mc + 1 } i = i + 1 } 1795 // NAME POOL (seq905/seq975): each segment name used to get its OWN sys_mmap(128) -- a full 4 KiB page for 1796 // a 128-byte string, so an N-segment store burned N pages PER OPEN (the live 117-segment vault store: 1797 // ~468 KiB per open, leaked forever). One pooled allocation, sliced 128 bytes per name. 1798 // mf/szp are dead here (sz already extracted); free them rather than leak a page each per open. 1799 sys_munmap(mf, 512) 1800 sys_munmap(szp as *u8, 16) 1801 let namepool: *u8 = sys_mmap(128 * mc + 128) 1802 let segs: *i64 = sys_mmap(8 * mc + 64) as *i64 1803 // Hand the manifest read to ss_manifest_free via the slack slots (mc+8 slots exist; mc..mc+2 used). 1804 segs[mc] = SS_MF_MAGIC 1805 segs[mc + 1] = b as i64 1806 segs[mc + 2] = sz + 64 1807 out_segs[0] = segs as i64 1808 var cnt: i64 = 0 1809 var ls: i64 = 0 1810 i = 0 1811 while i < sz { 1812 if b[i] == (10 as u8) { 1813 let name: *u8 = sys_mmap(128) 1814 var t: i64 = 0 1815 while ls + t < i { name[t] = b[ls + t]; t = t + 1 } 1816 name[t] = 0 as u8 1817 if cnt < mc { let slot: *u8 = ((namepool as i64) + 128 * cnt) as *u8; var c2: i64 = 0; while name[c2] != (0 as u8) { slot[c2] = name[c2]; c2 = c2 + 1 } slot[c2] = 0 as u8; sys_munmap(name, 128); segs[cnt] = slot as i64; cnt = cnt + 1 } else { sys_munmap(name, 128) } 1818 ls = i + 1 1819 } 1820 i = i + 1 1821 } 1822 return cnt 1823} 1824// live-manifest convenience over ss_manifest_file_dyn (data-driven, no cap). 1825// ss_manifest_free -- release what ss_manifest_file_dyn handed back through out_segs. Names are POOLED 1826// contiguously from offset 0, so segs[0] IS the pool base -- no metadata slot and no signature change needed. 1827// Sizes are computed from the RETURNED count n, which is <= the allocated mc, so this UNDER-frees by at most 1828// a page rather than over-freeing: munmapping less than was mapped is safe, munmapping more can release a 1829// neighbouring mapping. Callers pair this with ss_manifest_dyn exactly as ss_close pairs with ss_open. 1830// SS_MF_MAGIC is declared at the TOP of this file, above ss_manifest_file_dyn which writes it. 1831// Declaring it here meant the writer read 0 and the sentinel never matched -- see the note there. 1832func ss_manifest_free(segs: *i64, n: i64) -> i64 { 1833 if (segs as i64) == 0 { return 0 } 1834 if n > 0 { if segs[0] != 0 { sys_munmap(segs[0] as *u8, 128 * n) } } 1835 // Stashed manifest buffer (sentinel-guarded; slots live in the +64 slack past mc, and n==mc in practice 1836 // because cnt increments exactly once per counted newline). This is the read `b` that ss_manifest_file_dyn 1837 // cannot free itself -- the name-parsing loop still needs it and that function has no single-exit tail. 1838 if segs[n] == SS_MF_MAGIC { if segs[n + 1] != 0 { if segs[n + 2] > 0 { sys_munmap(segs[n + 1] as *u8, segs[n + 2]) } } } 1839 sys_munmap(segs as *u8, 8 * n + 64) 1840 return 0 1841} 1842 1843func ss_manifest_dyn(prefix: *u8, out_segs: *i64) -> i64 { 1844 return ss_manifest_file_dyn(prefix, "manifest.txt" as *u8, out_segs) 1845} 1846 1847// CANONICAL next segment id for a commit under `prefix` = (max existing seg-<N> in the manifest) + 1, 1848// UNCAPPED. This is THE writer-side fix for the legacy-cap clobber class: deriving segid from the capped 1849// ss_manifest count returns the cap forever once exceeded, so every later commit overwrites the cap 1850// segment and concurrent writers clobber each other (proven: the toolreg store stuck at seg-256). 1851// Scanning for max is also duplicate-line-proof and hole-proof (ids only need uniqueness+monotonicity). 1852// Lifted from the proven reg_next_segid (nx_registry now delegates here). Empty/missing manifest -> 0. 1853func ss_next_segid(prefix: *u8) -> i64 { 1854 // DELEGATES to ss_max_segid, which had to be lifted above ss_commit so the commit path could 1855 // reach it (NishiLang resolves identifiers in textual order). ONE scan, ONE rule: two copies of 1856 // a max-segid rule that must agree is precisely how one registry drifts from its twin, and the 1857 // poisoned-id skip below is exactly the kind of rule that would get fixed in only one of them. 1858 let mxd: i64 = ss_max_segid(prefix) 1859 if mxd < 0 { return 0 } 1860 return mxd + 1 1861} 1862 1863// The pre-2026-08-06 body VERBATIM, retained as the equivalence ORACLE for ss_max_segid -- the same 1864// role sts_load_slow plays for sts_load. Not for production use; it is the control, not the code. 1865func ss_next_segid_oracle(prefix: *u8) -> i64 { 1866 let mf: *u8 = sys_mmap(512) 1867 var o: i64 = ss_cat(mf, 0, prefix) 1868 o = ss_cat(mf, o, "manifest.txt" as *u8) 1869 mf[o] = 0 as u8 1870 let szp: *i64 = sys_mmap(16) as *i64 1871 let b: *u8 = ss_readall(mf, szp) 1872 let sz: i64 = szp[0] 1873 if sz <= 0 { return 0 } 1874 var mx: i64 = 0 - 1 1875 var i: i64 = 0 1876 while i + 4 <= sz { 1877 var m: i64 = 0 1878 if b[i] == (115 as u8) { if b[i+1] == (101 as u8) { if b[i+2] == (103 as u8) { if b[i+3] == (45 as u8) { m = 1 } } } } 1879 if m == 1 { 1880 var j: i64 = i + 4 1881 var v: i64 = 0 1882 var any: i64 = 0 1883 var go: i64 = 1 1884 while go == 1 { go = 0; if j < sz { let c: i64 = b[j] as i64; if c >= (48 as i64) { if c <= (57 as i64) { v = v * (10 as i64) + (c - (48 as i64)); any = 1; j = j + 1; go = 1 } } } } 1885 // seq1730 REPAIR HALF: a poisoned id must not PIN the plane. Skipping pointer-shaped ids in 1886 // the max scan means an already-corrupted manifest heals itself on the next write instead of 1887 // staying stuck near 1.4e14 forever. The poisoned SEGMENT stays on disk and readable (rule 13, 1888 // additive-only) -- it simply stops dictating what the next id may be. 1889 if any == 1 { if ss_segid_ok(v) == 1 { if v > mx { mx = v } } } 1890 i = j 1891 } else { i = i + 1 } 1892 } 1893 if mx < 0 { return 0 } 1894 return mx + 1 1895} 1896 1897// author=tutor (GALX-PROD-FULL): backward-compatible higher-capacity manifest readers. 1898// IDENTICAL parse to ss_manifest_file/ss_manifest, but the segment-slot cap is a PARAMETER 1899// rather than the hard 256 baked into ss_manifest_file. Callers supplying a segs[] buffer of 1900// at least `cap` slots (8*cap bytes) can browse the WHOLE corpus, not just the first 256. 1901// The originals ss_manifest_file/ss_manifest are left BYTE-IDENTICAL (cap 256) so all 16 1902// existing callers (260-slot buffers) keep their exact behavior; this is purely additive. 1903func ss_manifest_file_cap(prefix: *u8, fname: *u8, segs: *i64, cap: i64) -> i64 { 1904 let mf: *u8 = sys_mmap(512) 1905 var o: i64 = 0 1906 o = ss_cat(mf, o, prefix) 1907 o = ss_cat(mf, o, fname) 1908 mf[o] = 0 as u8 1909 let szp: *i64 = sys_mmap(16) as *i64 1910 let b: *u8 = ss_readall(mf, szp) 1911 let sz: i64 = szp[0] 1912 if sz <= 0 { return 0 } 1913 var cnt: i64 = 0 1914 var i: i64 = 0 1915 var ls: i64 = 0 1916 while i < sz { 1917 if b[i] == (10 as u8) { 1918 let name: *u8 = sys_mmap(128) 1919 var t: i64 = 0 1920 while ls + t < i { name[t] = b[ls + t]; t = t + 1 } 1921 name[t] = 0 as u8 1922 if cnt < cap { segs[cnt] = name as i64; cnt = cnt + 1 } 1923 ls = i + 1 1924 } 1925 i = i + 1 1926 } 1927 return cnt 1928} 1929 1930// parse the LIVE manifest into segs[] honoring a caller-supplied cap; returns count 1931func ss_manifest_cap(prefix: *u8, segs: *i64, cap: i64) -> i64 { 1932 return ss_manifest_file_cap(prefix, "manifest.txt" as *u8, segs, cap) 1933} 1934 1935// author=tutor (GALX-PROD-FULL): cap-aware sibling of ss_scan. Identical chronological 1936// version walk, but the segment list is read with ss_manifest_cap so it sees ALL segments 1937// up to `cap` (not just 256). The per-key kinds/ptrs/lens buffers count VERSIONS of one key 1938// (tiny, caller-sized), independent of segment count. ss_scan is left untouched. 1939func ss_scan_cap(prefix: *u8, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64, cap: i64) -> i64 { 1940 let segs: *i64 = sys_mmap(8 * cap) as *i64 1941 let ns: i64 = ss_manifest_cap(prefix, segs, cap) 1942 let srcs: *i64 = sys_mmap(8 * cap) as *i64 1943 return ss_scan_seglist(prefix, segs, ns, key, kinds, ptrs, lens, srcs, 1, 0, SS_VER_WINDOW) 1944} 1945 1946// author=tutor (GALX-PROD-FULL): cap-aware sibling of ss_get for the FULL-corpus ingest 1947// dedup path. Same semantics (1=found,0=tombstoned,-1=absent) but scans up to `cap` 1948// segments so re-ingest stays idempotent past the 256th image. ss_get is left untouched. 1949func ss_get_cap(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64, cap: i64) -> i64 { 1950 let kinds: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1951 let ptrs: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1952 let lens: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1953 let n: i64 = ss_scan_cap(prefix, key, kinds, ptrs, lens, cap) 1954 if n == 0 { return 0 - 1 } 1955 let last: i64 = n - 1 1956 if kinds[last] == 2 { return 0 } 1957 ptrout[0] = ptrs[last] 1958 lenout[0] = lens[last] 1959 return 1 1960} 1961 1962// walk a segment list CHRONOLOGICALLY for `key`, appending versions at cnt0; 1963// srcs[i]=srcval marks where each version came from. Returns the new count. 1964// `cap` = the caller's version-buffer window. When a key has MORE versions than cap, the window SLIDES 1965// (oldest drops, newest kept) instead of silently truncating at the FIRST cap versions -- the old 1966// first-cap behavior made ss_get return a STALE value for any key with >cap versions (every registry's 1967// __idx__ index key gains one version per put, so registries past cap puts served a stale index). 1968func ss_scan_seglist(prefix: *u8, segs: *i64, ns: i64, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64, srcs: *i64, srcval: i64, cnt0: i64, cap: i64) -> i64 { 1969 let kl0: i64 = ss_len(key) 1970 var cnt: i64 = cnt0 1971 var s: i64 = 0 1972 while s < ns { 1973 let path: *u8 = sys_mmap(512) 1974 var o: i64 = 0 1975 o = ss_cat(path, o, prefix) 1976 o = ss_cat(path, o, segs[s] as *u8) 1977 o = ss_cat(path, o, ".docs" as *u8) 1978 path[o] = 0 as u8 1979 let szp: *i64 = sys_mmap(16) as *i64 1980 let b: *u8 = ss_readall(path, szp) 1981 let sz: i64 = szp[0] 1982 var i: i64 = 0 1983 while i + 9 <= sz { 1984 let kind: i64 = b[i] 1985 let kl: i64 = ss_r32(b, i + 1) 1986 let koff: i64 = i + 5 1987 let vl: i64 = ss_r32(b, koff + kl) 1988 let voff: i64 = koff + kl + 4 1989 var eq: i64 = 1 1990 if kl != kl0 { eq = 0 } 1991 var t: i64 = 0 1992 while t < kl { 1993 if eq == 1 { if b[koff + t] != key[t] { eq = 0 } } 1994 t = t + 1 1995 } 1996 if eq == 1 { 1997 if cnt < cap { 1998 kinds[cnt] = kind 1999 ptrs[cnt] = (b as i64) + voff 2000 lens[cnt] = vl 2001 srcs[cnt] = srcval 2002 cnt = cnt + 1 2003 } else { 2004 // window full: slide the OLDEST version out so the LATEST is always retained 2005 // (ss_get takes [cnt-1]; pre-fix this silently kept the FIRST cap versions = stale get) 2006 var sh: i64 = 1 2007 while sh < cap { kinds[sh-1] = kinds[sh]; ptrs[sh-1] = ptrs[sh]; lens[sh-1] = lens[sh]; srcs[sh-1] = srcs[sh]; sh = sh + 1 } 2008 kinds[cap-1] = kind 2009 ptrs[cap-1] = (b as i64) + voff 2010 lens[cap-1] = vl 2011 srcs[cap-1] = srcval 2012 } 2013 } 2014 i = voff + vl 2015 } 2016 s = s + 1 2017 } 2018 return cnt 2019} 2020 2021// scan LIVE committed segments CHRONOLOGICALLY for `key`; fills kinds/ptrs/lens 2022// (every version incl. tombstones = the time-travel surface); returns count. 2023func ss_scan(prefix: *u8, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64) -> i64 { 2024 let sp: *i64 = sys_mmap(8) as *i64 2025 let ns: i64 = ss_manifest_dyn(prefix, sp) 2026 let segs: *i64 = sp[0] as *i64 2027 let srcs: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 2028 return ss_scan_seglist(prefix, segs, ns, key, kinds, ptrs, lens, srcs, 1, 0, SS_VER_WINDOW) 2029} 2030 2031// ARCHIVE TIME-TRAVEL (IM-AR): the FULL history surface. Compaction retires 2032// segments into manifest-archive.txt without deleting them (additive law); 2033// this reads them back: every version of `key` across ARCHIVED segments 2034// (chronological, srcs=0) then LIVE segments (srcs=1). Post-compaction the 2035// live merged segment re-states the latest archived version -- that copy is 2036// reported honestly as its own LIVE row, never collapsed. Returns count. 2037func ss_scan_all(prefix: *u8, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64, srcs: *i64) -> i64 { 2038 let asp: *i64 = sys_mmap(8) as *i64 2039 let na: i64 = ss_manifest_file_dyn(prefix, "manifest-archive.txt" as *u8, asp) 2040 let asegs: *i64 = asp[0] as *i64 2041 var cnt: i64 = ss_scan_seglist(prefix, asegs, na, key, kinds, ptrs, lens, srcs, 0, 0, SS_VER_WINDOW) 2042 let lsp: *i64 = sys_mmap(8) as *i64 2043 let nl: i64 = ss_manifest_dyn(prefix, lsp) 2044 let lsegs: *i64 = lsp[0] as *i64 2045 cnt = ss_scan_seglist(prefix, lsegs, nl, key, kinds, ptrs, lens, srcs, 1, cnt, SS_VER_WINDOW) 2046 return cnt 2047} 2048 2049// Binary-search an in-memory .keys blob for key. 2050// Returns kind (1 put / 2 tombstone) with voff/vlen in outs, or -1 not present, 2051// or -2 blob malformed. 2052// NXK1 uses a four-byte signature/count and u32 offset table; entries hold 2053// one kind byte plus three u32 fields and key bytes. These are format widths. 2054const SS_IDX_U32: i64 = 4 2055const SS_IDX_HEADER: i64 = SS_IDX_U32 * 2 2056const SS_IDX_ENTRY_FIXED: i64 = 1 + SS_IDX_U32 * 3 2057func ss_idx_find(b: *u8, sz: i64, key: *u8, voffout: *i64, vlenout: *i64) -> i64 { 2058 voffout[0]=0; vlenout[0]=0 2059 if sz < SS_IDX_HEADER { return 0-2 } 2060 if (b as i64) == 0 { return 0-2 } 2061 if b[0] != (78 as u8) { return 0-2 } 2062 if b[1] != (88 as u8) { return 0-2 } 2063 if b[2] != (75 as u8) { return 0-2 } 2064 if b[3] != (49 as u8) { return 0-2 } 2065 let n: i64 = ss_r32(b,SS_IDX_U32) 2066 // Bound before multiplying or following any offset supplied by the file. 2067 if n > (sz-SS_IDX_HEADER)/SS_IDX_U32 { return 0-2 } 2068 let base: i64 = SS_IDX_HEADER+SS_IDX_U32*n 2069 let kl0: i64 = ss_len(key) 2070 var lo: i64 = 0 2071 var hi: i64 = n-1 2072 while lo <= hi { 2073 let mid: i64 = lo+(hi-lo)/2 2074 let offset: i64 = ss_r32(b,SS_IDX_HEADER+SS_IDX_U32*mid) 2075 if offset > sz-base { return 0-2 } 2076 let eo: i64 = base+offset 2077 if sz-eo < SS_IDX_ENTRY_FIXED { return 0-2 } 2078 let kind: i64 = b[eo] 2079 if kind != 1 { if kind != 2 { return 0-2 } } 2080 let kl: i64 = ss_r32(b,eo+1) 2081 if kl > sz-eo-SS_IDX_ENTRY_FIXED { return 0-2 } 2082 let key_at: i64 = eo+1+SS_IDX_U32 2083 let c: i64 = ss_kcmp((b as i64+key_at) as *u8,kl,key,kl0) 2084 if c == 0 { 2085 voffout[0]=ss_r32(b,key_at+kl) 2086 vlenout[0]=ss_r32(b,key_at+kl+SS_IDX_U32) 2087 return kind 2088 } 2089 if c < 0 { lo=mid+1 } 2090 if c > 0 { hi=mid-1 } 2091 } 2092 return 0-1 2093} 2094 2095// per-call wrapper: load ONE segment's .keys from disk and search it 2096func ss_idx_lookup(prefix: *u8, segname: *u8, key: *u8, voffout: *i64, vlenout: *i64) -> i64 { 2097 let outs: *i64 = sys_mmap(8 * 8) as *i64 2098 ss_load_aux(prefix, segname, outs) 2099 return ss_idx_find(outs[0] as *u8, outs[1], key, voffout, vlenout) 2100} 2101 2102// OPEN-STORE HANDLE (the race-named read optimization): load the manifest and 2103// every live segment's .keys + .docs ONCE; lookups then binary-search in 2104// memory with zero per-call file IO. Snapshot semantics: the handle sees the 2105// store as of open (immutable segments make this safe -- a later commit adds 2106// segments the handle simply does not list; reopen to see them). 2107// Layout: h[0]=nsegs; per segment i (8 slots): h[1+8i]=keys ptr, h[2+8i]=keys 2108// size, h[3+8i]=docs ptr, h[4+8i]=docs size, h[5+8i]=terms ptr, h[6+8i]=terms 2109// size, h[7+8i]=post ptr, h[8+8i]=post size; h[1+8*ns+i]=LIVE-DOC map ptr for 2110// segment i (1 byte per doc entry, 1 = this entry IS its key's current state 2111// -- computed ONCE here so term queries check currency in O(1) instead of a 2112// per-candidate keyed binary search). Returns 0 ptr if no manifest. 2113// default open: read every segment file fully into anon RAM (historical behavior; ALL non-search consumers 2114// keep this -- small shards, byte-for-byte unchanged). 2115// ss_close -- give back EVERYTHING ss_open/ss_open2 mapped. Its ABSENCE was the seq905 keystone: ss_open 2116// loads every live segment's .keys/.docs/.terms/.post plus a derived live-doc map, and NOTHING ever released 2117// them, so EVERY seg-store consumer leaked BY CONSTRUCTION (`func ss_close` grepped to 0 matches ecosystem- 2118// wide while ss_open had dozens of callers). Measured cost 2026-07-25: ~20.6 GB of 24.3 GB swap consumed on 2119// the NAS, which parks long-running consumers in uninterruptible sleep -- at which point they stop making 2120// progress AND stop responding to SIGKILL, so the leak presents as a HANG, not as an OOM. 2121// 2122// Every size is DERIVABLE from the handle, so this needs no layout change and no new bookkeeping: 2123// h[0]=ns; per segment i: keys(h[1+8i],h[2+8i]) docs(h[3+8i],h[4+8i]) terms(h[5+8i],h[6+8i]) 2124// post(h[7+8i],h[8+8i]); live-doc map h[1+8ns+i], sized dsz/9+16 exactly as ss_open2 allocated it. 2125// 2126// DELIBERATELY CONSERVATIVE on blob length: ss_readall maps sz+64 but REPORTS sz, while the usemmap=1 path 2127// maps exactly sz. We free the REPORTED size, never sz+64 -- unmapping past a file-backed mapping could 2128// release a NEIGHBOURING mapping, and a sub-page residue is strictly better than freeing memory we do not 2129// own. Pointers are nulled as freed and the handle is released LAST (all sizes are read out of it first), so 2130// a partially-populated handle from a failed open still closes cleanly. Null/zero entries are skipped. 2131// license_tier: ORIGINAL No hw writes (Rule 26). 2132// ss_open3 handle slots + limits (declared here, ABOVE ss_close, because the parser resolves module consts in file order) 2133const SS3_SLOT_NAMES: i64 = 7 2134const SS3_SLOT_TSEG: i64 = 8 2135const SS3_SLOT_TEO: i64 = 9 2136const SS3_SLOT_CAP: i64 = 10 2137const SS3_SLOT_INS: i64 = 11 2138const SS3_SLOT_MODE: i64 = 12 2139// ---- AUX ROWS (2026-09-14, search plan L3: index-resident candidacy) ------------------------------ 2140// Slot 13 of the handle tail holds ONE anonymous array of SS_AUXW words PER SEGMENT, built beside the live 2141// map and moved/freed with it: [0] d2k = doc index -> .keys entry offset (so a posting hit reads its key 2142// from the small, warm .keys slice instead of faulting the multi-GB .docs mmap -- MEASURED 2026-09-14 by 2143// the search phase timers as 7.5-7.9 s of an 11.5-12.5 s cold two-common-term query, every byte of it 2144// stage-1 key reads), [1] d2k words, [2] tsample = every SS_TSAMPLE-th sorted term's SS_TPREFIX-byte packed 2145// prefix + ordinal (a RAM index over the .terms dictionary so an exact or prefix lookup touches ONE window 2146// of the blob instead of log2(n) cold pages), [3] tsample entries. Both are DERIVED from bytes the open 2147// already reads, so every reader they serve answers byte-for-byte what the unindexed search answered; a 2148// row that is absent (0) makes every consumer fall back to the old path. 2149const SS3_SLOT_AUX: i64 = 13 2150const SS_AUXW: i64 = 8 2151const SS_TSAMPLE: i64 = 64 2152const SS_TPREFIX: i64 = 7 2153const SS3_MODE_FULL: i64 = 0 2154const SS3_MODE_INCR: i64 = 1 2155const SS3_STATBUF: i64 = 160 2156const SS3_ST_SIZE_OFF: i64 = 48 2157const SS3_ANN_CAP: i64 = 256 2158const SS3_PATHCAP: i64 = 512 2159const SS3_KEYMAX: i64 = 500 2160// TABLE LOAD BOUND for an incremental insert: distinct keys (retained INS + every new entry, an upper bound) may fill 2161// at most 3/4 of the open-addressed table -- linear probing averages ~2.5 probes per hit there, and the FULL build 2162// sizes the table at 2x ENTRIES so this leaves headroom equal to roughly the whole prior key count. The gate fixture 2163// (128-slot table, 45 keys, 18 new entries) is exactly what a half-load rule refused on 2026-09-02. 2164const SS3_LOAD_NUM: i64 = 3 2165const SS3_LOAD_DEN: i64 = 4 2166// PER-SEGMENT RELEASE, extracted from ss_close 2026-09-02 so the incremental open (ss_open3) can back out 2167// the rows it loaded itself without a second copy of the merged-.idx single-mapping arithmetic. 2168// ---- AUX ROW HELPERS (2026-09-14) -- see SS3_SLOT_AUX above for what the rows hold and why ------ 2169func ss_aux(h: *i64) -> *i64 { 2170 if (h as i64) == 0 { return 0 as *i64 } 2171 let ns: i64 = h[0] 2172 if ns <= 0 { return 0 as *i64 } 2173 return h[SS3_SLOT_AUX + 9 * ns] as *i64 2174} 2175func ss_aux_row(h: *i64, s: i64) -> *i64 { 2176 let ax: *i64 = ss_aux(h) 2177 if (ax as i64) == 0 { return 0 as *i64 } 2178 if s < 0 { return 0 as *i64 } 2179 if s >= h[0] { return 0 as *i64 } 2180 return ((ax as i64) + 8 * SS_AUXW * s) as *i64 2181} 2182// release one segment's derived aux rows (d2k + term sample); idempotent, pointers zeroed 2183func ss_aux_free_row(h: *i64, s: i64) -> i64 { 2184 let r: *i64 = ss_aux_row(h, s) 2185 if (r as i64) == 0 { return 0 } 2186 if r[0] != 0 { if r[1] > 0 { sys_munmap(r[0] as *u8, 8 * r[1]) } } 2187 if r[2] != 0 { if r[3] > 0 { sys_munmap(r[2] as *u8, 16 * r[3]) } } 2188 r[0] = 0; r[1] = 0; r[2] = 0; r[3] = 0 2189 return 0 2190} 2191// pack the first SS_TPREFIX bytes of a key big-endian into a NON-NEGATIVE i64: 7 bytes leave the sign bit 2192// clear, so ordinary signed compare orders packed prefixes exactly as ss_kcmp orders the same bytes, and a 2193// shorter key pads with zeros and therefore sorts first -- ss_kcmp's shorter-is-less rule. 2194func ss_tpack(p: *u8, n: i64) -> i64 { 2195 var v: i64 = 0 2196 var i: i64 = 0 2197 while i < SS_TPREFIX { 2198 var b: i64 = 0 2199 if i < n { b = p[i] as i64 } 2200 v = v * 256 + b 2201 i = i + 1 2202 } 2203 return v 2204} 2205// build segment s's term sample from its .terms blob: (packed prefix, ordinal) for every SS_TSAMPLE-th sorted 2206// term -- one sequential pass over the entry stripe, the pages the open's WILLNEED already asked for. 2207// DOC-KEY PREDICATE, ONE COPY (search L5, 2026-09-14): a .keys entry whose kind byte is 1 and whose key starts 2208// with "doc:" -- the corpus-size predicate ss_doc_count has always used, extracted so the per-row count below 2209// and the whole-shard walk cannot drift apart. 2210const SS_AUX_DOCN: i64 = 4 2211func ss_is_doc_key(kb: *u8, eo: i64) -> i64 { 2212 if (kb[eo] as i64) != 1 { return 0 } 2213 let kl9: i64 = ss_r32(kb, eo + 1) 2214 if kl9 < 4 { return 0 } 2215 if kb[eo + 5] != (100 as u8) { return 0 } 2216 if kb[eo + 6] != (111 as u8) { return 0 } 2217 if kb[eo + 7] != (99 as u8) { return 0 } 2218 if kb[eo + 8] != (58 as u8) { return 0 } 2219 return 1 2220} 2221// PER-ROW DOC-KEY COUNT (search L5, 2026-09-14): counted ONCE when the row is loaded (a segment is immutable), 2222// stored as count+1 in aux slot SS_AUX_DOCN (0 = absent) and carried through every incremental reopen by 2223// ss3_move like the other aux words. ss_doc_count sums these instead of walking every .keys entry per query: 2224// the phase timers measured that walk at 324 ms of a 331 ms prep on every query, warm or cold. Same predicate, 2225// same number; the entry count is clamped to the mapped bytes exactly as the live-map loop clamps it. 2226func ss_doccount_build(h: *i64, s: i64) -> i64 { 2227 let r: *i64 = ss_aux_row(h, s) 2228 if (r as i64) == 0 { return 0 } 2229 let kb: *u8 = h[1 + 8 * s] as *u8 2230 let ksz: i64 = h[2 + 8 * s] 2231 if ksz < 8 { r[SS_AUX_DOCN] = 1; return 0 } 2232 var m9: i64 = ss_r32(kb, 4) 2233 if 8 + 4 * m9 > ksz { m9 = (ksz - 8) / 4 } 2234 var n: i64 = 0 2235 var e9: i64 = 0 2236 while e9 < m9 { 2237 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9) 2238 n = n + ss_is_doc_key(kb, eo) 2239 e9 = e9 + 1 2240 } 2241 r[SS_AUX_DOCN] = n + 1 2242 return n 2243} 2244func ss_tsample_build(h: *i64, s: i64) -> i64 { 2245 let r: *i64 = ss_aux_row(h, s) 2246 if (r as i64) == 0 { return 0 } 2247 let tb: *u8 = h[5 + 8 * s] as *u8 2248 let tsz: i64 = h[6 + 8 * s] 2249 if tsz < 8 { return 0 } 2250 if tb[0] != (78 as u8) { return 0 } 2251 if tb[2] != (84 as u8) { return 0 } 2252 var n: i64 = ss_r32(tb, 4) 2253 if 8 + 4 * n > tsz { n = (tsz - 8) / 4 } 2254 if n <= 0 { return 0 } 2255 let base: i64 = 8 + 4 * n 2256 let cnt: i64 = (n + SS_TSAMPLE - 1) / SS_TSAMPLE 2257 let sm: *i64 = sys_mmap(16 * cnt) as *i64 2258 var k: i64 = 0 2259 var i: i64 = 0 2260 while i < n { 2261 let eo: i64 = base + ss_r32(tb, 8 + 4 * i) 2262 var tl: i64 = 0 2263 if eo + 4 <= tsz { tl = ss_r32(tb, eo) } 2264 if eo + 4 + tl > tsz { tl = 0 } 2265 sm[2 * k] = ss_tpack(((tb as i64) + eo + 4) as *u8, tl) 2266 sm[2 * k + 1] = i 2267 k = k + 1 2268 i = i + SS_TSAMPLE 2269 } 2270 r[2] = sm as i64 2271 r[3] = k 2272 return k 2273} 2274// the ordinal window [lo, hi) that holds EVERY term whose packed prefix equals pk (and, for a lower-bound 2275// search, the first term whose prefix is >= pk): from the sample just BELOW the first sample >= pk to the 2276// first sample > pk. Without a sample the window is the whole dictionary, i.e. the pre-2026-09-14 search. 2277func ss_tsample_window(h: *i64, s: i64, pk: i64, n: i64, lohi: *i64) -> i64 { 2278 lohi[0] = 0 2279 lohi[1] = n 2280 let r: *i64 = ss_aux_row(h, s) 2281 if (r as i64) == 0 { return 0 } 2282 if r[2] == 0 { return 0 } 2283 let sm: *i64 = r[2] as *i64 2284 let cnt: i64 = r[3] 2285 if cnt <= 0 { return 0 } 2286 var lo: i64 = 0 2287 var hi: i64 = cnt 2288 while lo < hi { let mid: i64 = (lo + hi) / 2; if sm[2 * mid] < pk { lo = mid + 1 } else { hi = mid } } 2289 let first_ge: i64 = lo 2290 var lo2: i64 = first_ge 2291 var hi2: i64 = cnt 2292 while lo2 < hi2 { let mid2: i64 = (lo2 + hi2) / 2; if sm[2 * mid2] <= pk { lo2 = mid2 + 1 } else { hi2 = mid2 } } 2293 let first_gt: i64 = lo2 2294 if first_ge > 0 { lohi[0] = sm[2 * (first_ge - 1) + 1] } else { lohi[0] = 0 } 2295 if first_gt < cnt { lohi[1] = sm[2 * first_gt + 1] } else { lohi[1] = n } 2296 return 1 2297} 2298// EXACT term lookup in segment s through the sample: the same answer ss_terms_ordinal gives over the whole 2299// blob (the window provably contains every term with this prefix), touching one window instead of log2(n) 2300// scattered pages. Returns the ordinal (>= 0) with outs = postoff/postlen/dcount, -1 absent, -2 malformed. 2301static sst_lohi: *i64 2302func ss_terms_ordinal_h(h: *i64, s: i64, term: *u8, outs: *i64) -> i64 { 2303 let tb: *u8 = h[5 + 8 * s] as *u8 2304 let tsz: i64 = h[6 + 8 * s] 2305 if tsz < 8 { return 0 - 2 } 2306 if tb[0] != (78 as u8) { return 0 - 2 } 2307 if tb[2] != (84 as u8) { return 0 - 2 } 2308 let n: i64 = ss_r32(tb, 4) 2309 let base: i64 = 8 + 4 * n 2310 let tl0: i64 = ss_len(term) 2311 if (sst_lohi as i64) == 0 { sst_lohi = sys_mmap(16) as *i64 } 2312 ss_tsample_window(h, s, ss_tpack(term, tl0), n, sst_lohi) 2313 var lo: i64 = sst_lohi[0] 2314 var hi: i64 = sst_lohi[1] - 1 2315 while lo <= hi { 2316 let mid: i64 = (lo + hi) / 2 2317 let eo: i64 = base + ss_r32(tb, 8 + 4 * mid) 2318 let tl: i64 = ss_r32(tb, eo) 2319 let c: i64 = ss_kcmp((tb as i64 + eo + 4) as *u8, tl, term, tl0) 2320 if c == 0 { 2321 outs[0] = ss_r32(tb, eo + 4 + tl) 2322 outs[1] = ss_r32(tb, eo + 4 + tl + 4) 2323 outs[2] = ss_r32(tb, eo + 4 + tl + 8) 2324 return mid 2325 } 2326 if c < 0 { lo = mid + 1 } 2327 if c > 0 { hi = mid - 1 } 2328 } 2329 return 0 - 1 2330} 2331// ss_terms_find's contract (1 found / -1 absent / -2 malformed) over the sampled lookup 2332func ss_terms_find_h(h: *i64, s: i64, term: *u8, outs: *i64) -> i64 { 2333 let r: i64 = ss_terms_ordinal_h(h, s, term, outs) 2334 if r >= 0 { return 1 } 2335 return r 2336} 2337// the key of doc index d in segment s WITHOUT touching .docs: d2k maps it to its .keys entry, whose key bytes 2338// are the same bytes the .docs entry carries. 1 = kp/kl filled; 0 = no row or no entry (the caller reads the 2339// .docs entry exactly as before). 2340func ss_key_of_doc(h: *i64, s: i64, d: i64, kpout: *i64, klout: *i64) -> i64 { 2341 let r: *i64 = ss_aux_row(h, s) 2342 if (r as i64) == 0 { return 0 } 2343 if r[0] == 0 { return 0 } 2344 if d < 0 { return 0 } 2345 if d >= r[1] { return 0 } 2346 let d2k: *i64 = r[0] as *i64 2347 let eo: i64 = d2k[d] 2348 if eo <= 0 { return 0 } 2349 let kb: *u8 = h[1 + 8 * s] as *u8 2350 let ksz: i64 = h[2 + 8 * s] 2351 if eo + 5 > ksz { return 0 } 2352 let kl: i64 = ss_r32(kb, eo + 1) 2353 if eo + 5 + kl > ksz { return 0 } 2354 kpout[0] = (kb as i64) + eo + 5 2355 klout[0] = kl 2356 return 1 2357} 2358func ss_close_seg(h: *i64, ns: i64, s: i64) -> i64 { 2359 let kp: i64 = h[1 + 8 * s] 2360 let ksz: i64 = h[2 + 8 * s] 2361 // MERGED-.idx DETECTION (seq997). ss_load_aux2 returns keys/terms/post as INTERIOR SLICES of ONE 2362 // mapping when the segment has a merged .idx (base = keys-16, total = 16+kl+tl+pl). Freeing those as 2363 // three independent mappings is WRONG and never releases the base. The LEGACY 3-file fallback DOES 2364 // return three separate allocations, so this must branch, not assume. Discriminator: exact adjacency 2365 // FIRST (pure arithmetic, no dereference), and only then the NXI header magic -- so keys-16 is read 2366 // only once it is known to sit inside the same mapping, never off the front of a legacy blob. 2367 var merged: i64 = 0 2368 if kp != 0 { if ksz > 0 { let mbase: *u8 = (kp - 16) as *u8 2369 if h[5 + 8 * s] == kp + ksz { if h[7 + 8 * s] == h[5 + 8 * s] + h[6 + 8 * s] { 2370 if mbase[0] == (78 as u8) { if mbase[2] == (73 as u8) { merged = 1 } } } } } } 2371 if merged == 1 { sys_munmap((kp - 16) as *u8, 16 + ksz + h[6 + 8 * s] + h[8 + 8 * s]) } else { if kp != 0 { if ksz > 0 { sys_munmap(kp as *u8, ksz) } } } 2372 let dpp: i64 = h[3 + 8 * s] 2373 let dsz: i64 = h[4 + 8 * s] 2374 if dpp != 0 { if dsz > 0 { sys_munmap(dpp as *u8, dsz) } } 2375 let tp: i64 = h[5 + 8 * s] 2376 let tsz: i64 = h[6 + 8 * s] 2377 if merged == 0 { if tp != 0 { if tsz > 0 { sys_munmap(tp as *u8, tsz) } } } 2378 let pp: i64 = h[7 + 8 * s] 2379 let psz: i64 = h[8 + 8 * s] 2380 if merged == 0 { if pp != 0 { if psz > 0 { sys_munmap(pp as *u8, psz) } } } 2381 let lmp: i64 = h[1 + 8 * ns + s] 2382 if lmp != 0 { if dsz >= 0 { sys_munmap(lmp as *u8, dsz / 9 + 16) } } 2383 ss_aux_free_row(h, s) // AUX ROW (2026-09-14): d2k + term sample go with the live map 2384 h[1 + 8 * s] = 0 2385 h[3 + 8 * s] = 0 2386 h[5 + 8 * s] = 0 2387 h[7 + 8 * s] = 0 2388 h[1 + 8 * ns + s] = 0 2389 return 0 2390} 2391 2392func ss_close(h: *i64) -> i64 { 2393 if (h as i64) == 0 { return 0 } 2394 let ns: i64 = h[0] 2395 if ns < 0 { return 0 } 2396 var s: i64 = 0 2397 while s < ns { ss_close_seg(h, ns, s); s = s + 1 } 2398 h[0] = 0 2399 // QUERY SCRATCH ARENA -- six further mappings the handle OWNS, allocated in ss_open2's tail at 2400 // h[1+9ns]..h[6+9ns]. The first cut of ss_close missed these and the non-vacuous VmSize tooth caught it. 2401 // maxdocs is not stored anywhere, but it is DERIVABLE exactly as ss_open2 computed it (max over segments 2402 // of dsz/9+16, floor 16), so these free precisely with no handle-layout change. Sizes live in h[4+8q], 2403 // which the loop above never zeroes -- only pointers are nulled -- so this recompute is still valid here. 2404 var md: i64 = 16 2405 var q: i64 = 0 2406 while q < ns { 2407 let dz: i64 = h[4 + 8 * q] 2408 if dz / 9 + 16 > md { md = dz / 9 + 16 } 2409 q = q + 1 2410 } 2411 if h[1 + 9 * ns] != 0 { sys_munmap(h[1 + 9 * ns] as *u8, 64) } 2412 if h[2 + 9 * ns] != 0 { sys_munmap(h[2 + 9 * ns] as *u8, 32) } 2413 if h[3 + 9 * ns] != 0 { sys_munmap(h[3 + 9 * ns] as *u8, 8 * md) } 2414 if h[4 + 9 * ns] != 0 { sys_munmap(h[4 + 9 * ns] as *u8, 8 * md) } 2415 if h[5 + 9 * ns] != 0 { sys_munmap(h[5 + 9 * ns] as *u8, 8 * 68) } 2416 if h[6 + 9 * ns] != 0 { sys_munmap(h[6 + 9 * ns] as *u8, 8 * 68) } 2417 // RETAINED NAMES + TABLE (ss_open3, 2026-09-02): both live in the spare slots past the arena and are freed 2418 // here, so a handle that was reopened incrementally N times still owns exactly one copy of each. 2419 let nm9: i64 = h[SS3_SLOT_NAMES + 9 * ns] 2420 if nm9 != 0 { ss_manifest_free(nm9 as *i64, ns); h[SS3_SLOT_NAMES + 9 * ns] = 0 } 2421 if h[SS3_SLOT_TSEG + 9 * ns] != 0 { 2422 sys_munmap(h[SS3_SLOT_TSEG + 9 * ns] as *u8, 8 * h[SS3_SLOT_CAP + 9 * ns]) 2423 sys_munmap(h[SS3_SLOT_TEO + 9 * ns] as *u8, 8 * h[SS3_SLOT_CAP + 9 * ns]) 2424 h[SS3_SLOT_TSEG + 9 * ns] = 0 2425 h[SS3_SLOT_TEO + 9 * ns] = 0 2426 } 2427 // AUX ROW ARRAY (2026-09-14): every row's mappings were freed by ss_close_seg above; the array itself here. 2428 if h[SS3_SLOT_AUX + 9 * ns] != 0 { sys_munmap(h[SS3_SLOT_AUX + 9 * ns] as *u8, 8 * SS_AUXW * (ns + 1)); h[SS3_SLOT_AUX + 9 * ns] = 0 } 2429 sys_munmap(h as *u8, 8 * (9 * ns + 16)) 2430 return 0 2431} 2432 2433func ss_open(prefix: *u8) -> *i64 { 2434 return ss_open2(prefix, 0) 2435} 2436// usemmap=1: file-BACKED read-only maps for the segment files instead of read-all (the mmap-serve rung -- 2437// the big web shard uses this so the ~1.25M-doc RAM ceiling becomes disk-bound; shared across forked 2438// children + page-cache-persistent). The derived live-doc map + query scratch stay ANON (they are computed 2439// + mutated). Handle layout is IDENTICAL either way, so every ss_hget/ss_term/ss_phrase reader is unchanged. 2440// ---- LINEAR SHADOW TABLE (2026-08-01) -------------------------------------------------------- 2441// Defined HERE rather than imported: nx_livemap_fast.nx imports this file, so importing it back 2442// would be circular. These are the same functions that gate proved equivalent, renamed ssl_*. 2443// 2444// They replace the per-key rescan of all newer segments in ss_open2's live-doc map loop -- a scan 2445// that was O(keys x segments) and therefore QUADRATIC in segment count: 4,454,778 key entries over 2446// 96 segments on the live web shard, and worsening with every crawler commit. 2447// EMPTY IS 0, AND SEGMENTS ARE STORED +1 (changed 2026-08-15). It used to be -1, which forced a 2448// 16,777,216-slot CLEAR LOOP on every open just to write a sentinel a fresh mmap could have provided 2449// for free -- 134 MB of dirty pages per open on a host measured at swap 715 permil, where the memory 2450// pressure costs more than the milliseconds. sys_mmap returns ZERO-FILLED memory, so with 0 as EMPTY 2451// the table is correctly initialised the instant it is mapped and the loop disappears. 2452// Segment indices are 0-based and must stay distinguishable from empty, hence the +1 on store and the 2453// -1 on read -- the same offset-by-one this file already uses for the doc-offset index below. 2454// ★CHOOSE THE SENTINEL THE ALLOCATOR ALREADY GIVES YOU: a sentinel that disagrees with fresh memory 2455// buys an O(capacity) initialisation for nothing. 2456const SSL_EMPTY: i64 = 0 2457// TOMBSTONE (rung L1b, 2026-09-02): a slot whose row VANISHED in a fold. Never produced by ssl_build; only 2458// ss3_remap_table writes it, and ss3_insert_new may re-fill it. Lookups probe THROUGH it. 2459const SSL_TOMB: i64 = 0 - 1 2460 2461func ssl_pow2(n: i64) -> i64 { 2462 var p: i64 = 16 2463 while p < n { p = p * 2 } 2464 return p 2465} 2466 2467func ssl_total_keys(h: *i64, ns: i64) -> i64 { 2468 var t: i64 = 0 2469 var s: i64 = 0 2470 while s < ns { 2471 let kb: *u8 = h[1 + 8 * s] as *u8 2472 let ksz: i64 = h[2 + 8 * s] 2473 if ksz >= 8 { t = t + ss_r32(kb, 4) } 2474 s = s + 1 2475 } 2476 return t 2477} 2478 2479func ssl_key_eq(h: *i64, seg: i64, eo: i64, kb2: *u8, eo2: i64) -> i64 { 2480 let kb: *u8 = h[1 + 8 * seg] as *u8 2481 let kl: i64 = ss_r32(kb, eo + 1) 2482 let kl2: i64 = ss_r32(kb2, eo2 + 1) 2483 if kl != kl2 { return 0 } 2484 var i: i64 = 0 2485 while i < kl { 2486 if kb[eo + 5 + i] != kb2[eo2 + 5 + i] { return 0 } 2487 i = i + 1 2488 } 2489 return 1 2490} 2491 2492func ssl_hash_entry(kb: *u8, eo: i64) -> i64 { 2493 let kl: i64 = ss_r32(kb, eo + 1) 2494 var hsh: i64 = 0x811c9dc5 2495 var i: i64 = 0 2496 while i < kl { 2497 hsh = hsh ^ (kb[eo + 5 + i] as i64) 2498 hsh = (hsh * SS_MAGIC_16777619) & 0xffffffff 2499 i = i + 1 2500 } 2501 return hsh 2502} 2503 2504// Same FNV-1a family as ssl_hash_entry -- SAME seed, SAME named SS_MAGIC_16777619 -- over the 4 bytes of 2505// a 32-bit doc-table offset. One hash idiom in this file, not two, and no fresh magic multiplier. 2506func ssl_hash_i32(v: i64) -> i64 { 2507 var hsh: i64 = 0x811c9dc5 2508 var i: i64 = 0 2509 while i < 4 { 2510 hsh = hsh ^ ((v >> (8 * i)) & 0xff) 2511 hsh = (hsh * SS_MAGIC_16777619) & 0xffffffff 2512 i = i + 1 2513 } 2514 return hsh 2515} 2516 2517// Walk segments NEWEST-FIRST and refuse to overwrite an occupied slot: the first writer for a key 2518// is therefore the newest segment holding it, with no segment-number comparison needed. 2519func ssl_build(h: *i64, ns: i64, tbl_seg: *i64, tbl_eo: *i64, cap: i64) -> i64 { 2520 // (no clear loop: SSL_EMPTY is 0 and sys_mmap hands back zero-filled pages, so the table arrives 2521 // initialised. This removed 16,777,216 slot-writes -- 134 MB touched -- from every open.) 2522 let mask: i64 = cap - 1 2523 var inserted: i64 = 0 2524 var s: i64 = ns - 1 2525 while s >= 0 { 2526 let kb: *u8 = h[1 + 8 * s] as *u8 2527 let ksz: i64 = h[2 + 8 * s] 2528 if ksz >= 8 { 2529 let m: i64 = ss_r32(kb, 4) 2530 var e: i64 = 0 2531 while e < m { 2532 let eo: i64 = 8 + 4 * m + ss_r32(kb, 8 + 4 * e) 2533 let kl: i64 = ss_r32(kb, eo + 1) 2534 if kl < 500 { 2535 var slot: i64 = ssl_hash_entry(kb, eo) & mask 2536 var placed: i64 = 0 2537 while placed == 0 { 2538 if tbl_seg[slot] == SSL_EMPTY { 2539 tbl_seg[slot] = s + 1 2540 tbl_eo[slot] = eo 2541 inserted = inserted + 1 2542 placed = 1 2543 } else { 2544 if ssl_key_eq(h, tbl_seg[slot] - 1, tbl_eo[slot], kb, eo) == 1 { placed = 1 } else { slot = (slot + 1) & mask } 2545 } 2546 } 2547 } 2548 e = e + 1 2549 } 2550 } 2551 s = s - 1 2552 } 2553 return inserted 2554} 2555 2556func ssl_lookup(h: *i64, tbl_seg: *i64, tbl_eo: *i64, cap: i64, kb: *u8, eo: i64) -> i64 { 2557 let mask: i64 = cap - 1 2558 var slot: i64 = ssl_hash_entry(kb, eo) & mask 2559 var guard: i64 = 0 2560 while guard < cap { 2561 if tbl_seg[slot] == SSL_EMPTY { return 0 - 1 } 2562 if tbl_seg[slot] != SSL_TOMB { if ssl_key_eq(h, tbl_seg[slot] - 1, tbl_eo[slot], kb, eo) == 1 { return tbl_seg[slot] - 1 } } 2563 slot = (slot + 1) & mask 2564 guard = guard + 1 2565 } 2566 return 0 - 1 2567} 2568 2569// ---- OPT-IN OPEN TRACE (2026-08-15) -------------------------------------------------------------- 2570// WHY: opening the live web shard was MEASURED at ~9.8-10.0s (STARTUP warm_ms=9796/9976 in the docportal, 2571// and REQ done parent_us=9783316 when the same open runs on the accept path). That single number is the 2572// root cause of the /search 503s -- but "the open is slow" is not a diagnosis, and this function has 2573// THREE candidate costs: the per-segment file loads, ssl_build over ~4.45M key entries, and the 2574// per-segment live-doc map loop. I have guessed wrong about this subsystem repeatedly today, so the 2575// phases get measured before anything is optimised. 2576// DEFAULT OFF: ss_trace is 0 unless a caller opts in, so every production path is byte-for-byte 2577// unchanged in behaviour and pays one integer compare per phase. A TRACE THAT IS ALWAYS ON BECOMES 2578// OUTPUT NOBODY READS AND A COST EVERYBODY PAYS. 2579static ss_trace: i64 2580func ss_open_trace(v: i64) -> i64 { ss_trace = v; return 0 } 2581func ss_tr(lbl: *u8, us: i64) -> i64 { 2582 if ss_trace != 1 { return 0 } 2583 let b: *u8 = sys_mmap(128) 2584 var n: i64 = 0 2585 n = ss_cat(b, n, "SSOPEN " as *u8) 2586 n = ss_cat(b, n, lbl) 2587 n = ss_cat(b, n, " us=" as *u8) 2588 var v: i64 = us 2589 if v == 0 { b[n] = 48 as u8; n = n + 1 } else { 2590 var pw: i64 = 1 2591 while v / pw >= 10 { pw = pw * 10 } 2592 while pw > 0 { b[n] = (48 + ((v / pw) % 10)) as u8; n = n + 1; pw = pw / 10 } 2593 } 2594 b[n] = 10 as u8; n = n + 1 2595 sys_write(1, b, n) 2596 sys_munmap(b, 128) 2597 return 0 2598} 2599 2600// SEGMENT-ABSENT ANNOUNCE (2026-09-02, debt 1788361379). Printed by ss_open2 when a manifest row's .docs did 2601// NOT load (dsz == 0: absent, empty, or mid-rename by a writer). The segment then contributes NO live-doc 2602// marks -- absent docs cannot be served anyway, so skipping is the safe direction -- and the condition is 2603// VISIBLE in the daemon's own log instead of only in its symptoms. MEASURED on the serving daemon: the 2604// unguarded loop wrote past a one-page live map (lm is sized from dsz, the marks were bounded by the .post 2605// doc count), which produced one SIGSEGV in the accept-path refresh and then a respawn that spun 15.9h of 2606// CPU inside this function with the listen port never opened. Always written (fd 2), not gated on ss_trace. 2607func ss_open_seg_absent(segname: *u8) -> i64 { 2608 let b: *u8 = sys_mmap(512) 2609 var n: i64 = 0 2610 n = ss_cat(b, n, "SSOPEN SKIP seg=" as *u8) 2611 n = ss_cat(b, n, segname) 2612 n = ss_cat(b, n, " docs-absent: index present, .docs did not load -- segment contributes no live docs (debt 1788361379)\n" as *u8) 2613 sys_write(2, b, n) 2614 sys_munmap(b, 512) 2615 return 0 2616} 2617 2618 2619// ================= INCREMENTAL, WARM-PRESERVING REOPEN -- search plan rung L1, ss_open_incr (2026-09-02) ================= 2620// WHY: the docportal parent reopens the web shard on every manifest change, between accept() and fork(), and a 2621// reopen rebuilt ssl_build plus the live-doc map over EVERY segment -- 2.45 GB of page touches on a swapped array, 2622// measured as a 10.9 s parent stall against a 785 ms handler while the listen socket queued. Segments are IMMUTABLE 2623// and append-only, so when the new manifest is a strict PREFIX-EXTENSION of the previous one (same names, same 2624// .docs bytes on disk) everything derived for the old rows is still true except ONE thing: a key that a NEW row 2625// re-puts or tombstones is no longer live in its old row. ss_open3 therefore MOVES the old rows' mappings, live maps 2626// and the key->newest-segment table out of prev, loads only the new rows, inserts their keys newest-first, clears 2627// the old marks they shadow, and marks the new rows exactly as a FULL open would. EQUIVALENT BY CONSTRUCTION and 2628// PROVEN, not assumed: nx_segopen_incr_gate diffs every live map and every table answer against a FULL open of the 2629// same manifest and both must be byte-identical. 2630// FALLS BACK TO FULL, ANNOUNCED, on: no prev / prev empty / prev opened without retain (no table) / no names / 2631// manifest shrank / a leading name differs / a leading row's .docs size on disk differs / the table would pass half 2632// load. The FULL path is the pre-existing code, byte-for-byte in behaviour, so every one-shot caller of ss_open2 2633// (compact, pagerank, lexstat, crawl-step, segbloom, the gates) is unchanged. 2634// COST ENVELOPE (rule: name what is retained): the names array (8*ns+64 plus 128*ns bytes) is retained on every 2635// open; the two table halves (2 x cap x 8 B, cap = pow2(2 x distinct keys + 16), ~268 MB virtual for the 4.45M-key 2636// web shard) are retained ONLY when retain == 1 -- the daemon's parent refresh -- and exactly ONE copy exists at a 2637// time because ss3_move transfers it; ss_close frees both. A one-shot open (retain 0) frees the table as before. 2638// SLOTS: the handle is allocated 9*ns+16 wide and slots 7..12 past the arena were spare; they are the handle's own 2639// storage now (a handle built by anything other than ss_open2/ss_open3 has never existed in this tree). 2640 2641func ss3_streq(a: *u8, b: *u8) -> i64 { 2642 var i: i64 = 0 2643 while a[i] != (0 as u8) { if b[i] != a[i] { return 0 } i = i + 1 } 2644 if b[i] == (0 as u8) { return 1 } 2645 return 0 2646} 2647// READERS (pure, fork-safe): the mode the handle was built by, a manifest row's name, and the retained table. 2648func ss_open_mode(h: *i64) -> i64 { 2649 if (h as i64) == 0 { return 0 - 1 } 2650 let ns: i64 = h[0] 2651 if ns <= 0 { return 0 - 1 } 2652 return h[SS3_SLOT_MODE + 9 * ns] 2653} 2654func ss_open_segname(h: *i64, s: i64) -> *u8 { 2655 if (h as i64) == 0 { return 0 as *u8 } 2656 let ns: i64 = h[0] 2657 if s < 0 { return 0 as *u8 } 2658 if s >= ns { return 0 as *u8 } 2659 let names: *i64 = h[SS3_SLOT_NAMES + 9 * ns] as *i64 2660 if (names as i64) == 0 { return 0 as *u8 } 2661 return names[s] as *u8 2662} 2663func ss_open_table(h: *i64, outs: *i64) -> i64 { 2664 outs[0] = 0 2665 outs[1] = 0 2666 outs[2] = 0 2667 outs[3] = 0 2668 if (h as i64) == 0 { return 0 } 2669 let ns: i64 = h[0] 2670 if ns <= 0 { return 0 } 2671 if h[SS3_SLOT_TSEG + 9 * ns] == 0 { return 0 } 2672 outs[0] = h[SS3_SLOT_TSEG + 9 * ns] 2673 outs[1] = h[SS3_SLOT_TEO + 9 * ns] 2674 outs[2] = h[SS3_SLOT_CAP + 9 * ns] 2675 outs[3] = h[SS3_SLOT_INS + 9 * ns] 2676 return 1 2677} 2678// ANNOUNCE on fd 2, always (a reopen is a per-manifest-change event, bounded, and the daemon's stderr is its log): 2679// SSOPEN INCR reused= built= shadowed= us= or SSOPEN FULL reason=<why> ns= pn= <third>= us= 2680func ss3_ann(kind: *u8, k1: *u8, v1: i64, k2: *u8, v2: i64, k3: *u8, v3: i64, reason: *u8, us: i64) -> i64 { 2681 let b: *u8 = sys_mmap(SS3_ANN_CAP) 2682 var n: i64 = ss_cat(b, 0, "SSOPEN " as *u8) 2683 n = ss_cat(b, n, kind) 2684 if (reason as i64) != 0 { n = ss_cat(b, n, " reason=" as *u8); n = ss_cat(b, n, reason) } 2685 n = ss_cat(b, n, " " as *u8) 2686 n = ss_cat(b, n, k1) 2687 n = ss_catn(b, n, v1) 2688 n = ss_cat(b, n, " " as *u8) 2689 n = ss_cat(b, n, k2) 2690 n = ss_catn(b, n, v2) 2691 n = ss_cat(b, n, " " as *u8) 2692 n = ss_cat(b, n, k3) 2693 n = ss_catn(b, n, v3) 2694 n = ss_cat(b, n, " us=" as *u8) 2695 n = ss_catn(b, n, us) 2696 b[n] = 10 as u8 2697 n = n + 1 2698 sys_write(2, b, n) 2699 sys_munmap(b, SS3_ANN_CAP) 2700 return 0 2701} 2702func ss3_docs_size(prefix: *u8, segname: *u8) -> i64 { 2703 let p: *u8 = sys_mmap(SS3_PATHCAP) 2704 var o: i64 = ss_cat(p, 0, prefix) 2705 o = ss_cat(p, o, segname) 2706 o = ss_cat(p, o, ".docs" as *u8) 2707 p[o] = 0 as u8 2708 let stb: *u8 = sys_mmap(SS3_STATBUF) 2709 var sz: i64 = 0 - 1 2710 if sys_fstatat(p, stb) == 0 { let szp: *i64 = ((stb as i64) + SS3_ST_SIZE_OFF) as *i64; sz = szp[0] } 2711 sys_munmap(p, SS3_PATHCAP) 2712 sys_munmap(stb, SS3_STATBUF) 2713 return sz 2714} 2715// ADMISSION (rung L1b, generalised 2026-09-02 from the prefix-only rule shipped the same day). The live 2716// compactor is a RANGE FOLD: a contiguous run of rows is merged and replaced IN PLACE, concurrent ships are 2717// appended after it. So the new manifest is always common PREFIX + middle of NEW rows + common SUFFIX of 2718// survivors, and the prefix-only rule fell back to FULL on every fold (measured live: 3 FULL to 4 INCR). 2719// reuse[s] = prev row index for new row s, or -1 when s must be loaded. Returns the reused count; 0 = FULL. 2720// Names AND .docs size on disk must match for a row to be reused (segments are immutable; a same-named 2721// rewrite is refused by the size). Every refusal is announced when a prev was offered. 2722func ss3_admit(prefix: *u8, segs: *i64, ns: i64, prev: *i64, reuse: *i64, t0: i64) -> i64 { 2723 var i: i64 = 0 2724 while i < ns { reuse[i] = 0 - 1; i = i + 1 } 2725 if (prev as i64) == 0 { return 0 } 2726 let pn: i64 = prev[0] 2727 if pn <= 0 { ss3_ann("FULL" as *u8, "ns=" as *u8, ns, "pn=" as *u8, pn, "s0=" as *u8, 0, "prev-empty" as *u8, sys_now_us() - t0); return 0 } 2728 if prev[SS3_SLOT_TSEG + 9 * pn] == 0 { ss3_ann("FULL" as *u8, "ns=" as *u8, ns, "pn=" as *u8, pn, "s0=" as *u8, 0, "no-table" as *u8, sys_now_us() - t0); return 0 } 2729 let names: *i64 = prev[SS3_SLOT_NAMES + 9 * pn] as *i64 2730 if (names as i64) == 0 { ss3_ann("FULL" as *u8, "ns=" as *u8, ns, "pn=" as *u8, pn, "s0=" as *u8, 0, "no-names" as *u8, sys_now_us() - t0); return 0 } 2731 // common prefix: same name and same .docs bytes on disk 2732 var pre: i64 = 0 2733 var go: i64 = 1 2734 while go == 1 { 2735 if pre >= ns { go = 0 } else { if pre >= pn { go = 0 } else { 2736 if ss3_streq(segs[pre] as *u8, names[pre] as *u8) == 0 { go = 0 } else { 2737 if ss3_docs_size(prefix, names[pre] as *u8) != prev[4 + 8 * pre] { go = 0 } else { reuse[pre] = pre; pre = pre + 1 } 2738 } 2739 } } 2740 } 2741 // common suffix, never overlapping the prefix on either side 2742 var suf: i64 = 0 2743 go = 1 2744 while go == 1 { 2745 let ni: i64 = ns - 1 - suf 2746 let oi: i64 = pn - 1 - suf 2747 if ni < pre { go = 0 } else { if oi < pre { go = 0 } else { 2748 if ss3_streq(segs[ni] as *u8, names[oi] as *u8) == 0 { go = 0 } else { 2749 if ss3_docs_size(prefix, names[oi] as *u8) != prev[4 + 8 * oi] { go = 0 } else { reuse[ni] = oi; suf = suf + 1 } 2750 } 2751 } } 2752 } 2753 if pre + suf == 0 { ss3_ann("FULL" as *u8, "ns=" as *u8, ns, "pn=" as *u8, pn, "s0=" as *u8, 0, "no-common-rows" as *u8, sys_now_us() - t0); return 0 } 2754 return pre + suf 2755} 2756// MOVE every reused row (blob mappings + live map) and the table out of prev into h; remap[o] = new index of 2757// prev row o, or -1 when it vanished in the fold. prev keeps its header, dsz values and arena, so ss_close(prev) 2758// frees nothing h now owns. 2759func ss3_move(h: *i64, ns: i64, prev: *i64, reuse: *i64, remap: *i64) -> i64 { 2760 let pn: i64 = prev[0] 2761 var o: i64 = 0 2762 while o < pn { remap[o] = 0 - 1; o = o + 1 } 2763 var moved: i64 = 0 2764 var s: i64 = 0 2765 while s < ns { 2766 let old: i64 = reuse[s] 2767 if old >= 0 { 2768 var k: i64 = 1 2769 while k <= 8 { h[k + 8 * s] = prev[k + 8 * old]; k = k + 1 } 2770 h[1 + 8 * ns + s] = prev[1 + 8 * pn + old] 2771 // AUX ROW (2026-09-14) travels with its segment, ownership moved (prev's row zeroed) 2772 let axn: *i64 = ss_aux(h) 2773 let axp: *i64 = ss_aux(prev) 2774 if (axn as i64) != 0 { if (axp as i64) != 0 { 2775 var aw: i64 = 0 2776 while aw < SS_AUXW { axn[SS_AUXW * s + aw] = axp[SS_AUXW * old + aw]; axp[SS_AUXW * old + aw] = 0; aw = aw + 1 } 2777 } } 2778 prev[1 + 8 * old] = 0 2779 prev[3 + 8 * old] = 0 2780 prev[5 + 8 * old] = 0 2781 prev[7 + 8 * old] = 0 2782 prev[1 + 8 * pn + old] = 0 2783 remap[old] = s 2784 moved = moved + 1 2785 } 2786 s = s + 1 2787 } 2788 h[SS3_SLOT_TSEG + 9 * ns] = prev[SS3_SLOT_TSEG + 9 * pn] 2789 h[SS3_SLOT_TEO + 9 * ns] = prev[SS3_SLOT_TEO + 9 * pn] 2790 h[SS3_SLOT_CAP + 9 * ns] = prev[SS3_SLOT_CAP + 9 * pn] 2791 h[SS3_SLOT_INS + 9 * ns] = prev[SS3_SLOT_INS + 9 * pn] 2792 prev[SS3_SLOT_TSEG + 9 * pn] = 0 2793 prev[SS3_SLOT_TEO + 9 * pn] = 0 2794 prev[SS3_SLOT_CAP + 9 * pn] = 0 2795 prev[SS3_SLOT_INS + 9 * pn] = 0 2796 let pnames: *i64 = prev[SS3_SLOT_NAMES + 9 * pn] as *i64 2797 if (pnames as i64) != 0 { ss_manifest_free(pnames, pn) } 2798 prev[SS3_SLOT_NAMES + 9 * pn] = 0 2799 return moved 2800} 2801// REMAP the moved table from prev row indices to new ones; entries of vanished rows become TOMBSTONES. 2802// Returns the tombstone count (subtracted from the inserted count by the caller). O(cap) over resident 2803// anonymous pages -- no file page is touched. 2804func ss3_remap_table(tseg: *i64, cap: i64, remap: *i64, pn: i64) -> i64 { 2805 var tomb: i64 = 0 2806 var i: i64 = 0 2807 while i < cap { 2808 let v: i64 = tseg[i] 2809 if v > 0 { 2810 let old: i64 = v - 1 2811 var nw: i64 = 0 - 1 2812 if old < pn { nw = remap[old] } 2813 if nw >= 0 { tseg[i] = nw + 1 } else { tseg[i] = SSL_TOMB; tomb = tomb + 1 } 2814 } 2815 i = i + 1 2816 } 2817 return tomb 2818} 2819func ssl_total_keys_range(h: *i64, a: i64, b: i64) -> i64 { 2820 var t: i64 = 0 2821 var s: i64 = a 2822 while s < b { 2823 let kb: *u8 = h[1 + 8 * s] as *u8 2824 let ksz: i64 = h[2 + 8 * s] 2825 if ksz >= 8 { t = t + ss_r32(kb, 4) } 2826 s = s + 1 2827 } 2828 return t 2829} 2830func ssl_total_keys_mask(h: *i64, ns: i64, newmask: *i64) -> i64 { 2831 var t: i64 = 0 2832 var s: i64 = 0 2833 while s < ns { 2834 if newmask[s] == 1 { 2835 let kb: *u8 = h[1 + 8 * s] as *u8 2836 let ksz: i64 = h[2 + 8 * s] 2837 if ksz >= 8 { t = t + ss_r32(kb, 4) } 2838 } 2839 s = s + 1 2840 } 2841 return t 2842} 2843// CLEAR the live mark of the reused-row entry (oseg, oeo) that a new row has just shadowed. The doc index comes 2844// from the same doc-offset inverse index the FULL mark loop builds, built lazily ONCE per row a new key touches 2845// (dhk/dhv/dhc are per-row caches owned by ss3_insert_new). Returns 1 iff a set mark was cleared. 2846func ss3_clear_mark(h: *i64, ns: i64, oseg: i64, oeo: i64, dhk: *i64, dhv: *i64, dhc: *i64) -> i64 { 2847 let okb: *u8 = h[1 + 8 * oseg] as *u8 2848 let okl: i64 = ss_r32(okb, oeo + 1) 2849 let ovof: i64 = ss_r32(okb, oeo + 5 + okl) 2850 let ioff: i64 = ovof - 9 - okl 2851 let pb: *u8 = h[7 + 8 * oseg] as *u8 2852 let psz: i64 = h[8 + 8 * oseg] 2853 let dsz: i64 = h[4 + 8 * oseg] 2854 let lm: *u8 = h[1 + 8 * ns + oseg] as *u8 2855 if (lm as i64) == 0 { return 0 } 2856 if psz < 8 { return 0 } 2857 if dhk[oseg] == 0 { 2858 var nd: i64 = ss_r32(pb, 4) 2859 if dsz <= 0 { nd = 0 } 2860 if 8 + 4 * nd > psz { nd = (psz - 8) / 4 } 2861 let cap: i64 = ssl_pow2(nd * 2 + 16) 2862 let k: *i64 = sys_mmap(8 * cap) as *i64 2863 let v: *i64 = sys_mmap(8 * cap) as *i64 2864 var ci: i64 = 0 2865 while ci < cap { k[ci] = 0; ci = ci + 1 } 2866 var i: i64 = 0 2867 while i < nd { 2868 let dv0: i64 = ss_r32(pb, 8 + 4 * i) 2869 var slot: i64 = ssl_hash_i32(dv0) & (cap - 1) 2870 var placed: i64 = 0 2871 while placed == 0 { 2872 if k[slot] == 0 { k[slot] = dv0 + 1; v[slot] = i; placed = 1 } 2873 else { if k[slot] == dv0 + 1 { placed = 1 } else { slot = (slot + 1) & (cap - 1) } } 2874 } 2875 i = i + 1 2876 } 2877 dhk[oseg] = k as i64 2878 dhv[oseg] = v as i64 2879 dhc[oseg] = cap 2880 } 2881 let k2: *i64 = dhk[oseg] as *i64 2882 let v2: *i64 = dhv[oseg] as *i64 2883 let mask2: i64 = dhc[oseg] - 1 2884 var slot2: i64 = ssl_hash_i32(ioff) & mask2 2885 var looking: i64 = 1 2886 while looking == 1 { 2887 if k2[slot2] == 0 { looking = 0 } 2888 else { 2889 if k2[slot2] == ioff + 1 { 2890 let idx: i64 = v2[slot2] 2891 if idx < dsz / 9 + 16 { if lm[idx] == (1 as u8) { lm[idx] = 0 as u8; return 1 } } 2892 looking = 0 2893 } else { slot2 = (slot2 + 1) & mask2 } 2894 } 2895 } 2896 return 0 2897} 2898// INSERT the keys of every NEW row (newmask[s]==1) into the remapped table, newest row first. Probing runs 2899// THROUGH tombstones (a key may sit past one) and remembers the first tombstone as the placement slot. On an 2900// equal key from row r: r < s means r is a REUSED row the new row now shadows (new rows older than s are not 2901// inserted yet, descending order) -> clear r's mark and overwrite; r > s means a newer new row already holds 2902// the key -> keep. This reproduces ssl_build's newest-wins over the new manifest order exactly. outs[0] += 2903// slots newly filled, outs[1] += tombstones re-filled. Returns the number of reused-row marks cleared. 2904func ss3_insert_new(h: *i64, ns: i64, newmask: *i64, tseg: *i64, teo: *i64, cap: i64, outs: *i64) -> i64 { 2905 let mask: i64 = cap - 1 2906 var shadowed: i64 = 0 2907 let dhk: *i64 = sys_mmap(8 * (ns + 1)) as *i64 2908 let dhv: *i64 = sys_mmap(8 * (ns + 1)) as *i64 2909 let dhc: *i64 = sys_mmap(8 * (ns + 1)) as *i64 2910 var s: i64 = ns - 1 2911 while s >= 0 { 2912 if newmask[s] == 1 { 2913 let kb: *u8 = h[1 + 8 * s] as *u8 2914 let ksz: i64 = h[2 + 8 * s] 2915 if ksz >= 8 { 2916 var m: i64 = ss_r32(kb, 4) 2917 if 8 + 4 * m > ksz { m = (ksz - 8) / 4 } 2918 var e: i64 = 0 2919 while e < m { 2920 let eo: i64 = 8 + 4 * m + ss_r32(kb, 8 + 4 * e) 2921 let kl: i64 = ss_r32(kb, eo + 1) 2922 if kl < SS3_KEYMAX { 2923 var slot: i64 = ssl_hash_entry(kb, eo) & mask 2924 var tombslot: i64 = 0 - 1 2925 var placed: i64 = 0 2926 while placed == 0 { 2927 if tseg[slot] == SSL_EMPTY { 2928 if tombslot >= 0 { tseg[tombslot] = s + 1; teo[tombslot] = eo; outs[1] = outs[1] + 1 } 2929 else { tseg[slot] = s + 1; teo[slot] = eo; outs[0] = outs[0] + 1 } 2930 placed = 1 2931 } else { 2932 if tseg[slot] == SSL_TOMB { 2933 if tombslot < 0 { tombslot = slot } 2934 slot = (slot + 1) & mask 2935 } else { 2936 if ssl_key_eq(h, tseg[slot] - 1, teo[slot], kb, eo) == 1 { 2937 let r: i64 = tseg[slot] - 1 2938 if r < s { 2939 shadowed = shadowed + ss3_clear_mark(h, ns, r, teo[slot], dhk, dhv, dhc) 2940 tseg[slot] = s + 1 2941 teo[slot] = eo 2942 } 2943 placed = 1 2944 } else { slot = (slot + 1) & mask } 2945 } 2946 } 2947 } 2948 } 2949 e = e + 1 2950 } 2951 } 2952 } 2953 s = s - 1 2954 } 2955 s = 0 2956 while s < ns { if dhk[s] != 0 { sys_munmap(dhk[s] as *u8, 8 * dhc[s]); sys_munmap(dhv[s] as *u8, 8 * dhc[s]) } s = s + 1 } 2957 sys_munmap(dhk as *u8, 8 * (ns + 1)) 2958 sys_munmap(dhv as *u8, 8 * (ns + 1)) 2959 sys_munmap(dhc as *u8, 8 * (ns + 1)) 2960 return shadowed 2961} 2962// count tombstones still standing after the insert: every one is a vanished key NO new row carries, i.e. its 2963// second-newest entry (somewhere in a reused row) became live again and the table cannot name it -> FULL. 2964func ss3_tomb_count(tseg: *i64, cap: i64) -> i64 { 2965 var t: i64 = 0 2966 var i: i64 = 0 2967 while i < cap { if tseg[i] == SSL_TOMB { t = t + 1 } i = i + 1 } 2968 return t 2969} 2970// SEGMENT LOAD for manifest rows [a,b), extracted from ss_open2 2026-09-02: the FULL open loads every row, 2971// the INCR open loads only the rows prev does not already hold. Same body, one copy. 2972func ss3_load_range(prefix: *u8, segs: *i64, h: *i64, a: i64, b: i64, usemmap: i64, dp: *u8, dszp: *i64, aouts: *i64) -> i64 { 2973 var s: i64 = a 2974 while s < b { 2975 // dp hoisted above the loop (seq905) 2976 var o: i64 = 0 2977 o = ss_cat(dp, o, prefix) 2978 o = ss_cat(dp, o, segs[s] as *u8) 2979 o = ss_cat(dp, o, ".docs" as *u8) 2980 dp[o] = 0 as u8 2981 // dszp hoisted above the loop (seq905) 2982 h[3 + 8 * s] = ss_loadfile(dp, dszp, usemmap) as i64 2983 h[4 + 8 * s] = dszp[0] 2984 // index blobs: merged .idx slices (or legacy 3-file fallback) 2985 ss_load_aux2(prefix, segs[s] as *u8, aouts, usemmap) 2986 h[1 + 8 * s] = aouts[0] 2987 h[2 + 8 * s] = aouts[1] 2988 h[5 + 8 * s] = aouts[2] 2989 h[6 + 8 * s] = aouts[3] 2990 h[7 + 8 * s] = aouts[4] 2991 h[8 + 8 * s] = aouts[5] 2992 s = s + 1 2993 } 2994 return 0 2995} 2996 2997// ss_open2 keeps its signature and its behaviour: a FULL open with the table freed at the end (retain 0). 2998func ss_open2(prefix: *u8, usemmap: i64) -> *i64 { 2999 return ss_open3(prefix, usemmap, 0 as *i64, 0) 3000} 3001// ss_open_incr IS the incremental open the search plan rung L1 names (contract symbol ss_open_incr): reuse prev where the 3002// manifest is a prefix-extension, retain the table so the NEXT reopen can do the same. The daemon parent refresh calls this. 3003func ss_open_incr(prefix: *u8, usemmap: i64, prev: *i64) -> *i64 { 3004 return ss_open3(prefix, usemmap, prev, 1) 3005} 3006func ss_open3(prefix: *u8, usemmap: i64, prev: *i64, retain: i64) -> *i64 { 3007 let t_open: i64 = sys_now_us() 3008 let sp: *i64 = sys_mmap(8) as *i64 3009 let ns: i64 = ss_manifest_dyn(prefix, sp) 3010 let segs: *i64 = sp[0] as *i64 3011 let h: *i64 = sys_mmap(8 * (9 * ns + 16)) as *i64 3012 h[0] = ns 3013 // AUX ROWS (2026-09-14): one zero-filled row per segment; filled in the live-map loop, moved by ss3_move, 3014 // freed by ss_close_seg/ss_close. sys_mmap returns zero pages, so an unfilled row reads as absent. 3015 h[SS3_SLOT_AUX + 9 * ns] = sys_mmap(8 * SS_AUXW * (ns + 1)) as i64 3016 // INCREMENTAL ADMISSION (rungs L1 + L1b): reuse[s] names the prev row that new row s already holds warm 3017 // (common prefix and common suffix of the two manifests), -1 = load it. nreuse == 0 means FULL. 3018 let reuse: *i64 = sys_mmap(8 * (ns + 1)) as *i64 3019 let newmask: *i64 = sys_mmap(8 * (ns + 1)) as *i64 3020 var nreuse: i64 = ss3_admit(prefix, segs, ns, prev, reuse, t_open) 3021 var s0: i64 = 0 3022 if nreuse > 0 { s0 = 1 } 3023 var nm0: i64 = 0 3024 while nm0 < ns { if reuse[nm0] < 0 { newmask[nm0] = 1 } else { newmask[nm0] = 0 } nm0 = nm0 + 1 } 3025 let aouts: *i64 = sys_mmap(8 * 8) as *i64 3026 // HOISTED (seq905): dp/dszp were allocated INSIDE the per-segment loop, so opening an N-segment store 3027 // leaked N page-sized scratch mappings PER OPEN -- and nothing could ever reclaim them because they are 3028 // not reachable from the returned handle, so even a correct ss_close could not help. One allocation, 3029 // reused per segment. Same lesson nx_treepack banked as "buffers hoisted, no mmap in inner loops". 3030 let dp: *u8 = sys_mmap(512) 3031 let dszp: *i64 = sys_mmap(16) as *i64 3032 var s: i64 = 0 3033 while s < ns { if newmask[s] == 1 { ss3_load_range(prefix, segs, h, s, s + 1, usemmap, dp, dszp, aouts) } s = s + 1 } 3034 // TABLE-LOAD CHECK before anything is moved out of prev: the retained table was sized 2x entries at its last 3035 // FULL build; past SS3_LOAD_NUM/SS3_LOAD_DEN load open addressing degrades and 3036 // the honest answer is a FULL rebuild -- announced, the rows this open loaded released via ss_close_seg, 3037 // and prev untouched so the caller still owns a complete handle. 3038 var vanished: i64 = 0 3039 var tombs: i64 = 0 3040 let remap: *i64 = sys_mmap(8 * (ns + 1 + 256)) as *i64 3041 if s0 > 0 { 3042 let pn9: i64 = prev[0] 3043 let newk: i64 = ssl_total_keys_mask(h, ns, newmask) 3044 if (prev[SS3_SLOT_INS + 9 * pn9] + newk) * SS3_LOAD_DEN > prev[SS3_SLOT_CAP + 9 * pn9] * SS3_LOAD_NUM { 3045 ss3_ann("FULL" as *u8, "ns=" as *u8, ns, "pn=" as *u8, pn9, "newkeys=" as *u8, newk, "table-load" as *u8, sys_now_us() - t_open) 3046 var rs: i64 = 0 3047 while rs < ns { if newmask[rs] == 1 { ss_close_seg(h, ns, rs) } newmask[rs] = 1; rs = rs + 1 } 3048 s0 = 0 3049 nreuse = 0 3050 ss3_load_range(prefix, segs, h, 0, ns, usemmap, dp, dszp, aouts) 3051 } 3052 } 3053 if s0 > 0 { 3054 let pn8: i64 = prev[0] 3055 // the remap array is sized for prev's rows; pn8 can exceed ns (a fold shrinks the manifest) 3056 var rmp: *i64 = remap 3057 if pn8 > ns + 256 { rmp = sys_mmap(8 * (pn8 + 1)) as *i64 } 3058 ss3_move(h, ns, prev, reuse, rmp) 3059 vanished = pn8 - nreuse 3060 tombs = ss3_remap_table(h[SS3_SLOT_TSEG + 9 * ns] as *i64, h[SS3_SLOT_CAP + 9 * ns], rmp, pn8) 3061 } 3062 // PREFETCH THE INDEX BLOBS BEFORE THE TWO WHOLE-CORPUS WALKS BELOW (2026-09-01). 3063 // THE SAME PROVEN IDIOM, ONE LAYER DOWN. nx_docportal_search_seg's PASS 0 already batches 3064 // madvise(MADV_WILLNEED) over candidate doc extents before its serial tf/prox walk, and measured 3065 // 1350 candidates at 8.7s cold versus 375ms warm -- the delta IS the serial fault chain. ssl_build 3066 // and the live-doc map loop below are that same shape one level lower: this function's OWN trace 3067 // measured them at SSOPEN livemap us=9953625 of TOTAL us=12346598, explicitly because they are 3068 // "tens of millions of page touches" served ONE FAULT AT A TIME off a contended array. 3069 // WHY IT MATTERS HERE: this open runs between accept() and fork() in nx_docportal_admin_daemon, so 3070 // its cost is charged to a live request. MEASURED 2026-09-01 on the public SERP: wall 11142ms 3071 // against a handler that reported only 785ms -- the daemon's own timer brackets the forked CHILD 3072 // and is structurally blind to the parent's block, which is why this stayed unattributed for weeks. 3073 // CORRECTNESS-NEUTRAL BY CONSTRUCTION: madvise is ADVISORY and computes nothing. The loops below 3074 // read the identical bytes, just warm, so this cannot make any verdict wrong -- only slower or 3075 // faster. The return value is deliberately ignored, as at every other WILLNEED site in this estate. 3076 // GATED ON usemmap, DELIBERATELY: that flag is this codebase's "large FILE-BACKED shard" signal. 3077 // ss_open() is ss_open2(prefix, 0) (read-all into anonymous memory, already resident), so the DEFAULT 3078 // open path is untouched and no small store pays for readahead it cannot use. 3079 // ⚠SCOPE MEASURED, NOT ASSUMED -- AND WIDER THAN ONE CALLER. A first draft of this comment claimed 3080 // "dss_open_maybe_cached passes 1 and nothing else does"; a grep REFUTED that before it shipped. 3081 // Callers passing 1 (FLOOR, not a total -- the scan returned corpus_complete=0 BUDGET-EXCEEDED, so 3082 // there may be more): nx_docportal_search_seg (the serving daemon), nx_web_shard_compact, 3083 // nx_pagerank_build, nx_doc_lexstat, nx_web_crawl_step, nx_segbloom, nx_livemap_fast_gate, 3084 // nx_docportal_search_sticky_gate. 3085 // THAT WIDER SET IS ACCEPTABLE AND MOSTLY BENEFICIAL: every one of them opens a large shard and then 3086 // WALKS it (compact, pagerank, lexstat, crawl-step, the livemap gate), which is precisely the access 3087 // pattern batched readahead exists for. The one shape that pays without gaining is an organ that 3088 // opens a big shard and reads only a little -- the sticky gate is the candidate -- and its cost is 3089 // bounded readahead the kernel is free to ignore, never a wrong answer. 3090 // ★A HEADER IS NOT A MEASUREMENT: this comment names the callers a grep FOUND, with the envelope 3091 // that says the list is a floor, rather than restating an assumption as a scope. 3092 // TERMS BLOB DELIBERATELY NOT PREFETCHED: ss_terms_find is a BINARY search done at QUERY time, not 3093 // during the open, so warming it here would be readahead this function cannot show a cost for. 3094 // Name the scope of a fix rather than widening it until something improves. 3095 if usemmap == 1 { 3096 var pf: i64 = 0 3097 while pf < ns { 3098 if newmask[pf] == 0 { pf = pf + 1 } else { 3099 let kba: i64 = h[1 + 8 * pf] 3100 let kbn: i64 = h[2 + 8 * pf] 3101 if kba != 0 { 3102 if kbn > 0 { 3103 let kal: i64 = (kba / SS_MAGIC_4096) * SS_MAGIC_4096 3104 sys_madvise(kal as *u8, (kba - kal) + kbn, SS_MADV_WILLNEED) 3105 } 3106 } 3107 // TERMS SLICE NOW PREFETCHED TOO (2026-09-14): the sample build below reads every SS_TSAMPLE-th entry 3108 // of it sequentially, so the readahead has a consumer the old comment said it lacked. 3109 let tba: i64 = h[5 + 8 * pf] 3110 let tbn: i64 = h[6 + 8 * pf] 3111 if tba != 0 { 3112 if tbn > 0 { 3113 let tal: i64 = (tba / SS_MAGIC_4096) * SS_MAGIC_4096 3114 sys_madvise(tal as *u8, (tba - tal) + tbn, SS_MADV_WILLNEED) 3115 } 3116 } 3117 let pba: i64 = h[7 + 8 * pf] 3118 let pbn: i64 = h[8 + 8 * pf] 3119 if pba != 0 { 3120 if pbn > 0 { 3121 let pfl: i64 = (pba / SS_MAGIC_4096) * SS_MAGIC_4096 3122 sys_madvise(pfl as *u8, (pba - pfl) + pbn, SS_MADV_WILLNEED) 3123 } 3124 } 3125 pf = pf + 1 3126 } 3127 } 3128 } 3129 // live-doc maps: derived from the .keys indexes, ONCE per open. A .keys 3130 // entry is already its key's LATEST entry within its segment, so an entry 3131 // is current iff NO NEWER segment's index knows the key (put or 3132 // tombstone both shadow). Docs not in their segment's .keys (shadowed 3133 // within the segment) stay dead. Single-segment stores need ZERO probes. 3134 let vo9: *i64 = sys_mmap(16) as *i64 3135 let vl9: *i64 = sys_mmap(16) as *i64 3136 let kcp: *u8 = sys_mmap(512) 3137 // key -> NEWEST segment holding it, built ONCE per open (2026-08-01). Replaces the per-key 3138 // rescan of all newer segments in the loop below. Sized 2x the total entry count and rounded to 3139 // a power of two so the probe masks instead of dividing; open addressing wants <=50% load. 3140 var ssl_total: i64 = 0 3141 var ssl_cap: i64 = 0 3142 var ssl_seg: *i64 = 0 as *i64 3143 var ssl_eo: *i64 = 0 as *i64 3144 var ssl_ins: i64 = 0 3145 var ssl_shadowed: i64 = 0 3146 ss_tr("segload" as *u8, sys_now_us() - t_open) 3147 let t_ssl: i64 = sys_now_us() 3148 if s0 > 0 { 3149 // INCR: the table came across from prev in ss3_move; only the new rows' keys are inserted and every old 3150 // mark they shadow is cleared, so the marks of rows [0,s0) are correct BEFORE the loop below marks the rest. 3151 ssl_seg = h[SS3_SLOT_TSEG + 9 * ns] as *i64 3152 ssl_eo = h[SS3_SLOT_TEO + 9 * ns] as *i64 3153 ssl_cap = h[SS3_SLOT_CAP + 9 * ns] 3154 let insp: *i64 = sys_mmap(16) as *i64 3155 insp[0] = 0 3156 insp[1] = 0 3157 ssl_shadowed = ss3_insert_new(h, ns, newmask, ssl_seg, ssl_eo, ssl_cap, insp) 3158 ssl_ins = h[SS3_SLOT_INS + 9 * ns] - tombs + insp[0] + insp[1] 3159 sys_munmap(insp as *u8, 16) 3160 // COVERAGE: a tombstone left standing is a vanished key no new row carries -- its next-newest entry lives 3161 // in a reused row the table cannot name, so the incremental marks would be WRONG. Rebuild everything 3162 // derived, in place, from the rows h already holds (no file is re-mapped): announced as FULL. 3163 let tleft: i64 = ss3_tomb_count(ssl_seg, ssl_cap) 3164 if tleft > 0 { 3165 ss3_ann("FULL" as *u8, "ns=" as *u8, ns, "vanished=" as *u8, vanished, "uncovered=" as *u8, tleft, "uncovered-vanished-keys" as *u8, sys_now_us() - t_open) 3166 var rz: i64 = 0 3167 while rz < ns { 3168 let lmz: i64 = h[1 + 8 * ns + rz] 3169 if lmz != 0 { sys_munmap(lmz as *u8, h[4 + 8 * rz] / 9 + 16); h[1 + 8 * ns + rz] = 0 } 3170 newmask[rz] = 1 3171 rz = rz + 1 3172 } 3173 sys_munmap(ssl_seg as *u8, 8 * ssl_cap) 3174 sys_munmap(ssl_eo as *u8, 8 * ssl_cap) 3175 s0 = 0 3176 ssl_total = ssl_total_keys(h, ns) 3177 ssl_cap = ssl_pow2(ssl_total * 2 + 16) 3178 ssl_seg = sys_mmap(8 * ssl_cap) as *i64 3179 ssl_eo = sys_mmap(8 * ssl_cap) as *i64 3180 ssl_ins = ssl_build(h, ns, ssl_seg, ssl_eo, ssl_cap) 3181 } 3182 } else { 3183 ssl_total = ssl_total_keys(h, ns) 3184 ssl_cap = ssl_pow2(ssl_total * 2 + 16) 3185 ssl_seg = sys_mmap(8 * ssl_cap) as *i64 3186 ssl_eo = sys_mmap(8 * ssl_cap) as *i64 3187 ssl_ins = ssl_build(h, ns, ssl_seg, ssl_eo, ssl_cap) 3188 } 3189 ss_tr("sslbuild" as *u8, sys_now_us() - t_ssl) 3190 let t_live: i64 = sys_now_us() 3191 var maxdocs: i64 = 16 3192 s = 0 3193 while s < ns { let dz0: i64 = h[4 + 8 * s]; if dz0 / 9 + 16 > maxdocs { maxdocs = dz0 / 9 + 16 } s = s + 1 } 3194 s = 0 3195 while s < ns { 3196 if newmask[s] == 0 { s = s + 1 } else { 3197 let kb: *u8 = h[1 + 8 * s] as *u8 3198 let ksz: i64 = h[2 + 8 * s] 3199 let pb: *u8 = h[7 + 8 * s] as *u8 3200 let dsz: i64 = h[4 + 8 * s] 3201 let lm: *u8 = sys_mmap(dsz / 9 + 16) 3202 h[1 + 8 * ns + s] = lm as i64 3203 if dsz / 9 + 16 > maxdocs { maxdocs = dsz / 9 + 16 } 3204 if ksz >= 8 { if h[8 + 8 * s] >= 8 { 3205 var nd9: i64 = ss_r32(pb, 4) 3206 // FAIL-SAFE ADMISSION (2026-09-02, debt 1788361379). lm above is sized from dsz, but every mark below 3207 // is indexed by a doc number read from the .post blob. If this segment's .docs did NOT load (dsz == 0: 3208 // absent, empty, mid-rename) the two disagree and the marks overflow a one-page lm -- SIGSEGV when the 3209 // next page is unmapped, silent table corruption and an endless probe when it is; BOTH were measured 3210 // on the serving daemon (one crash in the accept-path refresh, then a respawn spinning 15.9h of CPU 3211 // here with the port never opened). nd9 = 0 makes the inverse index empty, so no mark is written and 3212 // the segment serves no docs, which is the truth. The blob-derived counts are ALSO clamped to the 3213 // bytes actually mapped so a short or corrupt .post/.idx cannot steer a read past its mapping, and 3214 // lmcap9 bounds the mark itself. Refereed by nx_segopen_absent_gate (fixture: idx present, docs gone). 3215 let lmcap9: i64 = dsz / 9 + 16 3216 if dsz <= 0 { nd9 = 0; ss_open_seg_absent(segs[s] as *u8) } 3217 if 8 + 4 * nd9 > h[8 + 8 * s] { nd9 = (h[8 + 8 * s] - 8) / 4 } 3218 // AUX ROWS FOR THIS SEGMENT (2026-09-14): d2k (doc index -> .keys entry offset, filled beside the live 3219 // mark below for every live key -- a dead doc is never emitted, so it needs no entry) and the term 3220 // sample. The row is freed first so the in-place FULL rebuild above cannot leak a previous build. 3221 ss_aux_free_row(h, s) 3222 let d2kn: i64 = nd9 + 1 3223 let d2k9: *i64 = sys_mmap(8 * d2kn) as *i64 3224 let axr9: *i64 = ss_aux_row(h, s) 3225 if (axr9 as i64) != 0 { axr9[0] = d2k9 as i64; axr9[1] = d2kn } 3226 ss_tsample_build(h, s) 3227 ss_doccount_build(h, s) // search L5: the row's doc-key count, once, here in the parent 3228 var m9: i64 = ss_r32(kb, 4) 3229 if 8 + 4 * m9 > ksz { m9 = (ksz - 8) / 4 } // clamp to the bytes mapped (2026-09-02, debt 1788361379) 3230 // DOC-OFFSET INVERSE INDEX (2026-08-15). The loop below used to BINARY SEARCH pb once per key 3231 // entry. MEASURED with the ss_trace probe: that loop is 80.6pct of the entire shard open 3232 // (SSOPEN livemap us=9953625 of TOTAL us=12346598), because ~5.28M entries x ~log2(nd9) 3233 // random probes over mmap'd pages is tens of millions of page touches -- and the shard open is 3234 // in turn the root cause of the /search 503s (paid at startup AND on the accept path). 3235 // Build the inverse ONCE per segment instead: offset -> doc index, then each key is ONE probe. 3236 // Complexity goes from O(m log nd) to O(nd + m). 3237 // EQUIVALENT BY CONSTRUCTION, NOT BY HOPE: the binary search only ever acted on an EXACT match 3238 // (dv == ioff), so an exact-match hash decides the identical predicate. It is still PROVEN -- 3239 // nx_livemap_fast_gate diffs every rebuilt map against the reference across the real shard and 3240 // must stay MISMATCHED=0 (baseline 14 segments / 5282736 entries / 369993949 bytes / 7-7 GREEN). 3241 // ⚠I FIRST PROPOSED A TWO-POINTER MERGE HERE AND MEASUREMENT KILLED IT: 2480606 of 5282736 key 3242 // entries (47pct) are NOT in ascending offset order, so a merge would have silently produced a 3243 // WRONG visibility map. A hash needs no ordering assumption at all. 3244 // DOC-OFFSET INVERSE INDEX. Replaces a per-key BINARY SEARCH of the .post doc table, which 3245 // the ss_trace phase timers measured at 80.6pct of the whole shard open (SSOPEN livemap 3246 // us=9953625 of TOTAL us=12346598) -- and that open is the root cause of the /search 503s, 3247 // paid at startup AND on the accept path. O(m log nd) -> O(nd + m). 3248 // EQUIVALENT BY CONSTRUCTION: the binary search only ever acted on an EXACT match, so an 3249 // exact-match hash decides the identical predicate. PROVEN, not assumed: nx_livemap_fast_gate 3250 // 7/7 GREEN, MISMATCHED=0 over 15 segments / 5283159 entries / 370396017 bytes. 3251 // ⚠A two-pointer merge was proposed here first and MEASUREMENT KILLED IT: 47pct of key 3252 // entries are not in ascending offset order, so a merge would have produced a WRONG 3253 // visibility map. A hash needs no ordering assumption. 3254 // PRODUCTION-VERIFIED, which is a separate axis from the gate and from the CLI: with this 3255 // live the daemon's refresh (a full open on the accept path) measured parent_us=7757144 3256 // versus 9783316 before, and per-request REQ child ms stayed 0-4. 3257 let dh_cap: i64 = ssl_pow2(nd9 * 2 + 16) 3258 let dh_mask: i64 = dh_cap - 1 3259 let dh_key: *i64 = sys_mmap(8 * dh_cap) as *i64 3260 let dh_val: *i64 = sys_mmap(8 * dh_cap) as *i64 3261 var dh_i: i64 = 0 3262 while dh_i < dh_cap { dh_key[dh_i] = 0; dh_i = dh_i + 1 } 3263 dh_i = 0 3264 while dh_i < nd9 { 3265 let dv0: i64 = ss_r32(pb, 8 + 4 * dh_i) 3266 var dslot: i64 = ssl_hash_i32(dv0) & dh_mask 3267 var dplaced: i64 = 0 3268 while dplaced == 0 { 3269 if dh_key[dslot] == 0 { dh_key[dslot] = dv0 + 1; dh_val[dslot] = dh_i; dplaced = 1 } 3270 else { 3271 if dh_key[dslot] == dv0 + 1 { dplaced = 1 } else { dslot = (dslot + 1) & dh_mask } 3272 } 3273 } 3274 dh_i = dh_i + 1 3275 } 3276 var e9: i64 = 0 3277 while e9 < m9 { 3278 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9) 3279 let kind9: i64 = kb[eo] 3280 let kl9: i64 = ss_r32(kb, eo + 1) 3281 let vof: i64 = ss_r32(kb, eo + 5 + kl9) 3282 if kind9 == 1 { if kl9 < 500 { 3283 // shadowed by any newer segment knowing this key? 3284 var shadowed: i64 = 0 3285 // SHADOW TEST, NOW LINEAR (2026-08-01). This block asked `does any NEWER segment 3286 // know this key` by RESCANNING every newer segment, once per key -- O(keys x 3287 // segments) index probes, QUADRATIC in segment count: 4,454,778 key entries across 3288 // 96 segments on the live web shard. It is the measured cause of the ~8 minute 3289 // docportal pre-warm, and it got worse every time the crawler committed (87 -> 96 3290 // segments in one session once the crawler's segments=0 bug was fixed). A correct 3291 // fix at the leaf had surfaced a latent quadratic in the keystone. 3292 // ssl_tbl is built ONCE per open, walking segments NEWEST-FIRST, so the first 3293 // writer for a key is by construction the newest segment holding it. The entire 3294 // rescan collapses to one hash probe. 3295 // PROVEN BEFORE THE SWITCH, NOT AFTER: nx_livemap_fast_gate rebuilt every live-doc 3296 // map this way and diffed it against the old implementation on the REAL shard -- 3297 // 96 segments, 4,454,778 entries, 177,262,743 bytes compared, MISMATCHED=0, 7/7. 3298 // These maps decide which documents are visible at all, so a faster map that is 3299 // subtly different would not make search fast, it would make search WRONG. 3300 let newest9: i64 = ssl_lookup(h, ssl_seg, ssl_eo, ssl_cap, kb, eo) 3301 if newest9 > s { shadowed = 1 } 3302 if shadowed == 0 { 3303 // map entry offset -> doc index: ONE probe into the per-segment inverse index. 3304 // An empty slot terminates the walk exactly where the binary search used to fail 3305 // to find a match -- both simply mark nothing. 3306 // ⚠I BRIEFLY REVERTED THIS ON A FALSE ALARM. Web requests hit ~10.7s shortly after 3307 // the deploy, I attributed it to this change, "rolled back", and saw recovery. 3308 // The rollback NEVER LANDED -- the deploy returned PROMOTED while the live .elf 3309 // stayed 3809f5da -- so the recovery came from the RESTART, not the revert, and 3310 // the slow window was my own three back-to-back CLI probes each mapping the full 3311 // 1.6GB shard through the page cache this daemon shares. 3312 // ★★★★★★A ROLLBACK THAT DID NOT LAND TURNS THE NEXT RECOVERY INTO FALSE EVIDENCE 3313 // FOR WHATEVER YOU THOUGHT YOU REVERTED -- verify the artifact bytes, never the 3314 // deploy receipt, BEFORE reading the result as a control. 3315 // ★AND DO NOT MEASURE A SERVICE WHILE GENERATING LOAD AGAINST ITS OWN PAGE CACHE. 3316 let ioff: i64 = vof - 9 - kl9 3317 var dslot2: i64 = ssl_hash_i32(ioff) & dh_mask 3318 var looking: i64 = 1 3319 while looking == 1 { 3320 if dh_key[dslot2] == 0 { looking = 0 } else { 3321 if dh_key[dslot2] == ioff + 1 { if dh_val[dslot2] < lmcap9 { lm[dh_val[dslot2]] = 1 as u8; if dh_val[dslot2] < d2kn { d2k9[dh_val[dslot2]] = eo } } looking = 0 } 3322 else { dslot2 = (dslot2 + 1) & dh_mask } 3323 } 3324 } 3325 } 3326 } } 3327 e9 = e9 + 1 3328 } 3329 // Per-segment scratch: freed HERE, not left to ss_close, because it is not reachable from the 3330 // returned handle -- the exact leak class seq905 recorded for dp/dszp a few lines above. 3331 sys_munmap(dh_key as *u8, 8 * dh_cap) 3332 sys_munmap(dh_val as *u8, 8 * dh_cap) 3333 } } 3334 s = s + 1 3335 } 3336 } 3337 ss_tr("livemap" as *u8, sys_now_us() - t_live) 3338 ss_tr("TOTAL" as *u8, sys_now_us() - t_open) 3339 // (A one-shot ordering probe stood here to decide whether key entries arrive in ascending ioff 3340 // order -- the precondition for replacing the binary search with a two-pointer merge. IT ANSWERED 3341 // PERMANENTLY: 2480801 of 5283159 entries, 47pct, are NOT ascending, so that merge is invalid and 3342 // was never written. The answer is banked in debt 1786816618; the probe is removed because a 3343 // diagnostic that has settled its question is dead weight in a hot library, and re-deriving it is 3344 // a 30-line probe away if the writer ever changes. The reusable PHASE timers above stay.) 3345 // query scratch arena (handle is single-threaded; queries allocate nothing) 3346 h[1 + 9 * ns] = sys_mmap(64) as i64 3347 h[2 + 9 * ns] = sys_mmap(32) as i64 3348 h[3 + 9 * ns] = sys_mmap(8 * maxdocs) as i64 3349 h[4 + 9 * ns] = sys_mmap(8 * maxdocs) as i64 3350 h[5 + 9 * ns] = sys_mmap(8 * 68) as i64 3351 h[6 + 9 * ns] = sys_mmap(8 * 68) as i64 3352 // PER-OPEN SCRATCH RELEASE (seq905/seq975): none of these are reachable from the returned handle, so 3353 // ss_close can NEVER reclaim them -- they must be given back HERE or every open leaks them forever. 3354 // All are dead past this point: dp/dszp and aouts served the segment loop; vo9/vl9/kcp served the 3355 // live-doc-map loop; sp was only the out-param holder. ss_manifest_free releases the segment-name pool 3356 // and the segs array (paired with ss_manifest_dyn exactly as ss_close pairs with ss_open). 3357 h[SS3_SLOT_NAMES + 9 * ns] = segs as i64 3358 sys_munmap(dp, 512) 3359 sys_munmap(dszp as *u8, 16) 3360 sys_munmap(aouts as *u8, 8 * 8) 3361 sys_munmap(vo9 as *u8, 16) 3362 sys_munmap(vl9 as *u8, 16) 3363 sys_munmap(kcp, 512) 3364 sys_munmap(sp as *u8, 8) 3365 // ssl_seg/ssl_eo (the 2026-08-01 key->newest-segment table) are DEAD past the live-doc loop and, 3366 // like everything else in this block, unreachable from the returned handle -- ss_close can never 3367 // reclaim them. They were the ONLY per-open allocations missing from this release list, and at 3368 // web-shard scale the pair is 2 x 8 x pow2(2 x 4.45M keys) = 256 MB PER REOPEN -- measured live 3369 // 2026-08-01 as orphaned 256MB anon VMAs accumulating in nx_docportal_admin_daemon (debt 3370 // 1785622778), one leaked pair per manifest change under an active crawler. 3371 // THE CHECKLIST LAW: WHEN YOU ADD A PER-OPEN ALLOCATION, ADD ITS RELEASE IN THE SAME EDIT -- 3372 // this block exists precisely because seq905 paid for the same lesson. 3373 if retain == 1 { 3374 h[SS3_SLOT_TSEG + 9 * ns] = ssl_seg as i64 3375 h[SS3_SLOT_TEO + 9 * ns] = ssl_eo as i64 3376 h[SS3_SLOT_CAP + 9 * ns] = ssl_cap 3377 h[SS3_SLOT_INS + 9 * ns] = ssl_ins 3378 } else { 3379 h[SS3_SLOT_TSEG + 9 * ns] = 0 3380 h[SS3_SLOT_TEO + 9 * ns] = 0 3381 h[SS3_SLOT_CAP + 9 * ns] = 0 3382 h[SS3_SLOT_INS + 9 * ns] = 0 3383 sys_munmap(ssl_seg as *u8, 8 * ssl_cap) 3384 sys_munmap(ssl_eo as *u8, 8 * ssl_cap) 3385 } 3386 if s0 > 0 { 3387 h[SS3_SLOT_MODE + 9 * ns] = SS3_MODE_INCR 3388 ss3_ann("INCR" as *u8, "reused=" as *u8, nreuse, "built=" as *u8, ns - nreuse, "shadowed=" as *u8, ssl_shadowed, 0 as *u8, sys_now_us() - t_open) 3389 ss3_ann("INCR-FOLD" as *u8, "vanished=" as *u8, vanished, "tombstoned=" as *u8, tombs, "ins=" as *u8, ssl_ins, 0 as *u8, sys_now_us() - t_open) 3390 } else { 3391 h[SS3_SLOT_MODE + 9 * ns] = SS3_MODE_FULL 3392 } 3393 return h 3394} 3395 3396// handle get: identical semantics to ss_get_idx, zero file IO per call 3397// ---- CACHED OPEN (added 2026-07-25, debt seq962) ------------------------------------------------- 3398// WHY: ss_open/ss_open2 read EVERY segment fully into anonymous RAM and there is NO ss_close anywhere 3399// in the tree, so a caller that opens PER REQUEST leaks the whole store per request. MEASURED on the 3400// live box: nx_hub_gw does 3 opens per HTTP request and grew VmData 1336 kB PER REQUEST, with 3401// VmSize==VmPeak (monotonic). Nine long-lived organs carried that same fingerprint, ~23.6 GiB of 3402// anonymous address space, which drove swap to 92.3% and the box into sustained thrash. 3403// 3404// THIS IS NOT A NEW IDEA -- it is an EXISTING in-tree remedy finally promoted into the primitive. 3405// The same cache was independently rediscovered at least three times and never generalized: 3406// 1. nx_docportal_search_seg.nx:128 dss_open_maybe_cached (manifest-signature invalidation) 3407// 2. nx_docportal_admin_daemon.nx:652 per-request ss_open measured ~0.7s/query without this 3408// 3. nx_store_seed_lib.nx:94 helped the reader family OOM the host. NOW: ss_open ONCE 3409// Three local patches, zero generalization -- fix the CLASS, not the call site. 3410// 3411// PURELY ADDITIVE BY DESIGN: there is NO munmap here. That is deliberate. ss_load_aux2 publishes 3412// INTERIOR POINTERS into a single mapping on the merged-.idx path but three INDEPENDENT mappings on 3413// the legacy path, and ss_open2 discards the return value that distinguishes them -- so any freeing 3414// close is a partial-unmap/use-after-free hazard until that ledger is fixed (debt seq947). Caching 3415// removes the leak without ever freeing: you cannot leak what you never allocate twice. 3416// 3417// LIVE-EDIT PRESERVED (nx_hub_gw depends on it): invalidation is the manifest (st_size, st_mtime), 3418// so editing a store is still picked up with NO restart -- on the next call. 3419// RESIDUAL (honest): a miss re-opens WITHOUT freeing the previous handle, so re-opens fall from 3420// per-request to per-store-EDIT. Bounded and rare; ss_close remains worth building for that path. 3421const SSC_SLOTS: i64 = 16 3422const SSC_W: i64 = 4 3423// RETIRE RING (seq1057). Capacity in HANDLES; slot 0 of the mapping holds the count, handles live at 1..n. 3424const SSC_RETIRE: i64 = 32 3425static ssc_tab: *i64 // SSC_SLOTS x SSC_W: [0]=prefix-copy ptr [1]=st_size [2]=st_mtime [3]=handle 3426static ssc_names: *u8 // SSC_SLOTS x 256 bytes of prefix copies (slot i at +i*256) 3427static ssc_scr: *u8 // one-shot scratch: [0..559]=path [560..719]=statbuf [720..735]=sig 3428static ssc_retired: *i64 // [0]=count, [1..SSC_RETIRE]=orphaned handles awaiting an explicit reap 3429// CACHE TELEMETRY (2026-08-14). The fail-open fallback in ss_open_cached was UNCOUNTED, so a process 3430// that exhausted the 16 slots leaked a whole store mapping per call with nothing anywhere to show it. 3431// An unbounded leak behind a silent fallback is invisible by construction; these counters make the 3432// condition READABLE before it becomes a 3 GB daemon. 3433const SSC_ST_HIT: i64 = 0 3434const SSC_ST_REOPEN: i64 = 1 3435const SSC_ST_NEWSLOT: i64 = 2 3436const SSC_ST_FALLBACK: i64 = 3 // slots exhausted -> per-call open (now retired, formerly leaked) 3437const SSC_ST_DROP: i64 = 4 // retire ring full -> pointer dropped (the documented bounded leak) 3438const SSC_ST_SLOTS: i64 = 8 3439static ssc_stats: *i64 3440 3441// allocate the cache ONCE (static POINTERS to lazy-mmap'd tables -- a BSS static ARRAY silently 3442// crashes handler modules on startup, so never use [N]i64 here). 3443func ssc_init() -> i64 { 3444 if (ssc_tab as i64) != 0 { return 1 } 3445 ssc_tab = sys_mmap(8 * SSC_SLOTS * SSC_W) as *i64 3446 ssc_names = sys_mmap(SSC_SLOTS * 256) 3447 ssc_scr = sys_mmap(SS_MAGIC_1024) 3448 ssc_retired = sys_mmap(8 * (SSC_RETIRE + 1)) as *i64 3449 ssc_stats = sys_mmap(8 * SSC_ST_SLOTS) as *i64 3450 return 1 3451} 3452// Read one telemetry counter (SSC_ST_*). Observability for gates, daemons and leak triage. 3453func ss_cache_stat(idx: i64) -> i64 { 3454 ssc_init() 3455 if idx < 0 { return 0 } 3456 if idx >= SSC_ST_SLOTS { return 0 } 3457 return ssc_stats[idx] 3458} 3459 3460// ---- RETIRE / REAP (seq1057) ------------------------------------------------------------------- 3461// THE PROBLEM: on a stale manifest signature ss_open_cached re-opens and OVERWRITES the cached handle. 3462// The previous handle's whole mapping set (segments, keys, docs, terms, post, live-doc maps, scratch 3463// arena) was simply dropped on the floor -- orphaned on every edit to that store. 3464// 3465// WHY NOT JUST ss_close(old) THERE: ss_open_cached hands back a RAW handle and promises the same handle 3466// semantics as ss_open. There is no refcount and no generation, so a caller that took the handle before 3467// the invalidation is still dereferencing it. Freeing inline would turn a bounded LEAK into an unbounded 3468// USE-AFTER-FREE in the most-shared primitive in the ecosystem. That trade is never worth it. 3469// 3470// THE SHAPE THAT IS SOUND WITHOUT TOUCHING ~180 READERS: retiring NEVER frees, so it cannot fault. The 3471// orphan is merely remembered instead of lost. Reclamation is an EXPLICIT, OPT-IN call that a consumer 3472// makes at a point where it knows it holds no handle -- the top of a request loop, between batches. Until 3473// a consumer opts in, behaviour is byte-for-byte what it is today, so this can never regress a caller. 3474// 3475// FAIL-SAFE DIRECTION: if the ring is full we DROP the pointer (leak it) rather than free it. Overflow 3476// degrades to exactly today's behaviour, never to a free that might still be in use. 3477func ssc_retire_handle(h: i64) -> i64 { 3478 if h == 0 { return 0 } 3479 let n: i64 = ssc_retired[0] 3480 // The ring-full DROP is a deliberate, documented, bounded leak (dropping beats a free that may still 3481 // be in use). It was never COUNTED, though, so nobody could tell whether it ever actually happened in 3482 // production -- a leak-by-design must be bounded AND named AND observable, not just the first two. 3483 if n >= SSC_RETIRE { ssc_stats[SSC_ST_DROP] = ssc_stats[SSC_ST_DROP] + 1; return 0 } 3484 ssc_retired[1 + n] = h 3485 ssc_retired[0] = n + 1 3486 return 1 3487} 3488 3489// Release every retired handle. Returns how many were closed. 3490// *** CONTRACT: the CALLER guarantees no seg-store handle from ss_open_cached is still in use. 3491// Call it where that is structurally true (top of a request loop, between batches) -- never from inside 3492// a function that is holding a handle, and never from the primitive itself. 3493func ss_cache_reap() -> i64 { 3494 ssc_init() 3495 let n: i64 = ssc_retired[0] 3496 var i: i64 = 0 3497 var freed: i64 = 0 3498 while i < n { 3499 let h: i64 = ssc_retired[1 + i] 3500 if h != 0 { ss_close(h as *i64); freed = freed + 1 } 3501 ssc_retired[1 + i] = 0 3502 i = i + 1 3503 } 3504 ssc_retired[0] = 0 3505 return freed 3506} 3507 3508// How many orphans are awaiting a reap (observability for gates and daemons). 3509func ss_cache_retired() -> i64 { 3510 ssc_init() 3511 return ssc_retired[0] 3512} 3513func ssc_streq(a: *u8, b: *u8) -> i64 { 3514 var i: i64 = 0 3515 while a[i] != (0 as u8) { 3516 if b[i] != a[i] { return 0 } 3517 i = i + 1 3518 } 3519 if b[i] == (0 as u8) { return 1 } 3520 return 0 3521} 3522// manifest signature -> sig[0]=st_size sig[1]=st_mtime-sec ((0,0) when absent). 3523// Reuses the ONE static scratch buffer, so this probe allocates nothing per call. 3524func ssc_sig_of(prefix: *u8, sig: *i64) -> i64 { 3525 let mp: *u8 = ssc_scr 3526 var o: i64 = 0 3527 o = ss_cat(mp, o, prefix) 3528 o = ss_cat(mp, o, "manifest.txt" as *u8) 3529 mp[o] = 0 as u8 3530 let stb: *u8 = ((ssc_scr as i64) + 560) as *u8 3531 sig[0] = 0 3532 sig[1] = 0 3533 if sys_fstatat(mp, stb) == 0 { 3534 let szp: *i64 = ((stb as i64) + 48) as *i64 3535 let mtp: *i64 = ((stb as i64) + 88) as *i64 3536 sig[0] = szp[0] 3537 sig[1] = mtp[0] 3538 } 3539 return 0 3540} 3541// THE ONE OPEN long-lived readers should use. Same handle semantics as ss_open -- every ss_hget / 3542// ss_term / ss_phrase reader is unchanged. Fail-open: if the table is full we fall back to a plain 3543// ss_open rather than refusing, so caching can never break a caller. 3544func ss_open_cached(prefix: *u8) -> *i64 { 3545 ssc_init() 3546 let sig: *i64 = ((ssc_scr as i64) + 720) as *i64 3547 ssc_sig_of(prefix, sig) 3548 var i: i64 = 0 3549 var freeslot: i64 = 0 - 1 3550 while i < SSC_SLOTS { 3551 let np: i64 = ssc_tab[i * SSC_W] 3552 if np == 0 { 3553 if freeslot < 0 { freeslot = i } 3554 } else { 3555 if ssc_streq(np as *u8, prefix) == 1 { 3556 if ssc_tab[i * SSC_W + 3] != 0 { 3557 if ssc_tab[i * SSC_W + 1] == sig[0] { 3558 if ssc_tab[i * SSC_W + 2] == sig[1] { return ssc_tab[i * SSC_W + 3] as *i64 } 3559 } 3560 } 3561 let hr: *i64 = ss_open(prefix) 3562 // seq1057: the previous handle used to be overwritten and lost here. Retiring does NOT 3563 // free it (a caller may still hold it) -- it only makes it reclaimable by ss_cache_reap. 3564 ssc_retire_handle(ssc_tab[i * SSC_W + 3]) 3565 ssc_tab[i * SSC_W + 1] = sig[0] 3566 ssc_tab[i * SSC_W + 2] = sig[1] 3567 ssc_tab[i * SSC_W + 3] = hr as i64 3568 return hr 3569 } 3570 } 3571 i = i + 1 3572 } 3573 if freeslot < 0 { 3574 // FAIL-OPEN WITHOUT LEAKING (2026-08-14). This branch used to be `return ss_open(prefix)` with the 3575 // mapping UNTRACKED: not placed in a slot, never retired, never reaped. Slots are NEVER released 3576 // once claimed, so as soon as 16 distinct prefixes existed in a process, EVERY later call here 3577 // leaked an entire store mapping -- permanently, and with no counter anywhere to show it. 3578 // MEASURED on nx_hub_gw (pid 15668, 2026-08-14): 239.93 kB leaked per request over 60 requests, 3579 // and a 12-sample series under load returned tau=1000permille rate=227102kB/min verdict=LEAK on a 3580 // perfectly monotone climb to 3.0 GB. That daemon's own code is clean (4 unbalanced funcs / 7 3581 // sites) and so is its whole import closure -- the leak was entirely this one untracked return. 3582 // THE FIX IS THE INCUMBENT MECHANISM, NOT A NEW ONE: retire the handle immediately. 3583 // ssc_retire_handle NEVER frees (it only records), so this cannot fault, and the handle is 3584 // reclaimed by the caller's next ss_cache_reap() under exactly the contract reap already states. 3585 // Slot-cached handles are deliberately NOT retired -- they are meant to outlive the call. Only 3586 // this per-call fallback is, because only it has no owner. 3587 let hf: *i64 = ss_open(prefix) 3588 ssc_stats[SSC_ST_FALLBACK] = ssc_stats[SSC_ST_FALLBACK] + 1 3589 ssc_retire_handle(hf as i64) 3590 return hf 3591 } 3592 let nm: *u8 = ((ssc_names as i64) + freeslot * 256) as *u8 3593 var k: i64 = 0 3594 while prefix[k] != (0 as u8) { if k < 255 { nm[k] = prefix[k] } k = k + 1 } 3595 if k > 255 { k = 255 } 3596 nm[k] = 0 as u8 3597 let hn: *i64 = ss_open(prefix) 3598 ssc_tab[freeslot * SSC_W] = nm as i64 3599 ssc_tab[freeslot * SSC_W + 1] = sig[0] 3600 ssc_tab[freeslot * SSC_W + 2] = sig[1] 3601 ssc_tab[freeslot * SSC_W + 3] = hn as i64 3602 return hn 3603} 3604 3605// PER-CALL SCRATCH, ALLOCATED ONCE (2026-07-30). ss_hget used to do TWO sys_mmap(16) calls PER 3606// INVOCATION and never free them. sys_mmap is a REAL mmap syscall (nx_syscalls.nx:167), not a bump 3607// allocator, so the kernel rounds each to a full page: ~8 KB LEAKED PER ss_hget CALL. ss_hget is the 3608// hottest read primitive in the ecosystem -- 205 call sites, and the loaders call it ONCE PER ROW -- 3609// so a 10k-row scan leaked ~80 MB in scratch alone, dwarfing the per-open leak this lane started on. 3610// 3611// This is the SAME defect seq905 already fixed one function away: ss_open2's dp/dszp were allocated 3612// inside the per-segment loop and were hoisted out ("buffers hoisted, no mmap in inner loops"). The 3613// identical pattern in ss_hget was missed because the loop is in the CALLER, not in the function. 3614// ★LAW: a per-call allocation in a primitive is a per-CALLER-LOOP leak -- audit the primitive's call 3615// frequency, not just its own body. 3616// 3617// SAFE AS A STATIC: vo/vl are pure scratch for ss_idx_find's two outputs, written then read immediately 3618// with no intervening call that could re-enter ss_hget (VERIFIED: ss_idx_find does not call ss_hget). 3619// Static POINTER + lazy mmap is the established idiom here -- a BSS static ARRAY silently crashes 3620// handler modules on startup, so never use [N]i64. 3621static ssh_vo: *i64 3622static ssh_vl: *i64 3623// FNV-1a over RAW key bytes -- the SAME arithmetic as ssl_hash_entry over a .keys entry, so a raw key lands in 3624// the slot ssl_build filled for its entry. One hash idiom, spelled twice only because one reads an entry and 3625// the other a caller's buffer. 3626func ssl_hash_raw(key: *u8, kl: i64) -> i64 { 3627 var hsh: i64 = 0x811c9dc5 3628 var i: i64 = 0 3629 while i < kl { 3630 hsh = hsh ^ (key[i] as i64) 3631 hsh = (hsh * SS_MAGIC_16777619) & 0xffffffff 3632 i = i + 1 3633 } 3634 return hsh 3635} 3636// KEY LOCATE THROUGH THE RETAINED TABLE (2026-09-14, search plan L3 residual). When the open retained its key 3637// table (ss_open_incr: the daemon's cached web handle, the referee bench) a key's NEWEST segment is one hash 3638// probe instead of a newest-first binary search of every segment's .keys -- 2048 candidates x 4-5 lookups x up 3639// to 33 segments per query, MEASURED by the search phase timers at 0.65-0.75 s of a 1.15 s cold query. 3640// SAME ANSWER BY CONSTRUCTION: ssl_build inserts every .keys entry shorter than SS3_KEYMAX newest-first and 3641// refuses to overwrite, and ss3_insert_new keeps that invariant across incremental reopens, so the slot IS the 3642// newest segment holding the key -- exactly where the loop stops. A key of SS3_KEYMAX bytes or more is never in 3643// the table and takes the loop (-2), as does a handle opened without retention. outs[0]=segment, outs[1]=entry 3644// offset in that segment's .keys. Returns 1 found, 0 absent (an EMPTY slot ends the probe exactly as the loop's 3645// exhaustion does), -2 no table (caller walks). 3646func ss_hlocate(h: *i64, key: *u8, kl: i64, outs: *i64) -> i64 { 3647 let ns: i64 = h[0] 3648 if ns <= 0 { return 0 - 2 } 3649 if kl >= SS3_KEYMAX { return 0 - 2 } 3650 let tseg: *i64 = h[SS3_SLOT_TSEG + 9 * ns] as *i64 3651 if (tseg as i64) == 0 { return 0 - 2 } 3652 let teo: *i64 = h[SS3_SLOT_TEO + 9 * ns] as *i64 3653 let cap: i64 = h[SS3_SLOT_CAP + 9 * ns] 3654 if cap <= 0 { return 0 - 2 } 3655 let mask: i64 = cap - 1 3656 var slot: i64 = ssl_hash_raw(key, kl) & mask 3657 var guard: i64 = 0 3658 while guard < cap { 3659 let tv: i64 = tseg[slot] 3660 if tv == SSL_EMPTY { return 0 } 3661 if tv != SSL_TOMB { 3662 let s: i64 = tv - 1 3663 let eo: i64 = teo[slot] 3664 let kb: *u8 = h[1 + 8 * s] as *u8 3665 let ksz: i64 = h[2 + 8 * s] 3666 if eo + 5 + kl <= ksz { if ss_r32(kb, eo + 1) == kl { 3667 var i: i64 = 0 3668 var eq: i64 = 1 3669 while i < kl { if kb[eo + 5 + i] != key[i] { eq = 0; i = kl } else { i = i + 1 } } 3670 if eq == 1 { outs[0] = s; outs[1] = eo; return 1 } 3671 } } 3672 } 3673 slot = (slot + 1) & mask 3674 guard = guard + 1 3675 } 3676 return 0 3677} 3678func ss_hget(h: *i64, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 3679 let ns: i64 = h[0] 3680 if ns == 0 { return 0 - 1 } 3681 if (ssh_vo as i64) == 0 { ssh_vo = sys_mmap(16) as *i64 } 3682 if (ssh_vl as i64) == 0 { ssh_vl = sys_mmap(16) as *i64 } 3683 let vo: *i64 = ssh_vo 3684 let vl: *i64 = ssh_vl 3685 // FAST PATH (2026-09-14): one probe of the retained key table; the walk below is the fallback and the proof 3686 let kl9: i64 = ss_len(key) 3687 let hl9: i64 = ss_hlocate(h, key, kl9, vo) 3688 if hl9 == 1 { 3689 let s9: i64 = vo[0] 3690 let eo9: i64 = vo[1] 3691 let kb9: *u8 = h[1 + 8 * s9] as *u8 3692 if kb9[eo9] == (2 as u8) { return 0 } // tombstone in the newest segment shadows, as the walk answers 3693 let voff9: i64 = ss_r32(kb9, eo9 + 5 + kl9) 3694 let vlen9: i64 = ss_r32(kb9, eo9 + 5 + kl9 + 4) 3695 if h[4 + 8 * s9] < voff9 + vlen9 { return 0 - 2 } 3696 ptrout[0] = h[3 + 8 * s9] + voff9 3697 lenout[0] = vlen9 3698 return 1 3699 } 3700 if hl9 == 0 { return 0 - 1 } 3701 var s: i64 = ns - 1 3702 while s >= 0 { 3703 let r: i64 = ss_idx_find(h[1 + 8 * s] as *u8, h[2 + 8 * s], key, vo, vl) 3704 if r == 2 { return 0 } 3705 if r == 1 { 3706 if h[4 + 8 * s] < vo[0] + vl[0] { return 0 - 2 } 3707 ptrout[0] = h[3 + 8 * s] + vo[0] 3708 lenout[0] = vl[0] 3709 return 1 3710 } 3711 s = s - 1 3712 } 3713 return 0 - 1 3714} 3715 3716// binary-search a .terms blob; returns 1 + postoff/postlen/dcount in outs, 3717// -1 not found, -2 malformed/absent (old-format segment -> caller fails LOUD) 3718func ss_terms_find(tb: *u8, tsz: i64, term: *u8, outs: *i64) -> i64 { 3719 if tsz < 8 { return 0 - 2 } 3720 if tb[0] != (78 as u8) { return 0 - 2 } 3721 if tb[2] != (84 as u8) { return 0 - 2 } 3722 let n: i64 = ss_r32(tb, 4) 3723 let base: i64 = 8 + 4 * n 3724 let tl0: i64 = ss_len(term) 3725 var lo: i64 = 0 3726 var hi: i64 = n - 1 3727 while lo <= hi { 3728 let mid: i64 = (lo + hi) / 2 3729 let eo: i64 = base + ss_r32(tb, 8 + 4 * mid) 3730 let tl: i64 = ss_r32(tb, eo) 3731 let c: i64 = ss_kcmp((tb as i64 + eo + 4) as *u8, tl, term, tl0) 3732 if c == 0 { 3733 outs[0] = ss_r32(tb, eo + 4 + tl) 3734 outs[1] = ss_r32(tb, eo + 4 + tl + 4) 3735 outs[2] = ss_r32(tb, eo + 4 + tl + 8) 3736 return 1 3737 } 3738 if c < 0 { lo = mid + 1 } 3739 if c > 0 { hi = mid - 1 } 3740 } 3741 return 0 - 1 3742} 3743 3744// TERM SEARCH with CURRENT-STATE semantics. A posting hit counts ONLY if it 3745// is the key's CURRENT entry (verified against the key index): a newer 3746// version without the term, or a tombstone, silently shadows older hits -- 3747// stale text never answers for a live record. Each current entry exists 3748// exactly once, so no dedup list is needed. Returns match count (key 3749// ptr/len pairs into the handle's docs buffers), or -2 if ANY live segment 3750// lacks a .terms index (old-format -- fail LOUD, compaction is the upgrade 3751// path; never a silent partial answer). 3752func ss_term(h: *i64, term: *u8, kpout: *i64, klout: *i64, max: i64) -> i64 { 3753 let ns: i64 = h[0] 3754 var cnt: i64 = 0 3755 // loud pre-check: every live segment must carry the term index 3756 var s: i64 = 0 3757 while s < ns { 3758 if h[6 + 8 * s] < 8 { return 0 - 2 } 3759 s = s + 1 3760 } 3761 let outs: *i64 = h[1 + 9 * ns] as *i64 3762 let pos: *i64 = h[2 + 9 * ns] as *i64 3763 s = ns - 1 3764 while s >= 0 { 3765 let tb: *u8 = h[5 + 8 * s] as *u8 3766 let pb: *u8 = h[7 + 8 * s] as *u8 3767 let db: i64 = h[3 + 8 * s] 3768 let lm: *u8 = h[1 + 8 * ns + s] as *u8 3769 let r: i64 = ss_terms_find_h(h, s, term, outs) // sampled window, same answer (2026-09-14) 3770 if r == 1 { 3771 pos[0] = outs[0] 3772 var prev: i64 = 0 3773 var k: i64 = 0 3774 while k < outs[2] { 3775 let d: i64 = prev + ss_vr(pb, pos) 3776 prev = d 3777 // currency check: O(1) via the handle's live-doc map (computed 3778 // once at open; identical semantics to the keyed re-verify) 3779 if lm[d] == (1 as u8) { 3780 if cnt < max { 3781 // INDEX-RESIDENT KEY (2026-09-14): .keys answers when its d2k row covers the doc 3782 if ss_key_of_doc(h, s, d, ((kpout as i64) + 8 * cnt) as *i64, ((klout as i64) + 8 * cnt) as *i64) == 0 { 3783 let eoff: i64 = ss_r32(pb, 8 + 4 * d) 3784 let ep: *u8 = (db + eoff) as *u8 3785 kpout[cnt] = db + eoff + 5 3786 klout[cnt] = ss_r32(ep, 1) 3787 } 3788 cnt = cnt + 1 3789 } 3790 } 3791 k = k + 1 3792 } 3793 } 3794 s = s - 1 3795 } 3796 return cnt 3797} 3798 3799// MULTI-TERM AND (IM2c): keys whose CURRENT record contains EVERY term. 3800// A key's current entry lives in exactly ONE segment, so the AND is computed 3801// per segment: every term's postings list (sorted ascending doc ids) is 3802// merge-intersected (O(sum of list lengths)), then survivors pass the O(1) 3803// live-doc currency check -- same current-state semantics as ss_term (stale 3804// versions and tombstones never answer). Returns match count into 3805// kpout/klout, or -2 when any live segment lacks a .terms index (fail LOUD, 3806// never a silent partial answer). 3807func ss_term_and(h: *i64, terms: *i64, nterms: i64, kpout: *i64, klout: *i64, max: i64) -> i64 { 3808 if nterms <= 0 { return 0 } 3809 let ns: i64 = h[0] 3810 var s: i64 = 0 3811 while s < ns { 3812 if h[6 + 8 * s] < 8 { return 0 - 2 } 3813 s = s + 1 3814 } 3815 if nterms == 1 { return ss_term(h, terms[0] as *u8, kpout, klout, max) } 3816 if nterms > 64 { return 0 - 3 } 3817 let outs: *i64 = h[1 + 9 * ns] as *i64 3818 let pos: *i64 = h[2 + 9 * ns] as *i64 3819 let poffs: *i64 = h[5 + 9 * ns] as *i64 3820 let pdcs: *i64 = h[6 + 9 * ns] as *i64 3821 var cnt: i64 = 0 3822 s = ns - 1 3823 while s >= 0 { 3824 let tb: *u8 = h[5 + 8 * s] as *u8 3825 let pb: *u8 = h[7 + 8 * s] as *u8 3826 let db: i64 = h[3 + 8 * s] 3827 let lm: *u8 = h[1 + 8 * ns + s] as *u8 3828 // every term must appear in THIS segment's dict or it contributes 0 3829 var allp: i64 = 1 3830 var t: i64 = 0 3831 while t < nterms { 3832 let r: i64 = ss_terms_find_h(h, s, terms[t] as *u8, outs) // sampled window (2026-09-14) 3833 if r == 1 { poffs[t] = outs[0]; pdcs[t] = outs[2] } else { allp = 0 } 3834 t = t + 1 3835 } 3836 if allp == 1 { 3837 let la: *i64 = h[3 + 9 * ns] as *i64 3838 let lb: *i64 = h[4 + 9 * ns] as *i64 3839 // decode term 0 into la 3840 pos[0] = poffs[0] 3841 var prev: i64 = 0 3842 var na: i64 = 0 3843 var k: i64 = 0 3844 while k < pdcs[0] { 3845 prev = prev + ss_vr(pb, pos) 3846 la[na] = prev 3847 na = na + 1 3848 k = k + 1 3849 } 3850 // streaming two-pointer intersect with each further term's list 3851 t = 1 3852 while t < nterms { 3853 if na == 0 { t = nterms } else { 3854 pos[0] = poffs[t] 3855 prev = 0 3856 var ai: i64 = 0 3857 var nb: i64 = 0 3858 k = 0 3859 while k < pdcs[t] { 3860 prev = prev + ss_vr(pb, pos) 3861 var adv: i64 = 1 3862 while adv == 1 { 3863 if ai >= na { adv = 0 } else { 3864 if la[ai] < prev { ai = ai + 1 } else { adv = 0 } 3865 } 3866 } 3867 if ai < na { if la[ai] == prev { lb[nb] = prev; nb = nb + 1; ai = ai + 1 } } 3868 k = k + 1 3869 } 3870 var c2: i64 = 0 3871 while c2 < nb { la[c2] = lb[c2]; c2 = c2 + 1 } 3872 na = nb 3873 t = t + 1 3874 } 3875 } 3876 // survivors: O(1) currency check, then emit key ptr/len 3877 var i2: i64 = 0 3878 while i2 < na { 3879 let d: i64 = la[i2] 3880 if lm[d] == (1 as u8) { 3881 let eoff: i64 = ss_r32(pb, 8 + 4 * d) 3882 let ep: *u8 = (db + eoff) as *u8 3883 if cnt < max { 3884 kpout[cnt] = db + eoff + 5 3885 klout[cnt] = ss_r32(ep, 1) 3886 cnt = cnt + 1 3887 } 3888 } 3889 i2 = i2 + 1 3890 } 3891 } 3892 s = s - 1 3893 } 3894 return cnt 3895} 3896 3897// ---- RANKING STATISTICS (additive, 2026-07-03: the BM25/IDF rung reads them) -------------------- 3898// n_t: how many docs carry `term`, summed from each live segment's .terms dcount (the statistic 3899// ss_build_terms already persists). Write-time counts: a shadowed older version still counts -- for 3900// IDF ranking that bias is negligible and the read is O(log terms) per segment, no posting decode. 3901// Same loud contract as ss_term: -2 when any live segment lacks a .terms index. 3902func ss_term_dcount(h: *i64, term: *u8) -> i64 { 3903 let ns: i64 = h[0] 3904 var s: i64 = 0 3905 while s < ns { 3906 if h[6 + 8 * s] < 8 { return 0 - 2 } 3907 s = s + 1 3908 } 3909 let outs: *i64 = h[1 + 9 * ns] as *i64 3910 var n: i64 = 0 3911 s = 0 3912 while s < ns { 3913 let r: i64 = ss_terms_find_h(h, s, term, outs) // sampled window (2026-09-14): idf lookups stop faulting the cold dictionary 3914 if r == 1 { n = n + outs[2] } 3915 s = s + 1 3916 } 3917 return n 3918} 3919// TERM-DICTIONARY ITERATION (additive, 2026-07-03: the typo/suggest rung reads the index's own sorted 3920// term dictionary -- the same .terms blobs the postings live in; no separate dictionary artifact). 3921// ss_term_count(h, s) = how many terms segment s indexes; ss_term_at fills term ptr/len + dcount for 3922// entry idx (terms are sorted within a segment). Returns 1 ok / 0 out-of-range or absent index. 3923func ss_term_count(h: *i64, s: i64) -> i64 { 3924 if s < 0 { return 0 } 3925 if s >= h[0] { return 0 } 3926 let tb: *u8 = h[5 + 8 * s] as *u8 3927 if h[6 + 8 * s] < 8 { return 0 } 3928 return ss_r32(tb, 4) 3929} 3930func ss_term_at(h: *i64, s: i64, idx: i64, tp_out: *i64, tl_out: *i64, dc_out: *i64) -> i64 { 3931 if s < 0 { return 0 } 3932 if s >= h[0] { return 0 } 3933 let tb: *u8 = h[5 + 8 * s] as *u8 3934 let tsz: i64 = h[6 + 8 * s] 3935 if tsz < 8 { return 0 } 3936 let n: i64 = ss_r32(tb, 4) 3937 if idx < 0 { return 0 } 3938 if idx >= n { return 0 } 3939 let base: i64 = 8 + 4 * n 3940 let eo: i64 = base + ss_r32(tb, 8 + 4 * idx) 3941 let tl: i64 = ss_r32(tb, eo) 3942 tp_out[0] = (tb as i64) + eo + 4 3943 tl_out[0] = tl 3944 dc_out[0] = ss_r32(tb, eo + 4 + tl + 8) 3945 return 1 3946} 3947// PREFIX LOWER BOUND over a segment's SORTED term dictionary -> the ordinal of the first term whose 3948// first `pfxlen` bytes are >= pfx[0..pfxlen). The dictionary is sorted BY CONSTRUCTION (ss_build_terms: 3949// "term dict ordering = bottom-up merge sort"), and ss_terms_find already binary-searches it for an 3950// EXACT key -- this is that same search generalised to a PREFIX, so a caller can walk only the matching 3951// run instead of the whole dictionary. Returns n when no term reaches the prefix (the caller's 3952// `while e < tc` then simply does not execute). pfxlen <= 0 returns 0 = "start at the beginning": an 3953// empty prefix selects the whole dictionary, which is exactly what a full scan would have done. 3954func ss_term_lb(h: *i64, s: i64, pfx: *u8, pfxlen: i64) -> i64 { 3955 if s < 0 { return 0 } 3956 if s >= h[0] { return 0 } 3957 let tb: *u8 = h[5 + 8 * s] as *u8 3958 let tsz: i64 = h[6 + 8 * s] 3959 if tsz < 8 { return 0 } 3960 let n: i64 = ss_r32(tb, 4) 3961 if pfxlen <= 0 { return 0 } 3962 let base: i64 = 8 + 4 * n 3963 // SAMPLED WINDOW (2026-09-14): the first term >= the prefix lies between the sample just below the first 3964 // sample >= the zero-padded prefix and the first sample above it (a term whose bytes are below the prefix 3965 // packs below it; one at or above packs at or above it), so the search starts there instead of at [0, n). 3966 if (sst_lohi as i64) == 0 { sst_lohi = sys_mmap(16) as *i64 } 3967 ss_tsample_window(h, s, ss_tpack(pfx, pfxlen), n, sst_lohi) 3968 var lo: i64 = sst_lohi[0] 3969 var hi: i64 = sst_lohi[1] 3970 while lo < hi { 3971 let mid: i64 = (lo + hi) / 2 3972 let eo: i64 = base + ss_r32(tb, 8 + 4 * mid) 3973 let tl: i64 = ss_r32(tb, eo) 3974 let tp: *u8 = (tb as i64 + eo + 4) as *u8 3975 // c = sign of compare(term[0..min(tl,pfxlen)), pfx[0..pfxlen)); a term SHORTER than the prefix 3976 // sorts below it. The exit uses a separate `done` flag and NEVER the cursor `k`, so the loop 3977 // cannot destroy the position it is reading. 3978 var c: i64 = 0 3979 var k: i64 = 0 3980 var done: i64 = 0 3981 while done == 0 { 3982 if k >= pfxlen { done = 1 } else { 3983 if k >= tl { c = 0 - 1; done = 1 } else { 3984 let av: i64 = tp[k] as i64 3985 let bv: i64 = pfx[k] as i64 3986 if av < bv { c = 0 - 1; done = 1 } else { 3987 if av > bv { c = 1; done = 1 } else { k = k + 1 } 3988 } 3989 } 3990 } 3991 } 3992 if c < 0 { lo = mid + 1 } else { hi = mid } 3993 } 3994 return lo 3995} 3996 3997// N: how many "doc:"-prefixed put-keys the live segments' key indexes hold (each .keys entry is its 3998// key's latest-in-segment; cross-segment re-adds are prevented upstream by skip-if-present ingest, so 3999// this is the corpus size for IDF at O(total keys) once per query -- exactness via compaction later). 4000func ss_doc_count_walk(h: *i64) -> i64 { 4001 let ns: i64 = h[0] 4002 var n: i64 = 0 4003 var s: i64 = 0 4004 while s < ns { 4005 let kb: *u8 = h[1 + 8 * s] as *u8 4006 if h[2 + 8 * s] >= 8 { 4007 let m9: i64 = ss_r32(kb, 4) 4008 var e9: i64 = 0 4009 while e9 < m9 { 4010 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9) 4011 if (kb[eo] as i64) == 1 { 4012 let kl9: i64 = ss_r32(kb, eo + 1) 4013 if kl9 >= 4 { 4014 if kb[eo + 5] == (100 as u8) { if kb[eo + 6] == (111 as u8) { if kb[eo + 7] == (99 as u8) { if kb[eo + 8] == (58 as u8) { 4015 n = n + 1 4016 } } } } 4017 } 4018 } 4019 e9 = e9 + 1 4020 } 4021 } 4022 s = s + 1 4023 } 4024 return n 4025} 4026// ss_doc_count: the corpus size for idf. FAST PATH (search L5, 2026-09-14): every row carries its own doc-key count 4027// from load time (ss_doccount_build, aux slot SS_AUX_DOCN), so this sums ns words; if any row lacks one (a handle 4028// built by an older path) it falls back to the walk above, so the number is the same either way. PROVEN on the 4029// live shard by nx_doccount_gate: sum == walk, a poisoned row moves the sum by exactly its poison while the walk 4030// does not, and a cleared row still answers the walk's number through the fallback. 4031func ss_doc_count(h: *i64) -> i64 { 4032 let ns: i64 = h[0] 4033 var sum: i64 = 0 4034 var all: i64 = 1 4035 var sq: i64 = 0 4036 while sq < ns { 4037 let r: *i64 = ss_aux_row(h, sq) 4038 if (r as i64) == 0 { all = 0 } else { if r[SS_AUX_DOCN] <= 0 { all = 0 } else { sum = sum + (r[SS_AUX_DOCN] - 1) } } 4039 sq = sq + 1 4040 } 4041 if all == 1 { return sum } 4042 return ss_doc_count_walk(h) 4043} 4044 4045// ---- PHRASE SUPPORT (2026-07-03, additive): the NXQ1 positions sidecar readers + the evaluator ---- 4046// like ss_terms_find but returns the term's SORTED ORDINAL (the NXQ1 entry index) instead of just 1 4047func ss_terms_ordinal(tb: *u8, tsz: i64, term: *u8, outs: *i64) -> i64 { 4048 if tsz < 8 { return 0 - 2 } 4049 if tb[0] != (78 as u8) { return 0 - 2 } 4050 if tb[2] != (84 as u8) { return 0 - 2 } 4051 let n: i64 = ss_r32(tb, 4) 4052 let base: i64 = 8 + 4 * n 4053 let tl0: i64 = ss_len(term) 4054 var lo: i64 = 0 4055 var hi: i64 = n - 1 4056 while lo <= hi { 4057 let mid: i64 = (lo + hi) / 2 4058 let eo: i64 = base + ss_r32(tb, 8 + 4 * mid) 4059 let tl: i64 = ss_r32(tb, eo) 4060 let c: i64 = ss_kcmp((tb as i64 + eo + 4) as *u8, tl, term, tl0) 4061 if c == 0 { 4062 outs[0] = ss_r32(tb, eo + 4 + tl) 4063 outs[1] = ss_r32(tb, eo + 4 + tl + 4) 4064 outs[2] = ss_r32(tb, eo + 4 + tl + 8) 4065 return mid 4066 } 4067 if c < 0 { lo = mid + 1 } 4068 if c > 0 { hi = mid - 1 } 4069 } 4070 return 0 - 1 4071} 4072// decode the position run for posting-rank `rank` of term-ordinal `ord` from an NXQ1 blob. 4073// Returns npos decoded into posout (cap maxpos; longer runs are truncated -- adjacency over the first 4074// maxpos occurrences), or -1 malformed/absent. 4075func ss_q_positions(qb: *u8, qsz: i64, ord: i64, rank: i64, posout: *i64, maxpos: i64) -> i64 { 4076 if qsz < 8 { return 0 - 1 } 4077 if qb[0] != (78 as u8) { return 0 - 1 } 4078 if qb[2] != (81 as u8) { return 0 - 1 } 4079 let nt: i64 = ss_r32(qb, 4) 4080 if ord < 0 { return 0 - 1 } 4081 if ord >= nt { return 0 - 1 } 4082 let qbase: i64 = 8 + 4 * nt 4083 let pos: *i64 = sys_mmap(16) as *i64 4084 pos[0] = qbase + ss_r32(qb, 8 + 4 * ord) 4085 // skip `rank` runs 4086 var r: i64 = 0 4087 while r < rank { 4088 let n0: i64 = ss_vr(qb, pos) 4089 var k0: i64 = 0 4090 while k0 < n0 { ss_vr(qb, pos); k0 = k0 + 1 } 4091 r = r + 1 4092 } 4093 let np: i64 = ss_vr(qb, pos) 4094 var cum: i64 = 0 4095 var w: i64 = 0 4096 var k: i64 = 0 4097 while k < np { 4098 cum = cum + ss_vr(qb, pos) 4099 if w < maxpos { posout[w] = cum; w = w + 1 } 4100 k = k + 1 4101 } 4102 return w 4103} 4104// IMPACT-ORDERED TERM SEARCH (2026-07-25, the WAND rung): like ss_term but when the term has more 4105// postings than `max`, the cap keeps the HIGHEST-tf postings across all live segments instead of the 4106// first `max` in (segment, doc-asc) order -- and each hit's tf is returned (tfout) so candidacy needs 4107// zero doc reads. Cost is O(segments * SS_IMP_K), NOT O(total postings): the full-list decode ss_term 4108// pays per query disappears. Requires EVERY live segment to carry .imp: returns -3 when any lacks it 4109// and the caller falls back to ss_term (exact legacy behavior; compaction / nx_seg_imp_build upgrade a 4110// shard in place). -2 = missing .terms (same loud contract as ss_term). Currency semantics identical: 4111// the per-segment live-doc map filters stale versions and tombstones. Ties in tf resolve newest- 4112// segment-first then ascending doc -- deterministic. 4113// satout[0] (1-slot): set to 1 iff the answer is TRUNCATED -- a segment's stored impact list was 4114// build-capped at SS_IMP_K, the cross-segment collect filled, or the emit dropped past `max`. This is 4115// the reader's OWN truncation knowledge: exact by construction, immune to the shadow-inflated 4116// write-time dcount statistic (a re-committed doc inflates dcnt but never this flag). 4117// ADDITIVE 2026-08-06 -- ss_term_top with an optional PER-SEGMENT SKIP MASK. mask[s]==0 means the 4118// caller has already PROVEN that segment s cannot contain this term (e.g. from a per-segment term 4119// bloom it owns and has verified sound against ground truth), so the segment is skipped ENTIRELY: 4120// its .imp is never opened and its terms block is never touched. 4121// 4122// WHY THIS IS THE HOT PATH: the loop below opens <prefix><segid>.imp for EVERY live segment BEFORE 4123// any term lookup happens -- 900 file opens per query on the web shard, whether or not the term 4124// exists anywhere in it. MEASURED 2026-08-06: that up-front load is the ~6.6s floor that makes a 4125// query matching ZERO documents cost the same as one matching 28,000 (a 42ms control on the same 4126// daemon proves the cost is here, not in transport, dispatch or ranking). 4127// 4128// ORDERING SEMANTICS ARE PRESERVED, NOT TRADED AWAY. The all-or-nothing .imp check exists so a 4129// half-upgraded shard cannot serve inconsistent ordering. A segment that provably lacks the term 4130// contributes NOTHING to this query's candidate set, so its .imp can affect neither the results nor 4131// their order -- skipping it is sound for exactly the reason the mask is. A bloom may return a false 4132// POSITIVE (costing one wasted probe) but NEVER a false negative, so no candidate is ever lost. 4133// 4134// The store deliberately learns NOTHING about blooms: importing a filter module HERE would make 4135// every consumer of nx_seg_store depend on it -- the trap nx_hostctl_keepbackoff_gate documents 4136// ("would leave the supervisor UNBUILDABLE on any host that has not yet received that module"). 4137// The caller owns the filter and hands down an opaque byte mask. 4138// mask == 0 => probe every segment == ss_term_top's exact historical behaviour. 4139func ss_term_top_mask(prefix: *u8, h: *i64, mask: *u8, term: *u8, kpout: *i64, klout: *i64, tfout: *i64, max: i64, satout: *i64) -> i64 { 4140 satout[0] = 0 4141 let ns: i64 = h[0] 4142 var s0: i64 = 0 4143 while s0 < ns { 4144 if h[6 + 8 * s0] < 8 { return 0 - 2 } 4145 s0 = s0 + 1 4146 } 4147 if max <= 0 { return 0 } 4148 let sp2: *i64 = sys_mmap(8) as *i64 4149 let ns2: i64 = ss_manifest_dyn(prefix, sp2) 4150 let segs: *i64 = sp2[0] as *i64 4151 var nseg: i64 = ns 4152 if ns2 < nseg { nseg = ns2 } 4153 if nseg <= 0 { return 0 } 4154 // load every live segment's .imp up front: ANY absent -> -3 (all-or-nothing keeps the ordering 4155 // semantics whole; a half-upgraded shard serves exactly like a non-upgraded one) 4156 let ibs: *i64 = sys_mmap(8 * (nseg + 4)) as *i64 4157 let iszs: *i64 = sys_mmap(8 * (nseg + 4)) as *i64 4158 let np9: *u8 = sys_mmap(512) 4159 let szp9: *i64 = sys_mmap(16) as *i64 4160 var s: i64 = 0 4161 while s < nseg { 4162 var o9: i64 = 0 4163 o9 = ss_cat(np9, o9, prefix) 4164 o9 = ss_cat(np9, o9, segs[s] as *u8) 4165 o9 = ss_cat(np9, o9, ".imp" as *u8) 4166 np9[o9] = 0 as u8 4167 szp9[0] = 0 4168 // MASK GATE: a segment the caller proved cannot hold this term never gets its .imp opened. 4169 // This is the 900-file-opens-per-query cost, and it is paid BEFORE any term lookup. 4170 // LAZY (2026-08-06): do NOT load .imp here at all. It is READ ONLY inside `od >= 0` below -- 4171 // i.e. only for a segment that actually holds the term -- so loading all 900 up front is pure 4172 // waste, and it IS the measured ~6.6s floor: a query matching ZERO documents paid 900 file 4173 // opens to read none of them. The load moves down to its single consumer. 4174 var want9: i64 = 0 4175 var ib: *u8 = 0 as *u8 4176 var ok9: i64 = 0 4177 if (ib as i64) != 0 { if szp9[0] >= 8 { if ib[0] == (78 as u8) { if ib[2] == (87 as u8) { ok9 = 1 } } } } 4178 // A MASKED-OUT SEGMENT IS NOT A MISSING .imp. The -3 refusal below means "this shard is half 4179 // upgraded, degrade the whole query"; a segment we deliberately skipped has not been examined 4180 // and must not be reported as broken, or the mask would turn every fast query into a refusal. 4181 if want9 == 0 { ok9 = 1 } 4182 if ok9 == 0 { return 0 - 3 } 4183 ibs[s] = ib as i64 4184 iszs[s] = szp9[0] 4185 s = s + 1 4186 } 4187 // collect live (segment, docidx, tf) from each impact list, newest segment first 4188 let cap: i64 = nseg * SS_IMP_K + 16 4189 let colls: *i64 = sys_mmap(8 * cap) as *i64 4190 let colld: *i64 = sys_mmap(8 * cap) as *i64 4191 let collt: *i64 = sys_mmap(8 * cap) as *i64 4192 var n: i64 = 0 4193 let outs: *i64 = sys_mmap(32) as *i64 4194 let vp: *i64 = sys_mmap(16) as *i64 4195 // PREFETCH PASS (2026-09-14, from the search phase timers: stage 1 read 0.2-0.5 s cold once its key reads 4196 // were index-resident, one cold impact-block fault per (segment, term) served in series). Resolve the term 4197 // in every segment first, map its .imp and hand the kernel the term's whole impact block, so the collect 4198 // below reads warm and the disk services all segments together. Advisory: the collect is unchanged. 4199 var sq: i64 = nseg - 1 4200 while sq >= 0 { 4201 var useq: i64 = 1 4202 if (mask as i64) != 0 { if mask[sq] == (0 as u8) { useq = 0 } } 4203 var odq: i64 = 0 - 1 4204 if useq == 1 { odq = ss_terms_ordinal_h(h, sq, term, outs) } 4205 if odq >= 0 { 4206 if ibs[sq] == 0 { 4207 var oq: i64 = 0 4208 oq = ss_cat(np9, oq, prefix) 4209 oq = ss_cat(np9, oq, segs[sq] as *u8) 4210 oq = ss_cat(np9, oq, ".imp" as *u8) 4211 np9[oq] = 0 as u8 4212 szp9[0] = 0 4213 let ibq: *u8 = ss_loadfile(np9, szp9, 1) 4214 var okq: i64 = 0 4215 if (ibq as i64) != 0 { if szp9[0] >= 8 { if ibq[0] == (78 as u8) { if ibq[2] == (87 as u8) { okq = 1 } } } } 4216 if okq == 1 { ibs[sq] = ibq as i64; iszs[sq] = szp9[0] } 4217 } 4218 if ibs[sq] != 0 { 4219 let ibq2: *u8 = ibs[sq] as *u8 4220 let ntq: i64 = ss_r32(ibq2, 4) 4221 if odq < ntq { 4222 let ibaseq: i64 = 8 + 4 * ntq 4223 let bsq: i64 = ibaseq + ss_r32(ibq2, 8 + 4 * odq) 4224 var beq: i64 = iszs[sq] 4225 if odq + 1 < ntq { beq = ibaseq + ss_r32(ibq2, 8 + 4 * (odq + 1)) } 4226 if beq > iszs[sq] { beq = iszs[sq] } 4227 if beq > bsq { 4228 let baq: i64 = (ibq2 as i64) + bsq 4229 let balq: i64 = (baq / SS_MAGIC_4096) * SS_MAGIC_4096 4230 sys_madvise(balq as *u8, (baq - balq) + (beq - bsq), SS_MADV_WILLNEED) 4231 } 4232 } 4233 } 4234 } 4235 sq = sq - 1 4236 } 4237 s = nseg - 1 4238 while s >= 0 { 4239 let tb: *u8 = h[5 + 8 * s] as *u8 4240 let lm: *u8 = h[1 + 8 * ns + s] as *u8 4241 // MASK GATE 2: skip the terms-block probe too. Together with the .imp gate above, a masked 4242 // segment costs ZERO page faults instead of one file open plus one terms-block fault. 4243 var od: i64 = 0 - 1 4244 var use9: i64 = 1 4245 if (mask as i64) != 0 { if mask[s] == (0 as u8) { use9 = 0 } } 4246 if use9 == 1 { od = ss_terms_ordinal_h(h, s, term, outs) } // sampled window, same answer (2026-09-14) 4247 if od >= 0 { 4248 // LAZY .imp LOAD -- the single consumer. Only a segment that ACTUALLY holds the term 4249 // needs its impact list, and -3 (shard not .imp-upgraded) still fires here, where it is 4250 // load-bearing: a segment that cannot contribute a candidate cannot affect ordering, so 4251 // never examining it is sound. Loaded once per segment per call, then cached in ibs[]. 4252 if ibs[s] == 0 { 4253 var oL: i64 = 0 4254 oL = ss_cat(np9, oL, prefix) 4255 oL = ss_cat(np9, oL, segs[s] as *u8) 4256 oL = ss_cat(np9, oL, ".imp" as *u8) 4257 np9[oL] = 0 as u8 4258 szp9[0] = 0 4259 let ibL: *u8 = ss_loadfile(np9, szp9, 1) 4260 var okL: i64 = 0 4261 if (ibL as i64) != 0 { if szp9[0] >= 8 { if ibL[0] == (78 as u8) { if ibL[2] == (87 as u8) { okL = 1 } } } } 4262 if okL == 0 { return 0 - 3 } 4263 ibs[s] = ibL as i64 4264 iszs[s] = szp9[0] 4265 } 4266 let ib2: *u8 = ibs[s] as *u8 4267 let nt9: i64 = ss_r32(ib2, 4) 4268 if od < nt9 { 4269 let ibase: i64 = 8 + 4 * nt9 4270 vp[0] = ibase + ss_r32(ib2, 8 + 4 * od) 4271 let k9: i64 = ss_vr(ib2, vp) 4272 if k9 >= SS_IMP_K { satout[0] = 1 } 4273 var i9: i64 = 0 4274 while i9 < k9 { 4275 if vp[0] > iszs[s] { i9 = k9 } else { 4276 let d9: i64 = ss_vr(ib2, vp) 4277 let f9: i64 = ss_vr(ib2, vp) 4278 if lm[d9] == (1 as u8) { 4279 if n < cap { colls[n] = s; colld[n] = d9; collt[n] = f9; n = n + 1 } else { satout[0] = 1 } 4280 } 4281 i9 = i9 + 1 4282 } 4283 } 4284 } 4285 } 4286 s = s - 1 4287 } 4288 if n == 0 { return 0 } 4289 // STRICT tf-DESCENDING emit via counting sort over the clamped-tf buckets: O(n + 1024), stable 4290 // within a bucket (collect order = newest segment first, list order within = ascending doc), so 4291 // ties are deterministic. tf >= 1023 collapses into the top bucket -- ordering granularity above 4292 // that is irrelevant to candidacy and the STORED tf stays true. A future WAND early-termination 4293 // reader can rely on this prefix-is-best contract. 4294 let hist2: *i64 = sys_mmap(8 * SS_MAGIC_1024) as *i64 4295 var ih: i64 = 0 4296 while ih < n { 4297 var cv: i64 = collt[ih] 4298 if cv > 1023 { cv = 1023 } 4299 hist2[cv] = hist2[cv] + 1 4300 ih = ih + 1 4301 } 4302 let boff: *i64 = sys_mmap(8 * SS_MAGIC_1024) as *i64 4303 var acc2: i64 = 0 4304 var bb: i64 = 1023 4305 while bb >= 0 { 4306 boff[bb] = acc2 4307 acc2 = acc2 + hist2[bb] 4308 bb = bb - 1 4309 } 4310 let ord: *i64 = sys_mmap(8 * (n + 4)) as *i64 4311 ih = 0 4312 while ih < n { 4313 var cv2: i64 = collt[ih] 4314 if cv2 > 1023 { cv2 = 1023 } 4315 ord[boff[cv2]] = ih 4316 boff[cv2] = boff[cv2] + 1 4317 ih = ih + 1 4318 } 4319 var k2: i64 = n 4320 if k2 > max { k2 = max; satout[0] = 1 } 4321 var cnt: i64 = 0 4322 while cnt < k2 { 4323 let i2: i64 = ord[cnt] 4324 let sx: i64 = colls[i2] 4325 let pb: *u8 = h[7 + 8 * sx] as *u8 4326 let db: i64 = h[3 + 8 * sx] 4327 // INDEX-RESIDENT KEY (2026-09-14): the .keys slice answers the key; only a segment without its d2k row 4328 // (or a doc the row does not cover) reads the .docs entry as before. Same bytes either way. 4329 if ss_key_of_doc(h, sx, colld[i2], ((kpout as i64) + 8 * cnt) as *i64, ((klout as i64) + 8 * cnt) as *i64) == 0 { 4330 let eoff: i64 = ss_r32(pb, 8 + 4 * colld[i2]) 4331 let ep: *u8 = (db + eoff) as *u8 4332 kpout[cnt] = db + eoff + 5 4333 klout[cnt] = ss_r32(ep, 1) 4334 } 4335 tfout[cnt] = collt[i2] 4336 cnt = cnt + 1 4337 } 4338 return cnt 4339} 4340 4341// BACK-COMPAT WRAPPER: every existing caller keeps its exact signature and its exact behaviour. 4342// mask == 0 makes both gates above no-ops, so this is the pre-2026-08-06 function byte-for-byte in 4343// effect. Rule 19: add the capability, never break the contract. 4344func ss_term_top(prefix: *u8, h: *i64, term: *u8, kpout: *i64, klout: *i64, tfout: *i64, max: i64, satout: *i64) -> i64 { 4345 return ss_term_top_mask(prefix, h, 0 as *u8, term, kpout, klout, tfout, max, satout) 4346} 4347 4348// THE PHRASE EVALUATOR: docs whose CURRENT text contains terms[0..nterms) as ADJACENT tokens, in order. 4349// Candidates come from the per-segment postings intersect (AND); adjacency is checked in the NXQ1 4350// sidecar. A segment WITHOUT a sidecar (pre-phrase format) contributes its AND matches and clears 4351// exactout[0] -- graceful degrade, never a refusal; compaction upgrades it. -2 = missing .terms (loud, 4352// same contract as ss_term). exactout[0]: 1 = adjacency enforced everywhere, 0 = degraded somewhere. 4353func ss_phrase(prefix: *u8, h: *i64, terms: *i64, nterms: i64, kpout: *i64, klout: *i64, max: i64, exactout: *i64) -> i64 { 4354 exactout[0] = 1 4355 if nterms < 2 { return 0 } 4356 if nterms > 8 { return 0 - 3 } 4357 let ns: i64 = h[0] 4358 var s0: i64 = 0 4359 while s0 < ns { 4360 if h[6 + 8 * s0] < 8 { return 0 - 2 } 4361 s0 = s0 + 1 4362 } 4363 let sp2: *i64 = sys_mmap(8) as *i64 4364 let ns2: i64 = ss_manifest_dyn(prefix, sp2) 4365 let segs: *i64 = sp2[0] as *i64 4366 var nseg: i64 = ns 4367 if ns2 < nseg { nseg = ns2 } 4368 let ords: *i64 = sys_mmap(8 * 8) as *i64 4369 let outs: *i64 = sys_mmap(32) as *i64 4370 let poffs: *i64 = sys_mmap(8 * 8) as *i64 4371 let pdcs: *i64 = sys_mmap(8 * 8) as *i64 4372 let p1: *i64 = sys_mmap(8 * 520) as *i64 4373 let p2: *i64 = sys_mmap(8 * 520) as *i64 4374 let qszp: *i64 = sys_mmap(16) as *i64 4375 let vpos: *i64 = sys_mmap(16) as *i64 4376 var cnt: i64 = 0 4377 var s: i64 = 0 4378 while s < nseg { 4379 let tb: *u8 = h[5 + 8 * s] as *u8 4380 let pb: *u8 = h[7 + 8 * s] as *u8 4381 let db: i64 = h[3 + 8 * s] 4382 let lm: *u8 = h[1 + 8 * ns + s] as *u8 4383 // every phrase term must exist in THIS segment's dict 4384 var allp: i64 = 1 4385 var t: i64 = 0 4386 while t < nterms { 4387 let od: i64 = ss_terms_ordinal_h(h, s, terms[t] as *u8, outs) // sampled window (2026-09-14) 4388 if od < 0 { allp = 0 } else { 4389 ords[t] = od 4390 poffs[t] = outs[0] 4391 pdcs[t] = outs[2] 4392 } 4393 t = t + 1 4394 } 4395 if allp == 1 { 4396 // decode each term's posting doc list (ascending) 4397 let dlist: *i64 = sys_mmap(8 * 8) as *i64 4398 var t2: i64 = 0 4399 while t2 < nterms { 4400 let arr: *i64 = sys_mmap(8 * (pdcs[t2] + 4)) as *i64 4401 vpos[0] = poffs[t2] 4402 var prev: i64 = 0 4403 var k: i64 = 0 4404 while k < pdcs[t2] { 4405 prev = prev + ss_vr(pb, vpos) 4406 arr[k] = prev 4407 k = k + 1 4408 } 4409 dlist[t2] = arr as i64 4410 t2 = t2 + 1 4411 } 4412 // the segment's positions sidecar (absent -> degrade this segment to AND) 4413 let qp: *u8 = sys_mmap(512) 4414 var qo2: i64 = 0 4415 qo2 = ss_cat(qp, qo2, prefix) 4416 qo2 = ss_cat(qp, qo2, segs[s] as *u8) 4417 qo2 = ss_cat(qp, qo2, ".pos" as *u8) 4418 qp[qo2] = 0 as u8 4419 qszp[0] = 0 4420 let qb: *u8 = ss_readall(qp, qszp) 4421 var haveq: i64 = 0 4422 if (qb as i64) != 0 { if qszp[0] >= 8 { haveq = 1 } } 4423 if haveq == 0 { exactout[0] = 0 } 4424 // walk term0's docs; a doc survives iff present in EVERY list (ranks captured for NXQ1) 4425 let l0: *i64 = dlist[0] as *i64 4426 var i0: i64 = 0 4427 while i0 < pdcs[0] { 4428 let d: i64 = l0[i0] 4429 var inall: i64 = 1 4430 let ranks: *i64 = sys_mmap(8 * 8) as *i64 4431 ranks[0] = i0 4432 var t3: i64 = 1 4433 while t3 < nterms { 4434 let lt: *i64 = dlist[t3] as *i64 4435 var lo2: i64 = 0 4436 var hi2: i64 = pdcs[t3] - 1 4437 var fnd: i64 = 0 - 1 4438 while lo2 <= hi2 { 4439 let mid2: i64 = (lo2 + hi2) / 2 4440 if lt[mid2] == d { fnd = mid2; lo2 = hi2 + 1 } else { 4441 if lt[mid2] < d { lo2 = mid2 + 1 } else { hi2 = mid2 - 1 } 4442 } 4443 } 4444 if fnd < 0 { inall = 0; t3 = nterms } else { ranks[t3] = fnd } 4445 t3 = t3 + 1 4446 } 4447 if inall == 1 { if lm[d] == (1 as u8) { 4448 var matched: i64 = 1 4449 if haveq == 1 { 4450 // adjacency ladder over the sidecar positions 4451 var na: i64 = ss_q_positions(qb, qszp[0], ords[0], ranks[0], p1, 512) 4452 var t4: i64 = 1 4453 while t4 < nterms { 4454 if na <= 0 { t4 = nterms } else { 4455 let nb: i64 = ss_q_positions(qb, qszp[0], ords[t4], ranks[t4], p2, 512) 4456 // S = { q in positions(t4) : q-1 in S_prev } (two-pointer, both ascending) 4457 var wa: i64 = 0 4458 var ia: i64 = 0 4459 var ib: i64 = 0 4460 while ia < na { 4461 if ib >= nb { ia = na } else { 4462 let want: i64 = p1[ia] + 1 4463 if p2[ib] == want { p1[wa] = want; wa = wa + 1; ia = ia + 1; ib = ib + 1 } else { 4464 if p2[ib] < want { ib = ib + 1 } else { ia = ia + 1 } 4465 } 4466 } 4467 } 4468 na = wa 4469 t4 = t4 + 1 4470 } 4471 } 4472 if na <= 0 { matched = 0 } 4473 } 4474 if matched == 1 { 4475 let eoff: i64 = ss_r32(pb, 8 + 4 * d) 4476 let ep: *u8 = (db + eoff) as *u8 4477 if cnt < max { 4478 kpout[cnt] = db + eoff + 5 4479 klout[cnt] = ss_r32(ep, 1) 4480 cnt = cnt + 1 4481 } 4482 } 4483 } } 4484 i0 = i0 + 1 4485 } 4486 } 4487 s = s + 1 4488 } 4489 return cnt 4490} 4491 4492// Indexed get (IM2): newest segment -> oldest, binary search each .keys; 4493// first hit decides (identical semantics to the chronological scan's 4494// last-match -- the gate proves equivalence against the scan as ORACLE). 4495// Any missing/malformed index => honest fallback to the scan path. 4496// SCRATCH ALLOCATED ONCE (2026-07-30, same class as the ss_hget fix above). ss_get_idx allocated FIVE 4497// page-granular scratch buffers PER CALL (sp/vo/vl/path/szp) and never freed them -- ~20 KB per call 4498// across 30 call sites. Statics are safe here for the same reason as ss_hget: these are pure scratch, 4499// written then read with no intervening call that re-enters ss_get_idx (VERIFIED: neither ss_get nor 4500// ss_idx_lookup calls back into it). 4501// âš DELIBERATELY NOT CACHED: the `b` buffer below (ss_readall of the whole .docs file) is what ptrout 4502// POINTS INTO, so it is returned data, not scratch -- caching or freeing it is the seq1356 4503// borrowed-pointer question and must not be done casually. Filed, not guessed at. 4504// âš ALSO STILL OPEN: this function re-reads the MANIFEST and re-maps a whole segment file on EVERY 4505// call -- the O(rows x segments) amplification seq356 already identified. The real remedy is the one 4506// sts_load adopted: ss_open ONCE + ss_hget per row. These statics only stop the bleeding. 4507static ssg_sp: *i64 4508static ssg_vo: *i64 4509static ssg_vl: *i64 4510func ss_get_idx(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 4511 return ss_get_idx_mode(prefix,key,ptrout,lenout,1) 4512} 4513// Authorization callers cannot interpret a failed index read as evidence of absence. 4514// Keep scan compatibility explicit for existing readers; strict callers receive -2. 4515func ss_get_idx_required(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 4516 return ss_get_idx_mode(prefix,key,ptrout,lenout,0) 4517} 4518func ss_get_idx_mode(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64, allow_scan: i64) -> i64 { 4519 if (ssg_sp as i64) == 0 { ssg_sp = sys_mmap(8) as *i64 } 4520 let sp: *i64 = ssg_sp 4521 let ns: i64 = ss_manifest_dyn(prefix, sp) 4522 let segs: *i64 = sp[0] as *i64 4523 if ns == 0 { return 0 - 1 } 4524 if (ssg_vo as i64) == 0 { ssg_vo = sys_mmap(16) as *i64 } 4525 if (ssg_vl as i64) == 0 { ssg_vl = sys_mmap(16) as *i64 } 4526 let vo: *i64 = ssg_vo 4527 let vl: *i64 = ssg_vl 4528 var s: i64 = ns - 1 4529 while s >= 0 { 4530 let r: i64 = ss_idx_lookup(prefix, segs[s] as *u8, key, vo, vl) 4531 if r == (0 - 2) { 4532 if allow_scan == 1 { return ss_get(prefix,key,ptrout,lenout) } 4533 return 0 - 2 4534 } 4535 if r == 2 { return 0 } 4536 if r == 1 { 4537 let path: *u8 = sys_mmap(512) 4538 var o: i64 = 0 4539 o = ss_cat(path, o, prefix) 4540 o = ss_cat(path, o, segs[s] as *u8) 4541 o = ss_cat(path, o, ".docs" as *u8) 4542 path[o] = 0 as u8 4543 let szp: *i64 = sys_mmap(16) as *i64 4544 let b: *u8 = ss_readall(path, szp) 4545 if szp[0] < vo[0] + vl[0] { return 0 - 2 } 4546 ptrout[0] = (b as i64) + vo[0] 4547 lenout[0] = vl[0] 4548 return 1 4549 } 4550 s = s - 1 4551 } 4552 return 0 - 1 4553} 4554 4555// latest state of key: 1=found (ptrout[0]/lenout[0] set), 0=tombstoned, -1=absent 4556func ss_get(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 4557 let kinds: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 4558 let ptrs: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 4559 let lens: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 4560 let n: i64 = ss_scan(prefix, key, kinds, ptrs, lens) 4561 if n == 0 { return 0 - 1 } 4562 let last: i64 = n - 1 4563 if kinds[last] == 2 { return 0 } 4564 ptrout[0] = ptrs[last] 4565 lenout[0] = lens[last] 4566 return 1 4567} 4568 4569// ===== SEQUENTIAL CURSOR (added 2026-07-30) ================================ 4570// THE DEFECT THIS KILLS: ss_get is a POINT lookup costing O(WHOLE-STORE BYTES) 4571// every call -- ss_scan -> ss_scan_seglist ss_readall()s the ENTIRE <seg>.docs 4572// of EVERY live segment per call and returns a pointer INTO that fresh mapping 4573// (so it can never be freed while in use). A whole-store pass built from point 4574// lookups is quadratic in time AND unbounded in mapped memory. 4575// MEASURED on the mvault store (37,058 records / 3 segments): 200 lookups 4576// survive, 10,000 DIES (rc=1, zero output). Read-side twin of the eaten 4577// seg-store quadratic WRITE defect. 4578// COST: one mapping per LIVE SEGMENT, not one per RECORD. 4579// CONTRACT: kout/vout point INTO the current mapping and are valid only until 4580// the NEXT ss_cur_next call. Records arrive chronologically, so for a key with 4581// several versions the LAST seen is current -- the same rule ss_get applies. 4582 4583const SS_CUR_SLOTS: i64 = 8 4584 4585func ss_cur_open(prefix: *u8) -> *i64 { 4586 let st: *i64 = sys_mmap(8 * SS_CUR_SLOTS) as *i64 4587 let sp: *i64 = sys_mmap(8) as *i64 4588 let ns: i64 = ss_manifest_dyn(prefix, sp) 4589 st[0] = sp[0] 4590 st[1] = ns 4591 st[2] = 0 4592 st[3] = 0 4593 st[4] = 0 4594 st[5] = 0 4595 st[6] = 0 4596 return st 4597} 4598 4599func ss_cur_next(prefix: *u8, st: *i64, 4600 kout: *i64, klout: *i64, vout: *i64, vlout: *i64) -> i64 { 4601 var guard: i64 = 0 4602 while guard < SS_MAGIC_1048576 { 4603 guard = guard + 1 4604 if st[3] == 0 { 4605 if st[2] >= st[1] { return 0 } 4606 let segs: *i64 = st[0] as *i64 4607 let path: *u8 = sys_mmap(512) 4608 var o: i64 = ss_cat(path, 0, prefix) 4609 o = ss_cat(path, o, segs[st[2]] as *u8) 4610 o = ss_cat(path, o, ".docs" as *u8) 4611 path[o] = 0 as u8 4612 let szp: *i64 = sys_mmap(16) as *i64 4613 let b: *u8 = ss_readall(path, szp) 4614 st[4] = szp[0] 4615 st[5] = 0 4616 if (b as i64) == 0 { st[4] = 0 } 4617 if st[4] <= 0 { 4618 st[3] = 0 4619 st[2] = st[2] + 1 4620 } else { 4621 st[3] = b as i64 4622 } 4623 } 4624 if st[3] != 0 { 4625 let b: *u8 = st[3] as *u8 4626 let sz: i64 = st[4] 4627 let i: i64 = st[5] 4628 var bad: i64 = 0 4629 if i + 9 > sz { bad = 1 } 4630 if bad == 0 { 4631 let kind: i64 = b[i] 4632 let kl: i64 = ss_r32(b, i + 1) 4633 let koff: i64 = i + 5 4634 if kl < 0 { bad = 1 } 4635 if koff + kl + 4 > sz { bad = 1 } 4636 if bad == 0 { 4637 let vl: i64 = ss_r32(b, koff + kl) 4638 let voff: i64 = koff + kl + 4 4639 if vl < 0 { bad = 1 } 4640 if voff + vl > sz { bad = 1 } 4641 if bad == 0 { 4642 st[5] = voff + vl 4643 st[6] = kind 4644 kout[0] = (b as i64) + koff 4645 klout[0] = kl 4646 vout[0] = (b as i64) + voff 4647 vlout[0] = vl 4648 return 1 4649 } 4650 } 4651 } 4652 st[3] = 0 4653 st[2] = st[2] + 1 4654 } 4655 } 4656 return 0 4657} 4658 4659// COMPACTION (IM4): fold all live segments into ONE new segment holding only 4660// the LATEST entry per key (superseded versions dropped; tombstones KEPT so 4661// get() semantics are EXACTLY preserved incl. GONE-vs-ABSENT -- tombstone GC 4662// is a later policy rung). HISTORY IS NOT DESTROYED: retired segment files 4663// stay on disk and their names are appended to <prefix>manifest-archive.txt 4664// BEFORE the live-manifest swap (crash between the two leaves the old 4665// manifest intact = consistent; the archive only ever gains rows). The swap 4666// itself is the same atomic temp->rename commit point. 4667// Returns the new segid (>0) or negative on failure. 4668// VERIFY-THEN-COMMIT (2026-08-07). Re-read the segment we just wrote and prove it reproduces the 4669// merged table EXACTLY. Called BEFORE the manifest swap, which is what makes it fail-closed BY 4670// CONSTRUCTION: a failed verify returns with the live manifest still pointing at the untouched 4671// originals, so the plane is exactly as it was and the next beat simply retries. 4672// 4673// MATCHED FROM THE KNOWN GOOD, THEN MADE AFFORDABLE. nx_store_compact already does verify-then-commit 4674// (snapshot -> compact -> re-read -> restore manifest on mismatch) and that shape is right. But it 4675// verifies with a PER-KEY ss_get, which is O(keys x data) -- the exact walk its own sc_snapshot header 4676// records as having leaked >15GB on a 1355-segment plane before it had to be killed. That cost is why 4677// the verifier never reached nx_seg_compact_cli, the compactor that actually runs every 10 minutes 4678// across ~1096 planes. Reusing the merge's OWN hash index makes the identical check O(output): one 4679// pass over the new segment, one expected-O(1) probe per record. 4680// 4681// WHAT THIS CATCHES THAT A SIZE CHECK CANNOT: it reads back the ARTIFACT, so it covers the WRITER -- 4682// ss_add2 framing, the writer cap, a short or partial write -- not just the merge decision. A byte 4683// count cannot tell a correct segment from a same-size mangled one. 4684// ★A COMPACTOR THAT NEVER RE-READS WHAT IT WROTE IS TRUSTING THE ONE STEP NOBODY CHECKED. 4685// 4686// `seen != nk` is the load-bearing line: it catches BOTH loss (a key missing from the output) and 4687// duplication (the writer emitted one twice). No per-record check can see either on its own. 4688// Returns 0 ok, -1 on any disagreement. 4689func ss_verify_merged(prefix: *u8, segid: i64, tkp: *i64, tkl: *i64, tkind: *i64, tvp: *i64, tvl: *i64, nk: i64, hidx: *i64, hcap: i64) -> i64 { 4690 let path: *u8 = sys_mmap(512) 4691 var o: i64 = 0 4692 o = ss_cat(path, o, prefix) 4693 o = ss_cat(path, o, "seg-" as *u8) 4694 o = ss_catn(path, o, segid) 4695 o = ss_cat(path, o, ".docs" as *u8) 4696 path[o] = 0 as u8 4697 let szp: *i64 = sys_mmap(16) as *i64 4698 let b: *u8 = ss_readall(path, szp) 4699 if (b as i64) == 0 { return 0 - 1 } 4700 let sz: i64 = szp[0] 4701 var seen: i64 = 0 4702 var i: i64 = 0 4703 while i + 9 <= sz { 4704 let kind: i64 = b[i] as i64 4705 let kl: i64 = ss_r32(b, i + 1) 4706 let koff: i64 = i + 5 4707 let vl: i64 = ss_r32(b, koff + kl) 4708 let voff: i64 = koff + kl + 4 4709 let kp: *u8 = ((b as i64) + koff) as *u8 4710 var probe: i64 = ss_khash(kp, kl) & (hcap - 1) 4711 var hit: i64 = 0 - 1 4712 var probing: i64 = 1 4713 while probing == 1 { 4714 let e: i64 = hidx[probe] 4715 if e == 0 { probing = 0 } else { 4716 if ss_kcmp(tkp[e - 1] as *u8, tkl[e - 1], kp, kl) == 0 { hit = e - 1; probing = 0 } 4717 else { probe = (probe + 1) & (hcap - 1) } 4718 } 4719 } 4720 if hit < 0 { return 0 - 1 } 4721 if tkind[hit] != kind { return 0 - 1 } 4722 if tvl[hit] != vl { return 0 - 1 } 4723 let va: *u8 = tvp[hit] as *u8 4724 let vb: *u8 = ((b as i64) + voff) as *u8 4725 var q: i64 = 0 4726 while q < vl { 4727 if va[q] != vb[q] { return 0 - 1 } 4728 q = q + 1 4729 } 4730 seen = seen + 1 4731 i = voff + vl 4732 } 4733 if seen != nk { return 0 - 1 } 4734 return 0 4735} 4736 4737func ss_compact(prefix: *u8, segid: i64) -> i64 { // THE COMPACTOR IS THE MOST DESTRUCTIVE WRITER ON THE PLANE, so it takes the same lock. 4738 // It lists the live segments below and then, at the bottom, swaps the manifest to name ONLY the 4739 // merged segment. Any ss_commit landing in between is therefore ERASED FROM THE MANIFEST -- not 4740 // a lost row but a lost SEGMENT, whose file is still on disk, fsynced, and unreachable forever. 4741 // That is a background beat silently deleting committed writes, and it is exactly why an 4742 // intermittent seg-store failure MOVES: which data vanishes depends purely on timing. 4743 // The lock covers the WHOLE merge, not just the swap, because the segment set is read before the 4744 // swap and must not change underneath. This does block writers to this plane for the duration of 4745 // a merge -- an availability cost taken knowingly, since the alternative is a compactor that 4746 // destroys committed data. Finer-grained (merge unlocked, then verify-and-swap locked) is the 4747 // follow-on; correctness first. 4748 let lk: i64 = ss_plane_lock(prefix) 4749 if lk < 0 { return 0 - 8 } 4750 let sp: *i64 = sys_mmap(8) as *i64 4751 let ns: i64 = ss_manifest_dyn(prefix, sp) 4752 let segs: *i64 = sp[0] as *i64 4753 if ns <= 0 { ss_plane_unlock(lk); return 0 - 1 } 4754 // read every live segment first; key-table capacity is DATA-DRIVEN from 4755 // the total bytes (every record >= 9 bytes), never a silent fixed cap 4756 let bptrs: *i64 = sys_mmap(8 * ns + 64) as *i64 4757 let bszs: *i64 = sys_mmap(8 * ns + 64) as *i64 4758 var total: i64 = 0 4759 var s: i64 = 0 4760 while s < ns { 4761 let path: *u8 = sys_mmap(512) 4762 var o: i64 = 0 4763 o = ss_cat(path, o, prefix) 4764 o = ss_cat(path, o, segs[s] as *u8) 4765 o = ss_cat(path, o, ".docs" as *u8) 4766 path[o] = 0 as u8 4767 let szp: *i64 = sys_mmap(16) as *i64 4768 bptrs[s] = ss_readall(path, szp) as i64 4769 bszs[s] = szp[0] 4770 if bszs[s] > 0 { total = total + bszs[s] } 4771 s = s + 1 4772 } 4773 let maxk: i64 = total / 9 + 16 4774 let tkp: *i64 = sys_mmap(8 * maxk) as *i64 4775 let tkl: *i64 = sys_mmap(8 * maxk) as *i64 4776 let tkind: *i64 = sys_mmap(8 * maxk) as *i64 4777 let tvp: *i64 = sys_mmap(8 * maxk) as *i64 4778 let tvl: *i64 = sys_mmap(8 * maxk) as *i64 4779 // DEDUP INDEX: open-addressed hash -> key-slot+1 (0 = empty). Power-of-two so the probe can mask 4780 // instead of divide. Sized 2x maxk to keep the load factor <= 0.5, which is where linear probing 4781 // stays flat rather than degrading back toward the scan this replaces. 4782 // maxk is already a deliberate WORST CASE (total/9, i.e. every record the 9-byte minimum), so this 4783 // table is large in address space and small in resident pages: sys_mmap is lazy, and only the ~nk 4784 // slots actually probed are ever faulted in. The five arrays above are provisioned the same way 4785 // for the same reason -- address space is not memory until it is touched. 4786 var hcap: i64 = 16 4787 while hcap < maxk * 2 { hcap = hcap * 2 } 4788 let hidx: *i64 = sys_mmap(8 * hcap) as *i64 4789 var nk: i64 = 0 4790 s = 0 4791 while s < ns { 4792 let b: *u8 = bptrs[s] as *u8 4793 let sz: i64 = bszs[s] 4794 var i: i64 = 0 4795 while i + 9 <= sz { 4796 let kind: i64 = b[i] 4797 let kl: i64 = ss_r32(b, i + 1) 4798 let koff: i64 = i + 5 4799 let vl: i64 = ss_r32(b, koff + kl) 4800 let voff: i64 = koff + kl + 4 4801 // find existing key slot (chronological walk => overwrite = last wins) 4802 // HASH-INDEXED LOOKUP (2026-08-06). The loop this replaces walked the ENTIRE key table for 4803 // EVERY record -- and had no early exit: `if hit < 0` skipped the comparison but the loop 4804 // still ran to nk every time, so a hit cost exactly as much as a miss. That is O(records x 4805 // keys) with the full scan always paid. 4806 // MEASURED CONSEQUENCE: nx_seg_compact_cli completed mvaultpath at ELEVEN live segments 4807 // (~155MB) on 2026-07-30 17:54 and has SIGKILLed at TWELVE (~170MB) on every sweep since -- 4808 // a 10% size increase flipping success to timeout is the signature of a quadratic, not of a 4809 // plane that is merely large. Because the CLI is fail-closed before the manifest swap, a 4810 // timeout changes nothing, so a plane that crosses that cliff ONCE can never be reduced 4811 // again: mvaultpath has been stuck at 12 for SEVEN DAYS, burning a 300s timeout every 4812 // sweep, and it is the single largest consumer on this box. 4813 // ORDER AND SEMANTICS ARE UNCHANGED BY CONSTRUCTION: the tkp/tkl/tkind/tvp/tvl arrays stay 4814 // insertion-ordered and the merge loop below still walks them 0..nk, so output byte order 4815 // and last-wins are identical. Only the LOOKUP changes. 4816 let kptr: *u8 = ((b as i64) + koff) as *u8 4817 var probe: i64 = ss_khash(kptr, kl) & (hcap - 1) 4818 var hit: i64 = 0 - 1 4819 var scan: i64 = 1 4820 while scan == 1 { 4821 let e: i64 = hidx[probe] 4822 if e == 0 { scan = 0 } else { 4823 // hidx stores slot+1 so 0 can mean EMPTY. Every probe CONFIRMS with ss_kcmp -- 4824 // the hash narrows the search, it never decides identity. 4825 if ss_kcmp(tkp[e - 1] as *u8, tkl[e - 1], kptr, kl) == 0 { hit = e - 1; scan = 0 } 4826 else { probe = (probe + 1) & (hcap - 1) } 4827 } 4828 } 4829 if hit < 0 { 4830 if nk >= maxk { ss_plane_unlock(lk); return 0 - 7 } 4831 hit = nk 4832 nk = nk + 1 4833 hidx[probe] = hit + 1 4834 tkp[hit] = (b as i64) + koff 4835 tkl[hit] = kl 4836 } 4837 if hit >= 0 { 4838 tkp[hit] = (b as i64) + koff 4839 tkl[hit] = kl 4840 tkind[hit] = kind 4841 tvp[hit] = (b as i64) + voff 4842 tvl[hit] = vl 4843 } 4844 i = voff + vl 4845 } 4846 s = s + 1 4847 } 4848 // merged segment = latest entry per key; writer sized data-driven from the merged total (no fixed cap) 4849 let w: *i64 = ss_begin_cap(total + SS_MAGIC_65536) 4850 var t2: i64 = 0 4851 while t2 < nk { 4852 if ss_add2(w, tkind[t2], tkp[t2] as *u8, tkl[t2], tvp[t2] as *u8, tvl[t2]) != 0 { ss_plane_unlock(lk); return 0 - 2 } 4853 t2 = t2 + 1 4854 } 4855 // CORRECTED 2026-08-07 -- THE BLOCK BELOW DESCRIBES A DESIGN THIS FUNCTION DOES NOT IMPLEMENT. 4856 // Read the code: ss_plane_lock is taken at the TOP and released only on return, so the merge runs 4857 // WITH the lock held, and there is NO staleness check anywhere in either compactor. The comment at 4858 // the head of ss_compact is the accurate one: 'The lock covers the WHOLE merge, not just the swap 4859 // ... Finer-grained (merge unlocked, then verify-and-swap locked) is the follow-on; correctness 4860 // first.' Kept rather than deleted because it records real intent and a real measurement -- but it 4861 // must not be read as a description of current behaviour. 4862 // **A COMMENT DESCRIBING AN INTENDED REFACTOR IS INDISTINGUISHABLE FROM ONE DESCRIBING THE CODE, 4863 // AND THE DANGEROUS READER IS THE ONE WHO 'RESTORES' THE SHORTER SECTION WITHOUT THE STALENESS 4864 // CHECK IT PROMISES -- that reader would ship exactly the erasure the lock exists to prevent. 4865 // SHORT CRITICAL SECTION (2026-08-06 refinement of debt 1786054662). The merge above now runs 4866 // WITHOUT the lock; only the write + manifest swap below need it. Holding it across the ENTIRE 4867 // merge was measured to cost real writes the moment ss_plane_lock gained a bounded wait: 4868 // nx_segrace_gate T4 showed FOUR concurrent writers REFUSED after 30s and 10 of 24 keys landing. 4869 // A compactor is OPTIONAL background work and must never cost a writer its commit. 4870 // STALENESS CHECK, and it is what makes the shorter section safe: if the live segment count moved 4871 // while we merged unlocked, a writer committed and our merged set is STALE -- swapping the 4872 // manifest now would drop their segment, which is the very erasure this lock exists to prevent. 4873 // So ABORT instead. Compaction retries on the next beat; a lost commit never does. 4874 // ★PREFER REDOING OPTIONAL WORK OVER DELAYING OR DESTROYING MANDATORY WORK. 4875 4876 if ss_write_seg(prefix, w, segid) != 0 { ss_plane_unlock(lk); return 0 - 3 } 4877 // VERIFY BEFORE COMMIT -- see ss_verify_merged. Deliberately placed between the write and 4878 // the manifest swap: on failure the live manifest is untouched, so the plane is exactly as 4879 // it was and the next beat retries. rc -10 is distinct so a verify refusal is never read as 4880 // a write failure. 4881 if ss_verify_merged(prefix, segid, tkp, tkl, tkind, tvp, tvl, nk, hidx, hcap) != 0 { ss_plane_unlock(lk); return 0 - 10 } 4882 // archive the retired segment names (append-only; survives any crash here) 4883 let ap: *u8 = sys_mmap(512) 4884 var ao: i64 = 0 4885 ao = ss_cat(ap, ao, prefix) 4886 ao = ss_cat(ap, ao, "manifest-archive.txt" as *u8) 4887 ap[ao] = 0 as u8 4888 let afd: i64 = sys_openat_append(ap, 0x1a4) 4889 if afd < 0 { ss_plane_unlock(lk); return 0 - 4 } 4890 s = 0 4891 while s < ns { 4892 let nm: *u8 = segs[s] as *u8 4893 sys_write(afd, nm, ss_len(nm)) 4894 sys_write(afd, "\n" as *u8, 1) 4895 s = s + 1 4896 } 4897 sys_fsync(afd) 4898 sys_close(afd) 4899 // atomic live-manifest swap to ONLY the merged segment 4900 let mf: *u8 = sys_mmap(512) 4901 let mt: *u8 = sys_mmap(512) 4902 var o2: i64 = 0 4903 o2 = ss_cat(mf, o2, prefix) 4904 o2 = ss_cat(mf, o2, "manifest.txt" as *u8) 4905 mf[o2] = 0 as u8 4906 o2 = 0 4907 o2 = ss_cat(mt, o2, prefix) 4908 o2 = ss_cat(mt, o2, "manifest.tmp" as *u8) 4909 mt[o2] = 0 as u8 4910 let nb: *u8 = sys_mmap(65536) 4911 var no: i64 = 0 4912 no = ss_cat(nb, no, "seg-" as *u8) 4913 no = ss_catn(nb, no, segid) 4914 nb[no] = 10 as u8 4915 no = no + 1 4916 if ss_writefile(mt, nb, no) != 0 { ss_plane_unlock(lk); return 0 - 5 } 4917 if sys_renameat(mt, mf) != 0 { ss_plane_unlock(lk); return 0 - 6 } 4918 ss_syncdir(prefix) 4919 ss_plane_unlock(lk) 4920 return segid 4921} 4922 4923// author=tutor (LM-028 fix): cap-aware sibling of ss_compact for stores with >256 live segments 4924// (the GALX-PROD-FULL family: galx-prod holds ONE segment per image, so thousands accumulate and 4925// each ss_get/ss_scan re-reads every seg .docs = O(n) per lookup). ss_compact CANNOT fold such a 4926// store -- OR RATHER IT COULD NOT WHEN THIS WAS WRITTEN. CORRECTED 2026-08-07 by reading the code: 4927// ss_compact now reads the manifest via ss_manifest_dyn (NO cap) and sizes its writer with 4928// ss_begin_cap(total + SS_MAGIC_65536), so BOTH limits named on the next line are GONE. This sibling 4929// still earns its place -- it takes an explicit cap argument and is the variant the standing fold 4930// beat drives -- but "ss_compact CANNOT fold a big store" is no longer true, and it must not be cited 4931// to justify an exclusion. It briefly misled me into believing a 913-segment plane was structurally 4932// unfoldable when the real remaining barrier is MEMORY (~1.7GB), not a cap. 4933// **RE-CHECK THE REASON BEHIND AN EXCLUSION, NEVER ITS LABEL** -- and re-read the code, because 4934// **A COMMENT THAT WAS TRUE WHEN WRITTEN IS STILL A LIE ONCE THE CODE MOVES.** 4935// (HISTORIC, NOW FALSE:) it read the manifest via ss_manifest (HARD cap 256) AND ss_begin() capped at 1MB, 4936// so on a big store it would merge only the first 256 segs (or overflow the writer) and then swap the 4937// live manifest to that partial segment -- silently dropping ~90% of the corpus. This mirror reads 4938// ss_manifest_cap(cap) and allocates a DATA-DRIVEN writer (>= total input bytes; dedup only shrinks), 4939// so any segment count folds to ONE with NO data loss. ss_compact is left BYTE-IDENTICAL (purely 4940// additive). Every failure path returns BEFORE the atomic manifest swap, so a failure leaves the live 4941// store intact (crash/fail-safe). Returns the new segid (>0) or negative on failure. 4942func ss_compact_cap(prefix: *u8, segid: i64, cap: i64) -> i64 { 4943 // Same lock, same reason as ss_compact -- and this is the variant the standing beat actually 4944 // drives (nx_store_fold_beat, nx_galx_compact, nx_shard_compact, nx_store_compact fold), so it is 4945 // the one most likely to be merging while a writer commits. 4946 let lk: i64 = ss_plane_lock(prefix) 4947 if lk < 0 { return 0 - 8 } 4948 let segs: *i64 = sys_mmap(8 * cap) as *i64 4949 let ns: i64 = ss_manifest_cap(prefix, segs, cap) 4950 if ns <= 0 { ss_plane_unlock(lk); return 0 - 1 } 4951 // read every live segment; key-table + writer capacities are DATA-DRIVEN from total bytes 4952 let bptrs: *i64 = sys_mmap(8 * cap) as *i64 4953 let bszs: *i64 = sys_mmap(8 * cap) as *i64 4954 var total: i64 = 0 4955 var s: i64 = 0 4956 while s < ns { 4957 let path: *u8 = sys_mmap(512) 4958 var o: i64 = 0 4959 o = ss_cat(path, o, prefix) 4960 o = ss_cat(path, o, segs[s] as *u8) 4961 o = ss_cat(path, o, ".docs" as *u8) 4962 path[o] = 0 as u8 4963 let szp: *i64 = sys_mmap(16) as *i64 4964 bptrs[s] = ss_readall(path, szp) as i64 4965 bszs[s] = szp[0] 4966 if bszs[s] > 0 { total = total + bszs[s] } 4967 s = s + 1 4968 } 4969 let maxk: i64 = total / 9 + 16 4970 let tkp: *i64 = sys_mmap(8 * maxk) as *i64 4971 let tkl: *i64 = sys_mmap(8 * maxk) as *i64 4972 let tkind: *i64 = sys_mmap(8 * maxk) as *i64 4973 let tvp: *i64 = sys_mmap(8 * maxk) as *i64 4974 let tvl: *i64 = sys_mmap(8 * maxk) as *i64 4975 // DEDUP INDEX: open-addressed hash -> key-slot+1 (0 = empty), power-of-two so the probe masks 4976 // instead of dividing, sized 2x maxk to hold the load factor <= 0.5 where linear probing stays 4977 // flat rather than degrading back into the scan it replaces. maxk is already a deliberate WORST 4978 // CASE (total/9, every record the 9-byte minimum), so this table is large in address space and 4979 // small in resident pages: sys_mmap is lazy, and only the ~nk slots actually probed fault in. 4980 var hcap: i64 = 16 4981 while hcap < maxk * 2 { hcap = hcap * 2 } 4982 let hidx: *i64 = sys_mmap(8 * hcap) as *i64 4983 var nk: i64 = 0 4984 s = 0 4985 while s < ns { 4986 let b: *u8 = bptrs[s] as *u8 4987 let sz: i64 = bszs[s] 4988 var i: i64 = 0 4989 while i + 9 <= sz { 4990 let kind: i64 = b[i] 4991 let kl: i64 = ss_r32(b, i + 1) 4992 let koff: i64 = i + 5 4993 let vl: i64 = ss_r32(b, koff + kl) 4994 let voff: i64 = koff + kl + 4 4995 // chronological walk => last write wins (find existing key slot). 4996 // HASH-INDEXED probe, ported 2026-08-07 from the ss_compact fix proven live the same day: 4997 // mvaultpath folded 12 -> 1 segments in under ~47s on a plane that had SIGKILLed at the 4998 // 300s timeout on EVERY sweep for the seven days prior. The loop this replaces walked the 4999 // ENTIRE key table for EVERY record with NO early exit -- the `if hit < 0` guard skipped 5000 // only the comparison, never the iteration, so a hit cost exactly as much as a miss. 5001 // This variant matters MORE than ss_compact: per the header above it is the one the 5002 // standing beat drives (nx_store_fold_beat, nx_galx_compact, nx_shard_compact). 5003 // The hash is an INDEX, never an identity -- every probe still byte-verifies with 5004 // ss_kcmp, so a collision costs one extra comparison and can never merge distinct keys. 5005 let kp: *u8 = ((b as i64) + koff) as *u8 5006 var probe: i64 = ss_khash(kp, kl) & (hcap - 1) 5007 var hit: i64 = 0 - 1 5008 var probing: i64 = 1 5009 while probing == 1 { 5010 let e: i64 = hidx[probe] 5011 if e == 0 { probing = 0 } else { 5012 if ss_kcmp(tkp[e - 1] as *u8, tkl[e - 1], kp, kl) == 0 { hit = e - 1; probing = 0 } 5013 else { probe = (probe + 1) & (hcap - 1) } 5014 } 5015 } 5016 if hit < 0 { 5017 if nk >= maxk { ss_plane_unlock(lk); return 0 - 7 } 5018 hit = nk 5019 hidx[probe] = nk + 1 5020 nk = nk + 1 5021 tkp[hit] = (b as i64) + koff 5022 tkl[hit] = kl 5023 } 5024 if hit >= 0 { 5025 tkp[hit] = (b as i64) + koff 5026 tkl[hit] = kl 5027 tkind[hit] = kind 5028 tvp[hit] = (b as i64) + voff 5029 tvl[hit] = vl 5030 } 5031 i = voff + vl 5032 } 5033 s = s + 1 5034 } 5035 // merged segment = latest entry per key; writer cap is DATA-DRIVEN (merged <= total + slack), 5036 // NOT the fixed 1MB of ss_begin() -- that is the second half of the cap fix. 5037 let w: *i64 = sys_mmap(32) as *i64 5038 let wcap: i64 = total + SS_MAGIC_1048576 5039 w[0] = sys_mmap(wcap) as i64 5040 w[1] = 0 5041 w[2] = wcap 5042 var t2: i64 = 0 5043 while t2 < nk { 5044 if ss_add2(w, tkind[t2], tkp[t2] as *u8, tkl[t2], tvp[t2] as *u8, tvl[t2]) != 0 { ss_plane_unlock(lk); return 0 - 2 } 5045 t2 = t2 + 1 5046 } 5047 // CORRECTED 2026-08-07 -- THE BLOCK BELOW DESCRIBES A DESIGN THIS FUNCTION DOES NOT IMPLEMENT. 5048 // Read the code: ss_plane_lock is taken at the TOP and released only on return, so the merge runs 5049 // WITH the lock held, and there is NO staleness check anywhere in either compactor. The comment at 5050 // the head of ss_compact is the accurate one: 'The lock covers the WHOLE merge, not just the swap 5051 // ... Finer-grained (merge unlocked, then verify-and-swap locked) is the follow-on; correctness 5052 // first.' Kept rather than deleted because it records real intent and a real measurement -- but it 5053 // must not be read as a description of current behaviour. 5054 // **A COMMENT DESCRIBING AN INTENDED REFACTOR IS INDISTINGUISHABLE FROM ONE DESCRIBING THE CODE, 5055 // AND THE DANGEROUS READER IS THE ONE WHO 'RESTORES' THE SHORTER SECTION WITHOUT THE STALENESS 5056 // CHECK IT PROMISES -- that reader would ship exactly the erasure the lock exists to prevent. 5057 // SHORT CRITICAL SECTION (2026-08-06 refinement of debt 1786054662). The merge above now runs 5058 // WITHOUT the lock; only the write + manifest swap below need it. Holding it across the ENTIRE 5059 // merge was measured to cost real writes the moment ss_plane_lock gained a bounded wait: 5060 // nx_segrace_gate T4 showed FOUR concurrent writers REFUSED after 30s and 10 of 24 keys landing. 5061 // A compactor is OPTIONAL background work and must never cost a writer its commit. 5062 // STALENESS CHECK, and it is what makes the shorter section safe: if the live segment count moved 5063 // while we merged unlocked, a writer committed and our merged set is STALE -- swapping the 5064 // manifest now would drop their segment, which is the very erasure this lock exists to prevent. 5065 // So ABORT instead. Compaction retries on the next beat; a lost commit never does. 5066 // ★PREFER REDOING OPTIONAL WORK OVER DELAYING OR DESTROYING MANDATORY WORK. 5067 5068 if ss_write_seg(prefix, w, segid) != 0 { ss_plane_unlock(lk); return 0 - 3 } 5069 // VERIFY BEFORE COMMIT -- see ss_verify_merged. Deliberately placed between the write and 5070 // the manifest swap: on failure the live manifest is untouched, so the plane is exactly as 5071 // it was and the next beat retries. rc -10 is distinct so a verify refusal is never read as 5072 // a write failure. 5073 if ss_verify_merged(prefix, segid, tkp, tkl, tkind, tvp, tvl, nk, hidx, hcap) != 0 { ss_plane_unlock(lk); return 0 - 10 } 5074 // archive the retired segment names (append-only; survives any crash here) 5075 let ap: *u8 = sys_mmap(512) 5076 var ao: i64 = 0 5077 ao = ss_cat(ap, ao, prefix) 5078 ao = ss_cat(ap, ao, "manifest-archive.txt" as *u8) 5079 ap[ao] = 0 as u8 5080 let afd: i64 = sys_openat_append(ap, 0x1a4) 5081 if afd < 0 { ss_plane_unlock(lk); return 0 - 4 } 5082 s = 0 5083 while s < ns { 5084 let nm: *u8 = segs[s] as *u8 5085 sys_write(afd, nm, ss_len(nm)) 5086 sys_write(afd, "\n" as *u8, 1) 5087 s = s + 1 5088 } 5089 sys_fsync(afd) 5090 sys_close(afd) 5091 // atomic live-manifest swap to ONLY the merged segment 5092 let mf: *u8 = sys_mmap(512) 5093 let mt: *u8 = sys_mmap(512) 5094 var o2: i64 = 0 5095 o2 = ss_cat(mf, o2, prefix) 5096 o2 = ss_cat(mf, o2, "manifest.txt" as *u8) 5097 mf[o2] = 0 as u8 5098 o2 = 0 5099 o2 = ss_cat(mt, o2, prefix) 5100 o2 = ss_cat(mt, o2, "manifest.tmp" as *u8) 5101 mt[o2] = 0 as u8 5102 let nb: *u8 = sys_mmap(65536) 5103 var no: i64 = 0 5104 no = ss_cat(nb, no, "seg-" as *u8) 5105 no = ss_catn(nb, no, segid) 5106 nb[no] = 10 as u8 5107 no = no + 1 5108 if ss_writefile(mt, nb, no) != 0 { ss_plane_unlock(lk); return 0 - 5 } 5109 if sys_renameat(mt, mf) != 0 { ss_plane_unlock(lk); return 0 - 6 } 5110 ss_syncdir(prefix) 5111 ss_plane_unlock(lk) 5112 return segid 5113}