code wiki / (root) / nx_seg_store.nx

nx_seg_store.nx source

↩ module page · 3231 lines · 143853 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 33const SS_MAGIC_1024: i64 = 1024 34const SS_MAGIC_1039: i64 = 1039 35const SS_MAGIC_1040: i64 = 1040 36const SS_MAGIC_1071: i64 = 1071 37const SS_MAGIC_1072: i64 = 1072 38const SS_MAGIC_1279: i64 = 1279 39const SS_MAGIC_12352: i64 = 12352 40const SS_MAGIC_12543: i64 = 12543 41const SS_MAGIC_3040: i64 = 3040 42const SS_MAGIC_13312: i64 = 13312 43const SS_MAGIC_19903: i64 = 19903 44const SS_MAGIC_3400: i64 = 3400 45const SS_MAGIC_19968: i64 = 19968 46const SS_MAGIC_40959: i64 = 40959 47const SS_MAGIC_44032: i64 = 44032 48const SS_MAGIC_55215: i64 = 55215 49const SS_MAGIC_63744: i64 = 63744 50const SS_MAGIC_64255: i64 = 64255 51const SS_MAGIC_1469598103934665603: i64 = 1469598103934665603 52const SS_MAGIC_1099511628211: i64 = 1099511628211 53 54func ss_len(s: *u8) -> i64 { 55 var n: i64 = 0 56 while s[n] != (0 as u8) { n = n + 1 } 57 return n 58} 59 60func ss_cat(dst: *u8, off: i64, s: *u8) -> i64 { 61 var i: i64 = 0 62 while s[i] != (0 as u8) { dst[off + i] = s[i]; i = i + 1 } 63 return off + i 64} 65 66// append decimal of v 67func ss_catn(dst: *u8, off: i64, v: i64) -> i64 { 68 var m: i64 = v 69 var o: i64 = off 70 if m < 0 { m = 0 - m; dst[o] = 45 as u8; o = o + 1 } 71 let t: *u8 = sys_mmap(28) 72 var k: i64 = 0 73 if m == 0 { t[0] = 48 as u8; k = 1 } 74 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 75 var i: i64 = 0 76 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 77 return o + k 78} 79 80func ss_w32(p: *u8, off: i64, v: i64) -> i64 { 81 p[off] = ((v >> 24) & 0xff) as u8 82 p[off + 1] = ((v >> 16) & 0xff) as u8 83 p[off + 2] = ((v >> 8) & 0xff) as u8 84 p[off + 3] = (v & 0xff) as u8 85 return off + 4 86} 87 88func ss_r32(p: *u8, off: i64) -> i64 { 89 let a: i64 = p[off] 90 let b: i64 = p[off + 1] 91 let c: i64 = p[off + 2] 92 let d: i64 = p[off + 3] 93 return (((((a << 8) | b) << 8) | c) << 8) | d 94} 95 96// read whole file into fresh mmap; szout[0] = size (-1 if absent) 97func ss_readall(path: *u8, szout: *i64) -> *u8 { 98 let fd: i64 = sys_openat_rd(path) 99 if fd < 0 { szout[0] = 0 - 1; return 0 as *u8 } 100 let sz: i64 = sys_lseek(fd, 0, 2) 101 sys_lseek(fd, 0, 0) 102 let buf: *u8 = sys_mmap(sz + 64) 103 var got: i64 = 0 104 var n: i64 = 1 105 while n > 0 { 106 n = sys_read(fd, (buf as i64 + got) as *u8, SS_MAGIC_65536) 107 if n > 0 { got = got + n } 108 } 109 sys_close(fd) 110 szout[0] = got 111 return buf 112} 113 114// SEGMENT LOADER (2026-07-23, the mmap-serve rung): read a segment file EITHER by reading it fully into 115// anon RAM (usemmap=0, the historical default -- every consumer keeps this) OR by a read-only FILE-BACKED 116// mmap (usemmap=1). Same (ptr, *szout) contract as ss_readall, so it is a drop-in swap. mmap mode means: 117// load time ~0 (nothing copied), only TOUCHED pages become resident (a query reads its hits, not the whole 118// 1.6GB shard), the mapping is SHARED across forked children (read-only, no COW) AND backed by the OS page 119// cache that survives daemon restarts. This is what turns the RAM-bound serve ceiling into a disk-bound one 120// (research: "memory-mapped immutable segments give fast read-heavy retrieval"). Segments are append-only 121// immutable and readers never mutate segment bytes, so PROT_READ is safe by construction. 122func ss_loadfile(path: *u8, szout: *i64, usemmap: i64) -> *u8 { 123 if usemmap == 1 { 124 let m: *u8 = sys_map_file(path, szout) 125 if (m as i64) != 0 { return m } 126 // mmap failed (missing/empty) -> szout already 0; fall through to a normal read so callers that 127 // treat "size 0" as absent behave identically to ss_readall's -1/empty on a missing file. 128 } 129 return ss_readall(path, szout) 130} 131 132func ss_writefile(path: *u8, bytes: *u8, n: i64) -> i64 { 133 let fd: i64 = sys_openat_wr(path, 0x1a4) 134 if fd < 0 { return 0 - 1 } 135 var off: i64 = 0 136 while off < n { 137 let wr: i64 = sys_write(fd, (bytes as i64 + off) as *u8, n - off) 138 if wr <= 0 { sys_close(fd); return 0 - 2 } 139 off = off + wr 140 } 141 sys_fsync(fd) 142 sys_close(fd) 143 return 0 144} 145 146// fsync the directory holding the store files so the rename(2) commit point 147// itself reaches stable storage (power-loss closure of the IM3 debt; without 148// this only process-crash safety is proven). prefix = ".../name-" => dir is 149// everything up to the last "/"; no slash => current directory. 150func ss_syncdir(prefix: *u8) -> i64 { 151 let d: *u8 = sys_mmap(512) 152 var last: i64 = 0 - 1 153 var i: i64 = 0 154 while prefix[i] != (0 as u8) { 155 if prefix[i] == (47 as u8) { last = i } 156 i = i + 1 157 } 158 if last < 0 { 159 d[0] = 46 as u8 160 d[1] = 0 as u8 161 } 162 if last >= 0 { 163 var t: i64 = 0 164 while t <= last { d[t] = prefix[t]; t = t + 1 } 165 d[t] = 0 as u8 166 } 167 let fd: i64 = sys_openat_rd(d) 168 if fd < 0 { return 0 - 1 } 169 let rc: i64 = sys_fsync(fd) 170 sys_close(fd) 171 return rc 172} 173 174// segment writer state: w[0]=buf ptr, w[1]=len, w[2]=cap (heap-mmap idiom, no stack structs) 175// writer with a caller-sized buffer (compaction sizes it from the merged total -- no fixed cap). 176func ss_begin_cap(cap: i64) -> *i64 { 177 let w: *i64 = sys_mmap(32) as *i64 178 w[0] = sys_mmap(cap) as i64 179 w[1] = 0 180 w[2] = cap 181 return w 182} 183// default single-write buffer (a document is ~KB; ss_add2 fail-closes if exceeded, never truncates). 184func ss_begin() -> *i64 { 185 return ss_begin_cap(SS_MAGIC_1048576) 186} 187 188func ss_add(w: *i64, kind: i64, key: *u8, val: *u8, vlen: i64) -> i64 { 189 return ss_add2(w, kind, key, ss_len(key), val, vlen) 190} 191 192// explicit key length (compaction feeds keys straight from .docs bytes, not null-terminated) 193func ss_add2(w: *i64, kind: i64, key: *u8, kl: i64, val: *u8, vlen: i64) -> i64 { 194 let buf: *u8 = w[0] as *u8 195 var o: i64 = w[1] 196 if o + kl + vlen + 16 > w[2] { return 0 - 1 } 197 buf[o] = kind as u8 198 o = o + 1 199 o = ss_w32(buf, o, kl) 200 var t: i64 = 0 201 while t < kl { buf[o] = key[t]; o = o + 1; t = t + 1 } 202 o = ss_w32(buf, o, vlen) 203 t = 0 204 while t < vlen { buf[o] = val[t]; o = o + 1; t = t + 1 } 205 w[1] = o 206 return 0 207} 208 209// build "<prefix>seg-<id>.docs" (+ ".tmp" if tmp==1) 210func ss_segname(prefix: *u8, segid: i64, tmp: i64, out: *u8) -> i64 { 211 var o: i64 = 0 212 o = ss_cat(out, o, prefix) 213 o = ss_cat(out, o, "seg-" as *u8) 214 o = ss_catn(out, o, segid) 215 o = ss_cat(out, o, ".docs" as *u8) 216 if tmp == 1 { o = ss_cat(out, o, ".tmp" as *u8) } 217 out[o] = 0 as u8 218 return o 219} 220 221// length-aware byte-lex compare: <0, 0, >0 (shorter strict-prefix sorts first) 222func ss_kcmp(a: *u8, al: i64, b: *u8, bl: i64) -> i64 { 223 var n: i64 = al 224 if bl < n { n = bl } 225 var i: i64 = 0 226 while i < n { 227 let ca: i64 = a[i] 228 let cb: i64 = b[i] 229 if ca != cb { return ca - cb } 230 i = i + 1 231 } 232 return al - bl 233} 234 235// Build the sorted key index blob for a writer buffer (IM2 rung). Within a 236// segment the LAST entry for a key wins (identical to scan semantics), so 237// duplicates are dropped keeping the later one. Layout: 238// "NXK1" | u32be N | N x u32be entry-rel-offset | entries: 239// u8 kind | u32be klen | key | u32be voff(into .docs) | u32be vlen 240// Returns index byte length written into kout. 241func ss_build_keys(w: *i64, kout: *u8) -> i64 { 242 let buf: *u8 = w[0] as *u8 243 let blen: i64 = w[1] 244 // capacity DATA-DRIVEN from the writer size (every record >= 9 bytes) 245 let maxn: i64 = blen / 9 + 16 246 let kls: *i64 = sys_mmap(8 * maxn) as *i64 247 let kps: *i64 = sys_mmap(8 * maxn) as *i64 248 let vos: *i64 = sys_mmap(8 * maxn) as *i64 249 let vls: *i64 = sys_mmap(8 * maxn) as *i64 250 let kinds: *i64 = sys_mmap(8 * maxn) as *i64 251 var n: i64 = 0 252 var i: i64 = 0 253 while i + 9 <= blen { 254 let kind: i64 = buf[i] 255 let kl: i64 = ss_r32(buf, i + 1) 256 let koff: i64 = i + 5 257 let vl: i64 = ss_r32(buf, koff + kl) 258 let voff: i64 = koff + kl + 4 259 if n >= maxn { return 0 - 1 } 260 kinds[n] = kind 261 kls[n] = kl 262 kps[n] = (buf as i64) + koff 263 vos[n] = voff 264 vls[n] = vl 265 n = n + 1 266 i = voff + vl 267 } 268 // sort by (key bytes, original index) -- bottom-up merge, stable; equal 269 // keys end up chronological, so the LAST of a run is the shadowing entry 270 let sidx: *i64 = sys_mmap(8 * (n + 16)) as *i64 271 let stmp: *i64 = sys_mmap(8 * (n + 16)) as *i64 272 i = 0 273 while i < n { sidx[i] = i; i = i + 1 } 274 // RESEARCH-DRIVEN FAST-PATH (Knuth TAOCP vol.3 5.2.5): when keys are UNIFORM length, a stable LSD radix 275 // sort is O(width*n) and produces a result BIT-IDENTICAL to the stable merge sort (a stable sort is 276 // unique). Variable-length keys fall through to the merge sort. Proven 5.1x on the index in nx_radix_index_bench. 277 var ss_uniform: i64 = 1 278 let ss_ul: i64 = kls[0] 279 i = 1 280 while i < n { if kls[i] != ss_ul { ss_uniform = 0 } i = i + 1 } 281 var ss_radixed: i64 = 0 282 if ss_uniform == 1 { if n > 64 { if ss_ul > 0 { 283 let rcnt: *i64 = sys_mmap(256 * 8) as *i64 284 var pos: i64 = ss_ul - 1 285 while pos >= 0 { 286 var c: i64 = 0 287 while c < 256 { rcnt[c] = 0; c = c + 1 } 288 i = 0 289 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 } 290 var sm: i64 = 0 291 c = 0 292 while c < 256 { let tc: i64 = rcnt[c]; rcnt[c] = sm; sm = sm + tc; c = c + 1 } 293 i = 0 294 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 } 295 i = 0 296 while i < n { sidx[i] = stmp[i]; i = i + 1 } 297 pos = pos - 1 298 } 299 ss_radixed = 1 300 } } } 301 if ss_radixed == 0 { 302 var width: i64 = 1 303 while width < n { 304 var lo: i64 = 0 305 while lo < n { 306 var mid: i64 = lo + width 307 if mid > n { mid = n } 308 var hi: i64 = lo + 2 * width 309 if hi > n { hi = n } 310 var a2: i64 = lo 311 var b2: i64 = mid 312 var o2: i64 = lo 313 while o2 < hi { 314 var takea: i64 = 0 315 if a2 < mid { 316 if b2 >= hi { takea = 1 } else { 317 if ss_kcmp(kps[sidx[a2]] as *u8, kls[sidx[a2]], kps[sidx[b2]] as *u8, kls[sidx[b2]]) <= 0 { takea = 1 } 318 } 319 } 320 if takea == 1 { stmp[o2] = sidx[a2]; a2 = a2 + 1 } else { stmp[o2] = sidx[b2]; b2 = b2 + 1 } 321 o2 = o2 + 1 322 } 323 o2 = lo 324 while o2 < hi { sidx[o2] = stmp[o2]; o2 = o2 + 1 } 325 lo = lo + 2 * width 326 } 327 width = width * 2 328 } 329 } 330 // collapse equal-key runs to their LAST (latest) entry 331 let idx: *i64 = sys_mmap(8 * (n + 16)) as *i64 332 var m: i64 = 0 333 i = 0 334 while i < n { 335 var j: i64 = i + 1 336 var run: i64 = 1 337 while run == 1 { 338 if j >= n { run = 0 } 339 if run == 1 { 340 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 } 341 } 342 } 343 idx[m] = sidx[j - 1] 344 m = m + 1 345 i = j 346 } 347 // emit 348 kout[0] = 78 as u8 // N 349 kout[1] = 88 as u8 // X 350 kout[2] = 75 as u8 // K 351 kout[3] = 49 as u8 // 1 352 ss_w32(kout, 4, m) 353 let tbl: i64 = 8 354 let base: i64 = 8 + 4 * m 355 var o: i64 = base 356 i = 0 357 while i < m { 358 let e: i64 = idx[i] 359 ss_w32(kout, tbl + 4 * i, o - base) 360 kout[o] = kinds[e] as u8 361 o = o + 1 362 o = ss_w32(kout, o, kls[e]) 363 let kp: *u8 = kps[e] as *u8 364 var t: i64 = 0 365 while t < kls[e] { kout[o] = kp[t]; o = o + 1; t = t + 1 } 366 o = ss_w32(kout, o, vos[e]) 367 o = ss_w32(kout, o, vls[e]) 368 i = i + 1 369 } 370 return o 371} 372 373// FAULT INJECTION: write ONLY the temp file = death before the commit point. 374// The gate uses this to prove a half-written segment is invisible. 375func ss_crashwrite(prefix: *u8, w: *i64, segid: i64) -> i64 { 376 let p: *u8 = sys_mmap(512) 377 ss_segname(prefix, segid, 1, p) 378 return ss_writefile(p, w[0] as *u8, w[1]) 379} 380 381// ---- IM2b: term postings (text search on the store) ---- 382// Tokenizer: lowercased alphanumeric runs, length 2..32 (longer runs 383// truncated at 32). IDENTICAL function serves index build, query, and the 384// gate's brute-force oracle -- consistency by construction. 385// .terms layout: "NXT1" | u32be nterms | nterms x u32be entry-off | entries: 386// u32be tlen | term | u32be postoff | u32be postlen | u32be dcount 387// .post layout: "NXP1" | u32be ndocs | ndocs x u32be docs-entry-offset | 388// per-term varint-delta docid streams (ascending; <=128 docs 389// per segment block = exactly the spec's varint tail block; 390// bitpacked 128-blocks = flagged optimization rung) 391 392// varint append (LE 7-bit, high bit = continue); returns new offset 393func ss_vw(p: *u8, off: i64, v: i64) -> i64 { 394 var m: i64 = v 395 var o: i64 = off 396 var go: i64 = 1 397 while go == 1 { 398 let b7: i64 = m % 128 399 m = m / 128 400 if m > 0 { p[o] = (b7 + 128) as u8 } else { p[o] = b7 as u8; go = 0 } 401 o = o + 1 402 } 403 return o 404} 405 406// varint read at pos[0]; advances pos 407func ss_vr(p: *u8, pos: *i64) -> i64 { 408 var v: i64 = 0 409 var sh: i64 = 0 410 var go: i64 = 1 411 while go == 1 { 412 let b: i64 = p[pos[0]] 413 pos[0] = pos[0] + 1 414 v = v + ((b % 128) << sh) 415 sh = sh + 7 416 if b < 128 { go = 0 } 417 } 418 return v 419} 420 421// 256-entry token class table: tbl[c] = 0 for separators, else the 422// lowercased byte. THE one tokenizer definition -- build, query and oracle 423// all tokenize through this table (semantic consistency by construction). 424func ss_tok_table(tbl: *u8) -> i64 { 425 var c: i64 = 0 426 while c < 256 { 427 var v: i64 = 0 428 if c >= 97 { if c <= 122 { v = c } } 429 if c >= 65 { if c <= 90 { v = c + 32 } } 430 if c >= 48 { if c <= 57 { v = c } } 431 tbl[c] = v as u8 432 c = c + 1 433 } 434 return 0 435} 436 437// ACCENT FOLD (2026-07-03, the Unicode rung): fold a decoded codepoint to its ASCII base letter -- 438// Latin-1 Supplement (U+00C0-U+00FF) + Latin Extended-A (U+0100-U+017F) via 64/128-entry base-letter 439// tables ('.' = stays a separator: multiplication/division signs). "Kyōka" tokenizes as "kyoka" on BOTH 440// the index and query sides (same ss_tok_next2), so accented pages match plain-ASCII queries; compaction 441// re-tokenizes shards through ss_build_terms = the live upgrade path. Non-Latin (CJK etc.) folds to 0 = 442// separator -- CJK SEGMENTATION is a separate named rung, honestly not claimed here. 443func ss_fold_cp(cp: i64) -> i64 { 444 if cp >= 192 { if cp <= 255 { 445 let t: *u8 = "aaaaaaaceeeeiiiidnooooo.ouuuuytsaaaaaaaceeeeiiiidnooooo.ouuuuyty" as *u8 446 let ch: i64 = t[cp - 192] as i64 447 if ch == 46 { return 0 } 448 return ch 449 } } 450 if cp >= 256 { if cp <= 383 { 451 let t2: *u8 = "aaaaaaccccccccddddeeeeeeeeeegggggggghhhhiiiiiiiiiiiijjkkkllllllllllnnnnnnnnnoooooooorrrrrrssssssssttttttuuuuuuuuuuuuwwyyyzzzzzzs" as *u8 452 return t2[cp - 256] as i64 453 } } 454 return 0 455} 456 457// MULTILINGUAL rung (2026-07-23, operator: index ru/eu/zh/ja/ko not just English): 458// decode ONE multibyte UTF-8 char at i -> packed (cp<<3)|adv; cp=0 when not a valid multibyte lead 459// (caller's ASCII fast path owns bytes <194). No allocations -- lives under the tf-scan hot loop. 460func ss_u8cp(b: *u8, sz: i64, i: i64) -> i64 { 461 if i >= sz { return 1 } 462 let c0: i64 = b[i] as i64 463 if c0 < 194 { return 1 } 464 if c0 < 224 { 465 if i + 1 < sz { let c1: i64 = b[i+1] as i64; if c1 >= 128 { if c1 < 192 { 466 return ((c0 - 192) * 64 + (c1 - 128)) * 8 + 2 467 } } } 468 return 1 469 } 470 if c0 < 240 { 471 if i + 2 < sz { 472 let c1: i64 = b[i+1] as i64 473 let c2: i64 = b[i+2] as i64 474 if c1 >= 128 { if c1 < 192 { if c2 >= 128 { if c2 < 192 { 475 return ((c0 - 224) * SS_MAGIC_4096 + (c1 - 128) * 64 + (c2 - 128)) * 8 + 3 476 } } } } 477 } 478 return 1 479 } 480 if i + 3 < sz { return 4 } 481 return 1 482} 483// case-fold a Cyrillic/Greek codepoint to its lowercase WORD form (kept as UTF-8 in the token, 484// unlike ss_fold_cp which folds TO ASCII). 0 = not a word char in these scripts. 485func ss_fold_word_cp(cp: i64) -> i64 { 486 if cp >= SS_MAGIC_1024 { if cp <= SS_MAGIC_1039 { return cp + 80 } } // U+0400-040F upper -> U+0450-045F 487 if cp >= SS_MAGIC_1040 { if cp <= SS_MAGIC_1071 { return cp + 32 } } // А-Я -> а-я 488 if cp >= SS_MAGIC_1072 { if cp <= SS_MAGIC_1279 { return cp } } // а-я + Cyrillic ext: identity 489 if cp >= 913 { if cp <= 937 { if cp != 930 { return cp + 32 } } } // Greek Α-Ω -> α-ω (03A2 hole) 490 if cp == 962 { return 963 } // final sigma -> sigma 491 if cp >= 945 { if cp <= 969 { return cp } } // α-ω identity 492 return 0 493} 494// CJK char class for BIGRAM tokenization (no-space scripts + Hangul): Han + Ext-A + compat, kana, Hangul 495// syllables. Standard CJK IR practice: overlapping character bigrams, symmetric index+query. 496func ss_is_cjk(cp: i64) -> i64 { 497 if cp >= SS_MAGIC_12352 { if cp <= SS_MAGIC_12543 { return 1 } } // Hiragana U+SS_MAGIC_3040-309F + Katakana U+30A0-30FF 498 if cp >= SS_MAGIC_13312 { if cp <= SS_MAGIC_19903 { return 1 } } // CJK Ext-A U+SS_MAGIC_3400-4DBF 499 if cp >= SS_MAGIC_19968 { if cp <= SS_MAGIC_40959 { return 1 } } // CJK Unified U+4E00-9FFF 500 if cp >= SS_MAGIC_44032 { if cp <= SS_MAGIC_55215 { return 1 } } // Hangul syllables U+AC00-D7AF 501 if cp >= SS_MAGIC_63744 { if cp <= SS_MAGIC_64255 { return 1 } } // CJK Compat U+F900-FAFF 502 return 0 503} 504 505// next token from b[pos[0]..sz): lowercased alnum run len>=2 (cap 32) into 506// tout (null-terminated); returns token length, or -1 when exhausted. 507// UTF-8 aware: Latin accents FOLD to ASCII (ss_fold_cp); Cyrillic/Greek case-fold and stay UTF-8 508// word chars (ss_fold_word_cp); CJK/kana/Hangul emit overlapping character BIGRAMS (isolated char = 509// unigram); everything else separates, whole sequences consumed so continuations never mangle a token. 510func ss_tok_next2(b: *u8, sz: i64, pos: *i64, tout: *u8, tbl: *u8) -> i64 { 511 var i: i64 = pos[0] 512 var l: i64 = 0 513 while i < sz { 514 // ASCII FAST PATH FIRST (one byte load, one compare added vs the pre-fold loop -- the tf scan 515 // over 32KB x 128 candidates lives here; the reversed branch order cost p95 169->211ms) 516 let c0: i64 = b[i] as i64 517 if c0 < 194 { 518 let m: i64 = tbl[c0] 519 if m != 0 { 520 if l < 32 { tout[l] = m as u8; l = l + 1 } 521 } 522 if m == 0 { 523 if l >= 2 { pos[0] = i + 1; tout[l] = 0 as u8; return l } 524 l = 0 525 } 526 i = i + 1 527 } else { 528 // UTF-8 lead byte (0xC2..0xF4): decode + classify 529 let v1: i64 = ss_u8cp(b, sz, i) 530 let cp: i64 = v1 >> 3 531 let adv: i64 = v1 & 7 532 // 1) Latin accent fold -> ASCII base letter continues the word 533 var f: i64 = 0 534 if cp > 0 { f = ss_fold_cp(cp) } 535 if f != 0 { 536 if l < 32 { tout[l] = f as u8; l = l + 1 } 537 i = i + adv 538 } else { 539 // 2) Cyrillic/Greek word char: case-fold, keep as UTF-8 (2 bytes) in the token 540 var wcp: i64 = 0 541 if cp > 0 { wcp = ss_fold_word_cp(cp) } 542 if wcp != 0 { 543 if l + 2 <= 32 { 544 tout[l] = (192 + wcp / 64) as u8 545 tout[l + 1] = (128 + (wcp % 64)) as u8 546 l = l + 2 547 } 548 i = i + adv 549 } else { 550 // 3) CJK/kana/Hangul: overlapping bigrams (run>=2) / unigram (isolated char) 551 var cjk: i64 = 0 552 if cp > 0 { cjk = ss_is_cjk(cp) } 553 if cjk == 1 { 554 if l >= 2 { pos[0] = i; tout[l] = 0 as u8; return l } // flush word; re-read this char next call 555 l = 0 556 let j: i64 = i + adv 557 let v2: i64 = ss_u8cp(b, sz, j) 558 let cp2: i64 = v2 >> 3 559 let adv2: i64 = v2 & 7 560 var cjk2: i64 = 0 561 if cp2 > 0 { cjk2 = ss_is_cjk(cp2) } 562 if cjk2 == 1 { 563 var x: i64 = 0 564 while x < adv { tout[x] = b[i + x]; x = x + 1 } 565 var y: i64 = 0 566 while y < adv2 { tout[adv + y] = b[j + y]; y = y + 1 } 567 tout[adv + adv2] = 0 as u8 568 // resume AT the second char while the run continues (overlap); past it at run end 569 let k: i64 = j + adv2 570 let v3: i64 = ss_u8cp(b, sz, k) 571 let cp3: i64 = v3 >> 3 572 var cjk3: i64 = 0 573 if cp3 > 0 { cjk3 = ss_is_cjk(cp3) } 574 if cjk3 == 1 { pos[0] = j } else { pos[0] = k } 575 return adv + adv2 576 } 577 var x2: i64 = 0 578 while x2 < adv { tout[x2] = b[i + x2]; x2 = x2 + 1 } 579 tout[adv] = 0 as u8 580 pos[0] = i + adv 581 return adv 582 } 583 // 4) separator (symbols, unclaimed scripts, stray sequences) 584 if l >= 2 { pos[0] = i + adv; tout[l] = 0 as u8; return l } 585 l = 0 586 i = i + adv 587 } 588 } 589 } 590 } 591 pos[0] = sz 592 if l >= 2 { tout[l] = 0 as u8; return l } 593 return 0 - 1 594} 595 596// compat wrapper (one-shot callers; hot loops build the table once instead) 597func ss_tok_next(b: *u8, sz: i64, pos: *i64, tout: *u8) -> i64 { 598 let tbl: *u8 = sys_mmap(272) 599 ss_tok_table(tbl) 600 return ss_tok_next2(b, sz, pos, tout, tbl) 601} 602 603// does the value contain token `term`? (the gate's brute-force oracle) 604func ss_tok_has(b: *u8, sz: i64, term: *u8) -> i64 { 605 let pos: *i64 = sys_mmap(16) as *i64 606 pos[0] = 0 607 let t: *u8 = sys_mmap(40) 608 let tbl: *u8 = sys_mmap(272) 609 ss_tok_table(tbl) 610 var go: i64 = 1 611 while go == 1 { 612 let l: i64 = ss_tok_next2(b, sz, pos, t, tbl) 613 if l < 0 { go = 0 } 614 if go == 1 { 615 var eq: i64 = 1 616 var x: i64 = 0 617 while eq == 1 { 618 if t[x] != term[x] { eq = 0 } 619 if eq == 1 { if t[x] == (0 as u8) { return 1 } } 620 x = x + 1 621 } 622 } 623 } 624 return 0 625} 626 627// 64-bit FNV-1a over a null-terminated token 628func ss_fnv(s: *u8) -> i64 { 629 var h: i64 = SS_MAGIC_1469598103934665603 630 var i: i64 = 0 631 while s[i] != (0 as u8) { 632 h = h ^ (s[i] as i64) 633 h = h * SS_MAGIC_1099511628211 634 i = i + 1 635 } 636 if h < 0 { h = 0 - h } 637 return h 638} 639 640// Build .terms (tblob) + .post (pblob) for a writer buffer; lengths into 641// louts[0]/louts[1]. Capacities are DATA-DRIVEN, derived from the writer 642// size (every record >= 9 bytes => docs <= blen/9; every counted (term,doc) 643// pair consumes >= 3 source bytes => pairs and unique terms <= blen/3) -- 644// no fixed caps to silently or loudly hit; the construction bounds are 645// defensive-checked LOUD (-1) anyway. Term lookup = open-addressing FNV 646// hash (O(1) per token); postings emit = counting buckets (O(pairs)); 647// term dict ordering = bottom-up merge sort (O(n log n)). 648// 2026-07-03 PHRASE RUNG (additive): qblob receives the POSITIONS sidecar ("NXQ1" | u32 nterms | 649// u32 entryoff[nterms] | per-term entries), louts[2] = its length. An entry holds, for each posting doc 650// IN THE SAME (sorted-term, ascending-doc) ORDER as .post: varint(npos) + delta-varint token indexes. 651// Positions are indexes among EMITTED tokens (the same space the query tokenizer sees; 1-char words are 652// invisible to both sides, so they never break adjacency -- documented semantics, not an accident). 653func ss_build_terms(w: *i64, tblob: *u8, pblob: *u8, qblob: *u8, louts: *i64) -> i64 { 654 let buf: *u8 = w[0] as *u8 655 let blen: i64 = w[1] 656 let maxdocs: i64 = blen / 9 + 16 657 let maxterms: i64 = blen / 3 + 64 658 let maxpairs: i64 = blen / 3 + 64 659 let maxocc: i64 = blen / 2 + 64 // every emitted token consumes >= 2 bytes of source 660 let docoff: *i64 = sys_mmap(8 * maxdocs) as *i64 661 let pool: *u8 = sys_mmap(blen + SS_MAGIC_65536) 662 var pooloff: i64 = 0 663 let terms: *i64 = sys_mmap(8 * maxterms) as *i64 664 var nterms: i64 = 0 665 let pterm: *i64 = sys_mmap(8 * maxpairs) as *i64 666 let pdoc: *i64 = sys_mmap(8 * maxpairs) as *i64 667 var npairs: i64 = 0 668 let oterm: *i64 = sys_mmap(8 * maxocc) as *i64 669 let odoc: *i64 = sys_mmap(8 * maxocc) as *i64 670 let opos: *i64 = sys_mmap(8 * maxocc) as *i64 671 var nocc: i64 = 0 672 var hts: i64 = 16 673 while hts < maxterms * 2 { hts = hts * 2 } 674 let ht: *i64 = sys_mmap(8 * hts) as *i64 675 // per-term last-doc-seen (stores nd+1; 0 = never) -- O(1) per-doc dedupe 676 let lastdoc: *i64 = sys_mmap(8 * maxterms) as *i64 677 let tlens: *i64 = sys_mmap(8 * maxterms) as *i64 678 var nd: i64 = 0 679 var i: i64 = 0 680 let pos: *i64 = sys_mmap(16) as *i64 681 let tok: *u8 = sys_mmap(40) 682 let ttbl: *u8 = sys_mmap(272) 683 ss_tok_table(ttbl) 684 while i + 9 <= blen { 685 let kind: i64 = buf[i] 686 let kl: i64 = ss_r32(buf, i + 1) 687 let vl: i64 = ss_r32(buf, i + 5 + kl) 688 let voff: i64 = i + 5 + kl + 4 689 if nd >= maxdocs { return 0 - 1 } 690 docoff[nd] = i 691 if kind == 1 { 692 pos[0] = voff 693 var tokidx: i64 = 0 694 var go: i64 = 1 695 while go == 1 { 696 let tl: i64 = ss_tok_next2(buf, voff + vl, pos, tok, ttbl) 697 if tl < 0 { go = 0 } 698 if go == 1 { 699 // find/create term id via the hash table (slot holds tid+1) 700 var tid: i64 = 0 - 1 701 var slot: i64 = ss_fnv(tok) % hts 702 var probe: i64 = 1 703 while probe == 1 { 704 if ht[slot] == 0 { probe = 0 } else { 705 let cand: i64 = ht[slot] - 1 706 let tp: *u8 = terms[cand] as *u8 707 var eq: i64 = 1 708 var x: i64 = 0 709 while eq == 1 { 710 if tp[x] != tok[x] { eq = 0 } 711 if eq == 1 { if tp[x] == (0 as u8) { tid = cand; eq = 0 } } 712 x = x + 1 713 } 714 if tid >= 0 { probe = 0 } else { 715 slot = slot + 1 716 if slot >= hts { slot = 0 } 717 } 718 } 719 } 720 if tid < 0 { 721 if nterms >= maxterms { return 0 - 1 } 722 if pooloff + tl + 2 >= blen + SS_MAGIC_65536 { return 0 - 1 } 723 let dst: *u8 = (pool as i64 + pooloff) as *u8 724 var x2: i64 = 0 725 while x2 <= tl { dst[x2] = tok[x2]; x2 = x2 + 1 } 726 terms[nterms] = dst as i64 727 tlens[nterms] = tl 728 tid = nterms 729 nterms = nterms + 1 730 pooloff = pooloff + tl + 1 731 ht[slot] = tid + 1 732 } 733 // per-doc dedupe: O(1) via the per-term last-doc-seen mark 734 if lastdoc[tid] != nd + 1 { 735 lastdoc[tid] = nd + 1 736 if npairs >= maxpairs { return 0 - 1 } 737 pterm[npairs] = tid 738 pdoc[npairs] = nd 739 npairs = npairs + 1 740 } 741 // EVERY occurrence carries its token index (the phrase rung's raw material) 742 if nocc >= maxocc { return 0 - 1 } 743 oterm[nocc] = tid 744 odoc[nocc] = nd 745 opos[nocc] = tokidx 746 nocc = nocc + 1 747 tokidx = tokidx + 1 748 } 749 } 750 } 751 nd = nd + 1 752 i = voff + vl 753 } 754 // sort term ids by term bytes (bottom-up merge sort over sidx) 755 let sidx: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 756 let stmp: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 757 var t3: i64 = 0 758 while t3 < nterms { sidx[t3] = t3; t3 = t3 + 1 } 759 var width: i64 = 1 760 while width < nterms { 761 var lo: i64 = 0 762 while lo < nterms { 763 var mid: i64 = lo + width 764 if mid > nterms { mid = nterms } 765 var hi: i64 = lo + 2 * width 766 if hi > nterms { hi = nterms } 767 var a2: i64 = lo 768 var b2: i64 = mid 769 var o2: i64 = lo 770 while o2 < hi { 771 var takea: i64 = 0 772 if a2 < mid { 773 if b2 >= hi { takea = 1 } else { 774 if ss_kcmp(terms[sidx[a2]] as *u8, tlens[sidx[a2]], terms[sidx[b2]] as *u8, tlens[sidx[b2]]) <= 0 { takea = 1 } 775 } 776 } 777 if takea == 1 { stmp[o2] = sidx[a2]; a2 = a2 + 1 } else { stmp[o2] = sidx[b2]; b2 = b2 + 1 } 778 o2 = o2 + 1 779 } 780 o2 = lo 781 while o2 < hi { sidx[o2] = stmp[o2]; o2 = o2 + 1 } 782 lo = lo + 2 * width 783 } 784 width = width * 2 785 } 786 // bucket pairs by term (counting sort; docs stay ascending within a term) 787 let dcnt: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 788 var p9: i64 = 0 789 while p9 < npairs { dcnt[pterm[p9]] = dcnt[pterm[p9]] + 1; p9 = p9 + 1 } 790 let bstart: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 791 let bfill: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 792 var acc: i64 = 0 793 var t9: i64 = 0 794 while t9 < nterms { 795 bstart[t9] = acc 796 bfill[t9] = acc 797 acc = acc + dcnt[t9] 798 t9 = t9 + 1 799 } 800 let bdoc: *i64 = sys_mmap(8 * (npairs + 16)) as *i64 801 p9 = 0 802 while p9 < npairs { 803 bdoc[bfill[pterm[p9]]] = pdoc[p9] 804 bfill[pterm[p9]] = bfill[pterm[p9]] + 1 805 p9 = p9 + 1 806 } 807 // emit .post: magic | ndocs | doc-offset table | per-term delta-varint streams 808 pblob[0] = 78 as u8 809 pblob[1] = 88 as u8 810 pblob[2] = 80 as u8 811 pblob[3] = 49 as u8 812 ss_w32(pblob, 4, nd) 813 var po: i64 = 8 + 4 * nd 814 var d2: i64 = 0 815 while d2 < nd { ss_w32(pblob, 8 + 4 * d2, docoff[d2]); d2 = d2 + 1 } 816 let postoffs: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 817 let postlens: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 818 let dcounts: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 819 t3 = 0 820 while t3 < nterms { 821 let tid2: i64 = sidx[t3] 822 postoffs[t3] = po 823 var prev: i64 = 0 824 var p4: i64 = bstart[tid2] 825 let pend: i64 = bstart[tid2] + dcnt[tid2] 826 while p4 < pend { 827 po = ss_vw(pblob, po, bdoc[p4] - prev) 828 prev = bdoc[p4] 829 p4 = p4 + 1 830 } 831 postlens[t3] = po - postoffs[t3] 832 dcounts[t3] = dcnt[tid2] 833 t3 = t3 + 1 834 } 835 // emit .terms: magic | nterms | entry-off table | entries 836 tblob[0] = 78 as u8 837 tblob[1] = 88 as u8 838 tblob[2] = 84 as u8 839 tblob[3] = 49 as u8 840 ss_w32(tblob, 4, nterms) 841 let base: i64 = 8 + 4 * nterms 842 var to: i64 = base 843 t3 = 0 844 while t3 < nterms { 845 ss_w32(tblob, 8 + 4 * t3, to - base) 846 let tp2: *u8 = terms[sidx[t3]] as *u8 847 let tl2: i64 = tlens[sidx[t3]] 848 to = ss_w32(tblob, to, tl2) 849 var x3: i64 = 0 850 while x3 < tl2 { tblob[to] = tp2[x3]; to = to + 1; x3 = x3 + 1 } 851 to = ss_w32(tblob, to, postoffs[t3]) 852 to = ss_w32(tblob, to, postlens[t3]) 853 to = ss_w32(tblob, to, dcounts[t3]) 854 t3 = t3 + 1 855 } 856 // emit the POSITIONS sidecar blob (NXQ1): bucket occurrences by term (counting sort preserves the 857 // (doc asc, position asc) collection order), then per SORTED term emit doc-runs aligned with .post: 858 // varint(npos) + delta-varint token indexes. NOTE the magic: .post already uses "NXP1" -- NXQ1 here. 859 let ocnt: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 860 var o9: i64 = 0 861 while o9 < nocc { ocnt[oterm[o9]] = ocnt[oterm[o9]] + 1; o9 = o9 + 1 } 862 let ostart: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 863 let ofill: *i64 = sys_mmap(8 * (nterms + 16)) as *i64 864 var oacc: i64 = 0 865 var t8: i64 = 0 866 while t8 < nterms { 867 ostart[t8] = oacc 868 ofill[t8] = oacc 869 oacc = oacc + ocnt[t8] 870 t8 = t8 + 1 871 } 872 let obdoc: *i64 = sys_mmap(8 * (nocc + 16)) as *i64 873 let obpos: *i64 = sys_mmap(8 * (nocc + 16)) as *i64 874 o9 = 0 875 while o9 < nocc { 876 let f9: i64 = ofill[oterm[o9]] 877 obdoc[f9] = odoc[o9] 878 obpos[f9] = opos[o9] 879 ofill[oterm[o9]] = f9 + 1 880 o9 = o9 + 1 881 } 882 qblob[0] = 78 as u8 883 qblob[1] = 88 as u8 884 qblob[2] = 81 as u8 885 qblob[3] = 49 as u8 886 ss_w32(qblob, 4, nterms) 887 let qbase: i64 = 8 + 4 * nterms 888 var qo: i64 = qbase 889 t3 = 0 890 while t3 < nterms { 891 let tid3: i64 = sidx[t3] 892 ss_w32(qblob, 8 + 4 * t3, qo - qbase) 893 var r0: i64 = ostart[tid3] 894 let rend: i64 = ostart[tid3] + ocnt[tid3] 895 while r0 < rend { 896 // one doc-run: occurrences of THIS doc are contiguous (collection order) 897 let dcur: i64 = obdoc[r0] 898 var r1: i64 = r0 899 var run9: i64 = 1 900 while run9 == 1 { 901 if r1 >= rend { run9 = 0 } else { 902 if obdoc[r1] == dcur { r1 = r1 + 1 } else { run9 = 0 } 903 } 904 } 905 qo = ss_vw(qblob, qo, r1 - r0) 906 var prevp: i64 = 0 907 var r2: i64 = r0 908 while r2 < r1 { 909 qo = ss_vw(qblob, qo, obpos[r2] - prevp) 910 prevp = obpos[r2] 911 r2 = r2 + 1 912 } 913 r0 = r1 914 } 915 t3 = t3 + 1 916 } 917 louts[0] = to 918 louts[1] = po 919 louts[2] = qo 920 return 0 921} 922 923// ---- IMPACT-ORDERED POSTINGS sidecar (2026-07-25, the WAND rung, seq606) ------------------------ 924// .imp layout: "NXW1" | u32be nterms | nterms x u32be entry-off (relative to table end) | per SORTED 925// term (same ordinal as .terms/NXQ1): varint(k) then k x (varint docidx, varint tf) -- the k highest- 926// tf postings of the term (k = min(dcount, SS_IMP_K); set membership is by tf, walk order within). 927// WHY: per-term candidacy caps truncated in ASCENDING-DOC order, so a high-tf doc past the cap (an 928// entity's PROFILE page under a common name) could never become a candidate at any score. The impact 929// list makes the cap keep the BEST postings, and the stored tf makes candidacy need zero doc reads. 930// OPTIONAL like .pos: absent file -> readers fall back to ss_term; ss_write_seg emits it, so both 931// compaction AND nx_seg_imp_build (in-place, additive-only) are format-upgrade paths. 932const SS_IMP_K: i64 = 512 933 934// top-k selection threshold via a tf histogram (tf clamped into 1023 buckets so the scan is O(n+1024) 935// whatever the tf range): outs[0]=T, outs[1]=count(clamped tf > T). Emit contract used by builder and 936// reader: every clamped-tf>T item plus (k - outs[1]) clamped-tf==T items = exactly min(n,k) items. 937func ss_imp_thresh(tfs: *i64, n: i64, k: i64, outs: *i64) -> i64 { 938 if n <= k { outs[0] = 0; outs[1] = n; return 0 } 939 let hist: *i64 = sys_mmap(8 * SS_MAGIC_1024) as *i64 940 var i: i64 = 0 941 while i < n { 942 var v: i64 = tfs[i] 943 if v > 1023 { v = 1023 } 944 if v < 0 { v = 0 } 945 hist[v] = hist[v] + 1 946 i = i + 1 947 } 948 var acc: i64 = 0 949 var b: i64 = 1023 950 var t: i64 = 0 951 var going: i64 = 1 952 while going == 1 { 953 if b < 0 { going = 0 } else { 954 if acc + hist[b] >= k { t = b; going = 0 } else { acc = acc + hist[b]; b = b - 1 } 955 } 956 } 957 outs[0] = t 958 outs[1] = acc 959 return 0 960} 961 962// build the .imp blob from the three emitted index blobs alone (no writer-buffer access), so a LIVE 963// segment can be upgraded from its .idx + .pos without recompacting. Returns blob length, -1 LOUD on 964// malformed input. tf of (term,doc) = the NXQ1 doc-run occurrence count (npos), true value stored 965// unclamped -- the 1023 clamp exists only inside selection. 966func ss_build_imp(tb: *u8, tsz: i64, pb: *u8, psz: i64, qb: *u8, qsz: i64, wblob: *u8) -> i64 { 967 if tsz < 8 { return 0 - 1 } 968 if psz < 8 { return 0 - 1 } 969 if qsz < 8 { return 0 - 1 } 970 if qb[0] != (78 as u8) { return 0 - 1 } 971 if qb[2] != (81 as u8) { return 0 - 1 } 972 let nterms: i64 = ss_r32(tb, 4) 973 if ss_r32(qb, 4) != nterms { return 0 - 1 } 974 let nd: i64 = ss_r32(pb, 4) 975 let tbase: i64 = 8 + 4 * nterms 976 let qbase: i64 = 8 + 4 * nterms 977 let docs: *i64 = sys_mmap(8 * (nd + 16)) as *i64 978 let tfs: *i64 = sys_mmap(8 * (nd + 16)) as *i64 979 let pv: *i64 = sys_mmap(16) as *i64 980 let qv: *i64 = sys_mmap(16) as *i64 981 let touts: *i64 = sys_mmap(32) as *i64 982 wblob[0] = 78 as u8 983 wblob[1] = 88 as u8 984 wblob[2] = 87 as u8 985 wblob[3] = 49 as u8 986 ss_w32(wblob, 4, nterms) 987 let wbase: i64 = 8 + 4 * nterms 988 var wo: i64 = wbase 989 var t: i64 = 0 990 while t < nterms { 991 ss_w32(wblob, 8 + 4 * t, wo - wbase) 992 let eo: i64 = tbase + ss_r32(tb, 8 + 4 * t) 993 let tl: i64 = ss_r32(tb, eo) 994 let postoff: i64 = ss_r32(tb, eo + 4 + tl) 995 let dcount: i64 = ss_r32(tb, eo + 4 + tl + 8) 996 if dcount > nd { return 0 - 1 } 997 // lockstep walk: .post deltas give ascending docs, the NXQ1 run headers give per-doc tf 998 pv[0] = postoff 999 qv[0] = qbase + ss_r32(qb, 8 + 4 * t) 1000 var prev: i64 = 0 1001 var i: i64 = 0 1002 while i < dcount { 1003 if qv[0] >= qsz { return 0 - 1 } 1004 prev = prev + ss_vr(pb, pv) 1005 docs[i] = prev 1006 let np: i64 = ss_vr(qb, qv) 1007 var sk: i64 = 0 1008 while sk < np { ss_vr(qb, qv); sk = sk + 1 } 1009 tfs[i] = np 1010 i = i + 1 1011 } 1012 ss_imp_thresh(tfs, dcount, SS_IMP_K, touts) 1013 let thr: i64 = touts[0] 1014 var k2: i64 = dcount 1015 if k2 > SS_IMP_K { k2 = SS_IMP_K } 1016 var room: i64 = k2 - touts[1] 1017 wo = ss_vw(wblob, wo, k2) 1018 // pass 1: every clamped tf > T; pass 2: clamped tf == T fills the remaining room 1019 var pass: i64 = 0 1020 while pass < 2 { 1021 i = 0 1022 while i < dcount { 1023 var cv: i64 = tfs[i] 1024 if cv > 1023 { cv = 1023 } 1025 var take: i64 = 0 1026 if pass == 0 { if cv > thr { take = 1 } } 1027 if pass == 1 { if cv == thr { if room > 0 { take = 1; room = room - 1 } } } 1028 if take == 1 { wo = ss_vw(wblob, wo, docs[i]); wo = ss_vw(wblob, wo, tfs[i]) } 1029 i = i + 1 1030 } 1031 pass = pass + 1 1032 } 1033 t = t + 1 1034 } 1035 return wo 1036} 1037 1038// build "<prefix>seg-<id>.terms" / ".post" (+ ".tmp") 1039func ss_auxname(prefix: *u8, segid: i64, ext: *u8, tmp: i64, out: *u8) -> i64 { 1040 var o: i64 = 0 1041 o = ss_cat(out, o, prefix) 1042 o = ss_cat(out, o, "seg-" as *u8) 1043 o = ss_catn(out, o, segid) 1044 o = ss_cat(out, o, ext) 1045 if tmp == 1 { o = ss_cat(out, o, ".tmp" as *u8) } 1046 out[o] = 0 as u8 1047 return o 1048} 1049 1050// write one segment's .docs + .keys files (temp -> rename each); NOT yet 1051// visible to readers until a manifest names it (commit or compaction swap) 1052// seq1730 COMPACTOR HOLE CLOSED: the guard used to sit at ss_commit ONLY, but compactors 1053// (nx_web_shard_compact.nx:165, nx_shard_compact.nx) call ss_write_seg DIRECTLY and then hand-write 1054// the manifest -- they never touch ss_commit, so the old placement had a hole EXACTLY where 1055// dp-web-pub- was actually corrupted. ss_write_seg is the deepest point BOTH paths share. 1056// ss_segid_ok is defined further down this file; forward references resolve (probed in isolation 1057// via nx_fwdref_probe: build exit=0 AND run exit=0), so no definition move and no compile-break 1058// window on a lib every organ imports. 1059func ss_write_seg(prefix: *u8, w: *i64, segid: i64) -> i64 { 1060 if ss_segid_ok(segid) == 0 { 1061 let eb: *u8 = sys_mmap(512) 1062 var eo: i64 = ss_cat(eb, 0, "SS-WRITE-SEG REFUSED: segid=" as *u8) 1063 eo = ss_catn(eb, eo, segid) 1064 eo = ss_cat(eb, eo, " is an ADDRESS or negative, not a segment id -- refusing to poison plane " as *u8) 1065 eo = ss_cat(eb, eo, prefix) 1066 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) 1067 sys_write(2, eb, eo) 1068 return 0 - 7 1069 } 1070 let pt: *u8 = sys_mmap(512) 1071 let pf: *u8 = sys_mmap(512) 1072 ss_segname(prefix, segid, 1, pt) 1073 ss_segname(prefix, segid, 0, pf) 1074 if ss_writefile(pt, w[0] as *u8, w[1]) != 0 { return 0 - 1 } 1075 if sys_renameat(pt, pf) != 0 { return 0 - 2 } 1076 // MERGED aux index (IM6c): keys + terms + post in ONE seg-<id>.idx file 1077 // "NXI1" | u32be klen | u32be tlen | u32be plen | keys | terms | post 1078 // -- one write + fsync + rename instead of three (the race-localized 1079 // fsync-ceremony cost), same temp -> rename discipline. Readers slice. 1080 let kblob: *u8 = sys_mmap(w[1] * 3 + SS_MAGIC_65536) 1081 let klen2: i64 = ss_build_keys(w, kblob) 1082 if klen2 < 0 { return 0 - 5 } 1083 let tblob: *u8 = sys_mmap(w[1] * 8 + SS_MAGIC_65536) 1084 let pblob: *u8 = sys_mmap(w[1] * 8 + SS_MAGIC_65536) 1085 let qblob: *u8 = sys_mmap(w[1] * 4 + SS_MAGIC_65536) 1086 let louts: *i64 = sys_mmap(32) as *i64 1087 if ss_build_terms(w, tblob, pblob, qblob, louts) != 0 { return 0 - 11 } 1088 let isz: i64 = 16 + klen2 + louts[0] + louts[1] 1089 let iblob: *u8 = sys_mmap(isz + 64) 1090 iblob[0] = 78 as u8 1091 iblob[1] = 88 as u8 1092 iblob[2] = 73 as u8 1093 iblob[3] = 49 as u8 1094 ss_w32(iblob, 4, klen2) 1095 ss_w32(iblob, 8, louts[0]) 1096 ss_w32(iblob, 12, louts[1]) 1097 var co: i64 = 16 1098 var ci: i64 = 0 1099 while ci < klen2 { iblob[co] = kblob[ci]; co = co + 1; ci = ci + 1 } 1100 ci = 0 1101 while ci < louts[0] { iblob[co] = tblob[ci]; co = co + 1; ci = ci + 1 } 1102 ci = 0 1103 while ci < louts[1] { iblob[co] = pblob[ci]; co = co + 1; ci = ci + 1 } 1104 let it: *u8 = sys_mmap(512) 1105 let if2: *u8 = sys_mmap(512) 1106 ss_auxname(prefix, segid, ".idx" as *u8, 1, it) 1107 ss_auxname(prefix, segid, ".idx" as *u8, 0, if2) 1108 if ss_writefile(it, iblob, isz) != 0 { return 0 - 7 } 1109 if sys_renameat(it, if2) != 0 { return 0 - 8 } 1110 // POSITIONS SIDECAR (2026-07-03, phrase rung): seg-<id>.pos = the NXQ1 blob. OPTIONAL by design -- 1111 // an old segment simply lacks the file and phrase queries degrade to AND there; compaction rebuilds 1112 // through THIS function, so compacting a shard upgrades it. Same temp->rename discipline, fail LOUD. 1113 let qt: *u8 = sys_mmap(512) 1114 let qf: *u8 = sys_mmap(512) 1115 ss_auxname(prefix, segid, ".pos" as *u8, 1, qt) 1116 ss_auxname(prefix, segid, ".pos" as *u8, 0, qf) 1117 if ss_writefile(qt, qblob, louts[2]) != 0 { return 0 - 13 } 1118 if sys_renameat(qt, qf) != 0 { return 0 - 14 } 1119 // IMPACT sidecar (2026-07-25, WAND rung): seg-<id>.imp = the NXW1 blob. OPTIONAL like .pos -- 1120 // readers fall back when absent; compaction runs through here so compacting upgrades a shard. 1121 let wcap: i64 = louts[0] * 2 + louts[1] * 10 + SS_MAGIC_65536 1122 let wblob2: *u8 = sys_mmap(wcap) 1123 let wlen: i64 = ss_build_imp(tblob, louts[0], pblob, louts[1], qblob, louts[2], wblob2) 1124 if wlen < 0 { return 0 - 15 } 1125 let wt: *u8 = sys_mmap(512) 1126 let wf: *u8 = sys_mmap(512) 1127 ss_auxname(prefix, segid, ".imp" as *u8, 1, wt) 1128 ss_auxname(prefix, segid, ".imp" as *u8, 0, wf) 1129 if ss_writefile(wt, wblob2, wlen) != 0 { return 0 - 16 } 1130 if sys_renameat(wt, wf) != 0 { return 0 - 17 } 1131 return 0 1132} 1133 1134// load a segment's index blobs: NEW merged .idx (sliced) or LEGACY 3-file 1135// (.keys/.terms/.post) fallback -- compaction is the format-upgrade path. 1136// outs: [0]=keys ptr [1]=keys sz [2]=terms ptr [3]=terms sz [4]=post ptr [5]=post sz 1137func ss_load_aux(prefix: *u8, segname: *u8, outs: *i64) -> i64 { 1138 return ss_load_aux2(prefix, segname, outs, 0) 1139} 1140func ss_load_aux2(prefix: *u8, segname: *u8, outs: *i64, usemmap: i64) -> i64 { 1141 let path: *u8 = sys_mmap(512) 1142 var o: i64 = 0 1143 o = ss_cat(path, o, prefix) 1144 o = ss_cat(path, o, segname) 1145 o = ss_cat(path, o, ".idx" as *u8) 1146 path[o] = 0 as u8 1147 let szp: *i64 = sys_mmap(16) as *i64 1148 let ib: *u8 = ss_loadfile(path, szp, usemmap) 1149 if szp[0] >= 16 { if ib[0] == (78 as u8) { if ib[2] == (73 as u8) { 1150 let kl: i64 = ss_r32(ib, 4) 1151 let tl: i64 = ss_r32(ib, 8) 1152 let pl: i64 = ss_r32(ib, 12) 1153 if 16 + kl + tl + pl <= szp[0] { 1154 outs[0] = (ib as i64) + 16 1155 outs[1] = kl 1156 outs[2] = (ib as i64) + 16 + kl 1157 outs[3] = tl 1158 outs[4] = (ib as i64) + 16 + kl + tl 1159 outs[5] = pl 1160 // Release this call's scratch before handing back (seq997). `ib` is NOT freed -- outs[] point 1161 // INTO it and the handle owns it from here; ss_close reclaims it as ONE mapping via the 1162 // merged-.idx branch. Leaking these two cost ~2 pages x nsegs x opens, the last T8 residual. 1163 sys_munmap(path, 512) 1164 sys_munmap(szp as *u8, 16) 1165 return 1 1166 } 1167 } } } 1168 // legacy fallback: three separate files 1169 var fi: i64 = 0 1170 while fi < 3 { 1171 var ext: *u8 = ".keys" as *u8 1172 if fi == 1 { ext = ".terms" as *u8 } 1173 if fi == 2 { ext = ".post" as *u8 } 1174 o = 0 1175 o = ss_cat(path, o, prefix) 1176 o = ss_cat(path, o, segname) 1177 o = ss_cat(path, o, ext) 1178 path[o] = 0 as u8 1179 outs[fi * 2] = ss_loadfile(path, szp, usemmap) as i64 1180 outs[fi * 2 + 1] = szp[0] 1181 fi = fi + 1 1182 } 1183 return 0 1184} 1185 1186// COMMIT: seg temp -> rename into place; manifest rewritten via temp -> rename. 1187// The manifest rename IS the commit point (atomicity without a txn engine). 1188// ---- seg-id sanity (seq1730: POINTER-SHAPED SEGIDS SILENTLY CORRUPTED SEVEN PLANES) --------------- 1189// A segment id is either a small counter (1, 2, 1001) or an epoch (sec ~1.8e9, ms ~1.8e12, us ~1.8e15). 1190// It is NEVER AN ADDRESS. Seven planes under knowledge/store were poisoned by a caller passing a pointer 1191// into the segid slot, and because ss_next_segid derives max(existing)+1, ONE poisoned id PINS that plane 1192// near 1.4e14 FOREVER: every later epoch-derived write then sorts BELOW the poisoned segment in supersede 1193// order, so its rows are shadowed by older data -- silently, unboundedly, while the writer sees rc=0. 1194// THE BAND IS SEPARABLE BY CONSTRUCTION, NOT BY TASTE: x86-64 user-space mmap lives under 2^47, and the 1195// observed poison sat at 1.35e14-1.41e14. Epoch-ms (~1.8e12) is two orders BELOW the low bound; epoch-us 1196// (~1.8e15) is an order ABOVE the high bound. So no legitimate id scale can collide with this window. 1197// BOUND HERE because ss_commit is THE ONE ACT EVERY WRITER PERFORMS -- 408 call sites, one chokepoint. 1198// A fix that needs 408 authors to remember it is not a fix. 1199const SS_SEGID_PTRBAND_LO: i64 = 10000000000000 1200const SS_SEGID_PTRBAND_HI: i64 = 140737488355328 1201 1202// 1 = a usable segment id, 0 = refuse (negative, or inside the user-space pointer band). 1203func ss_segid_ok(v: i64) -> i64 { 1204 if v < 0 { return 0 } 1205 if v >= SS_SEGID_PTRBAND_LO { if v < SS_SEGID_PTRBAND_HI { return 0 } } 1206 return 1 1207} 1208 1209func ss_commit(prefix: *u8, w: *i64, segid: i64) -> i64 { 1210 if ss_segid_ok(segid) == 0 { 1211 let eb: *u8 = sys_mmap(512) 1212 var eo: i64 = ss_cat(eb, 0, "SS-COMMIT REFUSED: segid=" as *u8) 1213 eo = ss_catn(eb, eo, segid) 1214 eo = ss_cat(eb, eo, " is an ADDRESS or negative, not a segment id -- refusing to poison plane " as *u8) 1215 eo = ss_cat(eb, eo, prefix) 1216 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) 1217 sys_write(2, eb, eo) 1218 return 0 - 7 1219 } 1220 let wrc: i64 = ss_write_seg(prefix, w, segid) 1221 if wrc != 0 { return wrc } 1222 let mf: *u8 = sys_mmap(512) 1223 let mt: *u8 = sys_mmap(512) 1224 var o: i64 = 0 1225 o = ss_cat(mf, o, prefix) 1226 o = ss_cat(mf, o, "manifest.txt" as *u8) 1227 mf[o] = 0 as u8 1228 o = 0 1229 o = ss_cat(mt, o, prefix) 1230 o = ss_cat(mt, o, "manifest.tmp" as *u8) 1231 mt[o] = 0 as u8 1232 let szp: *i64 = sys_mmap(16) as *i64 1233 let old: *u8 = ss_readall(mf, szp) 1234 var osz: i64 = szp[0] 1235 if osz < 0 { osz = 0 } 1236 let nb: *u8 = sys_mmap(osz + 128) 1237 var no: i64 = 0 1238 var t: i64 = 0 1239 while t < osz { nb[no] = old[t]; no = no + 1; t = t + 1 } 1240 no = ss_cat(nb, no, "seg-" as *u8) 1241 no = ss_catn(nb, no, segid) 1242 nb[no] = 10 as u8 1243 no = no + 1 1244 if ss_writefile(mt, nb, no) != 0 { return 0 - 3 } 1245 if sys_renameat(mt, mf) != 0 { return 0 - 4 } 1246 // power-loss closure: the renames themselves reach stable storage 1247 ss_syncdir(prefix) 1248 return 0 1249} 1250 1251// ---- BATCHED DURABILITY (added 2026-08-01 ws=legal, PROFILE-DRIVEN, debt 1785610685) -------------- 1252// MEASURED, not guessed: nx_regprof_test decomposed reg_put on a fresh plane and found 1253// lock_cycle=35us u00b7 ss_get_absent=19us u00b7 ss_get@20seg=371us u00b7 reg_put=129335us steady-state. 1254// The lock is 0.03% of a write and the read path 0.3%, and cost FALLS as segments accumulate -- so 1255// neither the plane lock nor the full-index-per-segment shape is the cost. What remains is the 1256// per-commit DIRECTORY FSYNC on the line above: one durability barrier per appended row. 1257// 1258// u2605THE POINT: ss_commit is CORRECT and stays byte-for-byte unchanged. A single logical transaction 1259// that appends N rows does not need N barriers -- it needs ONE, at the end. Paying per row is paying 1260// for a guarantee nobody asked for at that granularity. 1261// 1262// ADDITIVE BY CONSTRUCTION: ss_commit and all 261 of its callers are untouched. A caller opts in by 1263// using ss_commit_deferred for the rows and calling ss_sync_now ONCE afterwards. 1264// 1265// u2605u2605THE HONEST BOUND, stated so nobody mistakes what this buys: after ss_commit_deferred the data IS 1266// VISIBLE -- the segment file is written and the manifest rename has already happened, so any reader 1267// sees the row immediately. ONLY DURABILITY ACROSS POWER LOSS is deferred. A crash between the last 1268// deferred commit and ss_sync_now can lose the tail of the batch. That is exactly the trade a batch 1269// wants and exactly the trade a single critical write must NOT make: u2605USE ss_commit FOR ROWS THAT MUST 1270// SURVIVE A CRASH ON THEIR OWN, ss_commit_deferred ONLY INSIDE A BATCH YOU WILL SYNC. 1271func ss_commit_deferred(prefix: *u8, w: *i64, segid: i64) -> i64 { 1272 if ss_segid_ok(segid) == 0 { 1273 let eb: *u8 = sys_mmap(512) 1274 var eo: i64 = ss_cat(eb, 0, "SS-COMMIT-DEFERRED REFUSED: segid=" as *u8) 1275 eo = ss_catn(eb, eo, segid) 1276 eo = ss_cat(eb, eo, " is an ADDRESS or negative, not a segment id -- refusing to poison plane " as *u8) 1277 eo = ss_cat(eb, eo, prefix) 1278 eo = ss_cat(eb, eo, ". NOTHING WRITTEN.\n" as *u8) 1279 sys_write(2, eb, eo) 1280 return 0 - 7 1281 } 1282 let wrc: i64 = ss_write_seg(prefix, w, segid) 1283 if wrc != 0 { return wrc } 1284 let mf: *u8 = sys_mmap(512) 1285 let mt: *u8 = sys_mmap(512) 1286 var o: i64 = 0 1287 o = ss_cat(mf, o, prefix) 1288 o = ss_cat(mf, o, "manifest.txt" as *u8) 1289 mf[o] = 0 as u8 1290 o = 0 1291 o = ss_cat(mt, o, prefix) 1292 o = ss_cat(mt, o, "manifest.tmp" as *u8) 1293 mt[o] = 0 as u8 1294 let szp: *i64 = sys_mmap(16) as *i64 1295 let old: *u8 = ss_readall(mf, szp) 1296 var osz: i64 = szp[0] 1297 if osz < 0 { osz = 0 } 1298 let nb: *u8 = sys_mmap(osz + 128) 1299 var no: i64 = 0 1300 var t: i64 = 0 1301 while t < osz { nb[no] = old[t]; no = no + 1; t = t + 1 } 1302 no = ss_cat(nb, no, "seg-" as *u8) 1303 no = ss_catn(nb, no, segid) 1304 nb[no] = 10 as u8 1305 no = no + 1 1306 if ss_writefile(mt, nb, no) != 0 { return 0 - 3 } 1307 if sys_renameat(mt, mf) != 0 { return 0 - 4 } 1308 // u2605deliberately NO ss_syncdir here -- that is the whole difference, and the caller owes one. 1309 return 0 1310} 1311 1312// u2605THE BARRIER THE BATCH OWES. Call once after a run of ss_commit_deferred. Idempotent and cheap to 1313// over-call: syncing a directory that is already durable is a no-op, so when in doubt, call it. 1314func ss_sync_now(prefix: *u8) -> i64 { 1315 ss_syncdir(prefix) 1316 return 0 1317} 1318 1319// -- named caps (rule-11 burn-down of the 256/260 magic-number class) --------------------------------- 1320// SS_MANIFEST_LEGACY_CAP: the byte-identical legacy API cap (ss_manifest/_file). UNCAPPED paths exist and 1321// are canonical: ss_manifest_dyn (enumeration) + ss_next_segid (writers). New code must NOT use the legacy 1322// capped pair -- past the cap, count-as-segid CLOBBERS the cap segment (the reg_put persistence bug class). 1323const SS_MANIFEST_LEGACY_CAP: i64 = 256 1324const SS_VER_SLOTS: i64 = 260 // per-key version buffer allocation (kinds/ptrs/lens/srcs slots) 1325const SS_VER_WINDOW: i64 = 256 // versions of ONE key a scan retains; on overflow the OLDEST slides out 1326 // so the LATEST is ALWAYS kept (ss_get correctness for high-churn keys 1327 // like every registry's __idx__, which gains a version per put) 1328 1329// parse a manifest-format file (one "seg-<id>" name per line) into segs[]; 1330// returns count. Serves BOTH the live manifest and the compaction archive. 1331// LEGACY-CAPPED (SS_MANIFEST_LEGACY_CAP): kept byte-identical for old callers; use ss_manifest_dyn. 1332func ss_manifest_file(prefix: *u8, fname: *u8, segs: *i64) -> i64 { 1333 let mf: *u8 = sys_mmap(512) 1334 var o: i64 = 0 1335 o = ss_cat(mf, o, prefix) 1336 o = ss_cat(mf, o, fname) 1337 mf[o] = 0 as u8 1338 let szp: *i64 = sys_mmap(16) as *i64 1339 let b: *u8 = ss_readall(mf, szp) 1340 let sz: i64 = szp[0] 1341 if sz <= 0 { return 0 } 1342 var cnt: i64 = 0 1343 var i: i64 = 0 1344 var ls: i64 = 0 1345 while i < sz { 1346 if b[i] == (10 as u8) { 1347 let name: *u8 = sys_mmap(128) 1348 var t: i64 = 0 1349 while ls + t < i { name[t] = b[ls + t]; t = t + 1 } 1350 name[t] = 0 as u8 1351 if cnt < SS_MANIFEST_LEGACY_CAP { segs[cnt] = name as i64; cnt = cnt + 1 } 1352 ls = i + 1 1353 } 1354 i = i + 1 1355 } 1356 return cnt 1357} 1358 1359// parse the LIVE manifest into segs[]; returns count 1360// LEGACY-CAPPED: writers must use ss_next_segid; enumerators ss_manifest_dyn. 1361func ss_manifest(prefix: *u8, segs: *i64) -> i64 { 1362 return ss_manifest_file(prefix, "manifest.txt" as *u8, segs) 1363} 1364 1365// SOTA data-driven manifest read (NO hardcoded cap -- the 256/260 magic-number cap is the debt this eats): 1366// read the live manifest ONCE, size the segs[] buffer from the ACTUAL entry count, and return the WHOLE 1367// corpus's live segment list. *out_segs receives the freshly-allocated, exactly-sized buffer; returns count. 1368// Sentinel guarding the stashed manifest-buffer slots in the segs slack. ss_manifest_free only trusts 1369// those slots when it sees this value, so a short read (cnt < mc, leaving a NAME pointer at segs[n]) 1370// degrades to leaking rather than munmapping an address that was never a manifest buffer. 1371// 1372// DECLARED HERE, ABOVE ITS FIRST READER, 2026-07-26. It used to sit BELOW ss_manifest_file_dyn, which 1373// writes it at `segs[mc] = SS_MF_MAGIC`. A module const read before its declaration silently evaluated 1374// to 0, so the writer stamped 0 while ss_manifest_free compared against the real 0x53534D46 -- the 1375// sentinel COULD NEVER MATCH and the stashed manifest buffer (segs[n+1], size segs[n+2]) was NEVER 1376// munmapped. A real leak on every manifest read, in the most-shared primitive in the ecosystem, in the 1377// same memory class as seq905/seq975 that this file's own comments already reference. It stayed 1378// invisible until the sovereign compiler learned to REFUSE use-before-declaration instead of silently 1379// reading 0. KEEP THIS ABOVE ss_manifest_file_dyn. 1380const SS_MF_MAGIC: i64 = 0x53534D46 1381func ss_manifest_file_dyn(prefix: *u8, fname: *u8, out_segs: *i64) -> i64 { 1382 let mf: *u8 = sys_mmap(512) 1383 var o: i64 = 0 1384 o = ss_cat(mf, o, prefix) 1385 o = ss_cat(mf, o, fname) 1386 mf[o] = 0 as u8 1387 let szp: *i64 = sys_mmap(16) as *i64 1388 let b: *u8 = ss_readall(mf, szp) 1389 let sz: i64 = szp[0] 1390 if sz <= 0 { out_segs[0] = sys_mmap(64) as i64; return 0 } 1391 var mc: i64 = 0 1392 var i: i64 = 0 1393 while i < sz { if b[i] == (10 as u8) { mc = mc + 1 } i = i + 1 } 1394 // NAME POOL (seq905/seq975): each segment name used to get its OWN sys_mmap(128) -- a full 4 KiB page for 1395 // a 128-byte string, so an N-segment store burned N pages PER OPEN (the live 117-segment vault store: 1396 // ~468 KiB per open, leaked forever). One pooled allocation, sliced 128 bytes per name. 1397 // mf/szp are dead here (sz already extracted); free them rather than leak a page each per open. 1398 sys_munmap(mf, 512) 1399 sys_munmap(szp as *u8, 16) 1400 let namepool: *u8 = sys_mmap(128 * mc + 128) 1401 let segs: *i64 = sys_mmap(8 * mc + 64) as *i64 1402 // Hand the manifest read to ss_manifest_free via the slack slots (mc+8 slots exist; mc..mc+2 used). 1403 segs[mc] = SS_MF_MAGIC 1404 segs[mc + 1] = b as i64 1405 segs[mc + 2] = sz + 64 1406 out_segs[0] = segs as i64 1407 var cnt: i64 = 0 1408 var ls: i64 = 0 1409 i = 0 1410 while i < sz { 1411 if b[i] == (10 as u8) { 1412 let name: *u8 = sys_mmap(128) 1413 var t: i64 = 0 1414 while ls + t < i { name[t] = b[ls + t]; t = t + 1 } 1415 name[t] = 0 as u8 1416 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) } 1417 ls = i + 1 1418 } 1419 i = i + 1 1420 } 1421 return cnt 1422} 1423// live-manifest convenience over ss_manifest_file_dyn (data-driven, no cap). 1424// ss_manifest_free -- release what ss_manifest_file_dyn handed back through out_segs. Names are POOLED 1425// contiguously from offset 0, so segs[0] IS the pool base -- no metadata slot and no signature change needed. 1426// Sizes are computed from the RETURNED count n, which is <= the allocated mc, so this UNDER-frees by at most 1427// a page rather than over-freeing: munmapping less than was mapped is safe, munmapping more can release a 1428// neighbouring mapping. Callers pair this with ss_manifest_dyn exactly as ss_close pairs with ss_open. 1429// SS_MF_MAGIC is declared at the TOP of this file, above ss_manifest_file_dyn which writes it. 1430// Declaring it here meant the writer read 0 and the sentinel never matched -- see the note there. 1431func ss_manifest_free(segs: *i64, n: i64) -> i64 { 1432 if (segs as i64) == 0 { return 0 } 1433 if n > 0 { if segs[0] != 0 { sys_munmap(segs[0] as *u8, 128 * n) } } 1434 // Stashed manifest buffer (sentinel-guarded; slots live in the +64 slack past mc, and n==mc in practice 1435 // because cnt increments exactly once per counted newline). This is the read `b` that ss_manifest_file_dyn 1436 // cannot free itself -- the name-parsing loop still needs it and that function has no single-exit tail. 1437 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]) } } } 1438 sys_munmap(segs as *u8, 8 * n + 64) 1439 return 0 1440} 1441 1442func ss_manifest_dyn(prefix: *u8, out_segs: *i64) -> i64 { 1443 return ss_manifest_file_dyn(prefix, "manifest.txt" as *u8, out_segs) 1444} 1445 1446// CANONICAL next segment id for a commit under `prefix` = (max existing seg-<N> in the manifest) + 1, 1447// UNCAPPED. This is THE writer-side fix for the legacy-cap clobber class: deriving segid from the capped 1448// ss_manifest count returns the cap forever once exceeded, so every later commit overwrites the cap 1449// segment and concurrent writers clobber each other (proven: the toolreg store stuck at seg-256). 1450// Scanning for max is also duplicate-line-proof and hole-proof (ids only need uniqueness+monotonicity). 1451// Lifted from the proven reg_next_segid (nx_registry now delegates here). Empty/missing manifest -> 0. 1452func ss_next_segid(prefix: *u8) -> i64 { 1453 let mf: *u8 = sys_mmap(512) 1454 var o: i64 = ss_cat(mf, 0, prefix) 1455 o = ss_cat(mf, o, "manifest.txt" as *u8) 1456 mf[o] = 0 as u8 1457 let szp: *i64 = sys_mmap(16) as *i64 1458 let b: *u8 = ss_readall(mf, szp) 1459 let sz: i64 = szp[0] 1460 if sz <= 0 { return 0 } 1461 var mx: i64 = 0 - 1 1462 var i: i64 = 0 1463 while i + 4 <= sz { 1464 var m: i64 = 0 1465 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 } } } } 1466 if m == 1 { 1467 var j: i64 = i + 4 1468 var v: i64 = 0 1469 var any: i64 = 0 1470 var go: i64 = 1 1471 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 } } } } 1472 // seq1730 REPAIR HALF: a poisoned id must not PIN the plane. Skipping pointer-shaped ids in 1473 // the max scan means an already-corrupted manifest heals itself on the next write instead of 1474 // staying stuck near 1.4e14 forever. The poisoned SEGMENT stays on disk and readable (rule 13, 1475 // additive-only) -- it simply stops dictating what the next id may be. 1476 if any == 1 { if ss_segid_ok(v) == 1 { if v > mx { mx = v } } } 1477 i = j 1478 } else { i = i + 1 } 1479 } 1480 if mx < 0 { return 0 } 1481 return mx + 1 1482} 1483 1484// author=tutor (GALX-PROD-FULL): backward-compatible higher-capacity manifest readers. 1485// IDENTICAL parse to ss_manifest_file/ss_manifest, but the segment-slot cap is a PARAMETER 1486// rather than the hard 256 baked into ss_manifest_file. Callers supplying a segs[] buffer of 1487// at least `cap` slots (8*cap bytes) can browse the WHOLE corpus, not just the first 256. 1488// The originals ss_manifest_file/ss_manifest are left BYTE-IDENTICAL (cap 256) so all 16 1489// existing callers (260-slot buffers) keep their exact behavior; this is purely additive. 1490func ss_manifest_file_cap(prefix: *u8, fname: *u8, segs: *i64, cap: i64) -> i64 { 1491 let mf: *u8 = sys_mmap(512) 1492 var o: i64 = 0 1493 o = ss_cat(mf, o, prefix) 1494 o = ss_cat(mf, o, fname) 1495 mf[o] = 0 as u8 1496 let szp: *i64 = sys_mmap(16) as *i64 1497 let b: *u8 = ss_readall(mf, szp) 1498 let sz: i64 = szp[0] 1499 if sz <= 0 { return 0 } 1500 var cnt: i64 = 0 1501 var i: i64 = 0 1502 var ls: i64 = 0 1503 while i < sz { 1504 if b[i] == (10 as u8) { 1505 let name: *u8 = sys_mmap(128) 1506 var t: i64 = 0 1507 while ls + t < i { name[t] = b[ls + t]; t = t + 1 } 1508 name[t] = 0 as u8 1509 if cnt < cap { segs[cnt] = name as i64; cnt = cnt + 1 } 1510 ls = i + 1 1511 } 1512 i = i + 1 1513 } 1514 return cnt 1515} 1516 1517// parse the LIVE manifest into segs[] honoring a caller-supplied cap; returns count 1518func ss_manifest_cap(prefix: *u8, segs: *i64, cap: i64) -> i64 { 1519 return ss_manifest_file_cap(prefix, "manifest.txt" as *u8, segs, cap) 1520} 1521 1522// author=tutor (GALX-PROD-FULL): cap-aware sibling of ss_scan. Identical chronological 1523// version walk, but the segment list is read with ss_manifest_cap so it sees ALL segments 1524// up to `cap` (not just 256). The per-key kinds/ptrs/lens buffers count VERSIONS of one key 1525// (tiny, caller-sized), independent of segment count. ss_scan is left untouched. 1526func ss_scan_cap(prefix: *u8, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64, cap: i64) -> i64 { 1527 let segs: *i64 = sys_mmap(8 * cap) as *i64 1528 let ns: i64 = ss_manifest_cap(prefix, segs, cap) 1529 let srcs: *i64 = sys_mmap(8 * cap) as *i64 1530 return ss_scan_seglist(prefix, segs, ns, key, kinds, ptrs, lens, srcs, 1, 0, SS_VER_WINDOW) 1531} 1532 1533// author=tutor (GALX-PROD-FULL): cap-aware sibling of ss_get for the FULL-corpus ingest 1534// dedup path. Same semantics (1=found,0=tombstoned,-1=absent) but scans up to `cap` 1535// segments so re-ingest stays idempotent past the 256th image. ss_get is left untouched. 1536func ss_get_cap(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64, cap: i64) -> i64 { 1537 let kinds: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1538 let ptrs: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1539 let lens: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1540 let n: i64 = ss_scan_cap(prefix, key, kinds, ptrs, lens, cap) 1541 if n == 0 { return 0 - 1 } 1542 let last: i64 = n - 1 1543 if kinds[last] == 2 { return 0 } 1544 ptrout[0] = ptrs[last] 1545 lenout[0] = lens[last] 1546 return 1 1547} 1548 1549// walk a segment list CHRONOLOGICALLY for `key`, appending versions at cnt0; 1550// srcs[i]=srcval marks where each version came from. Returns the new count. 1551// `cap` = the caller's version-buffer window. When a key has MORE versions than cap, the window SLIDES 1552// (oldest drops, newest kept) instead of silently truncating at the FIRST cap versions -- the old 1553// first-cap behavior made ss_get return a STALE value for any key with >cap versions (every registry's 1554// __idx__ index key gains one version per put, so registries past cap puts served a stale index). 1555func 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 { 1556 let kl0: i64 = ss_len(key) 1557 var cnt: i64 = cnt0 1558 var s: i64 = 0 1559 while s < ns { 1560 let path: *u8 = sys_mmap(512) 1561 var o: i64 = 0 1562 o = ss_cat(path, o, prefix) 1563 o = ss_cat(path, o, segs[s] as *u8) 1564 o = ss_cat(path, o, ".docs" as *u8) 1565 path[o] = 0 as u8 1566 let szp: *i64 = sys_mmap(16) as *i64 1567 let b: *u8 = ss_readall(path, szp) 1568 let sz: i64 = szp[0] 1569 var i: i64 = 0 1570 while i + 9 <= sz { 1571 let kind: i64 = b[i] 1572 let kl: i64 = ss_r32(b, i + 1) 1573 let koff: i64 = i + 5 1574 let vl: i64 = ss_r32(b, koff + kl) 1575 let voff: i64 = koff + kl + 4 1576 var eq: i64 = 1 1577 if kl != kl0 { eq = 0 } 1578 var t: i64 = 0 1579 while t < kl { 1580 if eq == 1 { if b[koff + t] != key[t] { eq = 0 } } 1581 t = t + 1 1582 } 1583 if eq == 1 { 1584 if cnt < cap { 1585 kinds[cnt] = kind 1586 ptrs[cnt] = (b as i64) + voff 1587 lens[cnt] = vl 1588 srcs[cnt] = srcval 1589 cnt = cnt + 1 1590 } else { 1591 // window full: slide the OLDEST version out so the LATEST is always retained 1592 // (ss_get takes [cnt-1]; pre-fix this silently kept the FIRST cap versions = stale get) 1593 var sh: i64 = 1 1594 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 } 1595 kinds[cap-1] = kind 1596 ptrs[cap-1] = (b as i64) + voff 1597 lens[cap-1] = vl 1598 srcs[cap-1] = srcval 1599 } 1600 } 1601 i = voff + vl 1602 } 1603 s = s + 1 1604 } 1605 return cnt 1606} 1607 1608// scan LIVE committed segments CHRONOLOGICALLY for `key`; fills kinds/ptrs/lens 1609// (every version incl. tombstones = the time-travel surface); returns count. 1610func ss_scan(prefix: *u8, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64) -> i64 { 1611 let sp: *i64 = sys_mmap(8) as *i64 1612 let ns: i64 = ss_manifest_dyn(prefix, sp) 1613 let segs: *i64 = sp[0] as *i64 1614 let srcs: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 1615 return ss_scan_seglist(prefix, segs, ns, key, kinds, ptrs, lens, srcs, 1, 0, SS_VER_WINDOW) 1616} 1617 1618// ARCHIVE TIME-TRAVEL (IM-AR): the FULL history surface. Compaction retires 1619// segments into manifest-archive.txt without deleting them (additive law); 1620// this reads them back: every version of `key` across ARCHIVED segments 1621// (chronological, srcs=0) then LIVE segments (srcs=1). Post-compaction the 1622// live merged segment re-states the latest archived version -- that copy is 1623// reported honestly as its own LIVE row, never collapsed. Returns count. 1624func ss_scan_all(prefix: *u8, key: *u8, kinds: *i64, ptrs: *i64, lens: *i64, srcs: *i64) -> i64 { 1625 let asp: *i64 = sys_mmap(8) as *i64 1626 let na: i64 = ss_manifest_file_dyn(prefix, "manifest-archive.txt" as *u8, asp) 1627 let asegs: *i64 = asp[0] as *i64 1628 var cnt: i64 = ss_scan_seglist(prefix, asegs, na, key, kinds, ptrs, lens, srcs, 0, 0, SS_VER_WINDOW) 1629 let lsp: *i64 = sys_mmap(8) as *i64 1630 let nl: i64 = ss_manifest_dyn(prefix, lsp) 1631 let lsegs: *i64 = lsp[0] as *i64 1632 cnt = ss_scan_seglist(prefix, lsegs, nl, key, kinds, ptrs, lens, srcs, 1, cnt, SS_VER_WINDOW) 1633 return cnt 1634} 1635 1636// Binary-search an in-memory .keys blob for key. 1637// Returns kind (1 put / 2 tombstone) with voff/vlen in outs, or -1 not present, 1638// or -2 blob malformed. 1639func ss_idx_find(b: *u8, sz: i64, key: *u8, voffout: *i64, vlenout: *i64) -> i64 { 1640 if sz < 8 { return 0 - 2 } 1641 if b[0] != (78 as u8) { return 0 - 2 } 1642 if b[3] != (49 as u8) { return 0 - 2 } 1643 let n: i64 = ss_r32(b, 4) 1644 let base: i64 = 8 + 4 * n 1645 let kl0: i64 = ss_len(key) 1646 var lo: i64 = 0 1647 var hi: i64 = n - 1 1648 while lo <= hi { 1649 let mid: i64 = (lo + hi) / 2 1650 let eo: i64 = base + ss_r32(b, 8 + 4 * mid) 1651 let kind: i64 = b[eo] 1652 let kl: i64 = ss_r32(b, eo + 1) 1653 let c: i64 = ss_kcmp((b as i64 + eo + 5) as *u8, kl, key, kl0) 1654 if c == 0 { 1655 voffout[0] = ss_r32(b, eo + 5 + kl) 1656 vlenout[0] = ss_r32(b, eo + 5 + kl + 4) 1657 return kind 1658 } 1659 if c < 0 { lo = mid + 1 } 1660 if c > 0 { hi = mid - 1 } 1661 } 1662 return 0 - 1 1663} 1664 1665// per-call wrapper: load ONE segment's .keys from disk and search it 1666func ss_idx_lookup(prefix: *u8, segname: *u8, key: *u8, voffout: *i64, vlenout: *i64) -> i64 { 1667 let outs: *i64 = sys_mmap(8 * 8) as *i64 1668 ss_load_aux(prefix, segname, outs) 1669 return ss_idx_find(outs[0] as *u8, outs[1], key, voffout, vlenout) 1670} 1671 1672// OPEN-STORE HANDLE (the race-named read optimization): load the manifest and 1673// every live segment's .keys + .docs ONCE; lookups then binary-search in 1674// memory with zero per-call file IO. Snapshot semantics: the handle sees the 1675// store as of open (immutable segments make this safe -- a later commit adds 1676// segments the handle simply does not list; reopen to see them). 1677// Layout: h[0]=nsegs; per segment i (8 slots): h[1+8i]=keys ptr, h[2+8i]=keys 1678// size, h[3+8i]=docs ptr, h[4+8i]=docs size, h[5+8i]=terms ptr, h[6+8i]=terms 1679// size, h[7+8i]=post ptr, h[8+8i]=post size; h[1+8*ns+i]=LIVE-DOC map ptr for 1680// segment i (1 byte per doc entry, 1 = this entry IS its key's current state 1681// -- computed ONCE here so term queries check currency in O(1) instead of a 1682// per-candidate keyed binary search). Returns 0 ptr if no manifest. 1683// default open: read every segment file fully into anon RAM (historical behavior; ALL non-search consumers 1684// keep this -- small shards, byte-for-byte unchanged). 1685// ss_close -- give back EVERYTHING ss_open/ss_open2 mapped. Its ABSENCE was the seq905 keystone: ss_open 1686// loads every live segment's .keys/.docs/.terms/.post plus a derived live-doc map, and NOTHING ever released 1687// them, so EVERY seg-store consumer leaked BY CONSTRUCTION (`func ss_close` grepped to 0 matches ecosystem- 1688// wide while ss_open had dozens of callers). Measured cost 2026-07-25: ~20.6 GB of 24.3 GB swap consumed on 1689// the NAS, which parks long-running consumers in uninterruptible sleep -- at which point they stop making 1690// progress AND stop responding to SIGKILL, so the leak presents as a HANG, not as an OOM. 1691// 1692// Every size is DERIVABLE from the handle, so this needs no layout change and no new bookkeeping: 1693// 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]) 1694// post(h[7+8i],h[8+8i]); live-doc map h[1+8ns+i], sized dsz/9+16 exactly as ss_open2 allocated it. 1695// 1696// DELIBERATELY CONSERVATIVE on blob length: ss_readall maps sz+64 but REPORTS sz, while the usemmap=1 path 1697// maps exactly sz. We free the REPORTED size, never sz+64 -- unmapping past a file-backed mapping could 1698// release a NEIGHBOURING mapping, and a sub-page residue is strictly better than freeing memory we do not 1699// own. Pointers are nulled as freed and the handle is released LAST (all sizes are read out of it first), so 1700// a partially-populated handle from a failed open still closes cleanly. Null/zero entries are skipped. 1701// license_tier: ORIGINAL No hw writes (Rule 26). 1702func ss_close(h: *i64) -> i64 { 1703 if (h as i64) == 0 { return 0 } 1704 let ns: i64 = h[0] 1705 if ns < 0 { return 0 } 1706 var s: i64 = 0 1707 while s < ns { 1708 let kp: i64 = h[1 + 8 * s] 1709 let ksz: i64 = h[2 + 8 * s] 1710 // MERGED-.idx DETECTION (seq997). ss_load_aux2 returns keys/terms/post as INTERIOR SLICES of ONE 1711 // mapping when the segment has a merged .idx (base = keys-16, total = 16+kl+tl+pl). Freeing those as 1712 // three independent mappings is WRONG and never releases the base. The LEGACY 3-file fallback DOES 1713 // return three separate allocations, so this must branch, not assume. Discriminator: exact adjacency 1714 // FIRST (pure arithmetic, no dereference), and only then the NXI header magic -- so keys-16 is read 1715 // only once it is known to sit inside the same mapping, never off the front of a legacy blob. 1716 var merged: i64 = 0 1717 if kp != 0 { if ksz > 0 { let mbase: *u8 = (kp - 16) as *u8 1718 if h[5 + 8 * s] == kp + ksz { if h[7 + 8 * s] == h[5 + 8 * s] + h[6 + 8 * s] { 1719 if mbase[0] == (78 as u8) { if mbase[2] == (73 as u8) { merged = 1 } } } } } } 1720 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) } } } 1721 let dpp: i64 = h[3 + 8 * s] 1722 let dsz: i64 = h[4 + 8 * s] 1723 if dpp != 0 { if dsz > 0 { sys_munmap(dpp as *u8, dsz) } } 1724 let tp: i64 = h[5 + 8 * s] 1725 let tsz: i64 = h[6 + 8 * s] 1726 if merged == 0 { if tp != 0 { if tsz > 0 { sys_munmap(tp as *u8, tsz) } } } 1727 let pp: i64 = h[7 + 8 * s] 1728 let psz: i64 = h[8 + 8 * s] 1729 if merged == 0 { if pp != 0 { if psz > 0 { sys_munmap(pp as *u8, psz) } } } 1730 let lmp: i64 = h[1 + 8 * ns + s] 1731 if lmp != 0 { if dsz >= 0 { sys_munmap(lmp as *u8, dsz / 9 + 16) } } 1732 h[1 + 8 * s] = 0 1733 h[3 + 8 * s] = 0 1734 h[5 + 8 * s] = 0 1735 h[7 + 8 * s] = 0 1736 h[1 + 8 * ns + s] = 0 1737 s = s + 1 1738 } 1739 h[0] = 0 1740 // QUERY SCRATCH ARENA -- six further mappings the handle OWNS, allocated in ss_open2's tail at 1741 // h[1+9ns]..h[6+9ns]. The first cut of ss_close missed these and the non-vacuous VmSize tooth caught it. 1742 // maxdocs is not stored anywhere, but it is DERIVABLE exactly as ss_open2 computed it (max over segments 1743 // of dsz/9+16, floor 16), so these free precisely with no handle-layout change. Sizes live in h[4+8q], 1744 // which the loop above never zeroes -- only pointers are nulled -- so this recompute is still valid here. 1745 var md: i64 = 16 1746 var q: i64 = 0 1747 while q < ns { 1748 let dz: i64 = h[4 + 8 * q] 1749 if dz / 9 + 16 > md { md = dz / 9 + 16 } 1750 q = q + 1 1751 } 1752 if h[1 + 9 * ns] != 0 { sys_munmap(h[1 + 9 * ns] as *u8, 64) } 1753 if h[2 + 9 * ns] != 0 { sys_munmap(h[2 + 9 * ns] as *u8, 32) } 1754 if h[3 + 9 * ns] != 0 { sys_munmap(h[3 + 9 * ns] as *u8, 8 * md) } 1755 if h[4 + 9 * ns] != 0 { sys_munmap(h[4 + 9 * ns] as *u8, 8 * md) } 1756 if h[5 + 9 * ns] != 0 { sys_munmap(h[5 + 9 * ns] as *u8, 8 * 68) } 1757 if h[6 + 9 * ns] != 0 { sys_munmap(h[6 + 9 * ns] as *u8, 8 * 68) } 1758 sys_munmap(h as *u8, 8 * (9 * ns + 16)) 1759 return 0 1760} 1761 1762func ss_open(prefix: *u8) -> *i64 { 1763 return ss_open2(prefix, 0) 1764} 1765// usemmap=1: file-BACKED read-only maps for the segment files instead of read-all (the mmap-serve rung -- 1766// the big web shard uses this so the ~1.25M-doc RAM ceiling becomes disk-bound; shared across forked 1767// children + page-cache-persistent). The derived live-doc map + query scratch stay ANON (they are computed 1768// + mutated). Handle layout is IDENTICAL either way, so every ss_hget/ss_term/ss_phrase reader is unchanged. 1769// ---- LINEAR SHADOW TABLE (2026-08-01) -------------------------------------------------------- 1770// Defined HERE rather than imported: nx_livemap_fast.nx imports this file, so importing it back 1771// would be circular. These are the same functions that gate proved equivalent, renamed ssl_*. 1772// 1773// They replace the per-key rescan of all newer segments in ss_open2's live-doc map loop -- a scan 1774// that was O(keys x segments) and therefore QUADRATIC in segment count: 4,454,778 key entries over 1775// 96 segments on the live web shard, and worsening with every crawler commit. 1776const SSL_EMPTY: i64 = 0 - 1 1777 1778func ssl_pow2(n: i64) -> i64 { 1779 var p: i64 = 16 1780 while p < n { p = p * 2 } 1781 return p 1782} 1783 1784func ssl_total_keys(h: *i64, ns: i64) -> i64 { 1785 var t: i64 = 0 1786 var s: i64 = 0 1787 while s < ns { 1788 let kb: *u8 = h[1 + 8 * s] as *u8 1789 let ksz: i64 = h[2 + 8 * s] 1790 if ksz >= 8 { t = t + ss_r32(kb, 4) } 1791 s = s + 1 1792 } 1793 return t 1794} 1795 1796func ssl_key_eq(h: *i64, seg: i64, eo: i64, kb2: *u8, eo2: i64) -> i64 { 1797 let kb: *u8 = h[1 + 8 * seg] as *u8 1798 let kl: i64 = ss_r32(kb, eo + 1) 1799 let kl2: i64 = ss_r32(kb2, eo2 + 1) 1800 if kl != kl2 { return 0 } 1801 var i: i64 = 0 1802 while i < kl { 1803 if kb[eo + 5 + i] != kb2[eo2 + 5 + i] { return 0 } 1804 i = i + 1 1805 } 1806 return 1 1807} 1808 1809func ssl_hash_entry(kb: *u8, eo: i64) -> i64 { 1810 let kl: i64 = ss_r32(kb, eo + 1) 1811 var hsh: i64 = 0x811c9dc5 1812 var i: i64 = 0 1813 while i < kl { 1814 hsh = hsh ^ (kb[eo + 5 + i] as i64) 1815 hsh = (hsh * SS_MAGIC_16777619) & 0xffffffff 1816 i = i + 1 1817 } 1818 return hsh 1819} 1820 1821// Walk segments NEWEST-FIRST and refuse to overwrite an occupied slot: the first writer for a key 1822// is therefore the newest segment holding it, with no segment-number comparison needed. 1823func ssl_build(h: *i64, ns: i64, tbl_seg: *i64, tbl_eo: *i64, cap: i64) -> i64 { 1824 var i: i64 = 0 1825 while i < cap { tbl_seg[i] = SSL_EMPTY; i = i + 1 } 1826 let mask: i64 = cap - 1 1827 var inserted: i64 = 0 1828 var s: i64 = ns - 1 1829 while s >= 0 { 1830 let kb: *u8 = h[1 + 8 * s] as *u8 1831 let ksz: i64 = h[2 + 8 * s] 1832 if ksz >= 8 { 1833 let m: i64 = ss_r32(kb, 4) 1834 var e: i64 = 0 1835 while e < m { 1836 let eo: i64 = 8 + 4 * m + ss_r32(kb, 8 + 4 * e) 1837 let kl: i64 = ss_r32(kb, eo + 1) 1838 if kl < 500 { 1839 var slot: i64 = ssl_hash_entry(kb, eo) & mask 1840 var placed: i64 = 0 1841 while placed == 0 { 1842 if tbl_seg[slot] == SSL_EMPTY { 1843 tbl_seg[slot] = s 1844 tbl_eo[slot] = eo 1845 inserted = inserted + 1 1846 placed = 1 1847 } else { 1848 if ssl_key_eq(h, tbl_seg[slot], tbl_eo[slot], kb, eo) == 1 { placed = 1 } else { slot = (slot + 1) & mask } 1849 } 1850 } 1851 } 1852 e = e + 1 1853 } 1854 } 1855 s = s - 1 1856 } 1857 return inserted 1858} 1859 1860func ssl_lookup(h: *i64, tbl_seg: *i64, tbl_eo: *i64, cap: i64, kb: *u8, eo: i64) -> i64 { 1861 let mask: i64 = cap - 1 1862 var slot: i64 = ssl_hash_entry(kb, eo) & mask 1863 var guard: i64 = 0 1864 while guard < cap { 1865 if tbl_seg[slot] == SSL_EMPTY { return 0 - 1 } 1866 if ssl_key_eq(h, tbl_seg[slot], tbl_eo[slot], kb, eo) == 1 { return tbl_seg[slot] } 1867 slot = (slot + 1) & mask 1868 guard = guard + 1 1869 } 1870 return 0 - 1 1871} 1872 1873func ss_open2(prefix: *u8, usemmap: i64) -> *i64 { 1874 let sp: *i64 = sys_mmap(8) as *i64 1875 let ns: i64 = ss_manifest_dyn(prefix, sp) 1876 let segs: *i64 = sp[0] as *i64 1877 let h: *i64 = sys_mmap(8 * (9 * ns + 16)) as *i64 1878 h[0] = ns 1879 let aouts: *i64 = sys_mmap(8 * 8) as *i64 1880 // HOISTED (seq905): dp/dszp were allocated INSIDE the per-segment loop, so opening an N-segment store 1881 // leaked N page-sized scratch mappings PER OPEN -- and nothing could ever reclaim them because they are 1882 // not reachable from the returned handle, so even a correct ss_close could not help. One allocation, 1883 // reused per segment. Same lesson nx_treepack banked as "buffers hoisted, no mmap in inner loops". 1884 let dp: *u8 = sys_mmap(512) 1885 let dszp: *i64 = sys_mmap(16) as *i64 1886 var s: i64 = 0 1887 while s < ns { 1888 // dp hoisted above the loop (seq905) 1889 var o: i64 = 0 1890 o = ss_cat(dp, o, prefix) 1891 o = ss_cat(dp, o, segs[s] as *u8) 1892 o = ss_cat(dp, o, ".docs" as *u8) 1893 dp[o] = 0 as u8 1894 // dszp hoisted above the loop (seq905) 1895 h[3 + 8 * s] = ss_loadfile(dp, dszp, usemmap) as i64 1896 h[4 + 8 * s] = dszp[0] 1897 // index blobs: merged .idx slices (or legacy 3-file fallback) 1898 ss_load_aux2(prefix, segs[s] as *u8, aouts, usemmap) 1899 h[1 + 8 * s] = aouts[0] 1900 h[2 + 8 * s] = aouts[1] 1901 h[5 + 8 * s] = aouts[2] 1902 h[6 + 8 * s] = aouts[3] 1903 h[7 + 8 * s] = aouts[4] 1904 h[8 + 8 * s] = aouts[5] 1905 s = s + 1 1906 } 1907 // live-doc maps: derived from the .keys indexes, ONCE per open. A .keys 1908 // entry is already its key's LATEST entry within its segment, so an entry 1909 // is current iff NO NEWER segment's index knows the key (put or 1910 // tombstone both shadow). Docs not in their segment's .keys (shadowed 1911 // within the segment) stay dead. Single-segment stores need ZERO probes. 1912 let vo9: *i64 = sys_mmap(16) as *i64 1913 let vl9: *i64 = sys_mmap(16) as *i64 1914 let kcp: *u8 = sys_mmap(512) 1915 // key -> NEWEST segment holding it, built ONCE per open (2026-08-01). Replaces the per-key 1916 // rescan of all newer segments in the loop below. Sized 2x the total entry count and rounded to 1917 // a power of two so the probe masks instead of dividing; open addressing wants <=50% load. 1918 let ssl_total: i64 = ssl_total_keys(h, ns) 1919 let ssl_cap: i64 = ssl_pow2(ssl_total * 2 + 16) 1920 let ssl_seg: *i64 = sys_mmap(8 * ssl_cap) as *i64 1921 let ssl_eo: *i64 = sys_mmap(8 * ssl_cap) as *i64 1922 ssl_build(h, ns, ssl_seg, ssl_eo, ssl_cap) 1923 var maxdocs: i64 = 16 1924 s = 0 1925 while s < ns { 1926 let kb: *u8 = h[1 + 8 * s] as *u8 1927 let ksz: i64 = h[2 + 8 * s] 1928 let pb: *u8 = h[7 + 8 * s] as *u8 1929 let dsz: i64 = h[4 + 8 * s] 1930 let lm: *u8 = sys_mmap(dsz / 9 + 16) 1931 h[1 + 8 * ns + s] = lm as i64 1932 if dsz / 9 + 16 > maxdocs { maxdocs = dsz / 9 + 16 } 1933 if ksz >= 8 { if h[8 + 8 * s] >= 8 { 1934 let nd9: i64 = ss_r32(pb, 4) 1935 let m9: i64 = ss_r32(kb, 4) 1936 var e9: i64 = 0 1937 while e9 < m9 { 1938 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9) 1939 let kind9: i64 = kb[eo] 1940 let kl9: i64 = ss_r32(kb, eo + 1) 1941 let vof: i64 = ss_r32(kb, eo + 5 + kl9) 1942 if kind9 == 1 { if kl9 < 500 { 1943 // shadowed by any newer segment knowing this key? 1944 var shadowed: i64 = 0 1945 // SHADOW TEST, NOW LINEAR (2026-08-01). This block asked `does any NEWER segment 1946 // know this key` by RESCANNING every newer segment, once per key -- O(keys x 1947 // segments) index probes, QUADRATIC in segment count: 4,454,778 key entries across 1948 // 96 segments on the live web shard. It is the measured cause of the ~8 minute 1949 // docportal pre-warm, and it got worse every time the crawler committed (87 -> 96 1950 // segments in one session once the crawler's segments=0 bug was fixed). A correct 1951 // fix at the leaf had surfaced a latent quadratic in the keystone. 1952 // ssl_tbl is built ONCE per open, walking segments NEWEST-FIRST, so the first 1953 // writer for a key is by construction the newest segment holding it. The entire 1954 // rescan collapses to one hash probe. 1955 // PROVEN BEFORE THE SWITCH, NOT AFTER: nx_livemap_fast_gate rebuilt every live-doc 1956 // map this way and diffed it against the old implementation on the REAL shard -- 1957 // 96 segments, 4,454,778 entries, 177,262,743 bytes compared, MISMATCHED=0, 7/7. 1958 // These maps decide which documents are visible at all, so a faster map that is 1959 // subtly different would not make search fast, it would make search WRONG. 1960 let newest9: i64 = ssl_lookup(h, ssl_seg, ssl_eo, ssl_cap, kb, eo) 1961 if newest9 > s { shadowed = 1 } 1962 if shadowed == 0 { 1963 // map entry offset -> doc index via the .post doc table 1964 let ioff: i64 = vof - 9 - kl9 1965 var lo9: i64 = 0 1966 var hi9: i64 = nd9 - 1 1967 while lo9 <= hi9 { 1968 let mid9: i64 = (lo9 + hi9) / 2 1969 let dv: i64 = ss_r32(pb, 8 + 4 * mid9) 1970 if dv == ioff { lm[mid9] = 1 as u8; lo9 = hi9 + 1 } else { 1971 if dv < ioff { lo9 = mid9 + 1 } else { hi9 = mid9 - 1 } 1972 } 1973 } 1974 } 1975 } } 1976 e9 = e9 + 1 1977 } 1978 } } 1979 s = s + 1 1980 } 1981 // query scratch arena (handle is single-threaded; queries allocate nothing) 1982 h[1 + 9 * ns] = sys_mmap(64) as i64 1983 h[2 + 9 * ns] = sys_mmap(32) as i64 1984 h[3 + 9 * ns] = sys_mmap(8 * maxdocs) as i64 1985 h[4 + 9 * ns] = sys_mmap(8 * maxdocs) as i64 1986 h[5 + 9 * ns] = sys_mmap(8 * 68) as i64 1987 h[6 + 9 * ns] = sys_mmap(8 * 68) as i64 1988 // PER-OPEN SCRATCH RELEASE (seq905/seq975): none of these are reachable from the returned handle, so 1989 // ss_close can NEVER reclaim them -- they must be given back HERE or every open leaks them forever. 1990 // All are dead past this point: dp/dszp and aouts served the segment loop; vo9/vl9/kcp served the 1991 // live-doc-map loop; sp was only the out-param holder. ss_manifest_free releases the segment-name pool 1992 // and the segs array (paired with ss_manifest_dyn exactly as ss_close pairs with ss_open). 1993 ss_manifest_free(segs, ns) 1994 sys_munmap(dp, 512) 1995 sys_munmap(dszp as *u8, 16) 1996 sys_munmap(aouts as *u8, 8 * 8) 1997 sys_munmap(vo9 as *u8, 16) 1998 sys_munmap(vl9 as *u8, 16) 1999 sys_munmap(kcp, 512) 2000 sys_munmap(sp as *u8, 8) 2001 // ssl_seg/ssl_eo (the 2026-08-01 key->newest-segment table) are DEAD past the live-doc loop and, 2002 // like everything else in this block, unreachable from the returned handle -- ss_close can never 2003 // reclaim them. They were the ONLY per-open allocations missing from this release list, and at 2004 // web-shard scale the pair is 2 x 8 x pow2(2 x 4.45M keys) = 256 MB PER REOPEN -- measured live 2005 // 2026-08-01 as orphaned 256MB anon VMAs accumulating in nx_docportal_admin_daemon (debt 2006 // 1785622778), one leaked pair per manifest change under an active crawler. 2007 // THE CHECKLIST LAW: WHEN YOU ADD A PER-OPEN ALLOCATION, ADD ITS RELEASE IN THE SAME EDIT -- 2008 // this block exists precisely because seq905 paid for the same lesson. 2009 sys_munmap(ssl_seg as *u8, 8 * ssl_cap) 2010 sys_munmap(ssl_eo as *u8, 8 * ssl_cap) 2011 return h 2012} 2013 2014// handle get: identical semantics to ss_get_idx, zero file IO per call 2015// ---- CACHED OPEN (added 2026-07-25, debt seq962) ------------------------------------------------- 2016// WHY: ss_open/ss_open2 read EVERY segment fully into anonymous RAM and there is NO ss_close anywhere 2017// in the tree, so a caller that opens PER REQUEST leaks the whole store per request. MEASURED on the 2018// live box: nx_hub_gw does 3 opens per HTTP request and grew VmData 1336 kB PER REQUEST, with 2019// VmSize==VmPeak (monotonic). Nine long-lived organs carried that same fingerprint, ~23.6 GiB of 2020// anonymous address space, which drove swap to 92.3% and the box into sustained thrash. 2021// 2022// THIS IS NOT A NEW IDEA -- it is an EXISTING in-tree remedy finally promoted into the primitive. 2023// The same cache was independently rediscovered at least three times and never generalized: 2024// 1. nx_docportal_search_seg.nx:128 dss_open_maybe_cached (manifest-signature invalidation) 2025// 2. nx_docportal_admin_daemon.nx:652 per-request ss_open measured ~0.7s/query without this 2026// 3. nx_store_seed_lib.nx:94 helped the reader family OOM the host. NOW: ss_open ONCE 2027// Three local patches, zero generalization -- fix the CLASS, not the call site. 2028// 2029// PURELY ADDITIVE BY DESIGN: there is NO munmap here. That is deliberate. ss_load_aux2 publishes 2030// INTERIOR POINTERS into a single mapping on the merged-.idx path but three INDEPENDENT mappings on 2031// the legacy path, and ss_open2 discards the return value that distinguishes them -- so any freeing 2032// close is a partial-unmap/use-after-free hazard until that ledger is fixed (debt seq947). Caching 2033// removes the leak without ever freeing: you cannot leak what you never allocate twice. 2034// 2035// LIVE-EDIT PRESERVED (nx_hub_gw depends on it): invalidation is the manifest (st_size, st_mtime), 2036// so editing a store is still picked up with NO restart -- on the next call. 2037// RESIDUAL (honest): a miss re-opens WITHOUT freeing the previous handle, so re-opens fall from 2038// per-request to per-store-EDIT. Bounded and rare; ss_close remains worth building for that path. 2039const SSC_SLOTS: i64 = 16 2040const SSC_W: i64 = 4 2041// RETIRE RING (seq1057). Capacity in HANDLES; slot 0 of the mapping holds the count, handles live at 1..n. 2042const SSC_RETIRE: i64 = 32 2043static ssc_tab: *i64 // SSC_SLOTS x SSC_W: [0]=prefix-copy ptr [1]=st_size [2]=st_mtime [3]=handle 2044static ssc_names: *u8 // SSC_SLOTS x 256 bytes of prefix copies (slot i at +i*256) 2045static ssc_scr: *u8 // one-shot scratch: [0..559]=path [560..719]=statbuf [720..735]=sig 2046static ssc_retired: *i64 // [0]=count, [1..SSC_RETIRE]=orphaned handles awaiting an explicit reap 2047 2048// allocate the cache ONCE (static POINTERS to lazy-mmap'd tables -- a BSS static ARRAY silently 2049// crashes handler modules on startup, so never use [N]i64 here). 2050func ssc_init() -> i64 { 2051 if (ssc_tab as i64) != 0 { return 1 } 2052 ssc_tab = sys_mmap(8 * SSC_SLOTS * SSC_W) as *i64 2053 ssc_names = sys_mmap(SSC_SLOTS * 256) 2054 ssc_scr = sys_mmap(SS_MAGIC_1024) 2055 ssc_retired = sys_mmap(8 * (SSC_RETIRE + 1)) as *i64 2056 return 1 2057} 2058 2059// ---- RETIRE / REAP (seq1057) ------------------------------------------------------------------- 2060// THE PROBLEM: on a stale manifest signature ss_open_cached re-opens and OVERWRITES the cached handle. 2061// The previous handle's whole mapping set (segments, keys, docs, terms, post, live-doc maps, scratch 2062// arena) was simply dropped on the floor -- orphaned on every edit to that store. 2063// 2064// WHY NOT JUST ss_close(old) THERE: ss_open_cached hands back a RAW handle and promises the same handle 2065// semantics as ss_open. There is no refcount and no generation, so a caller that took the handle before 2066// the invalidation is still dereferencing it. Freeing inline would turn a bounded LEAK into an unbounded 2067// USE-AFTER-FREE in the most-shared primitive in the ecosystem. That trade is never worth it. 2068// 2069// THE SHAPE THAT IS SOUND WITHOUT TOUCHING ~180 READERS: retiring NEVER frees, so it cannot fault. The 2070// orphan is merely remembered instead of lost. Reclamation is an EXPLICIT, OPT-IN call that a consumer 2071// makes at a point where it knows it holds no handle -- the top of a request loop, between batches. Until 2072// a consumer opts in, behaviour is byte-for-byte what it is today, so this can never regress a caller. 2073// 2074// FAIL-SAFE DIRECTION: if the ring is full we DROP the pointer (leak it) rather than free it. Overflow 2075// degrades to exactly today's behaviour, never to a free that might still be in use. 2076func ssc_retire_handle(h: i64) -> i64 { 2077 if h == 0 { return 0 } 2078 let n: i64 = ssc_retired[0] 2079 if n >= SSC_RETIRE { return 0 } 2080 ssc_retired[1 + n] = h 2081 ssc_retired[0] = n + 1 2082 return 1 2083} 2084 2085// Release every retired handle. Returns how many were closed. 2086// *** CONTRACT: the CALLER guarantees no seg-store handle from ss_open_cached is still in use. 2087// Call it where that is structurally true (top of a request loop, between batches) -- never from inside 2088// a function that is holding a handle, and never from the primitive itself. 2089func ss_cache_reap() -> i64 { 2090 ssc_init() 2091 let n: i64 = ssc_retired[0] 2092 var i: i64 = 0 2093 var freed: i64 = 0 2094 while i < n { 2095 let h: i64 = ssc_retired[1 + i] 2096 if h != 0 { ss_close(h as *i64); freed = freed + 1 } 2097 ssc_retired[1 + i] = 0 2098 i = i + 1 2099 } 2100 ssc_retired[0] = 0 2101 return freed 2102} 2103 2104// How many orphans are awaiting a reap (observability for gates and daemons). 2105func ss_cache_retired() -> i64 { 2106 ssc_init() 2107 return ssc_retired[0] 2108} 2109func ssc_streq(a: *u8, b: *u8) -> i64 { 2110 var i: i64 = 0 2111 while a[i] != (0 as u8) { 2112 if b[i] != a[i] { return 0 } 2113 i = i + 1 2114 } 2115 if b[i] == (0 as u8) { return 1 } 2116 return 0 2117} 2118// manifest signature -> sig[0]=st_size sig[1]=st_mtime-sec ((0,0) when absent). 2119// Reuses the ONE static scratch buffer, so this probe allocates nothing per call. 2120func ssc_sig_of(prefix: *u8, sig: *i64) -> i64 { 2121 let mp: *u8 = ssc_scr 2122 var o: i64 = 0 2123 o = ss_cat(mp, o, prefix) 2124 o = ss_cat(mp, o, "manifest.txt" as *u8) 2125 mp[o] = 0 as u8 2126 let stb: *u8 = ((ssc_scr as i64) + 560) as *u8 2127 sig[0] = 0 2128 sig[1] = 0 2129 if sys_fstatat(mp, stb) == 0 { 2130 let szp: *i64 = ((stb as i64) + 48) as *i64 2131 let mtp: *i64 = ((stb as i64) + 88) as *i64 2132 sig[0] = szp[0] 2133 sig[1] = mtp[0] 2134 } 2135 return 0 2136} 2137// THE ONE OPEN long-lived readers should use. Same handle semantics as ss_open -- every ss_hget / 2138// ss_term / ss_phrase reader is unchanged. Fail-open: if the table is full we fall back to a plain 2139// ss_open rather than refusing, so caching can never break a caller. 2140func ss_open_cached(prefix: *u8) -> *i64 { 2141 ssc_init() 2142 let sig: *i64 = ((ssc_scr as i64) + 720) as *i64 2143 ssc_sig_of(prefix, sig) 2144 var i: i64 = 0 2145 var freeslot: i64 = 0 - 1 2146 while i < SSC_SLOTS { 2147 let np: i64 = ssc_tab[i * SSC_W] 2148 if np == 0 { 2149 if freeslot < 0 { freeslot = i } 2150 } else { 2151 if ssc_streq(np as *u8, prefix) == 1 { 2152 if ssc_tab[i * SSC_W + 3] != 0 { 2153 if ssc_tab[i * SSC_W + 1] == sig[0] { 2154 if ssc_tab[i * SSC_W + 2] == sig[1] { return ssc_tab[i * SSC_W + 3] as *i64 } 2155 } 2156 } 2157 let hr: *i64 = ss_open(prefix) 2158 // seq1057: the previous handle used to be overwritten and lost here. Retiring does NOT 2159 // free it (a caller may still hold it) -- it only makes it reclaimable by ss_cache_reap. 2160 ssc_retire_handle(ssc_tab[i * SSC_W + 3]) 2161 ssc_tab[i * SSC_W + 1] = sig[0] 2162 ssc_tab[i * SSC_W + 2] = sig[1] 2163 ssc_tab[i * SSC_W + 3] = hr as i64 2164 return hr 2165 } 2166 } 2167 i = i + 1 2168 } 2169 if freeslot < 0 { return ss_open(prefix) } 2170 let nm: *u8 = ((ssc_names as i64) + freeslot * 256) as *u8 2171 var k: i64 = 0 2172 while prefix[k] != (0 as u8) { if k < 255 { nm[k] = prefix[k] } k = k + 1 } 2173 if k > 255 { k = 255 } 2174 nm[k] = 0 as u8 2175 let hn: *i64 = ss_open(prefix) 2176 ssc_tab[freeslot * SSC_W] = nm as i64 2177 ssc_tab[freeslot * SSC_W + 1] = sig[0] 2178 ssc_tab[freeslot * SSC_W + 2] = sig[1] 2179 ssc_tab[freeslot * SSC_W + 3] = hn as i64 2180 return hn 2181} 2182 2183// PER-CALL SCRATCH, ALLOCATED ONCE (2026-07-30). ss_hget used to do TWO sys_mmap(16) calls PER 2184// INVOCATION and never free them. sys_mmap is a REAL mmap syscall (nx_syscalls.nx:167), not a bump 2185// allocator, so the kernel rounds each to a full page: ~8 KB LEAKED PER ss_hget CALL. ss_hget is the 2186// hottest read primitive in the ecosystem -- 205 call sites, and the loaders call it ONCE PER ROW -- 2187// so a 10k-row scan leaked ~80 MB in scratch alone, dwarfing the per-open leak this lane started on. 2188// 2189// This is the SAME defect seq905 already fixed one function away: ss_open2's dp/dszp were allocated 2190// inside the per-segment loop and were hoisted out ("buffers hoisted, no mmap in inner loops"). The 2191// identical pattern in ss_hget was missed because the loop is in the CALLER, not in the function. 2192// ★LAW: a per-call allocation in a primitive is a per-CALLER-LOOP leak -- audit the primitive's call 2193// frequency, not just its own body. 2194// 2195// SAFE AS A STATIC: vo/vl are pure scratch for ss_idx_find's two outputs, written then read immediately 2196// with no intervening call that could re-enter ss_hget (VERIFIED: ss_idx_find does not call ss_hget). 2197// Static POINTER + lazy mmap is the established idiom here -- a BSS static ARRAY silently crashes 2198// handler modules on startup, so never use [N]i64. 2199static ssh_vo: *i64 2200static ssh_vl: *i64 2201func ss_hget(h: *i64, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 2202 let ns: i64 = h[0] 2203 if ns == 0 { return 0 - 1 } 2204 if (ssh_vo as i64) == 0 { ssh_vo = sys_mmap(16) as *i64 } 2205 if (ssh_vl as i64) == 0 { ssh_vl = sys_mmap(16) as *i64 } 2206 let vo: *i64 = ssh_vo 2207 let vl: *i64 = ssh_vl 2208 var s: i64 = ns - 1 2209 while s >= 0 { 2210 let r: i64 = ss_idx_find(h[1 + 8 * s] as *u8, h[2 + 8 * s], key, vo, vl) 2211 if r == 2 { return 0 } 2212 if r == 1 { 2213 if h[4 + 8 * s] < vo[0] + vl[0] { return 0 - 2 } 2214 ptrout[0] = h[3 + 8 * s] + vo[0] 2215 lenout[0] = vl[0] 2216 return 1 2217 } 2218 s = s - 1 2219 } 2220 return 0 - 1 2221} 2222 2223// binary-search a .terms blob; returns 1 + postoff/postlen/dcount in outs, 2224// -1 not found, -2 malformed/absent (old-format segment -> caller fails LOUD) 2225func ss_terms_find(tb: *u8, tsz: i64, term: *u8, outs: *i64) -> i64 { 2226 if tsz < 8 { return 0 - 2 } 2227 if tb[0] != (78 as u8) { return 0 - 2 } 2228 if tb[2] != (84 as u8) { return 0 - 2 } 2229 let n: i64 = ss_r32(tb, 4) 2230 let base: i64 = 8 + 4 * n 2231 let tl0: i64 = ss_len(term) 2232 var lo: i64 = 0 2233 var hi: i64 = n - 1 2234 while lo <= hi { 2235 let mid: i64 = (lo + hi) / 2 2236 let eo: i64 = base + ss_r32(tb, 8 + 4 * mid) 2237 let tl: i64 = ss_r32(tb, eo) 2238 let c: i64 = ss_kcmp((tb as i64 + eo + 4) as *u8, tl, term, tl0) 2239 if c == 0 { 2240 outs[0] = ss_r32(tb, eo + 4 + tl) 2241 outs[1] = ss_r32(tb, eo + 4 + tl + 4) 2242 outs[2] = ss_r32(tb, eo + 4 + tl + 8) 2243 return 1 2244 } 2245 if c < 0 { lo = mid + 1 } 2246 if c > 0 { hi = mid - 1 } 2247 } 2248 return 0 - 1 2249} 2250 2251// TERM SEARCH with CURRENT-STATE semantics. A posting hit counts ONLY if it 2252// is the key's CURRENT entry (verified against the key index): a newer 2253// version without the term, or a tombstone, silently shadows older hits -- 2254// stale text never answers for a live record. Each current entry exists 2255// exactly once, so no dedup list is needed. Returns match count (key 2256// ptr/len pairs into the handle's docs buffers), or -2 if ANY live segment 2257// lacks a .terms index (old-format -- fail LOUD, compaction is the upgrade 2258// path; never a silent partial answer). 2259func ss_term(h: *i64, term: *u8, kpout: *i64, klout: *i64, max: i64) -> i64 { 2260 let ns: i64 = h[0] 2261 var cnt: i64 = 0 2262 // loud pre-check: every live segment must carry the term index 2263 var s: i64 = 0 2264 while s < ns { 2265 if h[6 + 8 * s] < 8 { return 0 - 2 } 2266 s = s + 1 2267 } 2268 let outs: *i64 = h[1 + 9 * ns] as *i64 2269 let pos: *i64 = h[2 + 9 * ns] as *i64 2270 s = ns - 1 2271 while s >= 0 { 2272 let tb: *u8 = h[5 + 8 * s] as *u8 2273 let pb: *u8 = h[7 + 8 * s] as *u8 2274 let db: i64 = h[3 + 8 * s] 2275 let lm: *u8 = h[1 + 8 * ns + s] as *u8 2276 let r: i64 = ss_terms_find(tb, h[6 + 8 * s], term, outs) 2277 if r == 1 { 2278 pos[0] = outs[0] 2279 var prev: i64 = 0 2280 var k: i64 = 0 2281 while k < outs[2] { 2282 let d: i64 = prev + ss_vr(pb, pos) 2283 prev = d 2284 // currency check: O(1) via the handle's live-doc map (computed 2285 // once at open; identical semantics to the keyed re-verify) 2286 if lm[d] == (1 as u8) { 2287 let eoff: i64 = ss_r32(pb, 8 + 4 * d) 2288 let ep: *u8 = (db + eoff) as *u8 2289 let kl: i64 = ss_r32(ep, 1) 2290 if cnt < max { 2291 kpout[cnt] = db + eoff + 5 2292 klout[cnt] = kl 2293 cnt = cnt + 1 2294 } 2295 } 2296 k = k + 1 2297 } 2298 } 2299 s = s - 1 2300 } 2301 return cnt 2302} 2303 2304// MULTI-TERM AND (IM2c): keys whose CURRENT record contains EVERY term. 2305// A key's current entry lives in exactly ONE segment, so the AND is computed 2306// per segment: every term's postings list (sorted ascending doc ids) is 2307// merge-intersected (O(sum of list lengths)), then survivors pass the O(1) 2308// live-doc currency check -- same current-state semantics as ss_term (stale 2309// versions and tombstones never answer). Returns match count into 2310// kpout/klout, or -2 when any live segment lacks a .terms index (fail LOUD, 2311// never a silent partial answer). 2312func ss_term_and(h: *i64, terms: *i64, nterms: i64, kpout: *i64, klout: *i64, max: i64) -> i64 { 2313 if nterms <= 0 { return 0 } 2314 let ns: i64 = h[0] 2315 var s: i64 = 0 2316 while s < ns { 2317 if h[6 + 8 * s] < 8 { return 0 - 2 } 2318 s = s + 1 2319 } 2320 if nterms == 1 { return ss_term(h, terms[0] as *u8, kpout, klout, max) } 2321 if nterms > 64 { return 0 - 3 } 2322 let outs: *i64 = h[1 + 9 * ns] as *i64 2323 let pos: *i64 = h[2 + 9 * ns] as *i64 2324 let poffs: *i64 = h[5 + 9 * ns] as *i64 2325 let pdcs: *i64 = h[6 + 9 * ns] as *i64 2326 var cnt: i64 = 0 2327 s = ns - 1 2328 while s >= 0 { 2329 let tb: *u8 = h[5 + 8 * s] as *u8 2330 let pb: *u8 = h[7 + 8 * s] as *u8 2331 let db: i64 = h[3 + 8 * s] 2332 let lm: *u8 = h[1 + 8 * ns + s] as *u8 2333 // every term must appear in THIS segment's dict or it contributes 0 2334 var allp: i64 = 1 2335 var t: i64 = 0 2336 while t < nterms { 2337 let r: i64 = ss_terms_find(tb, h[6 + 8 * s], terms[t] as *u8, outs) 2338 if r == 1 { poffs[t] = outs[0]; pdcs[t] = outs[2] } else { allp = 0 } 2339 t = t + 1 2340 } 2341 if allp == 1 { 2342 let la: *i64 = h[3 + 9 * ns] as *i64 2343 let lb: *i64 = h[4 + 9 * ns] as *i64 2344 // decode term 0 into la 2345 pos[0] = poffs[0] 2346 var prev: i64 = 0 2347 var na: i64 = 0 2348 var k: i64 = 0 2349 while k < pdcs[0] { 2350 prev = prev + ss_vr(pb, pos) 2351 la[na] = prev 2352 na = na + 1 2353 k = k + 1 2354 } 2355 // streaming two-pointer intersect with each further term's list 2356 t = 1 2357 while t < nterms { 2358 if na == 0 { t = nterms } else { 2359 pos[0] = poffs[t] 2360 prev = 0 2361 var ai: i64 = 0 2362 var nb: i64 = 0 2363 k = 0 2364 while k < pdcs[t] { 2365 prev = prev + ss_vr(pb, pos) 2366 var adv: i64 = 1 2367 while adv == 1 { 2368 if ai >= na { adv = 0 } else { 2369 if la[ai] < prev { ai = ai + 1 } else { adv = 0 } 2370 } 2371 } 2372 if ai < na { if la[ai] == prev { lb[nb] = prev; nb = nb + 1; ai = ai + 1 } } 2373 k = k + 1 2374 } 2375 var c2: i64 = 0 2376 while c2 < nb { la[c2] = lb[c2]; c2 = c2 + 1 } 2377 na = nb 2378 t = t + 1 2379 } 2380 } 2381 // survivors: O(1) currency check, then emit key ptr/len 2382 var i2: i64 = 0 2383 while i2 < na { 2384 let d: i64 = la[i2] 2385 if lm[d] == (1 as u8) { 2386 let eoff: i64 = ss_r32(pb, 8 + 4 * d) 2387 let ep: *u8 = (db + eoff) as *u8 2388 if cnt < max { 2389 kpout[cnt] = db + eoff + 5 2390 klout[cnt] = ss_r32(ep, 1) 2391 cnt = cnt + 1 2392 } 2393 } 2394 i2 = i2 + 1 2395 } 2396 } 2397 s = s - 1 2398 } 2399 return cnt 2400} 2401 2402// ---- RANKING STATISTICS (additive, 2026-07-03: the BM25/IDF rung reads them) -------------------- 2403// n_t: how many docs carry `term`, summed from each live segment's .terms dcount (the statistic 2404// ss_build_terms already persists). Write-time counts: a shadowed older version still counts -- for 2405// IDF ranking that bias is negligible and the read is O(log terms) per segment, no posting decode. 2406// Same loud contract as ss_term: -2 when any live segment lacks a .terms index. 2407func ss_term_dcount(h: *i64, term: *u8) -> i64 { 2408 let ns: i64 = h[0] 2409 var s: i64 = 0 2410 while s < ns { 2411 if h[6 + 8 * s] < 8 { return 0 - 2 } 2412 s = s + 1 2413 } 2414 let outs: *i64 = h[1 + 9 * ns] as *i64 2415 var n: i64 = 0 2416 s = 0 2417 while s < ns { 2418 let r: i64 = ss_terms_find(h[5 + 8 * s] as *u8, h[6 + 8 * s], term, outs) 2419 if r == 1 { n = n + outs[2] } 2420 s = s + 1 2421 } 2422 return n 2423} 2424// TERM-DICTIONARY ITERATION (additive, 2026-07-03: the typo/suggest rung reads the index's own sorted 2425// term dictionary -- the same .terms blobs the postings live in; no separate dictionary artifact). 2426// ss_term_count(h, s) = how many terms segment s indexes; ss_term_at fills term ptr/len + dcount for 2427// entry idx (terms are sorted within a segment). Returns 1 ok / 0 out-of-range or absent index. 2428func ss_term_count(h: *i64, s: i64) -> i64 { 2429 if s < 0 { return 0 } 2430 if s >= h[0] { return 0 } 2431 let tb: *u8 = h[5 + 8 * s] as *u8 2432 if h[6 + 8 * s] < 8 { return 0 } 2433 return ss_r32(tb, 4) 2434} 2435func ss_term_at(h: *i64, s: i64, idx: i64, tp_out: *i64, tl_out: *i64, dc_out: *i64) -> i64 { 2436 if s < 0 { return 0 } 2437 if s >= h[0] { return 0 } 2438 let tb: *u8 = h[5 + 8 * s] as *u8 2439 let tsz: i64 = h[6 + 8 * s] 2440 if tsz < 8 { return 0 } 2441 let n: i64 = ss_r32(tb, 4) 2442 if idx < 0 { return 0 } 2443 if idx >= n { return 0 } 2444 let base: i64 = 8 + 4 * n 2445 let eo: i64 = base + ss_r32(tb, 8 + 4 * idx) 2446 let tl: i64 = ss_r32(tb, eo) 2447 tp_out[0] = (tb as i64) + eo + 4 2448 tl_out[0] = tl 2449 dc_out[0] = ss_r32(tb, eo + 4 + tl + 8) 2450 return 1 2451} 2452 2453// N: how many "doc:"-prefixed put-keys the live segments' key indexes hold (each .keys entry is its 2454// key's latest-in-segment; cross-segment re-adds are prevented upstream by skip-if-present ingest, so 2455// this is the corpus size for IDF at O(total keys) once per query -- exactness via compaction later). 2456func ss_doc_count(h: *i64) -> i64 { 2457 let ns: i64 = h[0] 2458 var n: i64 = 0 2459 var s: i64 = 0 2460 while s < ns { 2461 let kb: *u8 = h[1 + 8 * s] as *u8 2462 if h[2 + 8 * s] >= 8 { 2463 let m9: i64 = ss_r32(kb, 4) 2464 var e9: i64 = 0 2465 while e9 < m9 { 2466 let eo: i64 = 8 + 4 * m9 + ss_r32(kb, 8 + 4 * e9) 2467 if (kb[eo] as i64) == 1 { 2468 let kl9: i64 = ss_r32(kb, eo + 1) 2469 if kl9 >= 4 { 2470 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) { 2471 n = n + 1 2472 } } } } 2473 } 2474 } 2475 e9 = e9 + 1 2476 } 2477 } 2478 s = s + 1 2479 } 2480 return n 2481} 2482 2483// ---- PHRASE SUPPORT (2026-07-03, additive): the NXQ1 positions sidecar readers + the evaluator ---- 2484// like ss_terms_find but returns the term's SORTED ORDINAL (the NXQ1 entry index) instead of just 1 2485func ss_terms_ordinal(tb: *u8, tsz: i64, term: *u8, outs: *i64) -> i64 { 2486 if tsz < 8 { return 0 - 2 } 2487 if tb[0] != (78 as u8) { return 0 - 2 } 2488 if tb[2] != (84 as u8) { return 0 - 2 } 2489 let n: i64 = ss_r32(tb, 4) 2490 let base: i64 = 8 + 4 * n 2491 let tl0: i64 = ss_len(term) 2492 var lo: i64 = 0 2493 var hi: i64 = n - 1 2494 while lo <= hi { 2495 let mid: i64 = (lo + hi) / 2 2496 let eo: i64 = base + ss_r32(tb, 8 + 4 * mid) 2497 let tl: i64 = ss_r32(tb, eo) 2498 let c: i64 = ss_kcmp((tb as i64 + eo + 4) as *u8, tl, term, tl0) 2499 if c == 0 { 2500 outs[0] = ss_r32(tb, eo + 4 + tl) 2501 outs[1] = ss_r32(tb, eo + 4 + tl + 4) 2502 outs[2] = ss_r32(tb, eo + 4 + tl + 8) 2503 return mid 2504 } 2505 if c < 0 { lo = mid + 1 } 2506 if c > 0 { hi = mid - 1 } 2507 } 2508 return 0 - 1 2509} 2510// decode the position run for posting-rank `rank` of term-ordinal `ord` from an NXQ1 blob. 2511// Returns npos decoded into posout (cap maxpos; longer runs are truncated -- adjacency over the first 2512// maxpos occurrences), or -1 malformed/absent. 2513func ss_q_positions(qb: *u8, qsz: i64, ord: i64, rank: i64, posout: *i64, maxpos: i64) -> i64 { 2514 if qsz < 8 { return 0 - 1 } 2515 if qb[0] != (78 as u8) { return 0 - 1 } 2516 if qb[2] != (81 as u8) { return 0 - 1 } 2517 let nt: i64 = ss_r32(qb, 4) 2518 if ord < 0 { return 0 - 1 } 2519 if ord >= nt { return 0 - 1 } 2520 let qbase: i64 = 8 + 4 * nt 2521 let pos: *i64 = sys_mmap(16) as *i64 2522 pos[0] = qbase + ss_r32(qb, 8 + 4 * ord) 2523 // skip `rank` runs 2524 var r: i64 = 0 2525 while r < rank { 2526 let n0: i64 = ss_vr(qb, pos) 2527 var k0: i64 = 0 2528 while k0 < n0 { ss_vr(qb, pos); k0 = k0 + 1 } 2529 r = r + 1 2530 } 2531 let np: i64 = ss_vr(qb, pos) 2532 var cum: i64 = 0 2533 var w: i64 = 0 2534 var k: i64 = 0 2535 while k < np { 2536 cum = cum + ss_vr(qb, pos) 2537 if w < maxpos { posout[w] = cum; w = w + 1 } 2538 k = k + 1 2539 } 2540 return w 2541} 2542// IMPACT-ORDERED TERM SEARCH (2026-07-25, the WAND rung): like ss_term but when the term has more 2543// postings than `max`, the cap keeps the HIGHEST-tf postings across all live segments instead of the 2544// first `max` in (segment, doc-asc) order -- and each hit's tf is returned (tfout) so candidacy needs 2545// zero doc reads. Cost is O(segments * SS_IMP_K), NOT O(total postings): the full-list decode ss_term 2546// pays per query disappears. Requires EVERY live segment to carry .imp: returns -3 when any lacks it 2547// and the caller falls back to ss_term (exact legacy behavior; compaction / nx_seg_imp_build upgrade a 2548// shard in place). -2 = missing .terms (same loud contract as ss_term). Currency semantics identical: 2549// the per-segment live-doc map filters stale versions and tombstones. Ties in tf resolve newest- 2550// segment-first then ascending doc -- deterministic. 2551// satout[0] (1-slot): set to 1 iff the answer is TRUNCATED -- a segment's stored impact list was 2552// build-capped at SS_IMP_K, the cross-segment collect filled, or the emit dropped past `max`. This is 2553// the reader's OWN truncation knowledge: exact by construction, immune to the shadow-inflated 2554// write-time dcount statistic (a re-committed doc inflates dcnt but never this flag). 2555func ss_term_top(prefix: *u8, h: *i64, term: *u8, kpout: *i64, klout: *i64, tfout: *i64, max: i64, satout: *i64) -> i64 { 2556 satout[0] = 0 2557 let ns: i64 = h[0] 2558 var s0: i64 = 0 2559 while s0 < ns { 2560 if h[6 + 8 * s0] < 8 { return 0 - 2 } 2561 s0 = s0 + 1 2562 } 2563 if max <= 0 { return 0 } 2564 let sp2: *i64 = sys_mmap(8) as *i64 2565 let ns2: i64 = ss_manifest_dyn(prefix, sp2) 2566 let segs: *i64 = sp2[0] as *i64 2567 var nseg: i64 = ns 2568 if ns2 < nseg { nseg = ns2 } 2569 if nseg <= 0 { return 0 } 2570 // load every live segment's .imp up front: ANY absent -> -3 (all-or-nothing keeps the ordering 2571 // semantics whole; a half-upgraded shard serves exactly like a non-upgraded one) 2572 let ibs: *i64 = sys_mmap(8 * (nseg + 4)) as *i64 2573 let iszs: *i64 = sys_mmap(8 * (nseg + 4)) as *i64 2574 let np9: *u8 = sys_mmap(512) 2575 let szp9: *i64 = sys_mmap(16) as *i64 2576 var s: i64 = 0 2577 while s < nseg { 2578 var o9: i64 = 0 2579 o9 = ss_cat(np9, o9, prefix) 2580 o9 = ss_cat(np9, o9, segs[s] as *u8) 2581 o9 = ss_cat(np9, o9, ".imp" as *u8) 2582 np9[o9] = 0 as u8 2583 szp9[0] = 0 2584 let ib: *u8 = ss_loadfile(np9, szp9, 1) 2585 var ok9: i64 = 0 2586 if (ib as i64) != 0 { if szp9[0] >= 8 { if ib[0] == (78 as u8) { if ib[2] == (87 as u8) { ok9 = 1 } } } } 2587 if ok9 == 0 { return 0 - 3 } 2588 ibs[s] = ib as i64 2589 iszs[s] = szp9[0] 2590 s = s + 1 2591 } 2592 // collect live (segment, docidx, tf) from each impact list, newest segment first 2593 let cap: i64 = nseg * SS_IMP_K + 16 2594 let colls: *i64 = sys_mmap(8 * cap) as *i64 2595 let colld: *i64 = sys_mmap(8 * cap) as *i64 2596 let collt: *i64 = sys_mmap(8 * cap) as *i64 2597 var n: i64 = 0 2598 let outs: *i64 = sys_mmap(32) as *i64 2599 let vp: *i64 = sys_mmap(16) as *i64 2600 s = nseg - 1 2601 while s >= 0 { 2602 let tb: *u8 = h[5 + 8 * s] as *u8 2603 let lm: *u8 = h[1 + 8 * ns + s] as *u8 2604 let od: i64 = ss_terms_ordinal(tb, h[6 + 8 * s], term, outs) 2605 if od >= 0 { 2606 let ib2: *u8 = ibs[s] as *u8 2607 let nt9: i64 = ss_r32(ib2, 4) 2608 if od < nt9 { 2609 let ibase: i64 = 8 + 4 * nt9 2610 vp[0] = ibase + ss_r32(ib2, 8 + 4 * od) 2611 let k9: i64 = ss_vr(ib2, vp) 2612 if k9 >= SS_IMP_K { satout[0] = 1 } 2613 var i9: i64 = 0 2614 while i9 < k9 { 2615 if vp[0] > iszs[s] { i9 = k9 } else { 2616 let d9: i64 = ss_vr(ib2, vp) 2617 let f9: i64 = ss_vr(ib2, vp) 2618 if lm[d9] == (1 as u8) { 2619 if n < cap { colls[n] = s; colld[n] = d9; collt[n] = f9; n = n + 1 } else { satout[0] = 1 } 2620 } 2621 i9 = i9 + 1 2622 } 2623 } 2624 } 2625 } 2626 s = s - 1 2627 } 2628 if n == 0 { return 0 } 2629 // STRICT tf-DESCENDING emit via counting sort over the clamped-tf buckets: O(n + 1024), stable 2630 // within a bucket (collect order = newest segment first, list order within = ascending doc), so 2631 // ties are deterministic. tf >= 1023 collapses into the top bucket -- ordering granularity above 2632 // that is irrelevant to candidacy and the STORED tf stays true. A future WAND early-termination 2633 // reader can rely on this prefix-is-best contract. 2634 let hist2: *i64 = sys_mmap(8 * SS_MAGIC_1024) as *i64 2635 var ih: i64 = 0 2636 while ih < n { 2637 var cv: i64 = collt[ih] 2638 if cv > 1023 { cv = 1023 } 2639 hist2[cv] = hist2[cv] + 1 2640 ih = ih + 1 2641 } 2642 let boff: *i64 = sys_mmap(8 * SS_MAGIC_1024) as *i64 2643 var acc2: i64 = 0 2644 var bb: i64 = 1023 2645 while bb >= 0 { 2646 boff[bb] = acc2 2647 acc2 = acc2 + hist2[bb] 2648 bb = bb - 1 2649 } 2650 let ord: *i64 = sys_mmap(8 * (n + 4)) as *i64 2651 ih = 0 2652 while ih < n { 2653 var cv2: i64 = collt[ih] 2654 if cv2 > 1023 { cv2 = 1023 } 2655 ord[boff[cv2]] = ih 2656 boff[cv2] = boff[cv2] + 1 2657 ih = ih + 1 2658 } 2659 var k2: i64 = n 2660 if k2 > max { k2 = max; satout[0] = 1 } 2661 var cnt: i64 = 0 2662 while cnt < k2 { 2663 let i2: i64 = ord[cnt] 2664 let sx: i64 = colls[i2] 2665 let pb: *u8 = h[7 + 8 * sx] as *u8 2666 let db: i64 = h[3 + 8 * sx] 2667 let eoff: i64 = ss_r32(pb, 8 + 4 * colld[i2]) 2668 let ep: *u8 = (db + eoff) as *u8 2669 kpout[cnt] = db + eoff + 5 2670 klout[cnt] = ss_r32(ep, 1) 2671 tfout[cnt] = collt[i2] 2672 cnt = cnt + 1 2673 } 2674 return cnt 2675} 2676 2677// THE PHRASE EVALUATOR: docs whose CURRENT text contains terms[0..nterms) as ADJACENT tokens, in order. 2678// Candidates come from the per-segment postings intersect (AND); adjacency is checked in the NXQ1 2679// sidecar. A segment WITHOUT a sidecar (pre-phrase format) contributes its AND matches and clears 2680// exactout[0] -- graceful degrade, never a refusal; compaction upgrades it. -2 = missing .terms (loud, 2681// same contract as ss_term). exactout[0]: 1 = adjacency enforced everywhere, 0 = degraded somewhere. 2682func ss_phrase(prefix: *u8, h: *i64, terms: *i64, nterms: i64, kpout: *i64, klout: *i64, max: i64, exactout: *i64) -> i64 { 2683 exactout[0] = 1 2684 if nterms < 2 { return 0 } 2685 if nterms > 8 { return 0 - 3 } 2686 let ns: i64 = h[0] 2687 var s0: i64 = 0 2688 while s0 < ns { 2689 if h[6 + 8 * s0] < 8 { return 0 - 2 } 2690 s0 = s0 + 1 2691 } 2692 let sp2: *i64 = sys_mmap(8) as *i64 2693 let ns2: i64 = ss_manifest_dyn(prefix, sp2) 2694 let segs: *i64 = sp2[0] as *i64 2695 var nseg: i64 = ns 2696 if ns2 < nseg { nseg = ns2 } 2697 let ords: *i64 = sys_mmap(8 * 8) as *i64 2698 let outs: *i64 = sys_mmap(32) as *i64 2699 let poffs: *i64 = sys_mmap(8 * 8) as *i64 2700 let pdcs: *i64 = sys_mmap(8 * 8) as *i64 2701 let p1: *i64 = sys_mmap(8 * 520) as *i64 2702 let p2: *i64 = sys_mmap(8 * 520) as *i64 2703 let qszp: *i64 = sys_mmap(16) as *i64 2704 let vpos: *i64 = sys_mmap(16) as *i64 2705 var cnt: i64 = 0 2706 var s: i64 = 0 2707 while s < nseg { 2708 let tb: *u8 = h[5 + 8 * s] as *u8 2709 let pb: *u8 = h[7 + 8 * s] as *u8 2710 let db: i64 = h[3 + 8 * s] 2711 let lm: *u8 = h[1 + 8 * ns + s] as *u8 2712 // every phrase term must exist in THIS segment's dict 2713 var allp: i64 = 1 2714 var t: i64 = 0 2715 while t < nterms { 2716 let od: i64 = ss_terms_ordinal(tb, h[6 + 8 * s], terms[t] as *u8, outs) 2717 if od < 0 { allp = 0 } else { 2718 ords[t] = od 2719 poffs[t] = outs[0] 2720 pdcs[t] = outs[2] 2721 } 2722 t = t + 1 2723 } 2724 if allp == 1 { 2725 // decode each term's posting doc list (ascending) 2726 let dlist: *i64 = sys_mmap(8 * 8) as *i64 2727 var t2: i64 = 0 2728 while t2 < nterms { 2729 let arr: *i64 = sys_mmap(8 * (pdcs[t2] + 4)) as *i64 2730 vpos[0] = poffs[t2] 2731 var prev: i64 = 0 2732 var k: i64 = 0 2733 while k < pdcs[t2] { 2734 prev = prev + ss_vr(pb, vpos) 2735 arr[k] = prev 2736 k = k + 1 2737 } 2738 dlist[t2] = arr as i64 2739 t2 = t2 + 1 2740 } 2741 // the segment's positions sidecar (absent -> degrade this segment to AND) 2742 let qp: *u8 = sys_mmap(512) 2743 var qo2: i64 = 0 2744 qo2 = ss_cat(qp, qo2, prefix) 2745 qo2 = ss_cat(qp, qo2, segs[s] as *u8) 2746 qo2 = ss_cat(qp, qo2, ".pos" as *u8) 2747 qp[qo2] = 0 as u8 2748 qszp[0] = 0 2749 let qb: *u8 = ss_readall(qp, qszp) 2750 var haveq: i64 = 0 2751 if (qb as i64) != 0 { if qszp[0] >= 8 { haveq = 1 } } 2752 if haveq == 0 { exactout[0] = 0 } 2753 // walk term0's docs; a doc survives iff present in EVERY list (ranks captured for NXQ1) 2754 let l0: *i64 = dlist[0] as *i64 2755 var i0: i64 = 0 2756 while i0 < pdcs[0] { 2757 let d: i64 = l0[i0] 2758 var inall: i64 = 1 2759 let ranks: *i64 = sys_mmap(8 * 8) as *i64 2760 ranks[0] = i0 2761 var t3: i64 = 1 2762 while t3 < nterms { 2763 let lt: *i64 = dlist[t3] as *i64 2764 var lo2: i64 = 0 2765 var hi2: i64 = pdcs[t3] - 1 2766 var fnd: i64 = 0 - 1 2767 while lo2 <= hi2 { 2768 let mid2: i64 = (lo2 + hi2) / 2 2769 if lt[mid2] == d { fnd = mid2; lo2 = hi2 + 1 } else { 2770 if lt[mid2] < d { lo2 = mid2 + 1 } else { hi2 = mid2 - 1 } 2771 } 2772 } 2773 if fnd < 0 { inall = 0; t3 = nterms } else { ranks[t3] = fnd } 2774 t3 = t3 + 1 2775 } 2776 if inall == 1 { if lm[d] == (1 as u8) { 2777 var matched: i64 = 1 2778 if haveq == 1 { 2779 // adjacency ladder over the sidecar positions 2780 var na: i64 = ss_q_positions(qb, qszp[0], ords[0], ranks[0], p1, 512) 2781 var t4: i64 = 1 2782 while t4 < nterms { 2783 if na <= 0 { t4 = nterms } else { 2784 let nb: i64 = ss_q_positions(qb, qszp[0], ords[t4], ranks[t4], p2, 512) 2785 // S = { q in positions(t4) : q-1 in S_prev } (two-pointer, both ascending) 2786 var wa: i64 = 0 2787 var ia: i64 = 0 2788 var ib: i64 = 0 2789 while ia < na { 2790 if ib >= nb { ia = na } else { 2791 let want: i64 = p1[ia] + 1 2792 if p2[ib] == want { p1[wa] = want; wa = wa + 1; ia = ia + 1; ib = ib + 1 } else { 2793 if p2[ib] < want { ib = ib + 1 } else { ia = ia + 1 } 2794 } 2795 } 2796 } 2797 na = wa 2798 t4 = t4 + 1 2799 } 2800 } 2801 if na <= 0 { matched = 0 } 2802 } 2803 if matched == 1 { 2804 let eoff: i64 = ss_r32(pb, 8 + 4 * d) 2805 let ep: *u8 = (db + eoff) as *u8 2806 if cnt < max { 2807 kpout[cnt] = db + eoff + 5 2808 klout[cnt] = ss_r32(ep, 1) 2809 cnt = cnt + 1 2810 } 2811 } 2812 } } 2813 i0 = i0 + 1 2814 } 2815 } 2816 s = s + 1 2817 } 2818 return cnt 2819} 2820 2821// Indexed get (IM2): newest segment -> oldest, binary search each .keys; 2822// first hit decides (identical semantics to the chronological scan's 2823// last-match -- the gate proves equivalence against the scan as ORACLE). 2824// Any missing/malformed index => honest fallback to the scan path. 2825// SCRATCH ALLOCATED ONCE (2026-07-30, same class as the ss_hget fix above). ss_get_idx allocated FIVE 2826// page-granular scratch buffers PER CALL (sp/vo/vl/path/szp) and never freed them -- ~20 KB per call 2827// across 30 call sites. Statics are safe here for the same reason as ss_hget: these are pure scratch, 2828// written then read with no intervening call that re-enters ss_get_idx (VERIFIED: neither ss_get nor 2829// ss_idx_lookup calls back into it). 2830// ⚠DELIBERATELY NOT CACHED: the `b` buffer below (ss_readall of the whole .docs file) is what ptrout 2831// POINTS INTO, so it is returned data, not scratch -- caching or freeing it is the seq1356 2832// borrowed-pointer question and must not be done casually. Filed, not guessed at. 2833// ⚠ALSO STILL OPEN: this function re-reads the MANIFEST and re-maps a whole segment file on EVERY 2834// call -- the O(rows x segments) amplification seq356 already identified. The real remedy is the one 2835// sts_load adopted: ss_open ONCE + ss_hget per row. These statics only stop the bleeding. 2836static ssg_sp: *i64 2837static ssg_vo: *i64 2838static ssg_vl: *i64 2839func ss_get_idx(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 2840 if (ssg_sp as i64) == 0 { ssg_sp = sys_mmap(8) as *i64 } 2841 let sp: *i64 = ssg_sp 2842 let ns: i64 = ss_manifest_dyn(prefix, sp) 2843 let segs: *i64 = sp[0] as *i64 2844 if ns == 0 { return 0 - 1 } 2845 if (ssg_vo as i64) == 0 { ssg_vo = sys_mmap(16) as *i64 } 2846 if (ssg_vl as i64) == 0 { ssg_vl = sys_mmap(16) as *i64 } 2847 let vo: *i64 = ssg_vo 2848 let vl: *i64 = ssg_vl 2849 var s: i64 = ns - 1 2850 while s >= 0 { 2851 let r: i64 = ss_idx_lookup(prefix, segs[s] as *u8, key, vo, vl) 2852 if r == (0 - 2) { return ss_get(prefix, key, ptrout, lenout) } 2853 if r == 2 { return 0 } 2854 if r == 1 { 2855 let path: *u8 = sys_mmap(512) 2856 var o: i64 = 0 2857 o = ss_cat(path, o, prefix) 2858 o = ss_cat(path, o, segs[s] as *u8) 2859 o = ss_cat(path, o, ".docs" as *u8) 2860 path[o] = 0 as u8 2861 let szp: *i64 = sys_mmap(16) as *i64 2862 let b: *u8 = ss_readall(path, szp) 2863 if szp[0] < vo[0] + vl[0] { return 0 - 2 } 2864 ptrout[0] = (b as i64) + vo[0] 2865 lenout[0] = vl[0] 2866 return 1 2867 } 2868 s = s - 1 2869 } 2870 return 0 - 1 2871} 2872 2873// latest state of key: 1=found (ptrout[0]/lenout[0] set), 0=tombstoned, -1=absent 2874func ss_get(prefix: *u8, key: *u8, ptrout: *i64, lenout: *i64) -> i64 { 2875 let kinds: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 2876 let ptrs: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 2877 let lens: *i64 = sys_mmap(8 * SS_VER_SLOTS) as *i64 2878 let n: i64 = ss_scan(prefix, key, kinds, ptrs, lens) 2879 if n == 0 { return 0 - 1 } 2880 let last: i64 = n - 1 2881 if kinds[last] == 2 { return 0 } 2882 ptrout[0] = ptrs[last] 2883 lenout[0] = lens[last] 2884 return 1 2885} 2886 2887// ===== SEQUENTIAL CURSOR (added 2026-07-30) ================================ 2888// THE DEFECT THIS KILLS: ss_get is a POINT lookup costing O(WHOLE-STORE BYTES) 2889// every call -- ss_scan -> ss_scan_seglist ss_readall()s the ENTIRE <seg>.docs 2890// of EVERY live segment per call and returns a pointer INTO that fresh mapping 2891// (so it can never be freed while in use). A whole-store pass built from point 2892// lookups is quadratic in time AND unbounded in mapped memory. 2893// MEASURED on the mvault store (37,058 records / 3 segments): 200 lookups 2894// survive, 10,000 DIES (rc=1, zero output). Read-side twin of the eaten 2895// seg-store quadratic WRITE defect. 2896// COST: one mapping per LIVE SEGMENT, not one per RECORD. 2897// CONTRACT: kout/vout point INTO the current mapping and are valid only until 2898// the NEXT ss_cur_next call. Records arrive chronologically, so for a key with 2899// several versions the LAST seen is current -- the same rule ss_get applies. 2900 2901const SS_CUR_SLOTS: i64 = 8 2902 2903func ss_cur_open(prefix: *u8) -> *i64 { 2904 let st: *i64 = sys_mmap(8 * SS_CUR_SLOTS) as *i64 2905 let sp: *i64 = sys_mmap(8) as *i64 2906 let ns: i64 = ss_manifest_dyn(prefix, sp) 2907 st[0] = sp[0] 2908 st[1] = ns 2909 st[2] = 0 2910 st[3] = 0 2911 st[4] = 0 2912 st[5] = 0 2913 st[6] = 0 2914 return st 2915} 2916 2917func ss_cur_next(prefix: *u8, st: *i64, 2918 kout: *i64, klout: *i64, vout: *i64, vlout: *i64) -> i64 { 2919 var guard: i64 = 0 2920 while guard < SS_MAGIC_1048576 { 2921 guard = guard + 1 2922 if st[3] == 0 { 2923 if st[2] >= st[1] { return 0 } 2924 let segs: *i64 = st[0] as *i64 2925 let path: *u8 = sys_mmap(512) 2926 var o: i64 = ss_cat(path, 0, prefix) 2927 o = ss_cat(path, o, segs[st[2]] as *u8) 2928 o = ss_cat(path, o, ".docs" as *u8) 2929 path[o] = 0 as u8 2930 let szp: *i64 = sys_mmap(16) as *i64 2931 let b: *u8 = ss_readall(path, szp) 2932 st[4] = szp[0] 2933 st[5] = 0 2934 if (b as i64) == 0 { st[4] = 0 } 2935 if st[4] <= 0 { 2936 st[3] = 0 2937 st[2] = st[2] + 1 2938 } else { 2939 st[3] = b as i64 2940 } 2941 } 2942 if st[3] != 0 { 2943 let b: *u8 = st[3] as *u8 2944 let sz: i64 = st[4] 2945 let i: i64 = st[5] 2946 var bad: i64 = 0 2947 if i + 9 > sz { bad = 1 } 2948 if bad == 0 { 2949 let kind: i64 = b[i] 2950 let kl: i64 = ss_r32(b, i + 1) 2951 let koff: i64 = i + 5 2952 if kl < 0 { bad = 1 } 2953 if koff + kl + 4 > sz { bad = 1 } 2954 if bad == 0 { 2955 let vl: i64 = ss_r32(b, koff + kl) 2956 let voff: i64 = koff + kl + 4 2957 if vl < 0 { bad = 1 } 2958 if voff + vl > sz { bad = 1 } 2959 if bad == 0 { 2960 st[5] = voff + vl 2961 st[6] = kind 2962 kout[0] = (b as i64) + koff 2963 klout[0] = kl 2964 vout[0] = (b as i64) + voff 2965 vlout[0] = vl 2966 return 1 2967 } 2968 } 2969 } 2970 st[3] = 0 2971 st[2] = st[2] + 1 2972 } 2973 } 2974 return 0 2975} 2976 2977// COMPACTION (IM4): fold all live segments into ONE new segment holding only 2978// the LATEST entry per key (superseded versions dropped; tombstones KEPT so 2979// get() semantics are EXACTLY preserved incl. GONE-vs-ABSENT -- tombstone GC 2980// is a later policy rung). HISTORY IS NOT DESTROYED: retired segment files 2981// stay on disk and their names are appended to <prefix>manifest-archive.txt 2982// BEFORE the live-manifest swap (crash between the two leaves the old 2983// manifest intact = consistent; the archive only ever gains rows). The swap 2984// itself is the same atomic temp->rename commit point. 2985// Returns the new segid (>0) or negative on failure. 2986func ss_compact(prefix: *u8, segid: i64) -> i64 { 2987 let sp: *i64 = sys_mmap(8) as *i64 2988 let ns: i64 = ss_manifest_dyn(prefix, sp) 2989 let segs: *i64 = sp[0] as *i64 2990 if ns <= 0 { return 0 - 1 } 2991 // read every live segment first; key-table capacity is DATA-DRIVEN from 2992 // the total bytes (every record >= 9 bytes), never a silent fixed cap 2993 let bptrs: *i64 = sys_mmap(8 * ns + 64) as *i64 2994 let bszs: *i64 = sys_mmap(8 * ns + 64) as *i64 2995 var total: i64 = 0 2996 var s: i64 = 0 2997 while s < ns { 2998 let path: *u8 = sys_mmap(512) 2999 var o: i64 = 0 3000 o = ss_cat(path, o, prefix) 3001 o = ss_cat(path, o, segs[s] as *u8) 3002 o = ss_cat(path, o, ".docs" as *u8) 3003 path[o] = 0 as u8 3004 let szp: *i64 = sys_mmap(16) as *i64 3005 bptrs[s] = ss_readall(path, szp) as i64 3006 bszs[s] = szp[0] 3007 if bszs[s] > 0 { total = total + bszs[s] } 3008 s = s + 1 3009 } 3010 let maxk: i64 = total / 9 + 16 3011 let tkp: *i64 = sys_mmap(8 * maxk) as *i64 3012 let tkl: *i64 = sys_mmap(8 * maxk) as *i64 3013 let tkind: *i64 = sys_mmap(8 * maxk) as *i64 3014 let tvp: *i64 = sys_mmap(8 * maxk) as *i64 3015 let tvl: *i64 = sys_mmap(8 * maxk) as *i64 3016 var nk: i64 = 0 3017 s = 0 3018 while s < ns { 3019 let b: *u8 = bptrs[s] as *u8 3020 let sz: i64 = bszs[s] 3021 var i: i64 = 0 3022 while i + 9 <= sz { 3023 let kind: i64 = b[i] 3024 let kl: i64 = ss_r32(b, i + 1) 3025 let koff: i64 = i + 5 3026 let vl: i64 = ss_r32(b, koff + kl) 3027 let voff: i64 = koff + kl + 4 3028 // find existing key slot (chronological walk => overwrite = last wins) 3029 var hit: i64 = 0 - 1 3030 var t: i64 = 0 3031 while t < nk { 3032 if hit < 0 { 3033 if ss_kcmp(tkp[t] as *u8, tkl[t], (b as i64 + koff) as *u8, kl) == 0 { hit = t } 3034 } 3035 t = t + 1 3036 } 3037 if hit < 0 { 3038 if nk >= maxk { return 0 - 7 } 3039 hit = nk 3040 nk = nk + 1 3041 tkp[hit] = (b as i64) + koff 3042 tkl[hit] = kl 3043 } 3044 if hit >= 0 { 3045 tkp[hit] = (b as i64) + koff 3046 tkl[hit] = kl 3047 tkind[hit] = kind 3048 tvp[hit] = (b as i64) + voff 3049 tvl[hit] = vl 3050 } 3051 i = voff + vl 3052 } 3053 s = s + 1 3054 } 3055 // merged segment = latest entry per key; writer sized data-driven from the merged total (no fixed cap) 3056 let w: *i64 = ss_begin_cap(total + SS_MAGIC_65536) 3057 var t2: i64 = 0 3058 while t2 < nk { 3059 if ss_add2(w, tkind[t2], tkp[t2] as *u8, tkl[t2], tvp[t2] as *u8, tvl[t2]) != 0 { return 0 - 2 } 3060 t2 = t2 + 1 3061 } 3062 if ss_write_seg(prefix, w, segid) != 0 { return 0 - 3 } 3063 // archive the retired segment names (append-only; survives any crash here) 3064 let ap: *u8 = sys_mmap(512) 3065 var ao: i64 = 0 3066 ao = ss_cat(ap, ao, prefix) 3067 ao = ss_cat(ap, ao, "manifest-archive.txt" as *u8) 3068 ap[ao] = 0 as u8 3069 let afd: i64 = sys_openat_append(ap, 0x1a4) 3070 if afd < 0 { return 0 - 4 } 3071 s = 0 3072 while s < ns { 3073 let nm: *u8 = segs[s] as *u8 3074 sys_write(afd, nm, ss_len(nm)) 3075 sys_write(afd, "\n" as *u8, 1) 3076 s = s + 1 3077 } 3078 sys_fsync(afd) 3079 sys_close(afd) 3080 // atomic live-manifest swap to ONLY the merged segment 3081 let mf: *u8 = sys_mmap(512) 3082 let mt: *u8 = sys_mmap(512) 3083 var o2: i64 = 0 3084 o2 = ss_cat(mf, o2, prefix) 3085 o2 = ss_cat(mf, o2, "manifest.txt" as *u8) 3086 mf[o2] = 0 as u8 3087 o2 = 0 3088 o2 = ss_cat(mt, o2, prefix) 3089 o2 = ss_cat(mt, o2, "manifest.tmp" as *u8) 3090 mt[o2] = 0 as u8 3091 let nb: *u8 = sys_mmap(128) 3092 var no: i64 = 0 3093 no = ss_cat(nb, no, "seg-" as *u8) 3094 no = ss_catn(nb, no, segid) 3095 nb[no] = 10 as u8 3096 no = no + 1 3097 if ss_writefile(mt, nb, no) != 0 { return 0 - 5 } 3098 if sys_renameat(mt, mf) != 0 { return 0 - 6 } 3099 ss_syncdir(prefix) 3100 return segid 3101} 3102 3103// author=tutor (LM-028 fix): cap-aware sibling of ss_compact for stores with >256 live segments 3104// (the GALX-PROD-FULL family: galx-prod holds ONE segment per image, so thousands accumulate and 3105// each ss_get/ss_scan re-reads every seg .docs = O(n) per lookup). ss_compact CANNOT fold such a 3106// store: it reads the manifest via ss_manifest (HARD cap 256) AND its ss_begin() writer caps at 1MB, 3107// so on a big store it would merge only the first 256 segs (or overflow the writer) and then swap the 3108// live manifest to that partial segment -- silently dropping ~90% of the corpus. This mirror reads 3109// ss_manifest_cap(cap) and allocates a DATA-DRIVEN writer (>= total input bytes; dedup only shrinks), 3110// so any segment count folds to ONE with NO data loss. ss_compact is left BYTE-IDENTICAL (purely 3111// additive). Every failure path returns BEFORE the atomic manifest swap, so a failure leaves the live 3112// store intact (crash/fail-safe). Returns the new segid (>0) or negative on failure. 3113func ss_compact_cap(prefix: *u8, segid: i64, cap: i64) -> i64 { 3114 let segs: *i64 = sys_mmap(8 * cap) as *i64 3115 let ns: i64 = ss_manifest_cap(prefix, segs, cap) 3116 if ns <= 0 { return 0 - 1 } 3117 // read every live segment; key-table + writer capacities are DATA-DRIVEN from total bytes 3118 let bptrs: *i64 = sys_mmap(8 * cap) as *i64 3119 let bszs: *i64 = sys_mmap(8 * cap) as *i64 3120 var total: i64 = 0 3121 var s: i64 = 0 3122 while s < ns { 3123 let path: *u8 = sys_mmap(512) 3124 var o: i64 = 0 3125 o = ss_cat(path, o, prefix) 3126 o = ss_cat(path, o, segs[s] as *u8) 3127 o = ss_cat(path, o, ".docs" as *u8) 3128 path[o] = 0 as u8 3129 let szp: *i64 = sys_mmap(16) as *i64 3130 bptrs[s] = ss_readall(path, szp) as i64 3131 bszs[s] = szp[0] 3132 if bszs[s] > 0 { total = total + bszs[s] } 3133 s = s + 1 3134 } 3135 let maxk: i64 = total / 9 + 16 3136 let tkp: *i64 = sys_mmap(8 * maxk) as *i64 3137 let tkl: *i64 = sys_mmap(8 * maxk) as *i64 3138 let tkind: *i64 = sys_mmap(8 * maxk) as *i64 3139 let tvp: *i64 = sys_mmap(8 * maxk) as *i64 3140 let tvl: *i64 = sys_mmap(8 * maxk) as *i64 3141 var nk: i64 = 0 3142 s = 0 3143 while s < ns { 3144 let b: *u8 = bptrs[s] as *u8 3145 let sz: i64 = bszs[s] 3146 var i: i64 = 0 3147 while i + 9 <= sz { 3148 let kind: i64 = b[i] 3149 let kl: i64 = ss_r32(b, i + 1) 3150 let koff: i64 = i + 5 3151 let vl: i64 = ss_r32(b, koff + kl) 3152 let voff: i64 = koff + kl + 4 3153 // chronological walk => last write wins (find existing key slot) 3154 var hit: i64 = 0 - 1 3155 var t: i64 = 0 3156 while t < nk { 3157 if hit < 0 { 3158 if ss_kcmp(tkp[t] as *u8, tkl[t], (b as i64 + koff) as *u8, kl) == 0 { hit = t } 3159 } 3160 t = t + 1 3161 } 3162 if hit < 0 { 3163 if nk >= maxk { return 0 - 7 } 3164 hit = nk 3165 nk = nk + 1 3166 tkp[hit] = (b as i64) + koff 3167 tkl[hit] = kl 3168 } 3169 if hit >= 0 { 3170 tkp[hit] = (b as i64) + koff 3171 tkl[hit] = kl 3172 tkind[hit] = kind 3173 tvp[hit] = (b as i64) + voff 3174 tvl[hit] = vl 3175 } 3176 i = voff + vl 3177 } 3178 s = s + 1 3179 } 3180 // merged segment = latest entry per key; writer cap is DATA-DRIVEN (merged <= total + slack), 3181 // NOT the fixed 1MB of ss_begin() -- that is the second half of the cap fix. 3182 let w: *i64 = sys_mmap(32) as *i64 3183 let wcap: i64 = total + SS_MAGIC_1048576 3184 w[0] = sys_mmap(wcap) as i64 3185 w[1] = 0 3186 w[2] = wcap 3187 var t2: i64 = 0 3188 while t2 < nk { 3189 if ss_add2(w, tkind[t2], tkp[t2] as *u8, tkl[t2], tvp[t2] as *u8, tvl[t2]) != 0 { return 0 - 2 } 3190 t2 = t2 + 1 3191 } 3192 if ss_write_seg(prefix, w, segid) != 0 { return 0 - 3 } 3193 // archive the retired segment names (append-only; survives any crash here) 3194 let ap: *u8 = sys_mmap(512) 3195 var ao: i64 = 0 3196 ao = ss_cat(ap, ao, prefix) 3197 ao = ss_cat(ap, ao, "manifest-archive.txt" as *u8) 3198 ap[ao] = 0 as u8 3199 let afd: i64 = sys_openat_append(ap, 0x1a4) 3200 if afd < 0 { return 0 - 4 } 3201 s = 0 3202 while s < ns { 3203 let nm: *u8 = segs[s] as *u8 3204 sys_write(afd, nm, ss_len(nm)) 3205 sys_write(afd, "\n" as *u8, 1) 3206 s = s + 1 3207 } 3208 sys_fsync(afd) 3209 sys_close(afd) 3210 // atomic live-manifest swap to ONLY the merged segment 3211 let mf: *u8 = sys_mmap(512) 3212 let mt: *u8 = sys_mmap(512) 3213 var o2: i64 = 0 3214 o2 = ss_cat(mf, o2, prefix) 3215 o2 = ss_cat(mf, o2, "manifest.txt" as *u8) 3216 mf[o2] = 0 as u8 3217 o2 = 0 3218 o2 = ss_cat(mt, o2, prefix) 3219 o2 = ss_cat(mt, o2, "manifest.tmp" as *u8) 3220 mt[o2] = 0 as u8 3221 let nb: *u8 = sys_mmap(128) 3222 var no: i64 = 0 3223 no = ss_cat(nb, no, "seg-" as *u8) 3224 no = ss_catn(nb, no, segid) 3225 nb[no] = 10 as u8 3226 no = no + 1 3227 if ss_writefile(mt, nb, no) != 0 { return 0 - 5 } 3228 if sys_renameat(mt, mf) != 0 { return 0 - 6 } 3229 ss_syncdir(prefix) 3230 return segid 3231}