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}