src/Sstable.bend

mylsm-lsm-store@0.4.0.0 · 607 lines · 0 definitions

1import Base
2import mylsm-lsm-store@0.4.0.0/src/Keys.bend as Keys
3import mylsm-lsm-store@0.4.0.0/src/MemTable.bend as MemTable
4import mylsm-lsm-store@0.4.0.0/src/BitTree.bend as BitTree
5import mylsm-lsm-store@0.4.0.0/src/SortedRun.bend as SortedRun
6
7# SSTables (Task 4): immutable strict sorted runs with BitTree Bloom filters.
8# `build` canonicalizes newest-first raw entries; `from_sorted_unique` trusts an
9# already strict, unique run and deliberately performs no sorting.
10
11# Represent Bloom data used by the SSTable indexes, filters, and lookup.
12type Bloom is Data:
13  Blm{bits: BitTree.BitTree, size: Nat}
14
15# Represent Table data used by the SSTable indexes, filters, and lookup.
16type Table is Data:
17  Tbl{
18    entries: List<&2, MemTable.Entry>,
19    filter: Bloom,
20    nbits: Nat,
21    smallest: Maybe<&2, String>,
22    largest: Maybe<&2, String>,
23    count: Nat,
24    chunks: List<&2, List<&2, MemTable.Entry>>,
25    blocks: List<&2, String>
26  }
27
28# Handle the block entries for the SSTable indexes, filters, and lookup.
29def block_entries() -> Nat:
30  64n
31
32# Handle the chunk go for the SSTable indexes, filters, and lookup.
33def chunk_go(
34  fuel: Nat,
35  +entries: List<&2, MemTable.Entry>,
36  +cur: List<&2, MemTable.Entry>,
37  room: Nat,
38  acc: List<&2, List<&2, MemTable.Entry>>
39) -> List<&2, List<&2, MemTable.Entry>>:
40  match fuel:
41    case 0n:
42      List.reverse(&2, List<&2, MemTable.Entry>, Con{List.reverse(&2, MemTable.Entry, cur), acc})
43    case 1n+f:
44      match entries:
45        case Nil{}:
46          List.reverse(&2, List<&2, MemTable.Entry>, Con{List.reverse(&2, MemTable.Entry, cur), acc})
47        case Con{e, t}:
48          match room:
49            case 0n:
50              chunk_go(f, t, Con{e, Nil{}}, Nat.sub(block_entries(), 1n), Con{List.reverse(&2, MemTable.Entry, cur), acc})
51            case 1n+r:
52              chunk_go(f, t, Con{e, cur}, r, acc)
53
54# Handle chunk in the SSTable indexes, filters, and lookup.
55def chunk(+entries: List<&2, MemTable.Entry>) -> List<&2, List<&2, MemTable.Entry>>:
56  match entries:
57    case Nil{}:
58      Nil{}
59    case Con{e, t}:
60      chunk_go(List.length(&2, MemTable.Entry, entries), t, Con{e, Nil{}}, Nat.sub(block_entries(), 1n), Nil{})
61
62# Handle the chunk head key for the SSTable indexes, filters, and lookup.
63def chunk_head_key(+xs: List<&2, MemTable.Entry>) -> Maybe<&2, String>:
64  match xs:
65    case Nil{}:
66      None{}
67    case Con{MemTable.Entry{key, val}, t}:
68      Some{key}
69
70# Handle the block cons acc for the SSTable indexes, filters, and lookup.
71def block_cons_acc(head: Maybe<&2, String>, acc: List<&2, String>) -> List<&2, String>:
72  match head:
73    case None{}:
74      acc
75    case Some{k}:
76      Con{k, acc}
77
78# Handle the block keys for the SSTable indexes, filters, and lookup.
79def block_keys(+chunks: List<&2, List<&2, MemTable.Entry>>, acc: List<&2, String>) -> List<&2, String>:
80  match chunks:
81    case Nil{}:
82      List.reverse(&2, String, acc)
83    case Con{c, t}:
84      block_keys(t, block_cons_acc(chunk_head_key(c), acc))
85
86# Represent Metadata data used by the SSTable indexes, filters, and lookup.
87type Metadata is Data:
88  Meta{smallest: Maybe<&2, String>, largest: Maybe<&2, String>, count: Nat}
89
90# --- Bloom allocator (Monkey-style: smaller/upper levels get more bits) ---
91# Closed form pinned here; Dayan-exact coefficients are tuning follow-up.
92def bloom_bits(level: Nat, est_keys: Nat) -> Nat:
93  match level:
94    case 0n:
95      Nat.mul(est_keys, 20n)
96    case 1n+m:
97      match m:
98        case 0n:
99          Nat.mul(est_keys, 10n)
100        case 1n+p:
101          Nat.mul(est_keys, 5n)
102
103# --- Local byte hash (kept stable for on-disk/rebuild compatibility) ---
104def bhash(str: String, acc: U32) -> U32:
105  match str:
106    case SNil{}:
107      acc
108    case SCon{c, t}:
109      bhash(t, U32.add(U32.mul(acc, 31), Char.to_u32(c)))
110
111# --- U32->Nat bridges that stay checker-tractable ---
112# The Bend checker does not share across sequentially-chained U32.mul terms:
113# `U32.to_nat` applied to a hash accumulated over 4+ bytes diverges in
114# `{==}` elaboration (runtime is unaffected). The bridges below therefore
115# never materialize a chained U32 as a Nat: each `to_nat` covers at most two
116# U32 ops (shift+mask on the ORIGINAL word — parallel, shared), and all
117# accumulation happens in small-Nat land. Both are EXACT (no semantic
118# change): bytes are the base-256 digits of the word (Horner), so
119# u32_to_nat_exact(w) == U32.to_nat(w) and bhash_n computes the same mod.
120def u32_byte(+word: U32, shift: Nat) -> Nat:
121  U32.to_nat(U32.and(U32.shrn(word, shift), 255))
122
123# Encode or decode a 32-bit word for to nat exact for the SSTable indexes, filters, and lookup.
124def u32_to_nat_exact(+word: U32) -> Nat:
125  +b0 = u32_byte(word, 24n)
126  +b1 = u32_byte(word, 16n)
127  +b2 = u32_byte(word, 8n)
128  +b3 = u32_byte(word, 0n)
129  Nat.add(Nat.mul(b0, Nat.mul(Nat.mul(256n, 256n), 256n)), Nat.add(Nat.mul(b1, Nat.mul(256n, 256n)), Nat.add(Nat.mul(b2, 256n), b3)))
130
131# Handle bhash n pos in the SSTable indexes, filters, and lookup.
132def bhash_n_pos(+str: String, +seed: U32, +mod: Nat) -> Nat:
133  +w = bhash(str, seed)
134  +a0 = Nat.mod(u32_byte(w, 24n), mod)
135  +a1 = Nat.mod(Nat.add(Nat.mul(a0, 256n), u32_byte(w, 16n)), mod)
136  +a2 = Nat.mod(Nat.add(Nat.mul(a1, 256n), u32_byte(w, 8n)), mod)
137  Nat.mod(Nat.add(Nat.mul(a2, 256n), u32_byte(w, 0n)), mod)
138
139# Handle bhash n in the SSTable indexes, filters, and lookup.
140def bhash_n(str: String, seed: U32, mod: Nat) -> Nat:
141  match mod:
142    case 0n:
143      0n
144    case 1n+p:
145      bhash_n_pos(str, seed, mod)
146
147# BitTree stores whole U32 words while Bloom.size preserves the exact logical
148# bit count selected by bloom_bits.
149def bit_words(num: Nat) -> Nat:
150  match num:
151    case 0n:
152      0n
153    case 1n+m:
154      Nat.add(1n, Nat.div(m, 32n))
155
156# Handle bit data for new for the SSTable indexes, filters, and lookup.
157def bits_new(+num: Nat) -> BitTree.BitTree:
158  BitTree.new(bit_words(num))
159
160# Handle bit set in the SSTable indexes, filters, and lookup.
161def bit_set(idx: Nat, bits: BitTree.BitTree) -> BitTree.BitTree:
162  BitTree.set(bits, idx)
163
164# Handle bit get in the SSTable indexes, filters, and lookup.
165def bit_get(idx: Nat, bits: BitTree.BitTree) -> Bool:
166  BitTree.test(bits, idx)
167
168# Handle Bloom filter data for new for the SSTable indexes, filters, and lookup.
169def bloom_new(+num: Nat) -> Bloom:
170  Blm{bits_new(num), num}
171
172# Handle Bloom filter data for well formed for the SSTable indexes, filters, and lookup.
173def bloom_well_formed(bloom: Bloom) -> Bool:
174  match bloom:
175    case Blm{+bits, +size}:
176      Bool.and(
177        BitTree.well_formed(bits),
178        Bool.and(
179          Nat.is_eq(BitTree.word_count(bits), bit_words(size)),
180          Nat.is_le(size, BitTree.bit_count(bits))))
181
182# Handle Bloom filter data for add for the SSTable indexes, filters, and lookup.
183def bloom_add(bloom: Bloom, +key: String) -> Bloom:
184  match bloom:
185    case Blm{bits, +size}:
186      Blm{bit_set(bhash_n(key, 17, size), bit_set(bhash_n(key, 257, size), bits)), size}
187
188# Handle both in the SSTable indexes, filters, and lookup.
189def both(lhs: Bool, rhs: Bool) -> Bool:
190  match lhs:
191    case True{}:
192      rhs
193    case False{}:
194      False{}
195
196# Handle Bloom filter data for test for the SSTable indexes, filters, and lookup.
197def bloom_test(bloom: Bloom, +key: String) -> Bool:
198  match bloom:
199    case Blm{+bits, +size}:
200      both(bit_get(bhash_n(key, 17, size), bits), bit_get(bhash_n(key, 257, size), bits))
201
202# Return the table bits for the SSTable indexes, filters, and lookup.
203def table_bits(tab: Table) -> Nat:
204  match tab:
205    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
206      nbits
207
208# Handle the optional value for present for the SSTable indexes, filters, and lookup.
209def maybe_present(tab: Table, +key: String) -> Bool:
210  match tab:
211    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
212      bloom_test(filter, key)
213
214# --- Parallel probe counters (Task 1): sequential leaf, two-way fork, `!` gate ---
215# Single probe as 0/1 for counting.
216def probe_hit_if(present: Bool) -> Nat:
217  match present:
218    case True{}:
219      1n
220    case False{}:
221      0n
222
223# Count matching probes sequentially.
224def probe_hit(+tab: Table, +key: String) -> Nat:
225  probe_hit_if(maybe_present(tab, key))
226
227# Sequential probe count over a probe list.
228def probe_count_seq(+probes: List<&2, String>, +tab: Table) -> Nat:
229  match probes:
230    case Nil{}:
231      0n
232    case Con{+q, t}:
233      Nat.add(probe_count_seq(t, tab), probe_hit(tab, q))
234
235# Two-way parallel probe count; halves share no state.
236def probe_count_par(+probes: List<&2, String>, +tab: Table) -> Nat:
237  +half = Nat.div(List.length(&2, String, probes), 2n)
238  left right = probe_count_seq(List.take(&2, String, probes, half), tab) probe_count_seq(List.drop(&2, String, probes, half), tab)
239  Nat.add(left, right)
240
241# Bang gate: GPU pays only with enough probes to fill lanes.
242def probe_count_bang_gate(+num: Nat) -> Bool:
243  Nat.is_le(16384n, num)
244
245# Pick CPU-pool or GPU execution for the same fork tree.
246def probe_count_pick(gate: Bool, +probes: List<&2, String>, +tab: Table) -> Nat:
247  match gate:
248    case False{}:
249      probe_count_par(probes, tab)
250    case True{}:
251      probe_count_par!(probes, tab)
252
253# Size-gated entry point for parallel probe counting.
254def probe_count_auto(+probes: List<&2, String>, +tab: Table) -> Nat:
255  probe_count_pick(probe_count_bang_gate(List.length(&2, String, probes)), probes, tab)
256
257# --- Canonicalization and construction ---
258def bloom_of(entries: List<&2, MemTable.Entry>, +blm: Bloom) -> Bloom:
259  match entries:
260    case Nil{}:
261      blm
262    case Con{e, t}:
263      match e:
264        case MemTable.Entry{key, val}:
265          bloom_of(t, bloom_add(blm, key))
266
267# Handle Bloom filter data for union trees for the SSTable indexes, filters, and lookup.
268def bloom_union_trees(+abits: BitTree.BitTree, +bbits: BitTree.BitTree, size: Nat) -> Bloom:
269  match abits:
270    case BitTree.BT{aroot, awords, acap, aheight}:
271      match bbits:
272        case BitTree.BT{broot, bwords, bcap, bheight}:
273          Blm{BitTree.BT{BitTree.union_bits(aroot, broot), awords, acap, aheight}, size}
274
275# Handle Bloom filter data for union for the SSTable indexes, filters, and lookup.
276def bloom_union(ba: Bloom, bb: Bloom) -> Bloom:
277  match ba:
278    case Blm{abits, asize}:
279      match bb:
280        case Blm{bbits, bsize}:
281          bloom_union_trees(abits, bbits, Nat.max(asize, bsize))
282
283# Handle Bloom filter data for of chunk for the SSTable indexes, filters, and lookup.
284def bloom_of_chunk(+chunk: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
285  bloom_of(chunk, bloom_new(nb))
286
287# Two-half parallel build (spike 1): one spawn, one union. Sequential below.
288def bloom_of_par(+entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
289  +half = Nat.div(List.length(&2, MemTable.Entry, entries), 2n)
290  left right = bloom_of_chunk(List.take(&2, MemTable.Entry, entries, half), nb) bloom_of_chunk(List.drop(&2, MemTable.Entry, entries, half), nb)
291  bloom_union(left, right)
292
293# --- Four-chunk parallel build (Task 2): two-level fork, three unions ---
294# Third chunk slice as take-after-drop.
295def chunk_third(+entries: List<&2, MemTable.Entry>, +skip: Nat, +take: Nat) -> List<&2, MemTable.Entry>:
296  List.take(&2, MemTable.Entry, List.drop(&2, MemTable.Entry, entries, skip), take)
297
298# Four-way chunk build with a two-level fork tree.
299def bloom_of_chunks(+entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
300  +num = List.length(&2, MemTable.Entry, entries)
301  +q = Nat.div(num, 4n)
302  a b = bloom_of_chunk(List.take(&2, MemTable.Entry, entries, q), nb) bloom_of_chunk(chunk_third(entries, q, q), nb)
303  c d = bloom_of_chunk(chunk_third(entries, Nat.mul(q, 2n), q), nb) bloom_of_chunk(List.drop(&2, MemTable.Entry, entries, Nat.mul(q, 3n)), nb)
304  bloom_union(bloom_union(a, b), bloom_union(c, d))
305
306# Bang gate: GPU pays only with enough entries to feed lanes.
307def bloom_bang_gate(+num: Nat) -> Bool:
308  Nat.is_le(8192n, num)
309
310# Pick CPU-pool or GPU execution for the same chunk tree.
311def bloom_of_pick(gate: Bool, +entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
312  match gate:
313    case False{}:
314      bloom_of_chunks(entries, nb)
315    case True{}:
316      bloom_of_chunks!(entries, nb)
317
318# Size-gated `!` entry point: the bang fires only at >= 8192 entries, where
319# every flush in the durable write path lands (frozen merge of 4096 + 4096).
320def bloom_of_bang(+entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
321  bloom_of_pick(bloom_bang_gate(List.length(&2, MemTable.Entry, entries)), entries, nb)
322
323# --- Hybrid bloom (spike): parallel index collection, sequential assembly ---
324# Phase 1 emits a flat index list (two per key); joins concatenate, so no
325# chunk tree or union is ever built. Phase 2 folds the indexes into ONE tree
326# on the CPU. This removes the tree-allocation amplification that costs ~10%
327# at 1M scale in the chunk+union design, while keeping a fork tree that a
328# future `!` can ship once the GPU recovery crash is fixed upstream.
329# Both bit positions of one key as a flat two-element list.
330def key_idxs(+key: String, +size: Nat) -> List<&2, Nat>:
331  Con{bhash_n(key, 17, size), Con{bhash_n(key, 257, size), Nil{}}}
332
333# Sequential index collection over entries.
334def hash_idxs_seq(+entries: List<&2, MemTable.Entry>, +size: Nat) -> List<&2, Nat>:
335  match entries:
336    case Nil{}:
337      Nil{}
338    case Con{MemTable.Entry{key, val}, t}:
339      List.append(&2, Nat, key_idxs(key, size), hash_idxs_seq(t, size))
340
341# Balanced parallel index collection; reusable fuel bounds the fork depth.
342def hash_idxs_par_go(+fuel: Nat, +entries: List<&2, MemTable.Entry>, +size: Nat) -> List<&2, Nat>:
343  match fuel:
344    case 0n:
345      hash_idxs_seq(entries, size)
346    case 1n+f:
347      match entries:
348        case Nil{}:
349          Nil{}
350        case Con{MemTable.Entry{key, val}, Nil{}}:
351          key_idxs(key, size)
352        case Con{_, Con{_, _}}:
353          +half = Nat.div(List.length(&2, MemTable.Entry, entries), 2n)
354          left right = hash_idxs_par_go(f, List.take(&2, MemTable.Entry, entries, half), size) hash_idxs_par_go(f, List.drop(&2, MemTable.Entry, entries, half), size)
355          List.append(&2, Nat, left, right)
356
357# Parallel entry point; fuel covers the fork depth.
358def hash_idxs_par(+entries: List<&2, MemTable.Entry>, +size: Nat) -> List<&2, Nat>:
359  +fuel = List.length(&2, MemTable.Entry, entries)
360  hash_idxs_par_go(fuel, entries, size)
361
362# Sequential assembly of one filter from a flat index list.
363def bloom_set_all(+idxs: List<&2, Nat>, +blm: Bloom) -> Bloom:
364  match idxs:
365    case Nil{}:
366      blm
367    case Con{+i, t}:
368      match blm:
369        case Blm{bits, +size}:
370          bloom_set_all(t, Blm{bit_set(i, bits), size})
371
372# Hybrid build: parallel hashing, single-tree assembly.
373def bloom_of_hybrid(+entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
374  bloom_set_all(hash_idxs_par(entries, nb), bloom_new(nb))
375
376# Size-gated entry point: sequential on both sides of the gate.
377# MEASURED VERDICT (2026-09-26, 1M interleaved A/B, Darwin arm64, Bend 2.0.28):
378# sequential median 225105 ms (4442 ops/s) vs 4-chunk 249150 ms (4014 ops/s)
379# vs hybrid 261746 ms — the parallel builds cost ~10% (fork/join plus
380# allocation overhead dominates; invisible at 100k, compounds over 244
381# flushes). Confirmed under two machine states (all arms shifted +15k ms on
382# the slow day; relative order unchanged). The parallel builds stay as
383# law-covered spikes; the `!` variants stay opt-in behind the GPU recovery
384# crash documented below.
385def bloom_pick(small: Bool, +entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
386  match small:
387    case True{}:
388      bloom_of(entries, bloom_new(nb))
389    case False{}:
390      bloom_of(entries, bloom_new(nb))
391
392# Handle Bloom filter data for of auto for the SSTable indexes, filters, and lookup.
393def bloom_of_auto(+entries: List<&2, MemTable.Entry>, +nb: Nat) -> Bloom:
394  bloom_pick(Nat.is_lt(List.length(&2, MemTable.Entry, entries), 2048n), entries, nb)
395
396# Build table metadata for tail for the SSTable indexes, filters, and lookup.
397def metadata_tail(entries: List<&2, MemTable.Entry>, +first: String, last: String, +count: Nat) -> Metadata:
398  match entries:
399    case Nil{}:
400      Meta{Some{first}, Some{last}, count}
401    case Con{MemTable.Entry{key, val}, rest}:
402      metadata_tail(rest, first, key, Nat.add(count, 1n))
403
404# Handle metadata in the SSTable indexes, filters, and lookup.
405def metadata(entries: List<&2, MemTable.Entry>) -> Metadata:
406  match entries:
407    case Nil{}:
408      Meta{None{}, None{}, 0n}
409    case Con{MemTable.Entry{+key, val}, rest}:
410      metadata_tail(rest, key, key, 1n)
411
412# Handle from sorted unique est meta in the SSTable indexes, filters, and lookup.
413def from_sorted_unique_est_meta(+entries: List<&2, MemTable.Entry>, level: Nat, est_keys: Nat, meta: Metadata) -> Table:
414  match meta:
415    case Meta{smallest, largest, count}:
416      +nb = bloom_bits(level, est_keys)
417      Tbl{entries, bloom_of_auto(entries, nb), nb, smallest, largest, count, chunk(entries), block_keys(chunk(entries), Nil{})}
418
419# Handle from sorted unique meta in the SSTable indexes, filters, and lookup.
420def from_sorted_unique_meta(+entries: List<&2, MemTable.Entry>, level: Nat, meta: Metadata) -> Table:
421  match meta:
422    case Meta{smallest, largest, +count}:
423      +nb = bloom_bits(level, count)
424      Tbl{entries, bloom_of_auto(entries, nb), nb, smallest, largest, count, chunk(entries), block_keys(chunk(entries), Nil{})}
425
426# Handle from sorted unique in the SSTable indexes, filters, and lookup.
427def from_sorted_unique(+entries: List<&2, MemTable.Entry>, level: Nat) -> Table:
428  from_sorted_unique_meta(entries, level, metadata(entries))
429
430# Build sorted for the SSTable indexes, filters, and lookup.
431def build_sorted(+entries: List<&2, MemTable.Entry>, level: Nat, est_keys: Nat) -> Table:
432  from_sorted_unique_est_meta(entries, level, est_keys, metadata(entries))
433
434# Handle build in the SSTable indexes, filters, and lookup.
435def build(+entries: List<&2, MemTable.Entry>, level: Nat, est_keys: Nat) -> Table:
436  +effective = Nat.max(est_keys, List.length(&2, MemTable.Entry, entries))
437  build_sorted(SortedRun.sort_newest(entries), level, effective)
438
439# Metadata accessors keep clients independent of the Tbl field layout.
440def table_smallest(tab: Table) -> Maybe<&2, String>:
441  match tab:
442    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
443      smallest
444
445# Return the table largest for the SSTable indexes, filters, and lookup.
446def table_largest(tab: Table) -> Maybe<&2, String>:
447  match tab:
448    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
449      largest
450
451# Return the table count for the SSTable indexes, filters, and lookup.
452def table_count(tab: Table) -> Nat:
453  match tab:
454    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
455      count
456
457# Total table identity for the read cache: smallest|largest|count.
458def opt_name(+opt: Maybe<&2, String>) -> String:
459  match opt:
460    case None{}:
461      "<none>"
462    case Some{s}:
463      s
464
465# Return the table id for the SSTable indexes, filters, and lookup.
466def table_id(tab: Table) -> String:
467  match tab:
468    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
469      opt_name(smallest) ++ "|" ++ opt_name(largest) ++ "|" ++ Nat.show(count)
470
471# Open bridge: metadata is matched explicitly so the Bloom schedule projects
472# through `build` without relying on reduction of an open entry list.
473def table_bits_from_meta(
474  +entries: List<&2, MemTable.Entry>,
475  level: Nat,
476  est_keys: Nat,
477  meta: Metadata,
478) -> {table_bits(from_sorted_unique_est_meta(entries, level, est_keys, meta)) == bloom_bits(level, est_keys) : Nat}:
479  match meta:
480    case Meta{smallest, largest, count}:
481      {==}
482
483# Return the table bits build for the SSTable indexes, filters, and lookup.
484def table_bits_build(
485  +entries: List<&2, MemTable.Entry>,
486  level: Nat,
487  est_keys: Nat,
488) -> {table_bits(build(entries, level, est_keys)) == bloom_bits(level, Nat.max(est_keys, List.length(&2, MemTable.Entry, entries))) : Nat}:
489  +effective = Nat.max(est_keys, List.length(&2, MemTable.Entry, entries))
490  table_bits_from_meta(SortedRun.sort_newest(entries), level, effective, metadata(SortedRun.sort_newest(entries)))
491
492
493# Check key ranges for disjoint for the SSTable indexes, filters, and lookup.
494def ranges_disjoint(+ta: Table, +tb: Table) -> Bool:
495  SortedRun.range_disjoint_bounds(table_smallest(ta), table_largest(ta), table_smallest(tb), table_largest(tb))
496
497# Return the size of of in the SSTable indexes, filters, and lookup.
498def size_of(bloom: Bloom) -> Nat:
499  match bloom:
500    case Blm{bits, size}:
501      size
502
503# Bloom insertions only set tree bits and preserve the exact logical size.
504def bloom_size_stable(
505  entries: List<&2, MemTable.Entry>,
506  bits: BitTree.BitTree,
507  +size: Nat
508) -> {size_of(bloom_of(entries, Blm{bits, size})) == size : Nat}:
509  match entries:
510    case Nil{}:
511      {==}
512    case Con{e, t}:
513      match e:
514        case MemTable.Entry{+key, val}:
515          bloom_size_stable(t, bit_set(bhash_n(key, 17, size), bit_set(bhash_n(key, 257, size), bits)), size)
516
517# Block-scoped lookup. Checker constraints (proven by scratch experiments):
518# multi-scrutinee matches follow declaration order, and only Nat answers flow
519# through match arms — so selection returns an index, picked up by chunk_at_go.
520def pick_ix(bt: List<&2, String>, ct: List<&2, List<&2, MemTable.Entry>>, ix: Nat, +key: String, order: Cmp) -> Nat:
521  match bt ct order:
522    case _ _ GT{}:
523      Nat.sub(ix, 1n)
524    case Nil{} _ _:
525      ix
526    case _ Nil{} _:
527      ix
528    case Con{b2, bt2} Con{d2, ct2} LT{}:
529      pick_ix(bt2, ct2, Nat.add(ix, 1n), key, Keys.cmp(b2, key))
530    case Con{b2, bt2} Con{d2, ct2} EQ{}:
531      ix
532
533# Handle the chunk at go for the SSTable indexes, filters, and lookup.
534def chunk_at_go(fuel: Nat, +chunks: List<&2, List<&2, MemTable.Entry>>, ix: Nat) -> List<&2, MemTable.Entry>:
535  match fuel chunks:
536    case 0n _:
537      Nil{}
538    case 1n+f Nil{}:
539      Nil{}
540    case 1n+f Con{c, t}:
541      match ix:
542        case 0n:
543          c
544        case 1n+p:
545          chunk_at_go(f, t, p)
546
547# Handle the chunk at for the SSTable indexes, filters, and lookup.
548def chunk_at(+chunks: List<&2, List<&2, MemTable.Entry>>, ix: Nat) -> List<&2, MemTable.Entry>:
549  chunk_at_go(List.length(&2, List<&2, MemTable.Entry>, chunks), chunks, ix)
550
551# Handle the block scan for the SSTable indexes, filters, and lookup.
552def block_scan(+chunk: List<&2, MemTable.Entry>, +key: String) -> Maybe<&2, String>:
553  MemTable.get(MemTable.MT{chunk}, key)
554
555# Handle the block pick for the SSTable indexes, filters, and lookup.
556def block_pick(
557  +chunks: List<&2, List<&2, MemTable.Entry>>,
558  +blocks: List<&2, String>,
559  +key: String
560) -> List<&2, MemTable.Entry>:
561  match chunks blocks:
562    case Nil{} _:
563      Nil{}
564    case _ Nil{}:
565      Nil{}
566    case Con{c, ct} Con{b, bt}:
567      chunk_at(chunks, pick_ix(bt, ct, 0n, key, Keys.cmp(b, key)))
568
569# Handle the block finish for the SSTable indexes, filters, and lookup.
570def block_finish(
571  picked: List<&2, MemTable.Entry>,
572  +entries: List<&2, MemTable.Entry>,
573  +key: String
574) -> Maybe<&2, String>:
575  match picked:
576    case Nil{}:
577      MemTable.get(MemTable.MT{entries}, key)
578    case Con{e, t}:
579      block_scan(Con{e, t}, key)
580
581# Resolve the hit for finish for the SSTable indexes, filters, and lookup.
582def hit_finish(
583  picked: List<&2, MemTable.Entry>,
584  +entries: List<&2, MemTable.Entry>,
585  +key: String
586) -> Maybe<&2, Maybe<&2, String>>:
587  match picked:
588    case Nil{}:
589      MemTable.get_hit(MemTable.MT{entries}, key)
590    case Con{e, t}:
591      MemTable.get_hit(MemTable.MT{Con{e, t}}, key)
592
593# Handle the block get hit for the SSTable indexes, filters, and lookup.
594def block_get_hit(tab: Table, +key: String) -> Maybe<&2, Maybe<&2, String>>:
595  match tab:
596    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
597      hit_finish(block_pick(chunks, blocks, key), entries, key)
598
599# Handle the block get for the SSTable indexes, filters, and lookup.
600def block_get(tab: Table, +key: String) -> Maybe<&2, String>:
601  match tab:
602    case Tbl{entries, filter, nbits, smallest, largest, count, chunks, blocks}:
603      block_finish(block_pick(chunks, blocks, key), entries, key)
604
605# Handle lookup in the SSTable indexes, filters, and lookup.
606def lookup(tab: Table, +key: String) -> Maybe<&2, String>:
607  block_get(tab, key)