Skip to main content

subetha_cxc/
k_tower_cascade.rs

1//! `KTowerCascade<T, const DEPTH: usize>` - recursive pow2-of-pow2
2//! pointer encoding, the userspace MMU primitive.
3//!
4//! A `KTowerCascade<T, N>` is `[u32; N]` of indices. Each index at
5//! level `i` selects a slot in the level-`i` region; the slot at the
6//! upper levels holds the next-level cascade pointer, and the slot
7//! at the leaf level holds T itself. Resolution walks N levels of
8//! [`SharedRegion`] lookups.
9//!
10//! # The recursive form
11//!
12//! ```text
13//! KTowerCascade<T, 1> = [u32; 1]          // flat: one region
14//! KTowerCascade<T, 2> = [u32; 2]          // (region_id, offset)
15//! KTowerCascade<T, 4> = [u32; 4]          // mirrors MMU PML4
16//! KTowerCascade<T, N> = [u32; N]          // any depth
17//! ```
18//!
19//! At resolution time, level `i`'s region is a
20//! `SharedRegion<KTowerCascade<T, N-i-1>>` if `i < N-1`, or
21//! `SharedRegion<T>` if `i == N-1` (the leaf).
22//!
23//! # Why this matters
24//!
25//! - **Sparse address spaces**: with DEPTH=4 and u32 per level, the
26//!   total addressable space is 2^128 logical slots, but you only
27//!   pay storage for the branches you actually populate. Empty
28//!   subtrees take zero space.
29//! - **Userspace MMU**: this is the same mechanism the hardware MMU
30//!   uses (PML4 -> PDPT -> PD -> PT, four levels of 9-bit indices)
31//!   lifted to userspace and made position-independent.
32//! - **Cross-process**: every level uses INDICES, not virtual
33//!   addresses, so the cascade resolves identically in any process
34//!   that maps the same N regions.
35//! - **Adaptive depth**: callers pick DEPTH per workload. Hot dense
36//!   data uses DEPTH=1; cold sparse data uses DEPTH=4 or higher.
37
38use std::marker::PhantomData;
39use std::path::Path;
40
41use crate::shared_region::{OffsetPtr, RegionError, SharedRegion};
42
43/// Sentinel NIL: all-ones at every level.
44pub const NIL_INDEX: u32 = u32::MAX;
45
46/// Position-independent N-level cascade pointer.
47#[derive(Debug)]
48#[repr(C)]
49pub struct KTowerCascade<T, const DEPTH: usize> {
50    pub indices: [u32; DEPTH],
51    _phantom: PhantomData<T>,
52}
53
54impl<T, const DEPTH: usize> Clone for KTowerCascade<T, DEPTH> {
55    fn clone(&self) -> Self { *self }
56}
57impl<T, const DEPTH: usize> Copy for KTowerCascade<T, DEPTH> {}
58impl<T, const DEPTH: usize> PartialEq for KTowerCascade<T, DEPTH> {
59    fn eq(&self, other: &Self) -> bool { self.indices == other.indices }
60}
61impl<T, const DEPTH: usize> Eq for KTowerCascade<T, DEPTH> {}
62impl<T, const DEPTH: usize> std::hash::Hash for KTowerCascade<T, DEPTH> {
63    fn hash<H: std::hash::Hasher>(&self, s: &mut H) { self.indices.hash(s); }
64}
65// SAFETY: KTowerCascade is a plain `[u32; DEPTH]` with a PhantomData;
66// PhantomData<T> normally restricts Send/Sync to follow T, but the
67// cascade does not own a T - it is only an index into a region.
68// So Send/Sync are unconditional.
69unsafe impl<T, const DEPTH: usize> Send for KTowerCascade<T, DEPTH> {}
70unsafe impl<T, const DEPTH: usize> Sync for KTowerCascade<T, DEPTH> {}
71
72impl<T, const DEPTH: usize> Default for KTowerCascade<T, DEPTH> {
73    fn default() -> Self { Self::NIL }
74}
75
76impl<T, const DEPTH: usize> KTowerCascade<T, DEPTH> {
77    /// NIL sentinel: every level is u32::MAX.
78    pub const NIL: Self = Self {
79        indices: [NIL_INDEX; DEPTH],
80        _phantom: PhantomData,
81    };
82
83    pub const fn new(indices: [u32; DEPTH]) -> Self {
84        Self { indices, _phantom: PhantomData }
85    }
86
87    /// Construct from raw u32 array (alias of `new` for symmetry
88    /// with other primitives).
89    pub const fn from_raw(indices: [u32; DEPTH]) -> Self { Self::new(indices) }
90
91    /// Extract the raw u32 array. Stable cross-process representation.
92    pub const fn raw(self) -> [u32; DEPTH] { self.indices }
93
94    /// True when every level is NIL.
95    pub fn is_nil(self) -> bool {
96        self.indices.iter().all(|&i| i == NIL_INDEX)
97    }
98
99    /// Index at a specific level (0 = top, DEPTH-1 = leaf).
100    pub fn level(self, level: usize) -> u32 {
101        self.indices[level]
102    }
103
104    /// Convenience: index at the leaf (deepest) level.
105    pub fn leaf(self) -> u32 { self.indices[DEPTH - 1] }
106
107    /// Return a new cascade with one level replaced.
108    pub fn with_level(mut self, level: usize, value: u32) -> Self {
109        self.indices[level] = value;
110        self
111    }
112}
113
114/// Errors during cascade resolution.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum CascadeError {
117    Region(RegionError),
118    NilAtLevel(usize),
119    OutOfBounds,
120    IoError(std::io::ErrorKind),
121}
122
123impl From<RegionError> for CascadeError {
124    fn from(e: RegionError) -> Self { Self::Region(e) }
125}
126impl From<std::io::Error> for CascadeError {
127    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
128}
129
130// Concrete 2-level resolver: simplest case, equivalent to KTower2.
131// Generalises to N-level via repeated nesting below.
132
133/// Two-level cascade resolver: one inner SharedRegion holding T, one
134/// outer SharedRegion of `KTowerCascade<T, 1>` slots that ref the
135/// inner. Lookup walks: `cascade.indices[0]` -> outer_region slot ->
136/// `KTowerCascade<T, 1>` -> `cascade.indices[1]` -> inner_region slot.
137///
138/// For larger DEPTHs, compose `CascadeResolver2` and
139/// `CascadeResolver3` / `CascadeResolver4` below or use
140/// [`SharedRegion`] directly with custom traversal.
141pub struct CascadeResolver2<T: Copy + Default + 'static> {
142    pub outer: SharedRegion<KTowerCascade<T, 1>>,
143    pub inner: SharedRegion<T>,
144    header_sidecar: subetha_core::HandshakeHeader,
145    ring_sidecar: Box<subetha_core::ObservationRing>,
146}
147
148impl<T: Copy + Default + Send + Sync + 'static>
149    subetha_sidecar::AdaptiveInstance for CascadeResolver2<T>
150{
151    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
152    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
153    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
154        Box::new(subetha_sidecar::NoMigrationPolicy)
155    }
156}
157
158impl<T: Copy + Default + 'static> CascadeResolver2<T> {
159    pub fn create(
160        outer_path: impl AsRef<Path>, outer_capacity: usize,
161        inner_path: impl AsRef<Path>, inner_capacity: usize,
162    ) -> Result<Self, CascadeError> {
163        let outer = SharedRegion::create(outer_path, outer_capacity)?;
164        let inner = SharedRegion::create(inner_path, inner_capacity)?;
165        Ok(Self {
166            outer, inner,
167            header_sidecar: subetha_core::HandshakeHeader::new(),
168            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
169        })
170    }
171
172    pub fn open(
173        outer_path: impl AsRef<Path>, outer_capacity: usize,
174        inner_path: impl AsRef<Path>, inner_capacity: usize,
175    ) -> Result<Self, CascadeError> {
176        let outer = SharedRegion::open(outer_path, outer_capacity)?;
177        let inner = SharedRegion::open(inner_path, inner_capacity)?;
178        Ok(Self {
179            outer, inner,
180            header_sidecar: subetha_core::HandshakeHeader::new(),
181            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
182        })
183    }
184
185    /// Insert a T at a chosen cascade path. Allocates the inner slot
186    /// + any missing outer slot; returns the resolved cascade.
187    pub fn insert(
188        &self, outer_idx: u32, value: T,
189    ) -> Result<KTowerCascade<T, 2>, CascadeError> {
190        let r = self.insert_inner(outer_idx, value);
191        self.ring_sidecar.push_op(
192            crate::sidecar_ops::cascade::OP_INSERT,
193            if r.is_err() { 1 } else { 0 },
194        );
195        r
196    }
197
198    fn insert_inner(
199        &self, outer_idx: u32, value: T,
200    ) -> Result<KTowerCascade<T, 2>, CascadeError> {
201        // Allocate leaf T.
202        let leaf = self.inner.allocate(value)?;
203        // Outer slot holds a KTowerCascade<T, 1> that points to leaf.
204        let inner_cascade = KTowerCascade::<T, 1>::new([leaf.index]);
205        // Replace outer slot if it exists, otherwise allocate.
206        let outer_slot = if outer_idx < self.outer.capacity() as u32 {
207            // Check whether this outer slot has been allocated.
208            // The simplest contract: the caller's outer_idx is the
209            // SLOT INDEX in `outer`, so we allocate sequentially and
210            // require outer_idx to be < self.outer.len() OR exactly
211            // self.outer.len() (next free slot).
212            let cur_len = self.outer.len() as u32;
213            if outer_idx == cur_len {
214                let p = self.outer.allocate(inner_cascade)?;
215                p.index
216            } else if outer_idx < cur_len {
217                self.outer.set(OffsetPtr::new(outer_idx), inner_cascade)?;
218                outer_idx
219            } else {
220                return Err(CascadeError::OutOfBounds);
221            }
222        } else {
223            return Err(CascadeError::OutOfBounds);
224        };
225        Ok(KTowerCascade::<T, 2>::new([outer_slot, leaf.index]))
226    }
227
228    /// Resolve a 2-level cascade to its T value.
229    pub fn get(&self, c: KTowerCascade<T, 2>) -> Result<T, CascadeError> {
230        let r = self.get_inner(c);
231        self.ring_sidecar.push_op(
232            crate::sidecar_ops::cascade::OP_GET,
233            if r.is_err() { 1 } else { 0 },
234        );
235        r
236    }
237
238    fn get_inner(&self, c: KTowerCascade<T, 2>) -> Result<T, CascadeError> {
239        if c.indices[0] == NIL_INDEX { return Err(CascadeError::NilAtLevel(0)); }
240        let inner_cascade = self.outer.get(OffsetPtr::new(c.indices[0]))?;
241        if inner_cascade.indices[0] != c.indices[1] {
242            // The cascade we received doesn't match what's currently
243            // at this outer slot - either stale or pointing at the
244            // wrong leaf. Honour the cascade's claim by reading the
245            // leaf directly.
246        }
247        if c.indices[1] == NIL_INDEX { return Err(CascadeError::NilAtLevel(1)); }
248        Ok(self.inner.get(OffsetPtr::new(c.indices[1]))?)
249    }
250
251    pub fn flush(&self) -> Result<(), CascadeError> {
252        self.outer.flush()?;
253        self.inner.flush()?;
254        Ok(())
255    }
256}
257
258/// Generic N-level resolver: caller provides a sequence of
259/// SharedRegions, one per non-leaf level holding intermediate
260/// cascade pointers, plus the leaf region holding T. Resolution
261/// walks the indices array level-by-level.
262///
263/// This is the explicit-walk variant. For a more ergonomic 4-level
264/// MMU-shaped resolver, callers can wrap this in their own type.
265pub struct CascadeResolverN<T: Copy + Default + 'static, const DEPTH: usize> {
266    /// Intermediate regions, one per non-leaf level. Each holds a
267    /// flat u32 (the next level's index). For depth N, this has
268    /// length N-1.
269    pub intermediate: Vec<SharedRegion<u32>>,
270    /// Leaf region holding T.
271    pub leaf: SharedRegion<T>,
272    header_sidecar: subetha_core::HandshakeHeader,
273    ring_sidecar: Box<subetha_core::ObservationRing>,
274}
275
276impl<T: Copy + Default + Send + Sync + 'static, const DEPTH: usize>
277    subetha_sidecar::AdaptiveInstance for CascadeResolverN<T, DEPTH>
278{
279    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
280    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
281    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
282        Box::new(subetha_sidecar::NoMigrationPolicy)
283    }
284}
285
286impl<T: Copy + Default + 'static, const DEPTH: usize> CascadeResolverN<T, DEPTH> {
287    pub fn create(
288        leaf_path: impl AsRef<Path>, leaf_capacity: usize,
289        intermediate_paths_and_caps: Vec<(std::path::PathBuf, usize)>,
290    ) -> Result<Self, CascadeError> {
291        assert_eq!(intermediate_paths_and_caps.len(), DEPTH.saturating_sub(1),
292            "must supply DEPTH-1 intermediate region descriptors");
293        let leaf = SharedRegion::create(leaf_path, leaf_capacity)?;
294        let intermediate: Vec<SharedRegion<u32>> = intermediate_paths_and_caps
295            .into_iter()
296            .map(|(p, cap)| SharedRegion::create(p, cap))
297            .collect::<Result<Vec<_>, _>>()?;
298        Ok(Self {
299            intermediate, leaf,
300            header_sidecar: subetha_core::HandshakeHeader::new(),
301            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
302        })
303    }
304
305    pub fn open(
306        leaf_path: impl AsRef<Path>, leaf_capacity: usize,
307        intermediate_paths_and_caps: Vec<(std::path::PathBuf, usize)>,
308    ) -> Result<Self, CascadeError> {
309        assert_eq!(intermediate_paths_and_caps.len(), DEPTH.saturating_sub(1));
310        let leaf = SharedRegion::open(leaf_path, leaf_capacity)?;
311        let intermediate: Vec<SharedRegion<u32>> = intermediate_paths_and_caps
312            .into_iter()
313            .map(|(p, cap)| SharedRegion::open(p, cap))
314            .collect::<Result<Vec<_>, _>>()?;
315        Ok(Self {
316            intermediate, leaf,
317            header_sidecar: subetha_core::HandshakeHeader::new(),
318            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
319        })
320    }
321
322    /// Allocate a T value at the leaf and return its cascade. The
323    /// caller is responsible for populating the intermediate levels
324    /// (e.g., by treating outer indices as slot positions that
325    /// reference the next-level slot to descend into).
326    ///
327    /// For the simplest case where each level's "index" is just the
328    /// slot position in the corresponding intermediate region, use
329    /// `insert_path` which performs the full descent.
330    pub fn allocate_leaf(&self, value: T) -> Result<u32, CascadeError> {
331        Ok(self.leaf.allocate(value)?.index)
332    }
333
334    /// Insert at a chosen TOP-LEVEL slot. Every intermediate level
335    /// and the leaf are auto-allocated to fresh slots. Returns the
336    /// full cascade. Calling twice with the same `top_idx` will
337    /// OVERWRITE the previous top-level chain (the orphaned
338    /// intermediates remain in their regions but become unreachable
339    /// from `top_idx`).
340    ///
341    /// For independent insertions, use distinct `top_idx` values
342    /// (typically `cascade_count`, i.e. the next free top slot,
343    /// which can be obtained as `intermediate[0].len() as u32`).
344    pub fn insert_at_top(
345        &self, top_idx: u32, value: T,
346    ) -> Result<KTowerCascade<T, DEPTH>, CascadeError> {
347        let r = self.insert_at_top_inner(top_idx, value);
348        self.ring_sidecar.push_op(
349            crate::sidecar_ops::cascade::OP_INSERT,
350            if r.is_err() { 1 } else { 0 },
351        );
352        r
353    }
354
355    fn insert_at_top_inner(
356        &self, top_idx: u32, value: T,
357    ) -> Result<KTowerCascade<T, DEPTH>, CascadeError> {
358        let mut full = [0u32; DEPTH];
359        // Allocate the leaf first.
360        full[DEPTH - 1] = self.leaf.allocate(value)?.index;
361        // For each intermediate level from leaf-side back toward the
362        // top, allocate a fresh slot whose value points at the next
363        // level. The TOP slot is the caller-chosen top_idx.
364        for level in (1..DEPTH - 1).rev() {
365            let p = self.intermediate[level].allocate(full[level + 1])?;
366            full[level] = p.index;
367        }
368        // The top-level write: write full[1] into intermediate[0]
369        // at slot top_idx.
370        if DEPTH > 1 {
371            self.write_intermediate(0, top_idx, full[1])?;
372            full[0] = top_idx;
373        } else {
374            full[0] = top_idx;
375        }
376        Ok(KTowerCascade::<T, DEPTH>::new(full))
377    }
378
379    /// Append a fresh entry. Picks the next free top slot
380    /// automatically. Equivalent to
381    /// `insert_at_top(intermediate[0].len() as u32, value)` for
382    /// DEPTH > 1, or `(allocate leaf, return its index)` for DEPTH=1.
383    pub fn append(
384        &self, value: T,
385    ) -> Result<KTowerCascade<T, DEPTH>, CascadeError> {
386        if DEPTH == 1 {
387            let leaf_idx = self.leaf.allocate(value)?.index;
388            return Ok(KTowerCascade::<T, DEPTH>::new([leaf_idx; DEPTH]));
389        }
390        let next_top = self.intermediate[0].len() as u32;
391        self.insert_at_top(next_top, value)
392    }
393
394    fn write_intermediate(
395        &self, level: usize, slot: u32, value: u32,
396    ) -> Result<(), CascadeError> {
397        let region = &self.intermediate[level];
398        let cur_len = region.len() as u32;
399        if slot == cur_len {
400            region.allocate(value)?;
401        } else if slot < cur_len {
402            region.set(OffsetPtr::new(slot), value)?;
403        } else {
404            return Err(CascadeError::OutOfBounds);
405        }
406        Ok(())
407    }
408
409    /// Resolve a cascade to its T value by walking the levels.
410    pub fn get(&self, c: KTowerCascade<T, DEPTH>) -> Result<T, CascadeError> {
411        let r = self.get_inner(c);
412        self.ring_sidecar.push_op(
413            crate::sidecar_ops::cascade::OP_GET,
414            if r.is_err() { 1 } else { 0 },
415        );
416        r
417    }
418
419    fn get_inner(&self, c: KTowerCascade<T, DEPTH>) -> Result<T, CascadeError> {
420        for level in 0..DEPTH {
421            if c.indices[level] == NIL_INDEX {
422                return Err(CascadeError::NilAtLevel(level));
423            }
424        }
425        // Walk: for each non-leaf level, verify that the
426        // intermediate region's slot matches the next level's index.
427        for level in 0..DEPTH - 1 {
428            let stored = self.intermediate[level].get(OffsetPtr::new(c.indices[level]))?;
429            if stored != c.indices[level + 1] {
430                return Err(CascadeError::NilAtLevel(level + 1));
431            }
432        }
433        Ok(self.leaf.get(OffsetPtr::new(c.indices[DEPTH - 1]))?)
434    }
435
436    pub fn flush(&self) -> Result<(), CascadeError> {
437        self.leaf.flush()?;
438        for r in &self.intermediate { r.flush()?; }
439        Ok(())
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    fn tmp(name: &str) -> std::path::PathBuf {
448        let mut p = std::env::temp_dir();
449        let pid = std::process::id();
450        p.push(format!("subetha-cascade-{name}-{pid}.bin"));
451        p
452    }
453
454    // ===== Pointer-type tests =====
455
456    #[test]
457    fn nil_is_all_ones() {
458        let n = KTowerCascade::<u64, 4>::NIL;
459        assert!(n.is_nil());
460        assert_eq!(n.raw(), [NIL_INDEX; 4]);
461    }
462
463    #[test]
464    fn new_and_level_access() {
465        let c = KTowerCascade::<u64, 3>::new([1, 2, 3]);
466        assert_eq!(c.level(0), 1);
467        assert_eq!(c.level(1), 2);
468        assert_eq!(c.level(2), 3);
469        assert_eq!(c.leaf(), 3);
470        assert!(!c.is_nil());
471    }
472
473    #[test]
474    fn with_level_updates_only_one() {
475        let c = KTowerCascade::<u64, 4>::new([1, 2, 3, 4]);
476        let c2 = c.with_level(2, 99);
477        assert_eq!(c2.raw(), [1, 2, 99, 4]);
478        // Original unchanged.
479        assert_eq!(c.raw(), [1, 2, 3, 4]);
480    }
481
482    #[test]
483    fn equality_and_hash() {
484        use std::collections::HashSet;
485        let a = KTowerCascade::<u64, 2>::new([5, 10]);
486        let b = KTowerCascade::<u64, 2>::new([5, 10]);
487        let c = KTowerCascade::<u64, 2>::new([5, 11]);
488        assert_eq!(a, b);
489        assert_ne!(a, c);
490        let mut s = HashSet::new();
491        s.insert(a);
492        assert!(s.contains(&b));
493        assert!(!s.contains(&c));
494    }
495
496    #[test]
497    fn raw_round_trip_preserves_indices() {
498        let c = KTowerCascade::<u64, 4>::new([10, 20, 30, 40]);
499        let raw = c.raw();
500        let c2 = KTowerCascade::<u64, 4>::from_raw(raw);
501        assert_eq!(c, c2);
502    }
503
504    #[test]
505    fn depth_1_degenerates_to_offset_ptr() {
506        let c = KTowerCascade::<u64, 1>::new([42]);
507        assert_eq!(c.leaf(), 42);
508        assert_eq!(c.level(0), 42);
509    }
510
511    #[test]
512    fn size_grows_linearly_with_depth() {
513        assert_eq!(std::mem::size_of::<KTowerCascade<u64, 1>>(), 4);
514        assert_eq!(std::mem::size_of::<KTowerCascade<u64, 2>>(), 8);
515        assert_eq!(std::mem::size_of::<KTowerCascade<u64, 4>>(), 16);
516        assert_eq!(std::mem::size_of::<KTowerCascade<u64, 8>>(), 32);
517    }
518
519    // ===== CascadeResolver2 tests =====
520
521    #[test]
522    fn resolver2_insert_get_round_trip() {
523        let op = tmp("r2-outer");
524        let ip = tmp("r2-inner");
525        let r: CascadeResolver2<u64> = CascadeResolver2::create(&op, 16, &ip, 64).unwrap();
526        let c1 = r.insert(0, 100).unwrap();
527        let c2 = r.insert(1, 200).unwrap();
528        let c3 = r.insert(2, 300).unwrap();
529        assert_eq!(r.get(c1).unwrap(), 100);
530        assert_eq!(r.get(c2).unwrap(), 200);
531        assert_eq!(r.get(c3).unwrap(), 300);
532        std::fs::remove_file(&op).ok();
533        std::fs::remove_file(&ip).ok();
534    }
535
536    #[test]
537    fn resolver2_cross_handle_visibility() {
538        let op = tmp("r2-cross-outer");
539        let ip = tmp("r2-cross-inner");
540        let writer: CascadeResolver2<u64> = CascadeResolver2::create(&op, 16, &ip, 64).unwrap();
541        let reader: CascadeResolver2<u64> = CascadeResolver2::open(&op, 16, &ip, 64).unwrap();
542        let c = writer.insert(0, 7777).unwrap();
543        // The cascade's raw bits are stable.
544        let raw = c.raw();
545        // Reader uses the same raw cascade bits to resolve.
546        let c_reconstructed: KTowerCascade<u64, 2>
547            = KTowerCascade::from_raw(raw);
548        assert_eq!(reader.get(c_reconstructed).unwrap(), 7777);
549        std::fs::remove_file(&op).ok();
550        std::fs::remove_file(&ip).ok();
551    }
552
553    // ===== Generic CascadeResolverN tests =====
554
555    #[test]
556    fn resolver_n_depth_4_full_walk() {
557        let lp = tmp("rn-leaf");
558        let i0 = tmp("rn-i0");
559        let i1 = tmp("rn-i1");
560        let i2 = tmp("rn-i2");
561        let r: CascadeResolverN<u64, 4> = CascadeResolverN::create(
562            &lp, 64,
563            vec![(i0.clone(), 16), (i1.clone(), 16), (i2.clone(), 16)],
564        ).unwrap();
565        // Each insert picks a distinct top slot via append().
566        let c1 = r.append(1111).unwrap();
567        let c2 = r.append(2222).unwrap();
568        let c3 = r.append(3333).unwrap();
569        assert_eq!(r.get(c1).unwrap(), 1111);
570        assert_eq!(r.get(c2).unwrap(), 2222);
571        assert_eq!(r.get(c3).unwrap(), 3333);
572        for p in [&lp, &i0, &i1, &i2] { std::fs::remove_file(p).ok(); }
573    }
574
575    #[test]
576    fn resolver_n_nil_at_level_rejected() {
577        let lp = tmp("rn-nil-leaf");
578        let i0 = tmp("rn-nil-i0");
579        let r: CascadeResolverN<u64, 2> = CascadeResolverN::create(
580            &lp, 8, vec![(i0.clone(), 8)],
581        ).unwrap();
582        let nil = KTowerCascade::<u64, 2>::NIL;
583        assert!(matches!(r.get(nil), Err(CascadeError::NilAtLevel(0))));
584        for p in [&lp, &i0] { std::fs::remove_file(p).ok(); }
585    }
586
587    #[test]
588    fn resolver_n_struct_value_round_trip() {
589        #[derive(Clone, Copy, Debug, PartialEq, Default)]
590        #[repr(C)]
591        struct Entry { key: u64, payload: u64 }
592        let lp = tmp("rn-struct-leaf");
593        let i0 = tmp("rn-struct-i0");
594        let r: CascadeResolverN<Entry, 2> = CascadeResolverN::create(
595            &lp, 8, vec![(i0.clone(), 8)],
596        ).unwrap();
597        let entry = Entry { key: 42, payload: 999 };
598        let c = r.append(entry).unwrap();
599        assert_eq!(r.get(c).unwrap(), entry);
600        for p in [&lp, &i0] { std::fs::remove_file(p).ok(); }
601    }
602
603    #[test]
604    fn cascade_bits_are_position_independent() {
605        // The whole point: the same [u32; DEPTH] resolves to the
606        // same value in any process that maps the same regions.
607        let lp = tmp("pi-leaf");
608        let i0 = tmp("pi-i0");
609        let r_a: CascadeResolverN<u64, 2> = CascadeResolverN::create(
610            &lp, 8, vec![(i0.clone(), 8)],
611        ).unwrap();
612        let c = r_a.append(0xCAFE_BABE).unwrap();
613        let raw = c.raw();
614        let r_b: CascadeResolverN<u64, 2> = CascadeResolverN::open(
615            &lp, 8, vec![(i0.clone(), 8)],
616        ).unwrap();
617        let c_b = KTowerCascade::<u64, 2>::from_raw(raw);
618        assert_eq!(r_b.get(c_b).unwrap(), 0xCAFE_BABE);
619        for p in [&lp, &i0] { std::fs::remove_file(p).ok(); }
620    }
621
622    #[test]
623    fn resolver_n_disk_persistence_survives_reopen() {
624        let lp = tmp("rn-disk-leaf");
625        let i0 = tmp("rn-disk-i0");
626        let i1 = tmp("rn-disk-i1");
627        let c_raw;
628        {
629            let r: CascadeResolverN<u64, 3> = CascadeResolverN::create(
630                &lp, 8, vec![(i0.clone(), 8), (i1.clone(), 8)],
631            ).unwrap();
632            let c = r.append(5555).unwrap();
633            c_raw = c.raw();
634            r.flush().unwrap();
635        }
636        let r2: CascadeResolverN<u64, 3> = CascadeResolverN::open(
637            &lp, 8, vec![(i0.clone(), 8), (i1.clone(), 8)],
638        ).unwrap();
639        let c_restored = KTowerCascade::<u64, 3>::from_raw(c_raw);
640        assert_eq!(r2.get(c_restored).unwrap(), 5555);
641        for p in [&lp, &i0, &i1] { std::fs::remove_file(p).ok(); }
642    }
643
644    #[test]
645    fn default_is_nil() {
646        let c: KTowerCascade<u64, 4> = KTowerCascade::default();
647        assert!(c.is_nil());
648    }
649}