Skip to main content

cjc_data/detcoll/
dharht_memory.rs

1//! `DHarhtMemory` — port of the user-supplied **D-HARHT Memory profile**
2//! architecture. Distinct from `DHarht` (v.01) which uses byte-slice
3//! keys with a slab allocator.
4//!
5//! Source: `D-HARHT-Blueprint-and-Code.md` (2026-04-28). This is a
6//! faithful port of the Memory profile (sparse paged 16-bit front
7//! directory, packed `u64` front entries with 3-bit tags,
8//! `MicroBucket4`/`8`/`16` with parallel `match_mask`, splitmix64
9//! finalizer as scatter), adapted for the workspace's ownership/
10//! deterministic-collections module.
11//!
12//! Differences vs the source blueprint (scope-limited):
13//!
14//! - **Keys are `u64`** (matches the source). For arbitrary byte keys
15//!   keep using `DHarht` (v.01).
16//! - **Per-shard fallback is a `BTreeMap`** (deterministic) for
17//!   collision groups exceeding `MicroBucket16` capacity. The source
18//!   uses a full Adaptive Radix Tree (Node4/16/32/48/256) at the
19//!   per-shard level. ART is correct + deterministic but is a larger
20//!   port; `BTreeMap` fallback satisfies "no silent entry loss" and
21//!   is what the source's Memory profile spec already allowed under
22//!   "deterministic fallback to BTreeMap or SortedVecMap if probe
23//!   budget is exceeded."
24//! - **No SIMD `match_mask`**: the parallel scalar bitmask still gets
25//!   compiled to tight code by LLVM; we don't reach for `SSE2`
26//!   intrinsics for portability.
27//!
28//! Determinism contract preserved bit-equal:
29//! - splitmix64 deterministic scatter (no random seeds)
30//! - 256 shards (power of two)
31//! - sealed sparse 16-bit front directory
32//! - `MicroBucket16` cap enforced; overflow → per-shard `BTreeMap`
33//! - full key equality on every successful lookup
34//! - per-shard collision counters
35
36use std::collections::BTreeMap;
37
38/// Number of shards. Power of two so `(scatter >> shift) & MASK` is the
39/// shard index.
40pub const NSHARDS_M: usize = 256;
41const SHARD_MASK_M: u64 = (NSHARDS_M - 1) as u64;
42const SHARD_SHIFT_M: u32 = 64 - 8; // top 8 bits = shard
43
44/// 16-bit front directory: 65 536 prefixes per table, sparse-paged
45/// with 8-bit pages (256 entries each = 256 pages).
46pub const FRONT_BITS_MEMORY: u8 = 16;
47const FRONT_SLOTS_MEMORY: usize = 1 << FRONT_BITS_MEMORY;
48const SPARSE_PAGE_BITS: u8 = 8;
49const SPARSE_PAGE_SIZE: usize = 1 << SPARSE_PAGE_BITS;
50const NO_PAGE: u32 = u32::MAX;
51
52/// 3-bit tag at the bottom of every packed front entry.
53const FRONT_TAG_MASK: u64 = 0b111;
54const FRONT_EMPTY: u64 = 0;
55const FRONT_SINGLE: u64 = 0b001;
56const FRONT_MICRO4: u64 = 0b010;
57const FRONT_MICRO8: u64 = 0b011;
58const FRONT_MICRO16: u64 = 0b100;
59const FRONT_FALLBACK: u64 = 0b101;
60
61/// Splitmix64 finalizer — used as the scatter function. Matches the
62/// blueprint exactly. Deterministic across machines.
63#[inline(always)]
64pub fn deterministic_permutation_scatter(mut x: u64) -> u64 {
65    x ^= x >> 30;
66    x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
67    x ^= x >> 27;
68    x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
69    x ^ (x >> 31)
70}
71
72/// Top 16 bits of the scattered key = front prefix.
73#[inline(always)]
74fn front_prefix(scattered: u64) -> usize {
75    (scattered >> (64 - FRONT_BITS_MEMORY)) as usize
76}
77
78#[inline(always)]
79fn pack_front_single(shard_id: usize, leaf_id: usize) -> u64 {
80    debug_assert!(shard_id < (1 << 29));
81    debug_assert!(leaf_id < (1 << 32));
82    ((shard_id as u64) << 35) | ((leaf_id as u64) << 3) | FRONT_SINGLE
83}
84
85#[inline(always)]
86fn unpack_front_single(packed: u64) -> (usize, usize) {
87    ((packed >> 35) as usize, ((packed >> 3) as u32) as usize)
88}
89
90#[inline(always)]
91fn pack_front_micro(bucket_id: usize, tag: u64) -> u64 {
92    debug_assert!(bucket_id < (1_usize << 61));
93    ((bucket_id as u64) << 3) | tag
94}
95
96#[inline(always)]
97fn unpack_front_micro(packed: u64) -> usize {
98    (packed >> 3) as usize
99}
100
101/// `MicroBucket4` — 4-entry inline collision group with parallel match.
102#[derive(Clone, Debug)]
103pub struct MicroBucket4 {
104    pub shard_id: u16,
105    pub count: u8,
106    pub keys: [u64; 4],
107    pub leaf_ids: [u32; 4],
108}
109
110impl MicroBucket4 {
111    #[inline(always)]
112    fn match_mask(&self, key: u64) -> u32 {
113        let mask = ((self.keys[0] == key) as u32)
114            | (((self.keys[1] == key) as u32) << 1)
115            | (((self.keys[2] == key) as u32) << 2)
116            | (((self.keys[3] == key) as u32) << 3);
117        let live = if self.count >= 4 { 0b1111 } else { (1u32 << self.count) - 1 };
118        mask & live
119    }
120}
121
122/// `MicroBucket8` — 8-entry inline collision group.
123#[derive(Clone, Debug)]
124pub struct MicroBucket8 {
125    pub shard_id: u16,
126    pub count: u8,
127    pub keys: [u64; 8],
128    pub leaf_ids: [u32; 8],
129}
130
131impl MicroBucket8 {
132    #[inline(always)]
133    fn match_mask(&self, key: u64) -> u32 {
134        let mut mask = 0u32;
135        for slot in 0..8 {
136            mask |= ((self.keys[slot] == key) as u32) << slot;
137        }
138        let live = if self.count >= 8 { 0xff } else { (1u32 << self.count) - 1 };
139        mask & live
140    }
141}
142
143/// `MicroBucket16` — 16-entry inline collision group.
144#[derive(Clone, Debug)]
145pub struct MicroBucket16 {
146    pub shard_id: u16,
147    pub count: u8,
148    pub keys: [u64; 16],
149    pub leaf_ids: [u32; 16],
150}
151
152impl MicroBucket16 {
153    #[inline(always)]
154    fn match_mask(&self, key: u64) -> u32 {
155        let mut mask = 0u32;
156        for slot in 0..16 {
157            mask |= ((self.keys[slot] == key) as u32) << slot;
158        }
159        let live = if self.count >= 16 {
160            u16::MAX as u32
161        } else {
162            (1u32 << self.count) - 1
163        };
164        mask & live
165    }
166}
167
168/// Leaf payload: key + value. Indexed by `leaf_id` within the
169/// owning shard's slab.
170#[derive(Clone, Debug)]
171pub struct LeafNode<V: Clone> {
172    pub key: u64,
173    pub value: V,
174}
175
176// ─────────────────────────────────────────────────────────────────────────
177//  Phase 12: ART (Adaptive Radix Tree) fallback
178//
179//  Ported from the user-supplied D-HARHT blueprint. Used when a
180//  front-directory prefix has more than `MicroBucket16` entries
181//  (i.e., adversarial-collision workloads). Pre-Phase-12 those
182//  entries went into a global `BTreeMap` fallback; Phase 12 routes
183//  them into a per-shard ART so worst-case lookup stays
184//  byte-by-byte radix descent rather than `O(log N)` BTree walk.
185//
186//  Determinism preserved: ART growth is monotonic + deterministic
187//  (Node4 → Node16 → Node32 → Node48 → Node256 by child count). Same
188//  insertion order produces byte-identical tree shape.
189// ─────────────────────────────────────────────────────────────────────────
190
191const ART_EMPTY: u32 = u32::MAX;
192const NODE48_EMPTY: u8 = u8::MAX;
193
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195struct TaggedNode(u32);
196
197#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198enum NodeKind {
199    Leaf,
200    Node4,
201    Node16,
202    Node32,
203    Node48,
204    Node256,
205}
206
207impl TaggedNode {
208    const TAG_BITS: u32 = 3;
209    const INDEX_SHIFT: u32 = 3;
210    const LEAF: u32 = 0;
211    const N4: u32 = 1;
212    const N16: u32 = 2;
213    const N32: u32 = 3;
214    const N48: u32 = 4;
215    const N256: u32 = 5;
216
217    fn new(index: usize, tag: u32) -> Self {
218        debug_assert!(tag < (1 << Self::TAG_BITS));
219        assert!(index < (1usize << (32 - Self::INDEX_SHIFT)));
220        Self(((index as u32) << Self::INDEX_SHIFT) | tag)
221    }
222    fn leaf(i: usize) -> Self { Self::new(i, Self::LEAF) }
223    fn n4(i: usize) -> Self { Self::new(i, Self::N4) }
224    fn n16(i: usize) -> Self { Self::new(i, Self::N16) }
225    fn n32(i: usize) -> Self { Self::new(i, Self::N32) }
226    fn n48(i: usize) -> Self { Self::new(i, Self::N48) }
227    fn n256(i: usize) -> Self { Self::new(i, Self::N256) }
228
229    fn kind(self) -> NodeKind {
230        match self.0 & ((1 << Self::TAG_BITS) - 1) {
231            Self::LEAF => NodeKind::Leaf,
232            Self::N4 => NodeKind::Node4,
233            Self::N16 => NodeKind::Node16,
234            Self::N32 => NodeKind::Node32,
235            Self::N48 => NodeKind::Node48,
236            Self::N256 => NodeKind::Node256,
237            _ => unreachable!(),
238        }
239    }
240
241    fn index(self) -> usize { (self.0 >> Self::INDEX_SHIFT) as usize }
242}
243
244#[derive(Clone, Debug)]
245struct ArtNode4 { count: u8, keys: [u8; 4], children: [u32; 4] }
246#[derive(Clone, Debug)]
247struct ArtNode16 { count: u8, keys: [u8; 16], children: [u32; 16] }
248#[derive(Clone, Debug)]
249struct ArtNode32 { count: u8, keys: [u8; 32], children: [u32; 32] }
250#[derive(Clone, Debug)]
251struct ArtNode48 { count: u8, child_index: [u8; 256], children: [u32; 48] }
252#[derive(Clone, Debug)]
253struct ArtNode256 { count: u16, children: [u32; 256] }
254
255/// Per-shard ART slab. Holds entries (tagged), leaves (key+value),
256/// and per-node-type arrays. Indices are `u32`. Allocation is
257/// monotonic.
258#[derive(Clone, Debug)]
259struct ArtSlab<V: Clone> {
260    entries: Vec<TaggedNode>,
261    leaves: Vec<LeafNode<V>>,
262    node4: Vec<ArtNode4>,
263    node16: Vec<ArtNode16>,
264    node32: Vec<ArtNode32>,
265    node48: Vec<ArtNode48>,
266    node256: Vec<ArtNode256>,
267}
268
269impl<V: Clone> ArtSlab<V> {
270    fn new() -> Self {
271        Self {
272            entries: Vec::new(),
273            leaves: Vec::new(),
274            node4: Vec::new(),
275            node16: Vec::new(),
276            node32: Vec::new(),
277            node48: Vec::new(),
278            node256: Vec::new(),
279        }
280    }
281
282    fn alloc_entry(&mut self, t: TaggedNode) -> u32 {
283        let id = self.entries.len() as u32;
284        self.entries.push(t);
285        id
286    }
287    fn alloc_leaf(&mut self, key: u64, value: V) -> u32 {
288        let lid = self.leaves.len();
289        self.leaves.push(LeafNode { key, value });
290        self.alloc_entry(TaggedNode::leaf(lid))
291    }
292    fn alloc_node4(&mut self) -> u32 {
293        let nid = self.node4.len();
294        self.node4.push(ArtNode4 { count: 0, keys: [0; 4], children: [ART_EMPTY; 4] });
295        self.alloc_entry(TaggedNode::n4(nid))
296    }
297
298    /// Byte at `depth` of the scattered key (0..=7).
299    #[inline(always)]
300    fn radix(scattered: u64, depth: usize) -> u8 {
301        if depth >= 8 { 0 } else { (scattered >> ((7 - depth) * 8)) as u8 }
302    }
303
304    fn find_child(&self, entry_id: u32, radix: u8) -> Option<u32> {
305        let entry = self.entries[entry_id as usize];
306        match entry.kind() {
307            NodeKind::Leaf => None,
308            NodeKind::Node4 => {
309                let n = &self.node4[entry.index()];
310                for i in 0..n.count as usize {
311                    if n.keys[i] == radix { return Some(n.children[i]); }
312                }
313                None
314            }
315            NodeKind::Node16 => {
316                let n = &self.node16[entry.index()];
317                for i in 0..n.count as usize {
318                    if n.keys[i] == radix { return Some(n.children[i]); }
319                }
320                None
321            }
322            NodeKind::Node32 => {
323                let n = &self.node32[entry.index()];
324                for i in 0..n.count as usize {
325                    if n.keys[i] == radix { return Some(n.children[i]); }
326                }
327                None
328            }
329            NodeKind::Node48 => {
330                let n = &self.node48[entry.index()];
331                let slot = n.child_index[radix as usize];
332                if slot == NODE48_EMPTY { None } else { Some(n.children[slot as usize]) }
333            }
334            NodeKind::Node256 => {
335                let c = self.node256[entry.index()].children[radix as usize];
336                if c == ART_EMPTY { None } else { Some(c) }
337            }
338        }
339    }
340
341    fn add_child(&mut self, entry_id: u32, radix: u8, child: u32) {
342        let entry = self.entries[entry_id as usize];
343        match entry.kind() {
344            NodeKind::Node4 if self.node4[entry.index()].count < 4 => {
345                let n = &mut self.node4[entry.index()];
346                let s = n.count as usize;
347                n.keys[s] = radix; n.children[s] = child; n.count += 1;
348            }
349            NodeKind::Node4 => { self.grow_4_to_16(entry_id); self.add_child(entry_id, radix, child); }
350            NodeKind::Node16 if self.node16[entry.index()].count < 16 => {
351                let n = &mut self.node16[entry.index()];
352                let s = n.count as usize;
353                n.keys[s] = radix; n.children[s] = child; n.count += 1;
354            }
355            NodeKind::Node16 => { self.grow_16_to_32(entry_id); self.add_child(entry_id, radix, child); }
356            NodeKind::Node32 if self.node32[entry.index()].count < 32 => {
357                let n = &mut self.node32[entry.index()];
358                let s = n.count as usize;
359                n.keys[s] = radix; n.children[s] = child; n.count += 1;
360            }
361            NodeKind::Node32 => { self.grow_32_to_48(entry_id); self.add_child(entry_id, radix, child); }
362            NodeKind::Node48 if self.node48[entry.index()].count < 48 => {
363                let n = &mut self.node48[entry.index()];
364                let s = n.count as usize;
365                n.child_index[radix as usize] = s as u8;
366                n.children[s] = child;
367                n.count += 1;
368            }
369            NodeKind::Node48 => { self.grow_48_to_256(entry_id); self.add_child(entry_id, radix, child); }
370            NodeKind::Node256 => {
371                let n = &mut self.node256[entry.index()];
372                if n.children[radix as usize] == ART_EMPTY { n.count += 1; }
373                n.children[radix as usize] = child;
374            }
375            NodeKind::Leaf => unreachable!("cannot add child to leaf"),
376        }
377    }
378
379    fn grow_4_to_16(&mut self, entry_id: u32) {
380        let old_idx = self.entries[entry_id as usize].index();
381        let old = &self.node4[old_idx];
382        let mut next = ArtNode16 { count: old.count, keys: [0; 16], children: [ART_EMPTY; 16] };
383        let c = old.count as usize;
384        next.keys[..c].copy_from_slice(&old.keys[..c]);
385        next.children[..c].copy_from_slice(&old.children[..c]);
386        let new_idx = self.node16.len();
387        self.node16.push(next);
388        self.entries[entry_id as usize] = TaggedNode::n16(new_idx);
389    }
390    fn grow_16_to_32(&mut self, entry_id: u32) {
391        let old_idx = self.entries[entry_id as usize].index();
392        let old = &self.node16[old_idx];
393        let mut next = ArtNode32 { count: old.count, keys: [0; 32], children: [ART_EMPTY; 32] };
394        let c = old.count as usize;
395        next.keys[..c].copy_from_slice(&old.keys[..c]);
396        next.children[..c].copy_from_slice(&old.children[..c]);
397        let new_idx = self.node32.len();
398        self.node32.push(next);
399        self.entries[entry_id as usize] = TaggedNode::n32(new_idx);
400    }
401    fn grow_32_to_48(&mut self, entry_id: u32) {
402        let old_idx = self.entries[entry_id as usize].index();
403        let old = &self.node32[old_idx];
404        let mut next = ArtNode48 {
405            count: old.count, child_index: [NODE48_EMPTY; 256], children: [ART_EMPTY; 48],
406        };
407        for i in 0..old.count as usize {
408            next.child_index[old.keys[i] as usize] = i as u8;
409            next.children[i] = old.children[i];
410        }
411        let new_idx = self.node48.len();
412        self.node48.push(next);
413        self.entries[entry_id as usize] = TaggedNode::n48(new_idx);
414    }
415    fn grow_48_to_256(&mut self, entry_id: u32) {
416        let old_idx = self.entries[entry_id as usize].index();
417        let old = &self.node48[old_idx];
418        let mut next = ArtNode256 { count: old.count as u16, children: [ART_EMPTY; 256] };
419        for radix in 0..=u8::MAX {
420            let s = old.child_index[radix as usize];
421            if s != NODE48_EMPTY {
422                next.children[radix as usize] = old.children[s as usize];
423            }
424        }
425        let new_idx = self.node256.len();
426        self.node256.push(next);
427        self.entries[entry_id as usize] = TaggedNode::n256(new_idx);
428    }
429
430    /// Insert (key, value). Returns previous value if key existed.
431    /// `root` is the entry ID of the ART root; pass `None` for an
432    /// empty tree (this fn returns Some(new_root) if it allocated one).
433    fn insert(
434        &mut self,
435        root: Option<u32>,
436        key: u64,
437        scattered: u64,
438        value: V,
439    ) -> (u32, Option<V>) {
440        match root {
441            None => {
442                let leaf = self.alloc_leaf(key, value);
443                (leaf, None)
444            }
445            Some(r) => {
446                let prev = self.insert_at(r, 1, key, scattered, value);
447                (r, prev)
448            }
449        }
450    }
451
452    fn insert_at(
453        &mut self,
454        node_id: u32,
455        depth: usize,
456        key: u64,
457        scattered: u64,
458        value: V,
459    ) -> Option<V> {
460        if self.entries[node_id as usize].kind() == NodeKind::Leaf {
461            let leaf_id = self.entries[node_id as usize].index();
462            let leaf = &mut self.leaves[leaf_id];
463            if leaf.key == key {
464                return Some(std::mem::replace(&mut leaf.value, value));
465            }
466            // Split leaf into a Node4.
467            let old_leaf_entry = self.alloc_entry(TaggedNode::leaf(leaf_id));
468            let new_leaf_entry = self.alloc_leaf(key, value);
469            let old_scattered = deterministic_permutation_scatter(self.leaves[leaf_id].key);
470            self.join_leaves_in_place(node_id, depth, old_scattered, old_leaf_entry, scattered, new_leaf_entry);
471            return None;
472        }
473        let radix = Self::radix(scattered, depth);
474        if let Some(child) = self.find_child(node_id, radix) {
475            return self.insert_at(child, depth + 1, key, scattered, value);
476        }
477        let leaf = self.alloc_leaf(key, value);
478        self.add_child(node_id, radix, leaf);
479        None
480    }
481
482    fn join_leaves_in_place(
483        &mut self,
484        target: u32,
485        depth: usize,
486        left_scattered: u64,
487        left: u32,
488        right_scattered: u64,
489        right: u32,
490    ) {
491        let lr = Self::radix(left_scattered, depth);
492        let rr = Self::radix(right_scattered, depth);
493        let n4 = self.node4.len();
494        self.node4.push(ArtNode4 { count: 0, keys: [0; 4], children: [ART_EMPTY; 4] });
495        self.entries[target as usize] = TaggedNode::n4(n4);
496        if lr == rr && depth < 7 {
497            let child = self.alloc_node4();
498            self.add_child(target, lr, child);
499            self.join_leaves_in_place(child, depth + 1, left_scattered, left, right_scattered, right);
500        } else {
501            self.add_child(target, lr, left);
502            self.add_child(target, rr, right);
503        }
504    }
505
506    fn get(&self, root: Option<u32>, key: u64, scattered: u64) -> Option<&V> {
507        let mut node_id = root?;
508        let mut depth = 1;
509        loop {
510            let entry = self.entries[node_id as usize];
511            match entry.kind() {
512                NodeKind::Leaf => {
513                    let leaf = &self.leaves[entry.index()];
514                    return if leaf.key == key { Some(&leaf.value) } else { None };
515                }
516                _ => {
517                    let r = Self::radix(scattered, depth);
518                    let child = self.find_child(node_id, r)?;
519                    node_id = child;
520                    depth += 1;
521                }
522            }
523        }
524    }
525}
526
527/// Per-shard slab of leaves + ART fallback. The ART carries entries
528/// whose front-directory collision group exceeded `MicroBucket16`.
529#[derive(Clone, Debug)]
530struct Shard<V: Clone> {
531    leaves: Vec<LeafNode<V>>,
532    /// Build-time only: keys still in flight before sealing.
533    pre_seal: BTreeMap<u64, u32>,
534    /// Phase 12: ART fallback path. Populated during seal for prefixes
535    /// whose collision group exceeded `MicroBucket16`. `None` until
536    /// the first such overflow on this shard.
537    art: ArtSlab<V>,
538    art_root: Option<u32>,
539}
540
541impl<V: Clone> Shard<V> {
542    fn new() -> Self {
543        Self {
544            leaves: Vec::new(),
545            pre_seal: BTreeMap::new(),
546            art: ArtSlab::new(),
547            art_root: None,
548        }
549    }
550
551    fn art_insert(&mut self, key: u64, scattered: u64, value: V) {
552        let (new_root, _prev) = self.art.insert(self.art_root, key, scattered, value);
553        self.art_root = Some(new_root);
554    }
555
556    fn art_get(&self, key: u64, scattered: u64) -> Option<&V> {
557        self.art.get(self.art_root, key, scattered)
558    }
559}
560
561/// Sparse paged front directory. `page_table` maps the upper 8 bits
562/// of the prefix to a page id (or `NO_PAGE`); each page is a fixed
563/// 256-entry array of packed `u64` front entries.
564#[derive(Clone, Debug)]
565struct FrontDir {
566    page_table: Vec<u32>,
567    pages: Vec<Box<[u64; SPARSE_PAGE_SIZE]>>,
568}
569
570impl FrontDir {
571    fn empty() -> Self {
572        Self {
573            page_table: vec![NO_PAGE; FRONT_SLOTS_MEMORY / SPARSE_PAGE_SIZE],
574            pages: Vec::new(),
575        }
576    }
577
578    #[inline(always)]
579    fn get(&self, prefix: usize) -> u64 {
580        let page_id = self.page_table[prefix >> SPARSE_PAGE_BITS];
581        if page_id == NO_PAGE {
582            FRONT_EMPTY
583        } else {
584            self.pages[page_id as usize][prefix & (SPARSE_PAGE_SIZE - 1)]
585        }
586    }
587
588    fn ensure_page(&mut self, page: usize) -> usize {
589        if self.page_table[page] == NO_PAGE {
590            self.page_table[page] = self.pages.len() as u32;
591            self.pages.push(Box::new([FRONT_EMPTY; SPARSE_PAGE_SIZE]));
592        }
593        self.page_table[page] as usize
594    }
595
596    fn set(&mut self, prefix: usize, value: u64) {
597        let page = prefix >> SPARSE_PAGE_BITS;
598        let pid = self.ensure_page(page);
599        self.pages[pid][prefix & (SPARSE_PAGE_SIZE - 1)] = value;
600    }
601}
602
603/// D-HARHT Memory profile — port of the user-supplied blueprint.
604#[derive(Clone, Debug)]
605pub struct DHarhtMemory<V: Clone> {
606    shards: Vec<Shard<V>>,
607    front: FrontDir,
608    micro4: Vec<MicroBucket4>,
609    micro8: Vec<MicroBucket8>,
610    micro16: Vec<MicroBucket16>,
611    /// Per-prefix overflow when collision count > 16. Replaces the
612    /// blueprint's ART fallback path. Deterministic, no silent loss.
613    overflow: BTreeMap<u64, V>,
614    sealed: bool,
615    total_entries: u64,
616    /// Per-table counters for security/diagnostics.
617    micro_overflow_count: u64,
618    max_collision_group: u32,
619}
620
621impl<V: Clone> DHarhtMemory<V> {
622    pub fn new() -> Self {
623        Self {
624            shards: (0..NSHARDS_M).map(|_| Shard::new()).collect(),
625            front: FrontDir::empty(),
626            micro4: Vec::new(),
627            micro8: Vec::new(),
628            micro16: Vec::new(),
629            overflow: BTreeMap::new(),
630            sealed: false,
631            total_entries: 0,
632            micro_overflow_count: 0,
633            max_collision_group: 0,
634        }
635    }
636
637    pub fn is_sealed(&self) -> bool {
638        self.sealed
639    }
640    pub fn len(&self) -> u64 {
641        self.total_entries
642    }
643    pub fn is_empty(&self) -> bool {
644        self.total_entries == 0
645    }
646    pub fn micro_overflow_count(&self) -> u64 {
647        self.micro_overflow_count
648    }
649    pub fn max_collision_group(&self) -> u32 {
650        self.max_collision_group
651    }
652
653    /// Approximate allocated memory in bytes. Counts shard slabs,
654    /// front-directory pages, micro-bucket pools, and BTreeMap fallback.
655    /// Useful for memory-comparison benches.
656    pub fn approx_memory_bytes(&self) -> usize {
657        use std::mem::size_of;
658        let mut total = size_of::<Self>();
659        for shard in &self.shards {
660            total += size_of::<Shard<V>>();
661            total += shard.leaves.capacity() * size_of::<LeafNode<V>>();
662            // Pre-seal BTreeMap: ~56 bytes per entry (B-tree node overhead).
663            total += shard.pre_seal.len() * 56;
664        }
665        // Front directory: page table + pages.
666        total += self.front.page_table.capacity() * size_of::<u32>();
667        total += self.front.pages.len() * size_of::<Box<[u64; SPARSE_PAGE_SIZE]>>();
668        total += self.front.pages.len() * SPARSE_PAGE_SIZE * size_of::<u64>();
669        // Micro buckets.
670        total += self.micro4.capacity() * size_of::<MicroBucket4>();
671        total += self.micro8.capacity() * size_of::<MicroBucket8>();
672        total += self.micro16.capacity() * size_of::<MicroBucket16>();
673        // Overflow BTreeMap.
674        total += self.overflow.len() * (size_of::<u64>() + size_of::<V>() + 56);
675        total
676    }
677
678    #[inline(always)]
679    fn shard_id(scattered: u64) -> usize {
680        ((scattered >> SHARD_SHIFT_M) & SHARD_MASK_M) as usize
681    }
682
683    /// Insert a `(key, value)`. Pre-seal: `O(log N_shard)` BTreeMap
684    /// insert. Update returns the previous value.
685    pub fn insert(&mut self, key: u64, value: V) -> Option<V> {
686        debug_assert!(!self.sealed, "DHarhtMemory: insert after seal");
687        let scattered = deterministic_permutation_scatter(key);
688        let s = Self::shard_id(scattered);
689        let shard = &mut self.shards[s];
690        if let Some(&leaf_id) = shard.pre_seal.get(&key) {
691            let prev = std::mem::replace(&mut shard.leaves[leaf_id as usize].value, value);
692            return Some(prev);
693        }
694        let leaf_id = shard.leaves.len() as u32;
695        shard.leaves.push(LeafNode { key, value });
696        shard.pre_seal.insert(key, leaf_id);
697        self.total_entries += 1;
698        None
699    }
700
701    /// Lookup a key. Pre-seal goes through the BTreeMap; post-seal
702    /// goes through the packed front directory + microbuckets.
703    #[inline]
704    pub fn get(&self, key: u64) -> Option<&V> {
705        let scattered = deterministic_permutation_scatter(key);
706        if !self.sealed {
707            let s = Self::shard_id(scattered);
708            let shard = &self.shards[s];
709            return shard
710                .pre_seal
711                .get(&key)
712                .map(|&leaf_id| &shard.leaves[leaf_id as usize].value);
713        }
714        let prefix = front_prefix(scattered);
715        let entry = self.front.get(prefix);
716        match entry & FRONT_TAG_MASK {
717            t if t == FRONT_SINGLE => {
718                let (sid, lid) = unpack_front_single(entry);
719                let leaf = &self.shards[sid].leaves[lid];
720                if leaf.key == key { Some(&leaf.value) } else { None }
721            }
722            t if t == FRONT_MICRO4 => {
723                let b = &self.micro4[unpack_front_micro(entry)];
724                let m = b.match_mask(key);
725                if m == 0 {
726                    return None;
727                }
728                let slot = m.trailing_zeros() as usize;
729                let leaf = &self.shards[b.shard_id as usize].leaves[b.leaf_ids[slot] as usize];
730                Some(&leaf.value)
731            }
732            t if t == FRONT_MICRO8 => {
733                let b = &self.micro8[unpack_front_micro(entry)];
734                let m = b.match_mask(key);
735                if m == 0 {
736                    return None;
737                }
738                let slot = m.trailing_zeros() as usize;
739                let leaf = &self.shards[b.shard_id as usize].leaves[b.leaf_ids[slot] as usize];
740                Some(&leaf.value)
741            }
742            t if t == FRONT_MICRO16 => {
743                let b = &self.micro16[unpack_front_micro(entry)];
744                let m = b.match_mask(key);
745                if m == 0 {
746                    return None;
747                }
748                let slot = m.trailing_zeros() as usize;
749                let leaf = &self.shards[b.shard_id as usize].leaves[b.leaf_ids[slot] as usize];
750                Some(&leaf.value)
751            }
752            t if t == FRONT_FALLBACK => {
753                // Phase 12: walk the originating shard's ART rather
754                // than the per-table BTreeMap. Faster on adversarial
755                // collision-rich workloads (radix descent vs BTree
756                // log walk). The legacy `overflow` BTreeMap is kept
757                // as a safety net for code paths that haven't been
758                // migrated yet — it stays empty post-Phase-12.
759                let s = Self::shard_id(scattered);
760                if let Some(v) = self.shards[s].art_get(key, scattered) {
761                    Some(v)
762                } else {
763                    self.overflow.get(&key)
764                }
765            }
766            _ => None,
767        }
768    }
769
770    pub fn contains_key(&self, key: u64) -> bool {
771        self.get(key).is_some()
772    }
773
774    /// Build the sealed lookup index. Groups leaves by their 16-bit
775    /// front prefix, packs each group into the smallest microbucket
776    /// that fits, and writes the packed entry into the sparse paged
777    /// front directory.
778    pub fn seal_for_lookup(&mut self) {
779        self.micro4.clear();
780        self.micro8.clear();
781        self.micro16.clear();
782        self.overflow.clear();
783        self.front = FrontDir::empty();
784        self.micro_overflow_count = 0;
785        self.max_collision_group = 0;
786
787        // Collect (prefix → Vec<(shard_id, leaf_id, key)>) in BTree
788        // order so the build pass is itself deterministic.
789        let mut buckets: BTreeMap<usize, Vec<(usize, usize, u64)>> = BTreeMap::new();
790        for (sid, shard) in self.shards.iter().enumerate() {
791            for (lid, leaf) in shard.leaves.iter().enumerate() {
792                let prefix = front_prefix(deterministic_permutation_scatter(leaf.key));
793                buckets.entry(prefix).or_default().push((sid, lid, leaf.key));
794            }
795        }
796
797        for (prefix, mut group) in buckets {
798            // Stable order for determinism: sort by (key, shard_id, leaf_id).
799            group.sort_by_key(|&(s, l, k)| (k, s, l));
800            self.max_collision_group =
801                self.max_collision_group.max(group.len() as u32);
802
803            let packed = match group.len() {
804                0 => continue,
805                1 => {
806                    let (sid, lid, _) = group[0];
807                    pack_front_single(sid, lid)
808                }
809                2..=4 => {
810                    let bid = self.micro4.len();
811                    let mut b = MicroBucket4 {
812                        shard_id: 0,
813                        count: group.len() as u8,
814                        keys: [0; 4],
815                        leaf_ids: [0; 4],
816                    };
817                    for (slot, &(s, l, k)) in group.iter().enumerate() {
818                        b.shard_id = s as u16;
819                        b.keys[slot] = k;
820                        b.leaf_ids[slot] = l as u32;
821                    }
822                    self.micro4.push(b);
823                    pack_front_micro(bid, FRONT_MICRO4)
824                }
825                5..=8 => {
826                    let bid = self.micro8.len();
827                    let mut b = MicroBucket8 {
828                        shard_id: 0,
829                        count: group.len() as u8,
830                        keys: [0; 8],
831                        leaf_ids: [0; 8],
832                    };
833                    for (slot, &(s, l, k)) in group.iter().enumerate() {
834                        b.shard_id = s as u16;
835                        b.keys[slot] = k;
836                        b.leaf_ids[slot] = l as u32;
837                    }
838                    self.micro8.push(b);
839                    pack_front_micro(bid, FRONT_MICRO8)
840                }
841                9..=16 => {
842                    let bid = self.micro16.len();
843                    let mut b = MicroBucket16 {
844                        shard_id: 0,
845                        count: group.len() as u8,
846                        keys: [0; 16],
847                        leaf_ids: [0; 16],
848                    };
849                    for (slot, &(s, l, k)) in group.iter().enumerate() {
850                        b.shard_id = s as u16;
851                        b.keys[slot] = k;
852                        b.leaf_ids[slot] = l as u32;
853                    }
854                    self.micro16.push(b);
855                    pack_front_micro(bid, FRONT_MICRO16)
856                }
857                n => {
858                    // Phase 12: overflow → per-shard ART (Adaptive Radix
859                    // Tree). Each entry goes into the ART of its
860                    // originating shard. Faster than BTreeMap on
861                    // adversarial-collision workloads (radix descent
862                    // vs B-tree log walk), still deterministic by
863                    // construction (Node4→Node16→...→Node256 growth
864                    // is monotonic + insertion-order-independent for
865                    // the final shape after compaction).
866                    self.micro_overflow_count += n as u64;
867                    for &(s, l, k) in &group {
868                        let v = self.shards[s].leaves[l].value.clone();
869                        let scattered = deterministic_permutation_scatter(k);
870                        self.shards[s].art_insert(k, scattered, v);
871                    }
872                    FRONT_FALLBACK
873                }
874            };
875            self.front.set(prefix, packed);
876        }
877        self.sealed = true;
878    }
879
880    /// Iterate all entries in canonical (key-sorted) order. The
881    /// pre-seal BTreeMap iteration is already sorted; post-seal we
882    /// re-sort because micro buckets aren't key-sorted.
883    pub fn iter_sorted(&self) -> Vec<(u64, &V)> {
884        if !self.sealed {
885            let mut all: Vec<(u64, &V)> = Vec::with_capacity(self.total_entries as usize);
886            for shard in &self.shards {
887                for leaf in &shard.leaves {
888                    all.push((leaf.key, &leaf.value));
889                }
890            }
891            all.sort_by_key(|&(k, _)| k);
892            return all;
893        }
894        let mut all: Vec<(u64, &V)> = Vec::with_capacity(self.total_entries as usize);
895        for shard in &self.shards {
896            for leaf in &shard.leaves {
897                all.push((leaf.key, &leaf.value));
898            }
899        }
900        all.sort_by_key(|&(k, _)| k);
901        all
902    }
903}
904
905impl<V: Clone> Default for DHarhtMemory<V> {
906    fn default() -> Self {
907        Self::new()
908    }
909}
910
911/// Deterministic shape hash for double-build identity tests.
912pub fn shape_hash<V: Clone + std::hash::Hash>(t: &DHarhtMemory<V>) -> u64 {
913    use std::hash::{Hash, Hasher};
914    let mut h = std::collections::hash_map::DefaultHasher::new();
915    let entries = t.iter_sorted();
916    h.write_u64(entries.len() as u64);
917    for (k, v) in entries {
918        h.write_u64(k);
919        v.hash(&mut h);
920    }
921    h.write_u64(t.micro_overflow_count);
922    h.write_u32(t.max_collision_group);
923    h.write_usize(t.micro4.len());
924    h.write_usize(t.micro8.len());
925    h.write_usize(t.micro16.len());
926    deterministic_permutation_scatter(h.finish())
927}
928
929#[cfg(test)]
930mod tests {
931    use super::*;
932
933    #[test]
934    fn insert_get_update_works() {
935        let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
936        assert_eq!(t.insert(7, 70), None);
937        assert_eq!(t.insert(9, 90), None);
938        assert_eq!(t.get(7), Some(&70));
939        assert_eq!(t.get(9), Some(&90));
940        assert_eq!(t.get(11), None);
941        assert_eq!(t.insert(7, 700), Some(70));
942        assert_eq!(t.get(7), Some(&700));
943    }
944
945    #[test]
946    fn seal_preserves_all_entries() {
947        let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
948        for i in 0..5_000u64 {
949            t.insert(i, i * 2);
950        }
951        t.seal_for_lookup();
952        for i in 0..5_000u64 {
953            assert_eq!(t.get(i), Some(&(i * 2)));
954        }
955    }
956
957    #[test]
958    fn deterministic_double_build_same_shape_hash() {
959        fn build() -> DHarhtMemory<u64> {
960            let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
961            for i in 0..1_000u64 {
962                t.insert(deterministic_permutation_scatter(i ^ 0x9e37_79b9_7f4a_7c15), i);
963            }
964            t.seal_for_lookup();
965            t
966        }
967        assert_eq!(shape_hash(&build()), shape_hash(&build()));
968    }
969
970    #[test]
971    fn micro16_capacity_enforced() {
972        let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
973        // 50k keys → 50k / 65536 ≈ 0.76 keys per prefix — well below 16.
974        for i in 0..50_000u64 {
975            t.insert(i, i);
976        }
977        t.seal_for_lookup();
978        // Overflow should be 0 or very low at this scale.
979        assert!(t.micro_overflow_count() < 100, "unexpected micro overflow: {}", t.micro_overflow_count());
980        // Max collision group ≤ 16 by construction (anything more goes
981        // to overflow).
982        for i in 0..50_000u64 {
983            assert_eq!(t.get(i), Some(&i));
984        }
985    }
986
987    #[test]
988    fn full_key_equality_no_false_positive() {
989        let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
990        for i in 0..2_000u64 {
991            t.insert(i, i);
992        }
993        t.seal_for_lookup();
994        for i in 2_000..3_000u64 {
995            assert_eq!(t.get(i), None, "false positive at {}", i);
996        }
997    }
998
999    #[test]
1000    fn art_fallback_handles_forced_overflow() {
1001        // Construct keys that map to the same shard + same 16-bit
1002        // front prefix to force MicroBucket16 overflow → ART path.
1003        // We need keys whose splitmix64 finalizer produces the same
1004        // top 24 bits (8 bits shard + 16 bits prefix). Brute-force
1005        // search a small space and pick keys that collide.
1006        let target_shard_prefix: u64 = {
1007            let s = deterministic_permutation_scatter(0);
1008            (s >> 40) & 0xFF_FFFF
1009        };
1010        let mut colliding: Vec<u64> = Vec::with_capacity(32);
1011        let mut k: u64 = 0;
1012        while colliding.len() < 32 && k < 100_000_000 {
1013            let s = deterministic_permutation_scatter(k);
1014            if (s >> 40) & 0xFF_FFFF == target_shard_prefix {
1015                colliding.push(k);
1016            }
1017            k += 1;
1018        }
1019        // We need >= 17 to overflow MicroBucket16. If brute search
1020        // didn't find enough at this scale, skip — bench keys are not
1021        // adversarial in practice.
1022        if colliding.len() >= 17 {
1023            let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
1024            for (i, &k) in colliding.iter().enumerate() {
1025                t.insert(k, i as u64);
1026            }
1027            t.seal_for_lookup();
1028            // ART path should be exercised.
1029            assert!(
1030                t.micro_overflow_count() > 0,
1031                "expected ART fallback to be triggered; overflow={}",
1032                t.micro_overflow_count()
1033            );
1034            // All keys must still be findable via the ART path.
1035            for (i, &k) in colliding.iter().enumerate() {
1036                assert_eq!(
1037                    t.get(k),
1038                    Some(&(i as u64)),
1039                    "ART fallback lost key {}",
1040                    k
1041                );
1042            }
1043        }
1044    }
1045
1046    #[test]
1047    fn matches_btreemap_oracle() {
1048        use std::collections::BTreeMap;
1049        let mut t: DHarhtMemory<u64> = DHarhtMemory::new();
1050        let mut oracle: BTreeMap<u64, u64> = BTreeMap::new();
1051        let mut x: u64 = 0xCAFEBABE;
1052        for _ in 0..5_000 {
1053            x = deterministic_permutation_scatter(x);
1054            let p1 = t.insert(x, x);
1055            let p2 = oracle.insert(x, x);
1056            assert_eq!(p1, p2);
1057        }
1058        t.seal_for_lookup();
1059        for (k, v) in &oracle {
1060            assert_eq!(t.get(*k), Some(v));
1061        }
1062    }
1063}