nx_segopen_incr_gate.nx source
↩ module page · 512 lines · 27743 B
1// nx_segopen_incr_gate.nx -- THE REFEREE FOR ss_open3's INCREMENTAL, WARM-PRESERVING REOPEN (search plan rung L1
2// ss_open_incr, 2026-09-02). THE CLAIM UNDER TEST: when the manifest is a strict prefix-extension of the handle a
3// caller already holds, ss_open3(prefix, 1, prev, 1) reuses prev's segment mappings, live maps and key table, loads
4// only the new rows, clears exactly the old marks the new rows shadow, and produces a handle whose every live map
5// and every key->newest-row answer is BYTE-IDENTICAL to a FULL open of the same manifest. Any admission failure
6// (no table retained, a leading name differs, a leading .docs size differs on disk, the manifest shrank) must fall
7// back to FULL, ANNOUNCE its reason, and leave prev untouched.
8// FIXTURE, built at runtime in /tmp/<gate>/ (never knowledge/store, which the segguard beat sweeps): three rows
9// with overlapping keys -- A puts doc:1..40; B re-puts doc:11..20 and tombstones doc:21..25; C re-puts doc:16..18,
10// tombstones doc:31..35 and adds doc:41..50 -- so the incremental step must clear marks in BOTH older rows (3 in B,
11// 5 in A = 8) through a put AND a tombstone, and leave A's already-dead doc:21..25 alone. The expected mark counts
12// are computed from the fixture, never read back from the subject: A 25 -> 20, B 10 -> 7, C 13 of 18.
13// EVERY OUTCOME TOOTH IS PAIRED WITH A FIXTURE-REACHED TOOTH: mapping identity (the moved .docs pointer IS prev's),
14// prev gutted after the move, the value read through the moved mapping AFTER prev is closed.
15// GREEN iff every tooth passes; neg-controls named neg-control-* so the gate-law census can count them.
16// license_tier: ORIGINAL expect_exit: 0
17import "nx_seg_store.nx"
18import "nx_syscalls.nx"
19import "nx_gate_verdict.nx"
20
21const SI_DIR: *u8 = "/tmp/nx_segopen_incr_gate"
22const SI_PREFIX: *u8 = "/tmp/nx_segopen_incr_gate/soi-"
23const SI_ERR_PREV: *u8 = "/tmp/nx_segopen_incr_gate/stderr-prev.txt"
24const SI_ERR_INCR: *u8 = "/tmp/nx_segopen_incr_gate/stderr-incr.txt"
25const SI_ERR_FULL: *u8 = "/tmp/nx_segopen_incr_gate/stderr-full.txt"
26const SI_ERR_NEG1: *u8 = "/tmp/nx_segopen_incr_gate/stderr-neg-notable.txt"
27const SI_ERR_NEG2: *u8 = "/tmp/nx_segopen_incr_gate/stderr-neg-prefix.txt"
28const SI_ERR_NEG3: *u8 = "/tmp/nx_segopen_incr_gate/stderr-neg-size.txt"
29const SI_ERR_NEG4: *u8 = "/tmp/nx_segopen_incr_gate/stderr-neg-shrank.txt"
30const SI_ERR_FOLD: *u8 = "/tmp/nx_segopen_incr_gate/stderr-fold.txt"
31const SI_ERR_FOLDFULL: *u8 = "/tmp/nx_segopen_incr_gate/stderr-fold-full.txt"
32const SI_ERR_FS: *u8 = "/tmp/nx_segopen_incr_gate/stderr-fold-ship.txt"
33const SI_ERR_UN: *u8 = "/tmp/nx_segopen_incr_gate/stderr-uncovered.txt"
34const SI_ERR_UNFULL: *u8 = "/tmp/nx_segopen_incr_gate/stderr-uncovered-full.txt"
35const SI_ERR_REO: *u8 = "/tmp/nx_segopen_incr_gate/stderr-reorder.txt"
36const SI_ERR_REOFULL: *u8 = "/tmp/nx_segopen_incr_gate/stderr-reorder-full.txt"
37const SI_D_LIVE: i64 = 27 // D = latest of A u B: 35 puts, minus C re-puts 16..18 and tombstones 31..35
38const SI_D_LIVE_AFTER_E: i64 = 24 // E re-puts doc:1..3
39const SI_E_LO: i64 = 1
40const SI_E_HI: i64 = 3
41const SI_E_RECS: i64 = 3
42const SI_FOLD_ENTRIES: i64 = 58 // 40 + 18 key entries over [D,C]
43const SI_MODE_DIR: i64 = 493 // 0755
44const SI_MODE_FILE: i64 = 420 // 0644
45const SI_SEGCAP: i64 = 65536
46const SI_KIND_LIVE: i64 = 1
47const SI_KIND_TOMB: i64 = 2
48const SI_KEYCAP: i64 = 64
49const SI_VALCAP: i64 = 64
50const SI_PATHCAP: i64 = 512
51const SI_MANCAP: i64 = 4096
52const SI_SAVED_FD: i64 = 20 // where fd 2 is parked while an open's stderr is captured
53const SI_A_LO: i64 = 1 // row A: doc:1..40 live
54const SI_A_HI: i64 = 40
55const SI_B_LO: i64 = 11 // row B: doc:11..20 re-put, doc:21..25 tombstoned -> 15 records
56const SI_B_HI: i64 = 20
57const SI_B_TLO: i64 = 21
58const SI_B_THI: i64 = 25
59const SI_C_LO: i64 = 16 // row C: doc:16..18 re-put, doc:31..35 tombstoned, doc:41..50 new -> 18 records
60const SI_C_HI: i64 = 18
61const SI_C_TLO: i64 = 31
62const SI_C_THI: i64 = 35
63const SI_C_NLO: i64 = 41
64const SI_C_NHI: i64 = 50
65const SI_A_RECS: i64 = 40
66const SI_B_RECS: i64 = 15
67const SI_C_RECS: i64 = 18
68const SI_A_LIVE_PREV: i64 = 25 // 40 - 10 re-put by B - 5 tombstoned by B
69const SI_B_LIVE_PREV: i64 = 10
70const SI_A_LIVE_AFTER: i64 = 20 // 25 - 5 tombstoned by C
71const SI_B_LIVE_AFTER: i64 = 7 // 10 - 3 re-put by C
72const SI_C_LIVE: i64 = 13 // 3 re-puts + 10 new; the 5 tombstones carry no mark
73const SI_SHADOWED: i64 = 8 // 3 in B + 5 in A
74const SI_ENTRIES: i64 = 73 // 40 + 15 + 18 key entries the table comparison must cover
75const SI_SUBJECTS: i64 = 16 // opens driven: prev, incr, full, no-table x2, size, reorder x3, shrank x3, fold x2, fold-ship, uncovered x2
76
77func si_key(out: *u8, i: i64) -> i64 {
78 var o: i64 = 0
79 o = ss_cat(out, o, "doc:" as *u8)
80 o = ss_catn(out, o, i)
81 out[o] = 0 as u8
82 return o
83}
84func si_manifest_path(prefix: *u8, out: *u8) -> i64 {
85 var o: i64 = ss_cat(out, 0, prefix)
86 o = ss_cat(out, o, "manifest.txt" as *u8)
87 out[o] = 0 as u8
88 return o
89}
90func si_reset(prefix: *u8) -> i64 {
91 let mf: *u8 = sys_mmap(SI_PATHCAP)
92 si_manifest_path(prefix, mf)
93 let e: *u8 = sys_mmap(16)
94 ss_writefile(mf, e, 0)
95 return 0
96}
97func si_marks(h: *i64, s: i64, n: i64) -> i64 {
98 let ns: i64 = h[0]
99 let lm: *u8 = h[1 + 8 * ns + s] as *u8
100 if (lm as i64) == 0 { return 0 - 1 }
101 var c: i64 = 0
102 var i: i64 = 0
103 while i < n { if lm[i] == (1 as u8) { c = c + 1 } i = i + 1 }
104 return c
105}
106func si_seg_path(prefix: *u8, segname: *u8, suffix: *u8, out: *u8) -> i64 {
107 var o: i64 = ss_cat(out, 0, prefix)
108 o = ss_cat(out, o, segname)
109 o = ss_cat(out, o, suffix)
110 out[o] = 0 as u8
111 return o
112}
113func si_contains(hay: *u8, hn: i64, needle: *u8) -> i64 {
114 var nn: i64 = 0
115 while needle[nn] != (0 as u8) { nn = nn + 1 }
116 if hn < nn { return 0 }
117 var i: i64 = 0
118 while i + nn <= hn {
119 var j: i64 = 0
120 var ok: i64 = 1
121 while j < nn { if hay[i + j] != needle[j] { ok = 0; break } j = j + 1 }
122 if ok == 1 { return 1 }
123 i = i + 1
124 }
125 return 0
126}
127// run ONE ss_open3 with fd 2 redirected to errpath, restore fd 2, return the handle
128func si_open_captured(prefix: *u8, errpath: *u8, prev: *i64, retain: i64) -> *i64 {
129 let efd: i64 = sys_openat_wr(errpath, SI_MODE_FILE)
130 sys_dup3(2, SI_SAVED_FD, 0)
131 if efd >= 0 { sys_dup3(efd, 2, 0) }
132 let h: *i64 = ss_open3(prefix, 1, prev, retain)
133 sys_dup3(SI_SAVED_FD, 2, 0)
134 if efd >= 0 { sys_close(efd) }
135 return h
136}
137func si_err_has(errpath: *u8, needle: *u8) -> i64 {
138 let n: *i64 = sys_mmap(16) as *i64
139 let b: *u8 = ss_readall(errpath, n)
140 var len: i64 = n[0]
141 if len < 0 { len = 0 }
142 return si_contains(b, len, needle)
143}
144func si_add_range(wr: *i64, kind: i64, lo: i64, hi: i64, tag: *u8) -> i64 {
145 let key: *u8 = sys_mmap(SI_KEYCAP)
146 let val: *u8 = sys_mmap(SI_VALCAP)
147 var i: i64 = lo
148 var added: i64 = 0
149 while i <= hi {
150 si_key(key, i)
151 var vn: i64 = 0
152 if kind == SI_KIND_LIVE { vn = ss_cat(val, 0, tag); vn = ss_catn(val, vn, i) }
153 val[vn] = 0 as u8
154 if ss_add(wr, kind, key, val, vn) == 0 { added = added + 1 }
155 i = i + 1
156 }
157 sys_munmap(key, SI_KEYCAP)
158 sys_munmap(val, SI_VALCAP)
159 return added
160}
161// 1 iff ss_hget finds key LIVE through this handle with exactly the expected value bytes
162func si_val_is(h: *i64, key: *u8, expect: *u8) -> i64 {
163 let po: *i64 = sys_mmap(16) as *i64
164 let lo: *i64 = sys_mmap(16) as *i64
165 let r: i64 = ss_hget(h, key, po, lo)
166 if r != 1 { return 0 }
167 let v: *u8 = po[0] as *u8
168 let vl: i64 = lo[0]
169 var i: i64 = 0
170 while expect[i] != (0 as u8) { if i >= vl { return 0 } if v[i] != expect[i] { return 0 } i = i + 1 }
171 if i != vl { return 0 }
172 return 1
173}
174func si_hget_kind(h: *i64, key: *u8) -> i64 {
175 let po: *i64 = sys_mmap(16) as *i64
176 let lo: *i64 = sys_mmap(16) as *i64
177 return ss_hget(h, key, po, lo)
178}
179// byte-compare row s's live map between two handles over the map's own extent; adds to compared, returns mismatches
180func si_lm_diff(ha: *i64, hb: *i64, s: i64, compared: *i64) -> i64 {
181 let na: i64 = ha[0]
182 let nb: i64 = hb[0]
183 let la: *u8 = ha[1 + 8 * na + s] as *u8
184 let lb: *u8 = hb[1 + 8 * nb + s] as *u8
185 let ext: i64 = ha[4 + 8 * s] / 9 + 16
186 if hb[4 + 8 * s] != ha[4 + 8 * s] { return ext }
187 if (la as i64) == 0 { return ext }
188 if (lb as i64) == 0 { return ext }
189 var mm: i64 = 0
190 var i: i64 = 0
191 while i < ext { if la[i] != lb[i] { mm = mm + 1 } i = i + 1 }
192 compared[0] = compared[0] + ext
193 return mm
194}
195// every key entry of every row asked of BOTH retained tables: newest row must agree; adds to compared, returns mismatches
196func si_table_diff(ha: *i64, hb: *i64, compared: *i64) -> i64 {
197 let ta: *i64 = sys_mmap(32) as *i64
198 let tb: *i64 = sys_mmap(32) as *i64
199 if ss_open_table(ha, ta) == 0 { return 0 - 1 }
200 if ss_open_table(hb, tb) == 0 { return 0 - 1 }
201 let ns: i64 = ha[0]
202 var mm: i64 = 0
203 var s: i64 = 0
204 while s < ns {
205 let kb: *u8 = ha[1 + 8 * s] as *u8
206 let ksz: i64 = ha[2 + 8 * s]
207 if ksz >= 8 {
208 let m: i64 = ss_r32(kb, 4)
209 var e: i64 = 0
210 while e < m {
211 let eo: i64 = 8 + 4 * m + ss_r32(kb, 8 + 4 * e)
212 let ra: i64 = ssl_lookup(ha, ta[0] as *i64, ta[1] as *i64, ta[2], kb, eo)
213 let rb: i64 = ssl_lookup(hb, tb[0] as *i64, tb[1] as *i64, tb[2], kb, eo)
214 if ra != rb { mm = mm + 1 }
215 compared[0] = compared[0] + 1
216 e = e + 1
217 }
218 }
219 s = s + 1
220 }
221 return mm
222}
223// rewrite the manifest as the rows of h in the given order (a neg-control fixture; restored afterwards)
224func si_write_manifest(prefix: *u8, h: *i64, order: *i64, n: i64) -> i64 {
225 let mf: *u8 = sys_mmap(SI_PATHCAP)
226 si_manifest_path(prefix, mf)
227 let b: *u8 = sys_mmap(SI_MANCAP)
228 var o: i64 = 0
229 var i: i64 = 0
230 while i < n {
231 let nm: *u8 = ss_open_segname(h, order[i])
232 if (nm as i64) == 0 { return 0 - 1 }
233 o = ss_cat(b, o, nm)
234 b[o] = 10 as u8
235 o = o + 1
236 i = i + 1
237 }
238 ss_writefile(mf, b, o)
239 sys_munmap(b, SI_MANCAP)
240 sys_munmap(mf, SI_PATHCAP)
241 return n
242}
243
244func si_write_manifest_names(prefix: *u8, names: *i64, order: *i64, n: i64) -> i64 {
245 let mf: *u8 = sys_mmap(SI_PATHCAP)
246 si_manifest_path(prefix, mf)
247 let b: *u8 = sys_mmap(SI_MANCAP)
248 var o: i64 = 0
249 var i: i64 = 0
250 while i < n {
251 o = ss_cat(b, o, names[order[i]] as *u8)
252 b[o] = 10 as u8
253 o = o + 1
254 i = i + 1
255 }
256 ss_writefile(mf, b, o)
257 sys_munmap(b, SI_MANCAP)
258 sys_munmap(mf, SI_PATHCAP)
259 return n
260}
261func main(argc: i64, argv: *i64) -> i64 {
262 gv_head("nx_segopen_incr_gate -- ss_open3 incremental reopen is byte-identical to a FULL open and refuses honestly" as *u8)
263 let ctr: *i64 = gv_ctr()
264 gv_subjects("shard opens driven" as *u8, SI_SUBJECTS, ctr)
265
266 sys_mkdir(SI_DIR, SI_MODE_DIR)
267 si_reset(SI_PREFIX)
268 let key: *u8 = sys_mmap(SI_KEYCAP)
269
270 // ---- row A ----
271 let wa: *i64 = ss_begin_cap(SI_SEGCAP)
272 let addedA: i64 = si_add_range(wa, SI_KIND_LIVE, SI_A_LO, SI_A_HI, "A" as *u8)
273 gv_check("fixture-setup-row-A-40-puts-added" as *u8, addedA == SI_A_RECS, ctr)
274 gv_check("fixture-setup-row-A-committed" as *u8, ss_commit(SI_PREFIX, wa, ss_next_segid(SI_PREFIX)) == 0, ctr)
275 // ---- row B ----
276 let wb: *i64 = ss_begin_cap(SI_SEGCAP)
277 var addedB: i64 = si_add_range(wb, SI_KIND_LIVE, SI_B_LO, SI_B_HI, "B" as *u8)
278 addedB = addedB + si_add_range(wb, SI_KIND_TOMB, SI_B_TLO, SI_B_THI, "B" as *u8)
279 gv_check("fixture-setup-row-B-10-puts-5-tombstones-added" as *u8, addedB == SI_B_RECS, ctr)
280 gv_check("fixture-setup-row-B-committed" as *u8, ss_commit(SI_PREFIX, wb, ss_next_segid(SI_PREFIX)) == 0, ctr)
281
282 // ---- prev: the handle a daemon parent would already hold (FULL, table retained) ----
283 let prev: *i64 = si_open_captured(SI_PREFIX, SI_ERR_PREV, 0 as *i64, 1)
284 gv_check("prev-open-sees-two-rows" as *u8, prev[0] == 2, ctr)
285 gv_check("prev-open-mode-is-FULL" as *u8, ss_open_mode(prev) == SS3_MODE_FULL, ctr)
286 let tprev: *i64 = sys_mmap(32) as *i64
287 gv_check("prev-open-retained-its-key-table" as *u8, ss_open_table(prev, tprev) == 1, ctr)
288 gv_check("prev-row-A-marks-25-live-of-40" as *u8, si_marks(prev, 0, SI_A_RECS) == SI_A_LIVE_PREV, ctr)
289 gv_check("prev-row-B-marks-10-live-of-15" as *u8, si_marks(prev, 1, SI_B_RECS) == SI_B_LIVE_PREV, ctr)
290 gv_check("neg-control-an-open-with-no-prev-announces-nothing" as *u8, si_err_has(SI_ERR_PREV, "SSOPEN" as *u8) == 0, ctr)
291 let pa_docs: i64 = prev[3]
292 let pa_lm: i64 = prev[1 + 8 * 2]
293 gv_check("fixture-reached-prev-row-A-docs-mapped" as *u8, pa_docs != 0, ctr)
294
295 // ---- row C ships: the manifest becomes a strict prefix-extension ----
296 let wc: *i64 = ss_begin_cap(SI_SEGCAP)
297 var addedC: i64 = si_add_range(wc, SI_KIND_LIVE, SI_C_LO, SI_C_HI, "C" as *u8)
298 addedC = addedC + si_add_range(wc, SI_KIND_TOMB, SI_C_TLO, SI_C_THI, "C" as *u8)
299 addedC = addedC + si_add_range(wc, SI_KIND_LIVE, SI_C_NLO, SI_C_NHI, "C" as *u8)
300 gv_check("fixture-setup-row-C-3-reputs-5-tombstones-10-new-added" as *u8, addedC == SI_C_RECS, ctr)
301 gv_check("fixture-setup-row-C-committed" as *u8, ss_commit(SI_PREFIX, wc, ss_next_segid(SI_PREFIX)) == 0, ctr)
302
303 // ---- the reopen under test ----
304 let hi: *i64 = si_open_captured(SI_PREFIX, SI_ERR_INCR, prev, 1)
305 gv_check("incr-open-sees-three-rows" as *u8, hi[0] == 3, ctr)
306 gv_check("incr-open-mode-is-INCR" as *u8, ss_open_mode(hi) == SS3_MODE_INCR, ctr)
307 gv_check("incr-open-announces-reused-2-built-1-shadowed-8" as *u8, si_err_has(SI_ERR_INCR, "SSOPEN INCR reused=2 built=1 shadowed=8" as *u8) == 1, ctr)
308 gv_check("incr-row-A-docs-mapping-IS-prev's-mapping-not-a-remap" as *u8, hi[3] == pa_docs, ctr)
309 gv_check("incr-row-A-live-map-IS-prev's-map-corrected-in-place" as *u8, hi[1 + 8 * 3] == pa_lm, ctr)
310 gv_check("incr-prev-row-A-pointers-gutted-so-closing-prev-cannot-free-them" as *u8, prev[3] == 0, ctr)
311 gv_check("incr-prev-live-map-slot-gutted" as *u8, prev[1 + 8 * 2] == 0, ctr)
312 gv_check("incr-prev-table-slot-gutted" as *u8, ss_open_table(prev, tprev) == 0, ctr)
313 gv_check("incr-row-A-marks-20-live-after-C-tombstoned-5" as *u8, si_marks(hi, 0, SI_A_RECS) == SI_A_LIVE_AFTER, ctr)
314 gv_check("incr-row-B-marks-7-live-after-C-reput-3" as *u8, si_marks(hi, 1, SI_B_RECS) == SI_B_LIVE_AFTER, ctr)
315 gv_check("incr-row-C-marks-13-live-of-18" as *u8, si_marks(hi, 2, SI_C_RECS) == SI_C_LIVE, ctr)
316 ss_close(prev)
317 si_key(key, 1)
318 gv_check("incr-reads-row-A-value-through-the-moved-mapping-AFTER-prev-closed" as *u8, si_val_is(hi, key, "A1" as *u8) == 1, ctr)
319 si_key(key, 16)
320 gv_check("incr-newest-row-wins-doc16-reads-C16" as *u8, si_val_is(hi, key, "C16" as *u8) == 1, ctr)
321 si_key(key, 33)
322 gv_check("incr-tombstone-in-new-row-shadows-old-put-doc33-reads-tombstoned" as *u8, si_hget_kind(hi, key) == 0, ctr)
323 si_key(key, 23)
324 gv_check("incr-already-dead-doc23-stays-tombstoned" as *u8, si_hget_kind(hi, key) == 0, ctr)
325 si_key(key, 45)
326 gv_check("incr-new-key-doc45-reads-C45" as *u8, si_val_is(hi, key, "C45" as *u8) == 1, ctr)
327
328 // ---- the reference: a FULL open of the same manifest ----
329 let hf: *i64 = si_open_captured(SI_PREFIX, SI_ERR_FULL, 0 as *i64, 1)
330 gv_check("full-open-mode-is-FULL" as *u8, ss_open_mode(hf) == SS3_MODE_FULL, ctr)
331 gv_check("full-row-A-marks-20" as *u8, si_marks(hf, 0, SI_A_RECS) == SI_A_LIVE_AFTER, ctr)
332 gv_check("full-row-B-marks-7" as *u8, si_marks(hf, 1, SI_B_RECS) == SI_B_LIVE_AFTER, ctr)
333 gv_check("full-row-C-marks-13" as *u8, si_marks(hf, 2, SI_C_RECS) == SI_C_LIVE, ctr)
334 let cmp: *i64 = sys_mmap(16) as *i64
335 cmp[0] = 0
336 var lm_mm: i64 = 0
337 var r: i64 = 0
338 while r < 3 { lm_mm = lm_mm + si_lm_diff(hi, hf, r, cmp); r = r + 1 }
339 gv_check("equivalence-live-maps-compared-over-every-row-nonzero-bytes" as *u8, cmp[0] > 0, ctr)
340 gv_check("equivalence-live-maps-BYTE-IDENTICAL-incr-vs-full" as *u8, lm_mm == 0, ctr)
341 let cmp2: *i64 = sys_mmap(16) as *i64
342 cmp2[0] = 0
343 let tb_mm: i64 = si_table_diff(hi, hf, cmp2)
344 gv_check("equivalence-table-compared-every-key-entry-73" as *u8, cmp2[0] == SI_ENTRIES, ctr)
345 gv_check("equivalence-table-newest-row-answers-IDENTICAL-incr-vs-full" as *u8, tb_mm == 0, ctr)
346
347 // ---- neg-control 1: a prev opened WITHOUT retain has no table -> FULL, announced, prev untouched ----
348 let h0: *i64 = ss_open3(SI_PREFIX, 1, 0 as *i64, 0)
349 gv_check("neg-control-fixture-reached-retain-0-open-holds-no-table" as *u8, ss_open_table(h0, tprev) == 0, ctr)
350 let hx: *i64 = si_open_captured(SI_PREFIX, SI_ERR_NEG1, h0, 1)
351 gv_check("neg-control-no-table-falls-back-to-FULL" as *u8, ss_open_mode(hx) == SS3_MODE_FULL, ctr)
352 gv_check("neg-control-no-table-announces-its-reason" as *u8, si_err_has(SI_ERR_NEG1, "SSOPEN FULL reason=no-table" as *u8) == 1, ctr)
353 gv_check("neg-control-no-table-leaves-prev-untouched" as *u8, h0[3] != 0, ctr)
354 ss_close(hx)
355 ss_close(h0)
356
357 // ---- neg-control 3: a leading row's .docs changed size on disk -> FULL docs-size-mismatch (prev untouched) ----
358 let pdocs: *u8 = sys_mmap(SI_PATHCAP)
359 let phid: *u8 = sys_mmap(SI_PATHCAP)
360 si_seg_path(SI_PREFIX, ss_open_segname(hf, 0), ".docs" as *u8, pdocs)
361 si_seg_path(SI_PREFIX, ss_open_segname(hf, 0), ".docs.hidden" as *u8, phid)
362 let mrc: i64 = sys_renameat(pdocs, phid)
363 let one: *u8 = sys_mmap(16)
364 one[0] = 1 as u8
365 ss_writefile(pdocs, one, 1)
366 gv_check("neg-control-fixture-reached-row-A-docs-replaced-by-1-byte" as *u8, mrc == 0, ctr)
367 let hz: *i64 = si_open_captured(SI_PREFIX, SI_ERR_NEG3, hf, 1)
368 gv_check("size-changed-row-is-RELOADED-siblings-reused-mode-INCR" as *u8, ss_open_mode(hz) == SS3_MODE_INCR, ctr)
369 gv_check("size-changed-row-announces-reused-2-built-1" as *u8, si_err_has(SI_ERR_NEG3, "SSOPEN INCR reused=2 built=1" as *u8) == 1, ctr)
370 gv_check("size-changed-row-announces-vanished-1-the-old-A" as *u8, si_err_has(SI_ERR_NEG3, "INCR-FOLD vanished=1" as *u8) == 1, ctr)
371 gv_check("size-changed-row-A-was-NOT-reused-its-old-mapping-stays-with-prev" as *u8, hf[3] != 0, ctr)
372 gv_check("size-changed-row-siblings-B-C-moved-out-of-prev" as *u8, hf[3 + 8 * 1] == 0, ctr)
373 ss_close(hz)
374 gv_check("fixture-restored-row-A-docs" as *u8, sys_renameat(phid, pdocs) == 0, ctr)
375 ss_close(hf)
376
377 // canonical names of [A,B,C], captured from hi while it still owns them: every restore below writes these
378 let nmABC: *i64 = sys_mmap(32) as *i64
379 nmABC[0] = ss_open_segname(hi, 0) as i64
380 nmABC[1] = ss_open_segname(hi, 1) as i64
381 nmABC[2] = ss_open_segname(hi, 2) as i64
382 let ordABC: *i64 = sys_mmap(32) as *i64
383 ordABC[0] = 0
384 ordABC[1] = 1
385 ordABC[2] = 2
386 // ---- REORDER [B,A,C] (rung L1b): C is a common suffix, A and B vanish and come back as NEW rows in the
387 // new order, so the fold path serves it and must equal a FULL open of [B,A,C] (A now shadows B) ----
388 let hr0: *i64 = ss_open3(SI_PREFIX, 1, 0 as *i64, 1)
389 let order: *i64 = sys_mmap(32) as *i64
390 order[0] = 1
391 order[1] = 0
392 order[2] = 2
393 gv_check("reorder-fixture-reached-manifest-rewritten-B-A-C" as *u8, si_write_manifest(SI_PREFIX, hr0, order, 3) == 3, ctr)
394 let hr: *i64 = si_open_captured(SI_PREFIX, SI_ERR_REO, hr0, 1)
395 gv_check("reorder-is-served-incrementally" as *u8, ss_open_mode(hr) == SS3_MODE_INCR, ctr)
396 gv_check("reorder-announces-reused-1-built-2" as *u8, si_err_has(SI_ERR_REO, "SSOPEN INCR reused=1 built=2" as *u8) == 1, ctr)
397 gv_check("reorder-announces-vanished-2" as *u8, si_err_has(SI_ERR_REO, "INCR-FOLD vanished=2" as *u8) == 1, ctr)
398 ss_close(hr0)
399 let hrf: *i64 = si_open_captured(SI_PREFIX, SI_ERR_REOFULL, 0 as *i64, 1)
400 let cmpr: *i64 = sys_mmap(16) as *i64
401 cmpr[0] = 0
402 var rmm: i64 = 0
403 var rr: i64 = 0
404 while rr < 3 { rmm = rmm + si_lm_diff(hr, hrf, rr, cmpr); rr = rr + 1 }
405 gv_check("reorder-live-maps-compared-nonzero-bytes" as *u8, cmpr[0] > 0, ctr)
406 gv_check("reorder-live-maps-BYTE-IDENTICAL-to-FULL" as *u8, rmm == 0, ctr)
407 let cmpr2: *i64 = sys_mmap(16) as *i64
408 cmpr2[0] = 0
409 gv_check("reorder-table-answers-IDENTICAL-to-FULL" as *u8, si_table_diff(hr, hrf, cmpr2) == 0, ctr)
410 gv_check("reorder-table-compared-every-entry-73" as *u8, cmpr2[0] == SI_ENTRIES, ctr)
411 si_key(key, 23)
412 gv_check("reorder-A-now-newer-than-B-doc23-reads-A23" as *u8, si_val_is(hr, key, "A23" as *u8) == 1, ctr)
413 ss_close(hr)
414 ss_close(hrf)
415 gv_check("reorder-fixture-restored-A-B-C-from-canonical-names" as *u8, si_write_manifest_names(SI_PREFIX, nmABC, ordABC, 3) == 3, ctr)
416
417 // ---- PURE REMOVAL [A,B] (C gone, nothing replaces it): C's keys vanish uncovered, their next-newest entries
418 // in A and B become live again and the table cannot name them -> rebuilt in place, announced as FULL ----
419 let hs0: *i64 = ss_open3(SI_PREFIX, 1, 0 as *i64, 1)
420 gv_check("removal-fixture-prev-is-A-B-C-again" as *u8, hs0[0] == 3, ctr)
421 gv_check("removal-fixture-reached-manifest-shrunk-to-two-rows" as *u8, si_write_manifest_names(SI_PREFIX, nmABC, ordABC, 2) == 2, ctr)
422 let hs: *i64 = si_open_captured(SI_PREFIX, SI_ERR_NEG4, hs0, 1)
423 gv_check("removal-falls-back-to-FULL-rebuilt-in-place" as *u8, ss_open_mode(hs) == SS3_MODE_FULL, ctr)
424 gv_check("removal-announces-uncovered-vanished-keys" as *u8, si_err_has(SI_ERR_NEG4, "reason=uncovered-vanished-keys" as *u8) == 1, ctr)
425 gv_check("removal-consumed-prev-rows-A-B-moved" as *u8, hs0[3] == 0, ctr)
426 ss_close(hs0)
427 let hsf: *i64 = si_open_captured(SI_PREFIX, SI_ERR_UNFULL, 0 as *i64, 1)
428 let cmps: *i64 = sys_mmap(16) as *i64
429 cmps[0] = 0
430 var smm: i64 = 0
431 rr = 0
432 while rr < 2 { smm = smm + si_lm_diff(hs, hsf, rr, cmps); rr = rr + 1 }
433 gv_check("removal-live-maps-BYTE-IDENTICAL-to-a-fresh-FULL-open" as *u8, smm == 0, ctr)
434 gv_check("removal-row-A-marks-25-again-C-shadowing-gone" as *u8, si_marks(hs, 0, SI_A_RECS) == SI_A_LIVE_PREV, ctr)
435 ss_close(hs)
436 ss_close(hsf)
437 let hrest2: *i64 = ss_open3(SI_PREFIX, 1, 0 as *i64, 0)
438 // the manifest now names only [A,B]; put C back so the fold below starts from [A,B,C]
439 let sp3: *i64 = sys_mmap(8) as *i64
440 let n3: i64 = ss_manifest_dyn(SI_PREFIX, sp3)
441 let segs3: *i64 = sp3[0] as *i64
442 gv_check("removal-fixture-manifest-reads-two-rows" as *u8, n3 == 2, ctr)
443 ss_close(hrest2)
444
445 // ---- RANGE FOLD (rung L1b): rows A,B fold into merged D at position 0, C survives as the suffix ----
446 // D is written exactly as the compactor writes it: the latest entry per key of A u B, tombstones kept.
447 let wd: *i64 = ss_begin_cap(SI_SEGCAP)
448 var addedD: i64 = si_add_range(wd, SI_KIND_LIVE, SI_A_LO, SI_B_LO - 1, "A" as *u8)
449 addedD = addedD + si_add_range(wd, SI_KIND_LIVE, SI_B_LO, SI_B_HI, "B" as *u8)
450 addedD = addedD + si_add_range(wd, SI_KIND_TOMB, SI_B_TLO, SI_B_THI, "B" as *u8)
451 addedD = addedD + si_add_range(wd, SI_KIND_LIVE, SI_B_THI + 1, SI_A_HI, "A" as *u8)
452 gv_check("fold-fixture-D-holds-40-merged-records" as *u8, addedD == SI_A_RECS, ctr)
453 // restore [A,B,C] first (ss_commit appends to the CURRENT manifest), then commit D -> [A,B,C,D]
454 si_write_manifest_names(SI_PREFIX, nmABC, ordABC, 3)
455 gv_check("fold-fixture-D-committed" as *u8, ss_commit(SI_PREFIX, wd, ss_next_segid(SI_PREFIX)) == 0, ctr)
456 let sp4: *i64 = sys_mmap(8) as *i64
457 let n4: i64 = ss_manifest_dyn(SI_PREFIX, sp4)
458 let segs4: *i64 = sp4[0] as *i64
459 gv_check("fold-fixture-manifest-names-four-rows-before-the-fold" as *u8, n4 == 4, ctr)
460 // the compactor's manifest: [D, C]
461 let ord2: *i64 = sys_mmap(32) as *i64
462 ord2[0] = 3
463 ord2[1] = 2
464 gv_check("fold-fixture-reached-manifest-D-C" as *u8, si_write_manifest_names(SI_PREFIX, segs4, ord2, 2) == 2, ctr)
465 let pc_docs: i64 = hi[3 + 8 * 2]
466 let hfold: *i64 = si_open_captured(SI_PREFIX, SI_ERR_FOLD, hi, 1)
467 gv_check("fold-open-sees-two-rows" as *u8, hfold[0] == 2, ctr)
468 gv_check("fold-open-mode-is-INCR" as *u8, ss_open_mode(hfold) == SS3_MODE_INCR, ctr)
469 gv_check("fold-announces-reused-1-built-1" as *u8, si_err_has(SI_ERR_FOLD, "SSOPEN INCR reused=1 built=1" as *u8) == 1, ctr)
470 gv_check("fold-announces-vanished-2" as *u8, si_err_has(SI_ERR_FOLD, "INCR-FOLD vanished=2" as *u8) == 1, ctr)
471 gv_check("fold-survivor-C-docs-mapping-IS-prev's-mapping" as *u8, hfold[3 + 8 * 1] == pc_docs, ctr)
472 gv_check("fold-prev-survivor-slot-gutted" as *u8, hi[3 + 8 * 2] == 0, ctr)
473 gv_check("fold-row-D-marks-27-live-of-40" as *u8, si_marks(hfold, 0, SI_A_RECS) == SI_D_LIVE, ctr)
474 gv_check("fold-row-C-marks-13-unchanged" as *u8, si_marks(hfold, 1, SI_C_RECS) == SI_C_LIVE, ctr)
475 let hff: *i64 = si_open_captured(SI_PREFIX, SI_ERR_FOLDFULL, 0 as *i64, 1)
476 let cmpf: *i64 = sys_mmap(16) as *i64
477 cmpf[0] = 0
478 var fmm: i64 = 0
479 rr = 0
480 while rr < 2 { fmm = fmm + si_lm_diff(hfold, hff, rr, cmpf); rr = rr + 1 }
481 gv_check("fold-live-maps-compared-nonzero-bytes" as *u8, cmpf[0] > 0, ctr)
482 gv_check("fold-live-maps-BYTE-IDENTICAL-to-FULL" as *u8, fmm == 0, ctr)
483 let cmpf2: *i64 = sys_mmap(16) as *i64
484 cmpf2[0] = 0
485 gv_check("fold-table-answers-IDENTICAL-to-FULL" as *u8, si_table_diff(hfold, hff, cmpf2) == 0, ctr)
486 gv_check("fold-table-compared-every-entry-58" as *u8, cmpf2[0] == SI_FOLD_ENTRIES, ctr)
487 si_key(key, 5)
488 gv_check("fold-doc5-reads-A5-through-D" as *u8, si_val_is(hfold, key, "A5" as *u8) == 1, ctr)
489 si_key(key, 16)
490 gv_check("fold-doc16-reads-C16-survivor-newest" as *u8, si_val_is(hfold, key, "C16" as *u8) == 1, ctr)
491 si_key(key, 23)
492 gv_check("fold-doc23-tombstoned-inside-D" as *u8, si_hget_kind(hfold, key) == 0, ctr)
493 si_key(key, 33)
494 gv_check("fold-doc33-tombstoned-by-C" as *u8, si_hget_kind(hfold, key) == 0, ctr)
495 ss_close(hff)
496
497 // ---- FOLD THEN SHIP: E re-puts doc:1..3 -> [D,C,E], the prefix case on top of a folded handle ----
498 let we: *i64 = ss_begin_cap(SI_SEGCAP)
499 gv_check("fold-ship-fixture-E-3-reputs-added" as *u8, si_add_range(we, SI_KIND_LIVE, SI_E_LO, SI_E_HI, "E" as *u8) == SI_E_RECS, ctr)
500 gv_check("fold-ship-fixture-E-committed" as *u8, ss_commit(SI_PREFIX, we, ss_next_segid(SI_PREFIX)) == 0, ctr)
501 let hfs: *i64 = si_open_captured(SI_PREFIX, SI_ERR_FS, hfold, 1)
502 gv_check("fold-ship-open-mode-is-INCR" as *u8, ss_open_mode(hfs) == SS3_MODE_INCR, ctr)
503 gv_check("fold-ship-announces-reused-2-built-1" as *u8, si_err_has(SI_ERR_FS, "SSOPEN INCR reused=2 built=1" as *u8) == 1, ctr)
504 gv_check("fold-ship-announces-vanished-0" as *u8, si_err_has(SI_ERR_FS, "INCR-FOLD vanished=0" as *u8) == 1, ctr)
505 gv_check("fold-ship-row-D-marks-24-after-E-reput-3" as *u8, si_marks(hfs, 0, SI_A_RECS) == SI_D_LIVE_AFTER_E, ctr)
506 si_key(key, 2)
507 gv_check("fold-ship-doc2-reads-E2" as *u8, si_val_is(hfs, key, "E2" as *u8) == 1, ctr)
508 ss_close(hfold)
509 ss_close(hfs)
510 ss_close(hi)
511 return gv_verdict("nx_segopen_incr_gate" as *u8, ctr, "in-process over nx_seg_store ss_open3: a prefix, suffix or range-fold reopen reuses every surviving row's mappings, map and table entries, tombstones the vanished rows, inserts the new rows in manifest order, equals a FULL open byte-for-byte, and an uncovered removal rebuilds in place announced as FULL" as *u8)
512}