Skip to main content

opthash/
funnel.rs

1//! Paper-exact Funnel placement with explicit dynamic-map epoch extensions.
2use core::hash::{BuildHasher, Hash};
3use core::mem::{self, MaybeUninit};
4
5use alloc::{boxed::Box, vec::Vec};
6use allocator_api2::alloc::{Allocator, Global, Layout};
7use equivalent::Equivalent;
8
9use crate::ReserveFraction;
10use crate::common::DefaultHashBuilder;
11use crate::common::arena::{self, Arena, ArenaSlots, SlotEntry};
12use crate::common::config::GROUP_SIZE;
13use crate::common::control::{self, CTRL_EMPTY, CTRL_TOMBSTONE, ControlByte};
14use crate::common::error::{TryBuildError, TryReserveError};
15use crate::common::exact::geometry::PaperConfig;
16use crate::common::exact::probe::{
17    self, FunnelPrf, PreparedFastFunnelDomainProbe, PreparedFastFunnelProbe, PreparedProbeRange,
18    ProbeDomain,
19};
20use crate::common::math::capacity;
21use crate::common::membership::{self, MembershipKey, MembershipRegion};
22use crate::common::simd;
23use crate::epoch::{EpochSnapshot, EpochState, EpochTransition};
24use crate::{macros, map};
25
26const FUNNEL_PROBE_SEED: u64 = probe::WYHASH_DEFAULT_SECRET[3];
27const RANGE_WORD_CAP: u32 = 8;
28
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30struct LevelShape {
31    offset: usize,
32    bucket_range: PreparedProbeRange,
33    ordinary_counter_base: u64,
34}
35
36#[derive(Clone, Debug)]
37struct FunnelShape {
38    n: usize,
39    max_insertions: usize,
40    levels: Box<[LevelShape]>,
41    beta: usize,
42    loglog_ceiling: usize,
43    primary_offset: usize,
44    primary_range: PreparedProbeRange,
45    fallback_offset: usize,
46    fallback_bucket_width: usize,
47    fallback_bucket_range: PreparedProbeRange,
48}
49
50impl FunnelShape {
51    fn empty() -> Self {
52        Self {
53            n: 0,
54            max_insertions: 0,
55            levels: Box::new([]),
56            beta: 0,
57            loglog_ceiling: 0,
58            primary_offset: 0,
59            primary_range: PreparedProbeRange::empty(),
60            fallback_offset: 0,
61            fallback_bucket_width: 0,
62            fallback_bucket_range: PreparedProbeRange::empty(),
63        }
64    }
65
66    #[inline]
67    fn encode_logical_probe(&self, logical_probe: usize) -> u8 {
68        debug_assert!(logical_probe < self.loglog_ceiling);
69        #[allow(
70            clippy::cast_possible_truncation,
71            reason = "shape construction validates the counter lane"
72        )]
73        {
74            logical_probe as u8
75        }
76    }
77
78    fn from_slots(n: usize, reserve: ReserveFraction) -> Result<Self, TryReserveError> {
79        if n == 0 {
80            return Ok(Self::empty());
81        }
82        let config = PaperConfig::new(n, reserve.exponent())
83            .map_err(|_| TryReserveError::CapacityOverflow)?;
84        let plan = config
85            .funnel_plan()
86            .map_err(|_| TryReserveError::CapacityOverflow)?;
87        let mut offset = 0_usize;
88        let mut levels = Vec::new();
89        levels
90            .try_reserve_exact(plan.alpha())
91            .map_err(|_| TryReserveError::AllocError)?;
92        for (level_index, bucket_count) in plan.ordinary_bucket_counts().enumerate() {
93            levels.push(LevelShape {
94                offset,
95                bucket_range: PreparedProbeRange::new(bucket_count)
96                    .map_err(|_| TryReserveError::CapacityOverflow)?,
97                ordinary_counter_base: FunnelPrf::ordinary_counter_base(level_index as u64)
98                    .ok_or(TryReserveError::CapacityOverflow)?,
99            });
100            offset = offset
101                .checked_add(
102                    bucket_count
103                        .checked_mul(plan.beta())
104                        .ok_or(TryReserveError::CapacityOverflow)?,
105                )
106                .ok_or(TryReserveError::CapacityOverflow)?;
107        }
108        let primary_offset = offset;
109        let primary_len = plan.special_primary_len();
110        let primary_range =
111            PreparedProbeRange::new(primary_len).map_err(|_| TryReserveError::CapacityOverflow)?;
112        let fallback_offset = primary_offset
113            .checked_add(primary_len)
114            .ok_or(TryReserveError::CapacityOverflow)?;
115        let fallback_len = plan.special_fallback_len();
116        if fallback_offset
117            .checked_add(fallback_len)
118            .ok_or(TryReserveError::CapacityOverflow)?
119            != n
120        {
121            return Err(TryReserveError::CapacityOverflow);
122        }
123        let loglog_ceiling =
124            u8::try_from(plan.loglog_ceiling()).map_err(|_| TryReserveError::CapacityOverflow)?;
125        Ok(Self {
126            n,
127            max_insertions: config.target_insertions(),
128            levels: levels.into_boxed_slice(),
129            beta: plan.beta(),
130            loglog_ceiling: usize::from(loglog_ceiling),
131            primary_offset,
132            primary_range,
133            fallback_offset,
134            fallback_bucket_width: plan.fallback_bucket_width(),
135            fallback_bucket_range: PreparedProbeRange::new(plan.fallback_bucket_count())
136                .map_err(|_| TryReserveError::CapacityOverflow)?,
137        })
138    }
139
140    fn for_insert_budget(
141        requested: usize,
142        reserve: ReserveFraction,
143    ) -> Result<Self, TryReserveError> {
144        if requested == 0 {
145            return Ok(Self::empty());
146        }
147        let d = reserve.exponent();
148        if !(3..usize::BITS).contains(&d) {
149            return Err(TryReserveError::CapacityOverflow);
150        }
151        let scale = 1_u128 << d;
152        let denominator = scale - 1;
153        // Invert `n - floor(n / scale) >= requested` exactly. Using
154        // `ceil(requested * scale / (scale - 1))` skips a valid `n` at many
155        // floor boundaries and can change geometry for a request that the
156        // preceding map already reports as its capacity.
157        let requested_wide = requested as u128;
158        let n_lower_bound = requested_wide
159            .checked_add((requested_wide - 1) / denominator)
160            .ok_or(TryReserveError::CapacityOverflow)?;
161        let mut n = usize::try_from(n_lower_bound)
162            .map_err(|_| TryReserveError::CapacityOverflow)?
163            .max(2);
164
165        let alpha = usize::try_from(u128::from(d) * 4 + 10)
166            .map_err(|_| TryReserveError::CapacityOverflow)?;
167        let beta =
168            usize::try_from(u128::from(d) * 2).map_err(|_| TryReserveError::CapacityOverflow)?;
169        n = n.max(
170            alpha
171                .checked_mul(beta)
172                .and_then(|value| value.checked_add(2))
173                .ok_or(TryReserveError::CapacityOverflow)?,
174        );
175
176        loop {
177            if let Ok(shape) = Self::from_slots(n, reserve)
178                && shape.max_insertions >= requested
179            {
180                return Ok(shape);
181            }
182            n = n.checked_add(1).ok_or(TryReserveError::CapacityOverflow)?;
183        }
184    }
185}
186
187struct FlatStorage<T> {
188    ctrl_ptr: *mut u8,
189    data_ptr: *mut MaybeUninit<T>,
190    capacity: usize,
191}
192
193unsafe impl<T: Send> Send for FlatStorage<T> {}
194unsafe impl<T: Sync> Sync for FlatStorage<T> {}
195
196impl<T> ArenaSlots<T> for FlatStorage<T> {
197    #[inline]
198    fn ctrl_ptr(&self) -> *mut u8 {
199        self.ctrl_ptr
200    }
201
202    #[inline]
203    fn data_ptr(&self) -> *mut MaybeUninit<T> {
204        self.data_ptr
205    }
206
207    #[inline]
208    fn capacity(&self) -> usize {
209        self.capacity
210    }
211}
212
213impl<T> arena::RegionSet for FlatStorage<T> {
214    fn drop_all_values(&mut self) {
215        self.drop_values();
216    }
217}
218
219/// One word of the membership filter tail.
220type MembershipWord = u64;
221
222/// A loaded filter word paired with the key's bits, split from the test so the
223/// load can be issued before the work that hides its latency. An absent tail
224/// yields a gate that never passes, matching an empty table.
225#[derive(Clone, Copy)]
226struct MembershipGate {
227    word: MembershipWord,
228    bits: u64,
229}
230
231impl MembershipGate {
232    /// `false` proves no insert ever recorded this key.
233    #[inline]
234    const fn passes(self) -> bool {
235        self.word & self.bits == self.bits
236    }
237}
238
239/// Where each region of one Funnel epoch's arena lives.
240struct FunnelLayout {
241    layout: Layout,
242    data_offset: usize,
243    control_bytes: usize,
244    membership: MembershipRegion,
245}
246
247type FunnelStorageBuild<K, V> = (Arena, FlatStorage<SlotEntry<K, V>>, MembershipRegion);
248
249/// `Layout::extend` aligns the filter tail for [`MembershipWord`], so casting
250/// the arena base plus that offset is aligned by construction.
251#[allow(clippy::cast_ptr_alignment)]
252fn try_allocate_storage<K, V, A: Allocator>(
253    n: usize,
254    alloc: &A,
255) -> Result<FunnelStorageBuild<K, V>, TryReserveError> {
256    let shape = funnel_layout::<K, V>(n)?;
257    let arena = Arena::try_allocate_with_ctrl_zeroed(shape.layout, shape.control_bytes, alloc)?;
258    if shape.membership.words != 0 {
259        // The arena zeroes control bytes only; an empty filter must read as
260        // "nothing recorded".
261        unsafe {
262            core::ptr::write_bytes(
263                arena
264                    .as_ptr()
265                    .add(shape.membership.offset)
266                    .cast::<MembershipWord>(),
267                0,
268                shape.membership.words,
269            );
270        }
271    }
272    let storage = FlatStorage {
273        ctrl_ptr: arena.as_ptr(),
274        data_ptr: unsafe {
275            arena
276                .as_ptr()
277                .add(shape.data_offset)
278                .cast::<MaybeUninit<SlotEntry<K, V>>>()
279        },
280        capacity: n,
281    };
282    Ok((arena, storage, shape.membership))
283}
284
285fn funnel_layout<K, V>(n: usize) -> Result<FunnelLayout, TryReserveError> {
286    let control_bytes = if n == 0 {
287        0
288    } else {
289        n.checked_add(GROUP_SIZE - 1)
290            .ok_or(TryReserveError::CapacityOverflow)?
291    };
292    let (base_layout, data_offset) = arena::layout_for_extents::<K, V>(control_bytes, n)?;
293    let words = membership::word_count(n);
294    if words == 0 {
295        return Ok(FunnelLayout {
296            layout: base_layout,
297            data_offset,
298            control_bytes,
299            membership: MembershipRegion::EMPTY,
300        });
301    }
302    let tail = Layout::array::<MembershipWord>(words).map_err(|_| TryReserveError::AllocError)?;
303    let (layout, offset) = base_layout
304        .extend(tail)
305        .map_err(|_| TryReserveError::AllocError)?;
306    Ok(FunnelLayout {
307        layout: layout.pad_to_align(),
308        data_offset,
309        control_bytes,
310        membership: MembershipRegion { offset, words },
311    })
312}
313
314#[derive(Clone, Copy, Debug, Eq, PartialEq)]
315enum SearchResult {
316    Hit(usize),
317    Vacant(usize),
318    Full,
319    RangeFailure,
320}
321
322#[derive(Clone, Copy, Debug, Eq, PartialEq)]
323enum BucketScanResult<T> {
324    Hit(T),
325    Empty(usize),
326    Full,
327}
328
329/// Scans one masked SIMD group containing `logical_lanes` controls.
330///
331/// # Safety
332///
333/// `ctrl_ptr.add(start)` must be readable for `GROUP_SIZE` bytes,
334/// `logical_lanes <= GROUP_SIZE`, and every slot passed to `inspect_match`
335/// must be valid for the corresponding data arena.
336#[inline]
337unsafe fn scan_funnel_group<T>(
338    ctrl_ptr: *const u8,
339    start: usize,
340    logical_lanes: usize,
341    fingerprint: u8,
342    first_tombstone: &mut Option<usize>,
343    inspect_match: &mut impl FnMut(usize) -> Option<T>,
344) -> BucketScanResult<T> {
345    let group = unsafe { ctrl_ptr.add(start) };
346    let mut events = unsafe { simd::free_mask_group(group) };
347    events.0 |= unsafe { simd::eq_mask_group(group, fingerprint) }.0;
348    for lane in events {
349        if lane >= logical_lanes {
350            break;
351        }
352        let slot = start + lane;
353        let control = unsafe { *ctrl_ptr.add(slot) };
354        if control == CTRL_TOMBSTONE {
355            first_tombstone.get_or_insert(slot);
356        } else if control == CTRL_EMPTY {
357            return BucketScanResult::Empty(first_tombstone.unwrap_or(slot));
358        } else if let Some(hit) = inspect_match(slot) {
359            return BucketScanResult::Hit(hit);
360        }
361    }
362    BucketScanResult::Full
363}
364
365/// Scans exactly `length` logical controls in order using masked SIMD groups.
366///
367/// # Safety
368///
369/// `ctrl_ptr.add(start)` must be readable through `length + GROUP_SIZE - 1`
370/// bytes, and every slot passed to `inspect_match` must be a valid logical
371/// slot for the corresponding data arena.
372unsafe fn scan_funnel_bucket<T>(
373    ctrl_ptr: *const u8,
374    start: usize,
375    length: usize,
376    fingerprint: u8,
377    first_tombstone: &mut Option<usize>,
378    mut inspect_match: impl FnMut(usize) -> Option<T>,
379) -> BucketScanResult<T> {
380    if length <= GROUP_SIZE {
381        return unsafe {
382            scan_funnel_group(
383                ctrl_ptr,
384                start,
385                length,
386                fingerprint,
387                first_tombstone,
388                &mut inspect_match,
389            )
390        };
391    }
392    unsafe {
393        scan_funnel_bucket_multi_group(
394            ctrl_ptr,
395            start,
396            length,
397            fingerprint,
398            first_tombstone,
399            inspect_match,
400        )
401    }
402}
403
404/// Keeps the configurable multi-group case out of the default one-group path.
405///
406/// # Safety
407///
408/// The bounds requirements are identical to [`scan_funnel_bucket`], and
409/// `length` must be greater than `GROUP_SIZE`.
410#[inline(never)]
411unsafe fn scan_funnel_bucket_multi_group<T>(
412    ctrl_ptr: *const u8,
413    start: usize,
414    length: usize,
415    fingerprint: u8,
416    first_tombstone: &mut Option<usize>,
417    mut inspect_match: impl FnMut(usize) -> Option<T>,
418) -> BucketScanResult<T> {
419    debug_assert!(length > GROUP_SIZE);
420    let end = start + length;
421    let mut position = start;
422    while position < end {
423        let logical_lanes = GROUP_SIZE.min(end - position);
424        match unsafe {
425            scan_funnel_group(
426                ctrl_ptr,
427                position,
428                logical_lanes,
429                fingerprint,
430                first_tombstone,
431                &mut inspect_match,
432            )
433        } {
434            BucketScanResult::Full => {}
435            result => return result,
436        }
437        position += logical_lanes;
438    }
439    BucketScanResult::Full
440}
441
442/// Clean-epoch scan of one group: the first free lane is the terminating EMPTY.
443///
444/// # Safety
445///
446/// The bounds requirements are identical to [`scan_funnel_group`], and the
447/// logical group must contain no tombstones.
448#[inline]
449unsafe fn scan_clean_funnel_group<T>(
450    ctrl_ptr: *const u8,
451    start: usize,
452    logical_lanes: usize,
453    fingerprint: u8,
454    inspect_match: &mut impl FnMut(usize) -> Option<T>,
455) -> BucketScanResult<T> {
456    let group = unsafe { ctrl_ptr.add(start) };
457    let matches = unsafe { simd::eq_mask_group(group, fingerprint) };
458    let first_empty = unsafe { simd::free_mask_group(group) }
459        .into_iter()
460        .find(|&lane| lane < logical_lanes);
461    let semantic_lanes = first_empty.unwrap_or(logical_lanes);
462
463    for lane in matches {
464        if lane >= semantic_lanes {
465            break;
466        }
467        let slot = start + lane;
468        if let Some(hit) = inspect_match(slot) {
469            return BucketScanResult::Hit(hit);
470        }
471    }
472    if let Some(empty_lane) = first_empty {
473        return BucketScanResult::Empty(start + empty_lane);
474    }
475    BucketScanResult::Full
476}
477
478/// Clean-epoch specialization over `length` logical controls.
479///
480/// # Safety
481///
482/// The bounds requirements are identical to [`scan_funnel_bucket`], and the
483/// logical table must contain no tombstones.
484unsafe fn scan_clean_funnel_bucket<T>(
485    ctrl_ptr: *const u8,
486    start: usize,
487    length: usize,
488    fingerprint: u8,
489    mut inspect_match: impl FnMut(usize) -> Option<T>,
490) -> BucketScanResult<T> {
491    if length <= GROUP_SIZE {
492        return unsafe {
493            scan_clean_funnel_group(ctrl_ptr, start, length, fingerprint, &mut inspect_match)
494        };
495    }
496    let end = start + length;
497    let mut position = start;
498    while position < end {
499        let logical_lanes = GROUP_SIZE.min(end - position);
500        match unsafe {
501            scan_clean_funnel_group(
502                ctrl_ptr,
503                position,
504                logical_lanes,
505                fingerprint,
506                &mut inspect_match,
507            )
508        } {
509            BucketScanResult::Full => {}
510            result => return result,
511        }
512        position += logical_lanes;
513    }
514    BucketScanResult::Full
515}
516
517/// Paper-exact Funnel hashing within each table epoch. Deletion, growth, and
518/// exceptional collision recovery are built-in dynamic-map behavior beyond
519/// the paper's fixed-size insertion model.
520pub struct FunnelTable<K, V, S = DefaultHashBuilder, A: Allocator + Clone = Global> {
521    shape: FunnelShape,
522    storage: FlatStorage<SlotEntry<K, V>>,
523    len: usize,
524    tombstones: usize,
525    reserve_fraction: ReserveFraction,
526    hash_builder: S,
527    alloc: A,
528    arena: Arena,
529    epoch: EpochState,
530    exceptional_placement: bool,
531    /// Cached location of the membership filter tail. Lookups load one word
532    /// from it before probing; see [`crate::common::membership`].
533    membership: MembershipRegion,
534}
535
536unsafe impl<K: Send, V: Send, S: Send, A: Allocator + Clone + Send> Send
537    for FunnelTable<K, V, S, A>
538{
539}
540unsafe impl<K: Sync, V: Sync, S: Sync, A: Allocator + Clone + Sync> Sync
541    for FunnelTable<K, V, S, A>
542{
543}
544
545impl<K, V, S, A: Allocator + Clone> Drop for FunnelTable<K, V, S, A> {
546    fn drop(&mut self) {
547        let storage = &mut self.storage;
548        self.arena.drop_table(&self.alloc, || storage.drop_values());
549    }
550}
551
552macros::declare_backend_aliases! {
553    table = FunnelTable,
554    map_no_lifetime {
555        "Open-addressed hash map using funnel hashing." FunnelHashMap => HashMap,
556        "Consuming iterator over owned `(K, V)`." FunnelIntoIter => IntoIter,
557        "Owned `K` iterator." FunnelIntoKeys => IntoKeys,
558        "Owned `V` iterator." FunnelIntoValues => IntoValues,
559    },
560    map_ref {
561        "A view into a single entry, occupied or vacant." FunnelEntry => Entry,
562        "View of an occupied entry." FunnelOccupiedEntry => OccupiedEntry,
563        "View of a vacant entry." FunnelVacantEntry => VacantEntry,
564        "Error returned by `try_insert` on key collision." FunnelOccupiedError => OccupiedError,
565        "Borrowing iterator over `(&K, &V)`." FunnelIter => Iter,
566        "Borrowing iterator over `(&K, &mut V)`." FunnelIterMut => IterMut,
567        "`&K` iterator." FunnelKeys => Keys,
568        "`&V` iterator." FunnelValues => Values,
569        "`&mut V` iterator." FunnelValuesMut => ValuesMut,
570        "Draining iterator that empties the map." FunnelDrain => Drain,
571    },
572    map_extract_if {
573        "Iterator yielding entries removed by `extract_if`." FunnelExtractIf
574    },
575    set_no_lifetime {
576        "Hash set using funnel hashing." FunnelHashSet => HashSet,
577        "Consuming iterator over set values." FunnelSetIntoIter => IntoIter,
578    },
579    set_ref {
580        "Borrowing iterator over set values." FunnelSetIter => Iter,
581        "Draining iterator that empties the set." FunnelSetDrain => Drain,
582        "Iterator yielding values removed by set `extract_if`." FunnelSetExtractIf => ExtractIf,
583        "Iterator over values present only in the first set." FunnelDifference => Difference,
584        "Iterator over values present in both sets." FunnelIntersection => Intersection,
585        "Iterator over values present in exactly one set." FunnelSymmetricDifference => SymmetricDifference,
586        "Iterator over values present in either set." FunnelUnion => Union,
587        "A view into a single set entry." FunnelSetEntry => Entry,
588        "View of an occupied set entry." FunnelSetOccupiedEntry => OccupiedEntry,
589        "View of a vacant set entry." FunnelSetVacantEntry => VacantEntry,
590    },
591}
592
593impl<K, V, S, A> FunnelTable<K, V, S, A>
594where
595    K: Eq + Hash,
596    S: BuildHasher,
597    A: Allocator + Clone,
598{
599    fn try_from_shape(
600        shape: FunnelShape,
601        reserve_fraction: ReserveFraction,
602        hash_builder: S,
603        alloc: A,
604    ) -> Result<Self, TryReserveError> {
605        let (arena, storage, membership) = try_allocate_storage(shape.n, &alloc)?;
606        Ok(Self {
607            shape,
608            storage,
609            len: 0,
610            tombstones: 0,
611            reserve_fraction,
612            hash_builder,
613            alloc,
614            arena,
615            epoch: EpochState::initial(),
616            exceptional_placement: false,
617            membership,
618        })
619    }
620
621    fn try_with_insert_budget(
622        capacity: usize,
623        reserve_fraction: ReserveFraction,
624        hash_builder: S,
625        alloc: A,
626    ) -> Result<Self, TryBuildError> {
627        if reserve_fraction.exponent() < 3 {
628            return Err(TryBuildError::FunnelExponentBelowMinimum {
629                reserve_exponent: reserve_fraction.exponent(),
630                minimum: 3,
631            });
632        }
633        let shape = FunnelShape::for_insert_budget(capacity, reserve_fraction)?;
634        Self::try_from_shape(shape, reserve_fraction, hash_builder, alloc).map_err(Into::into)
635    }
636
637    #[inline]
638    fn sample(
639        prepared: &PreparedFastFunnelDomainProbe,
640        logical_probe: u8,
641        range: PreparedProbeRange,
642    ) -> Option<usize> {
643        probe::unbiased_prepared_funnel_probe_index_in_range(
644            prepared,
645            logical_probe,
646            range,
647            RANGE_WORD_CAP,
648        )
649        .ok()
650        .map(|probe| probe.index)
651    }
652
653    #[inline]
654    fn inspect_slot<Q>(&self, slot: usize, key_fingerprint: u8, key: &Q) -> Option<bool>
655    where
656        Q: Equivalent<K> + ?Sized,
657    {
658        let ctrl = self.storage.control_at(slot);
659        if ctrl == CTRL_EMPTY {
660            return Some(false);
661        }
662        if ctrl == key_fingerprint {
663            let entry = unsafe { self.storage.get_ref(slot) };
664            if key.equivalent(&entry.key) {
665                return Some(true);
666            }
667        }
668        None
669    }
670
671    /// Lookup-side search over a probe the caller already prepared.
672    ///
673    /// Always the general scan, even with no tombstones: the clean-epoch variant
674    /// drops the `first_tombstone` accumulator but needs a second mask walk to
675    /// find the terminating EMPTY, which measured worse on this walk.
676    fn search_exact_prepared<Q>(
677        &self,
678        key: &Q,
679        probe: PreparedFastFunnelProbe,
680        key_fingerprint: u8,
681    ) -> SearchResult
682    where
683        Q: Equivalent<K> + ?Sized,
684    {
685        self.search_exact_mode::<Q, false>(key, probe, key_fingerprint)
686    }
687
688    fn search_exact_for_insert<Q>(
689        &self,
690        key: &Q,
691        key_hash: u64,
692        key_fingerprint: u8,
693    ) -> SearchResult
694    where
695        Q: Equivalent<K> + ?Sized,
696    {
697        let probe = FunnelPrf::new(FUNNEL_PROBE_SEED).prepare(key_hash);
698        if self.tombstones == 0 {
699            self.search_exact_mode::<Q, true>(key, probe, key_fingerprint)
700        } else {
701            self.search_exact_mode::<Q, false>(key, probe, key_fingerprint)
702        }
703    }
704
705    fn search_exact_mode<Q, const CLEAN_EPOCH: bool>(
706        &self,
707        key: &Q,
708        probe: PreparedFastFunnelProbe,
709        key_fingerprint: u8,
710    ) -> SearchResult
711    where
712        Q: Equivalent<K> + ?Sized,
713    {
714        if self.shape.n == 0 {
715            return SearchResult::Full;
716        }
717        let mut first_tombstone = None;
718
719        // The walk stays strictly serial. Fetching the next level's control
720        // group one iteration ahead is legal — a bucket address depends only on
721        // the key — but the buckets are already L1-resident, so hiding that
722        // latency does not pay for the extra level of probe math a lookahead
723        // computes and usually discards.
724        //
725        // The membership gate stays out of this loop too, and out of the walk
726        // entirely. Deferring it behind the first level so a hit there never pays
727        // for the load lost every way it was built: as a shared level helper the
728        // walk cost 65%, as a per-level test misses cost 137%, and as a peeled
729        // first iteration hits cost 108% and misses 180%. The caller's eager load
730        // overlaps the probe's mix chain for free, while a deferred one serializes
731        // behind the level's control bytes and keeps a live word, a branch, and
732        // the level scan's second copy across the rest of the walk.
733        for level in &self.shape.levels {
734            let level_probe = probe.prepare_counter_base(level.ordinary_counter_base);
735            let Some(bucket) = Self::sample(&level_probe, 0, level.bucket_range) else {
736                return SearchResult::RangeFailure;
737            };
738            let start = level.offset + bucket * self.shape.beta;
739            let scan = if CLEAN_EPOCH {
740                unsafe {
741                    scan_clean_funnel_bucket(
742                        self.storage.ctrl_ptr(),
743                        start,
744                        self.shape.beta,
745                        key_fingerprint,
746                        |slot| {
747                            let entry = self.storage.get_ref(slot);
748                            key.equivalent(&entry.key).then_some(slot)
749                        },
750                    )
751                }
752            } else {
753                unsafe {
754                    scan_funnel_bucket(
755                        self.storage.ctrl_ptr(),
756                        start,
757                        self.shape.beta,
758                        key_fingerprint,
759                        &mut first_tombstone,
760                        |slot| {
761                            let entry = self.storage.get_ref(slot);
762                            key.equivalent(&entry.key).then_some(slot)
763                        },
764                    )
765                }
766            };
767            match scan {
768                BucketScanResult::Hit(slot) => return SearchResult::Hit(slot),
769                BucketScanResult::Empty(slot) => return SearchResult::Vacant(slot),
770                BucketScanResult::Full => {}
771            }
772        }
773
774        // The special-array tail stays inline. Outlining it as a `#[cold]` call
775        // costs the ordinary walk more in call setup than it recovers in
776        // register pressure, on hits and misses alike.
777        let primary_probe = probe
778            .prepare_domain(ProbeDomain::FunnelSpecialPrimary)
779            .expect("fixed Funnel primary domain must fit its counter encoding");
780        for logical_probe in 0..self.shape.loglog_ceiling {
781            let Some(local) = Self::sample(
782                &primary_probe,
783                self.shape.encode_logical_probe(logical_probe),
784                self.shape.primary_range,
785            ) else {
786                return SearchResult::RangeFailure;
787            };
788            let slot = self.shape.primary_offset + local;
789            if self.storage.control_at(slot) == CTRL_TOMBSTONE {
790                first_tombstone.get_or_insert(slot);
791                continue;
792            }
793            match self.inspect_slot(slot, key_fingerprint, key) {
794                Some(true) => return SearchResult::Hit(slot),
795                Some(false) => return SearchResult::Vacant(first_tombstone.unwrap_or(slot)),
796                None => {}
797            }
798        }
799
800        let first_probe = probe
801            .prepare_domain(ProbeDomain::FunnelSpecialFallbackChoiceA)
802            .expect("fixed Funnel fallback-A domain must fit its counter encoding");
803        let Some(first_bucket) = Self::sample(&first_probe, 0, self.shape.fallback_bucket_range)
804        else {
805            return SearchResult::RangeFailure;
806        };
807        let second_probe = probe
808            .prepare_domain(ProbeDomain::FunnelSpecialFallbackChoiceB)
809            .expect("fixed Funnel fallback-B domain must fit its counter encoding");
810        let Some(second_bucket) = Self::sample(&second_probe, 0, self.shape.fallback_bucket_range)
811        else {
812            return SearchResult::RangeFailure;
813        };
814        for slot_in_bucket in 0..self.shape.fallback_bucket_width {
815            for bucket in [first_bucket, second_bucket] {
816                let slot = self.shape.fallback_offset
817                    + bucket * self.shape.fallback_bucket_width
818                    + slot_in_bucket;
819                if self.storage.control_at(slot) == CTRL_TOMBSTONE {
820                    first_tombstone.get_or_insert(slot);
821                    continue;
822                }
823                match self.inspect_slot(slot, key_fingerprint, key) {
824                    Some(true) => return SearchResult::Hit(slot),
825                    Some(false) => {
826                        return SearchResult::Vacant(first_tombstone.unwrap_or(slot));
827                    }
828                    None => {}
829                }
830            }
831        }
832        first_tombstone.map_or(SearchResult::Full, SearchResult::Vacant)
833    }
834
835    fn find_by_full_scan<Q>(&self, key: &Q, key_fingerprint: u8) -> Option<usize>
836    where
837        Q: Equivalent<K> + ?Sized,
838    {
839        (0..self.shape.n).find(|&slot| {
840            self.storage.control_at(slot) == key_fingerprint
841                && key.equivalent(&unsafe { self.storage.get_ref(slot) }.key)
842        })
843    }
844
845    #[inline]
846    #[allow(clippy::cast_ptr_alignment)]
847    fn membership_ptr(&self) -> *mut MembershipWord {
848        unsafe {
849            self.arena
850                .as_ptr()
851                .add(self.membership.offset)
852                .cast::<MembershipWord>()
853        }
854    }
855
856    /// `false` proves no insert ever recorded this key.
857    #[cfg(test)]
858    #[inline]
859    fn membership_maybe_contains(&self, key_hash: u64) -> bool {
860        let words = self.membership.words;
861        if words == 0 {
862            return false;
863        }
864        let bits = MembershipKey::from_signature(key_hash).bits();
865        let word = MembershipKey::word(key_hash, words);
866        // SAFETY: `word` is a multiply-high reduction below `words`, and the
867        // arena's tail holds exactly that many zero-initialized words.
868        unsafe { *self.membership_ptr().add(word) & bits == bits }
869    }
870
871    /// Loads this key's filter word without testing it, so a lookup can run the
872    /// probe's mix chain while the load is in flight.
873    #[inline]
874    fn membership_gate(&self, key_hash: u64) -> MembershipGate {
875        let words = self.membership.words;
876        if words == 0 {
877            return MembershipGate {
878                word: 0,
879                bits: u64::MAX,
880            };
881        }
882        let word = MembershipKey::word(key_hash, words);
883        MembershipGate {
884            // SAFETY: as in `membership_maybe_contains`.
885            word: unsafe { *self.membership_ptr().add(word) },
886            bits: MembershipKey::from_signature(key_hash).bits(),
887        }
888    }
889
890    /// Records a key, including entries exceptional recovery places outside
891    /// their funnel.
892    #[inline]
893    fn record_membership(&mut self, key_hash: u64) {
894        let words = self.membership.words;
895        if words != 0 {
896            let bits = MembershipKey::from_signature(key_hash).bits();
897            let word = MembershipKey::word(key_hash, words);
898            // SAFETY: as in `membership_maybe_contains`.
899            unsafe { *self.membership_ptr().add(word) |= bits };
900        }
901    }
902
903    fn clear_membership(&mut self) {
904        let words = self.membership.words;
905        if words != 0 {
906            unsafe { core::ptr::write_bytes(self.membership_ptr(), 0, words) };
907        }
908    }
909
910    fn copy_membership_from(&mut self, source: &Self) {
911        let words = self.membership.words;
912        debug_assert_eq!(words, source.membership.words);
913        if words != 0 {
914            unsafe {
915                core::ptr::copy_nonoverlapping(
916                    source.membership_ptr(),
917                    self.membership_ptr(),
918                    words,
919                );
920            }
921        }
922    }
923
924    fn find_location<Q>(&self, key: &Q, key_hash: u64, key_fingerprint: u8) -> Option<usize>
925    where
926        Q: Equivalent<K> + ?Sized,
927    {
928        // Issue the filter load, then prepare the probe while it is in flight. A
929        // key the filter never recorded was never inserted, so neither the walk
930        // nor the exceptional full scan can find it.
931        let gate = self.membership_gate(key_hash);
932        let probe = FunnelPrf::new(FUNNEL_PROBE_SEED).prepare(key_hash);
933        if !gate.passes() {
934            return None;
935        }
936        match self.search_exact_prepared(key, probe, key_fingerprint) {
937            SearchResult::Hit(slot) => Some(slot),
938            _ if self.exceptional_placement => self.find_by_full_scan(key, key_fingerprint),
939            _ => None,
940        }
941    }
942
943    fn find_entry_ref<'a, Q>(
944        &'a self,
945        key: &Q,
946        key_hash: u64,
947        key_fingerprint: u8,
948    ) -> Option<&'a SlotEntry<K, V>>
949    where
950        Q: Equivalent<K> + ?Sized,
951    {
952        let slot = self.find_location(key, key_hash, key_fingerprint)?;
953        Some(unsafe { self.storage.get_ref(slot) })
954    }
955
956    fn first_free_global(&self) -> Option<usize> {
957        (0..self.shape.n).find(|&slot| self.storage.control_at(slot).is_free())
958    }
959
960    fn place_new_entry(
961        &mut self,
962        slot: usize,
963        key: K,
964        value: V,
965        key_hash: u64,
966        key_fingerprint: u8,
967        exceptional: bool,
968    ) -> usize {
969        let was_tombstone = self.storage.control_at(slot) == CTRL_TOMBSTONE;
970        self.storage
971            .write_with_control(slot, SlotEntry { key, value }, key_fingerprint);
972        self.record_membership(key_hash);
973        self.len += 1;
974        if was_tombstone {
975            self.tombstones -= 1;
976        }
977        if exceptional {
978            self.exceptional_placement = true;
979        }
980        slot
981    }
982
983    fn insert_unique(&mut self, key: K, value: V) -> bool {
984        let key_hash = self.hash_builder.hash_one(&key);
985        let key_fingerprint = control::control_fingerprint(key_hash);
986        let exact = self.search_exact_for_insert(&key, key_hash, key_fingerprint);
987        let (slot, exceptional) = match exact {
988            SearchResult::Vacant(slot) => (slot, false),
989            SearchResult::Full | SearchResult::RangeFailure => (
990                self.first_free_global()
991                    .expect("Funnel rebuild has enough logical capacity"),
992                true,
993            ),
994            SearchResult::Hit(_) => unreachable!("rebuild input contains duplicate keys"),
995        };
996        self.place_new_entry(slot, key, value, key_hash, key_fingerprint, exceptional);
997        exceptional
998    }
999
1000    fn next_growth_slots(&self, needed: usize) -> Option<usize> {
1001        if needed >= isize::MAX as usize {
1002            return None;
1003        }
1004        let requested = needed.max(self.shape.max_insertions.saturating_mul(2));
1005        FunnelShape::for_insert_budget(requested, self.reserve_fraction)
1006            .ok()
1007            .map(|shape| shape.n)
1008    }
1009
1010    fn prepare_vacant_insert(&mut self) -> bool {
1011        if self.len >= self.shape.max_insertions {
1012            let slots = self
1013                .next_growth_slots(self.len.saturating_add(1))
1014                .expect("capacity overflow");
1015            self.resize_with_transition(slots, EpochTransition::Growth);
1016            true
1017        } else {
1018            false
1019        }
1020    }
1021
1022    fn place_absent_after_search(
1023        &mut self,
1024        key: K,
1025        value: V,
1026        key_hash: u64,
1027        key_fingerprint: u8,
1028        exact: SearchResult,
1029    ) -> usize {
1030        let (slot, exceptional) = match exact {
1031            SearchResult::Vacant(slot) => (slot, false),
1032            SearchResult::Hit(_) => unreachable!("known-absent Funnel insertion found a key"),
1033            SearchResult::Full | SearchResult::RangeFailure => (
1034                self.first_free_global()
1035                    .expect("Funnel insertion limit reserves a free slot"),
1036                true,
1037            ),
1038        };
1039        let location =
1040            self.place_new_entry(slot, key, value, key_hash, key_fingerprint, exceptional);
1041        if exceptional {
1042            self.epoch.start_placement_recovery(self.len);
1043        }
1044        location
1045    }
1046
1047    fn insert_for_vacant_entry(&mut self, key: K, value: V, key_hash: u64) -> usize {
1048        self.prepare_vacant_insert();
1049        let key_fingerprint = control::control_fingerprint(key_hash);
1050        let exact = self.search_exact_for_insert(&key, key_hash, key_fingerprint);
1051        self.place_absent_after_search(key, value, key_hash, key_fingerprint, exact)
1052    }
1053
1054    fn resize_with_transition(&mut self, slots: usize, transition: EpochTransition) {
1055        let shape = FunnelShape::from_slots(slots, self.reserve_fraction)
1056            .expect("compatible Funnel resize geometry");
1057        let (new_arena, new_storage, new_membership) = try_allocate_storage(shape.n, &self.alloc)
1058            .unwrap_or_else(|_| {
1059                let shape = funnel_layout::<K, V>(shape.n).expect("constructed Funnel layout");
1060                allocator_api2::alloc::handle_alloc_error(shape.layout)
1061            });
1062
1063        let old_arena = mem::replace(&mut self.arena, new_arena);
1064        let old_storage = mem::replace(&mut self.storage, new_storage);
1065        self.membership = new_membership;
1066        self.shape = shape;
1067        self.len = 0;
1068        self.tombstones = 0;
1069        self.exceptional_placement = false;
1070
1071        let mut guard = arena::ArenaDropGuard::new(old_arena, old_storage, self.alloc.clone());
1072        let mut recovered = false;
1073        guard.regions_mut().drain_values_and_clear(|entry| {
1074            recovered |= self.insert_unique(entry.key, entry.value);
1075        });
1076        drop(guard);
1077        if recovered {
1078            self.epoch
1079                .start_with_placement_recovery(transition, self.len);
1080        } else {
1081            self.epoch.start(transition, self.len);
1082        }
1083    }
1084
1085    fn try_resize_exact(&mut self, slots: usize) -> Result<(), TryReserveError>
1086    where
1087        S: Clone,
1088    {
1089        let prior_epoch = self.epoch;
1090        let shape = FunnelShape::from_slots(slots, self.reserve_fraction)?;
1091        let mut old_map = Self::try_from_shape(
1092            shape,
1093            self.reserve_fraction,
1094            self.hash_builder.clone(),
1095            self.alloc.clone(),
1096        )?;
1097        mem::swap(self, &mut old_map);
1098        let mut recovered = false;
1099        old_map.storage.drain_values_and_clear(|entry| {
1100            recovered |= self.insert_unique(entry.key, entry.value);
1101        });
1102        drop(old_map);
1103        self.epoch = prior_epoch;
1104        if recovered {
1105            self.epoch
1106                .start_with_placement_recovery(EpochTransition::ExplicitResize, self.len);
1107        } else {
1108            self.epoch.start(EpochTransition::ExplicitResize, self.len);
1109        }
1110        Ok(())
1111    }
1112}
1113
1114#[allow(private_interfaces)]
1115impl<K, V, S, A> map::TableBackend<K, V> for FunnelTable<K, V, S, A>
1116where
1117    K: Eq + Hash,
1118    S: BuildHasher,
1119    A: Allocator + Clone,
1120{
1121    type Location = usize;
1122    type Hasher = S;
1123    type Alloc = A;
1124    type Scan = usize;
1125
1126    #[inline]
1127    fn hasher(&self) -> &S {
1128        &self.hash_builder
1129    }
1130
1131    #[inline]
1132    fn allocator(&self) -> &A {
1133        &self.alloc
1134    }
1135
1136    #[inline]
1137    fn len(&self) -> usize {
1138        self.len
1139    }
1140
1141    #[inline]
1142    fn capacity(&self) -> usize {
1143        self.shape.max_insertions
1144    }
1145
1146    #[inline]
1147    fn total_slots(&self) -> usize {
1148        self.shape.n
1149    }
1150
1151    #[inline]
1152    fn reserve_config(&self) -> ReserveFraction {
1153        self.reserve_fraction
1154    }
1155
1156    fn epoch_snapshot(&self) -> EpochSnapshot {
1157        self.epoch.snapshot(self.len)
1158    }
1159
1160    #[inline]
1161    unsafe fn slot_ref(&self, slot: usize) -> &SlotEntry<K, V> {
1162        unsafe { self.storage.get_ref(slot) }
1163    }
1164
1165    #[inline]
1166    unsafe fn slot_ptr(&self, slot: usize) -> *mut SlotEntry<K, V> {
1167        self.storage.slot_ptr(slot)
1168    }
1169
1170    #[inline]
1171    fn replace_value(&mut self, slot: usize, value: V) -> V {
1172        let entry = unsafe { self.storage.get_mut(slot) };
1173        mem::replace(&mut entry.value, value)
1174    }
1175
1176    #[inline]
1177    fn find<Q>(&self, key: &Q, hash: u64, fingerprint: u8) -> Option<usize>
1178    where
1179        Q: Hash + Equivalent<K> + ?Sized,
1180    {
1181        self.find_location(key, hash, fingerprint)
1182    }
1183
1184    #[inline]
1185    fn find_entry<'a, Q>(
1186        &'a self,
1187        key: &Q,
1188        hash: u64,
1189        fingerprint: u8,
1190    ) -> Option<&'a SlotEntry<K, V>>
1191    where
1192        Q: Hash + Equivalent<K> + ?Sized,
1193    {
1194        self.find_entry_ref(key, hash, fingerprint)
1195    }
1196
1197    #[inline]
1198    fn insert_for_vacant(&mut self, key: K, value: V, hash: u64) -> usize {
1199        self.insert_for_vacant_entry(key, value, hash)
1200    }
1201
1202    fn insert(&mut self, key: K, value: V, hash: u64) -> Option<V>
1203    where
1204        K: Hash + Eq,
1205    {
1206        let fingerprint = control::control_fingerprint(hash);
1207        let mut exact = self.search_exact_for_insert(&key, hash, fingerprint);
1208        if let SearchResult::Hit(slot) = exact {
1209            let entry = unsafe { self.storage.get_mut(slot) };
1210            return Some(mem::replace(&mut entry.value, value));
1211        }
1212        if self.exceptional_placement
1213            && let Some(slot) = self.find_by_full_scan(&key, fingerprint)
1214        {
1215            let entry = unsafe { self.storage.get_mut(slot) };
1216            return Some(mem::replace(&mut entry.value, value));
1217        }
1218        if self.prepare_vacant_insert() {
1219            exact = self.search_exact_for_insert(&key, hash, fingerprint);
1220        }
1221        self.place_absent_after_search(key, value, hash, fingerprint, exact);
1222        None
1223    }
1224
1225    fn remove(&mut self, slot: usize) -> (K, V) {
1226        let entry = unsafe { self.storage.take(slot) };
1227        self.storage.mark_tombstone(slot);
1228        self.len -= 1;
1229        self.tombstones += 1;
1230        self.epoch.note_delete();
1231        if self.tombstones > capacity::tombstone_cleanup_threshold(self.shape.n) {
1232            self.resize_with_transition(self.shape.n, EpochTransition::TombstoneCleanup);
1233        }
1234        (entry.key, entry.value)
1235    }
1236
1237    #[inline]
1238    fn tombstone_slot(&mut self, slot: usize) {
1239        self.storage.mark_tombstone(slot);
1240    }
1241
1242    #[inline]
1243    fn extract_finish(&mut self, slot: usize) {
1244        self.storage.mark_tombstone(slot);
1245        self.len -= 1;
1246        self.tombstones += 1;
1247        self.epoch.note_delete();
1248    }
1249
1250    fn finish_deferred_removals(&mut self) {
1251        if self.tombstones > capacity::tombstone_cleanup_threshold(self.shape.n) {
1252            self.resize_with_transition(self.shape.n, EpochTransition::TombstoneCleanup);
1253        }
1254    }
1255
1256    #[inline]
1257    fn scan(&self) -> usize {
1258        0
1259    }
1260
1261    fn scan_next(&self, scan: &mut usize) -> Option<(*mut SlotEntry<K, V>, usize)> {
1262        while *scan < self.shape.n {
1263            let slot = *scan;
1264            *scan += 1;
1265            if self.storage.control_at(slot).is_occupied() {
1266                return Some((self.storage.slot_ptr(slot), slot));
1267            }
1268        }
1269        None
1270    }
1271
1272    fn with_capacity_and_reserve_and_hasher_in(
1273        capacity: usize,
1274        reserve: ReserveFraction,
1275        hash_builder: S,
1276        alloc: A,
1277    ) -> Self {
1278        Self::try_with_insert_budget(capacity, reserve, hash_builder, alloc)
1279            .unwrap_or_else(|error| panic!("invalid Funnel construction: {error}"))
1280    }
1281
1282    fn try_with_capacity_and_reserve_and_hasher_in(
1283        capacity: usize,
1284        reserve: ReserveFraction,
1285        hash_builder: S,
1286        alloc: A,
1287    ) -> Result<Self, TryBuildError> {
1288        Self::try_with_insert_budget(capacity, reserve, hash_builder, alloc)
1289    }
1290
1291    fn grow_capacity_for(&self, needed: usize) -> Option<usize> {
1292        self.next_growth_slots(needed)
1293    }
1294
1295    fn resize(&mut self, new_capacity: usize) {
1296        self.resize_with_transition(new_capacity, EpochTransition::ExplicitResize);
1297    }
1298
1299    fn try_resize(&mut self, new_capacity: usize) -> Result<(), TryReserveError>
1300    where
1301        S: Clone,
1302    {
1303        self.try_resize_exact(new_capacity)
1304    }
1305
1306    fn shrink_to(&mut self, min_capacity: usize) {
1307        if self.len == 0 && min_capacity == 0 {
1308            if self.shape.n != 0 {
1309                self.resize_with_transition(0, EpochTransition::ExplicitResize);
1310            }
1311            return;
1312        }
1313        let requested = self.len.max(min_capacity);
1314        let Ok(shape) = FunnelShape::for_insert_budget(requested, self.reserve_fraction) else {
1315            panic!("capacity overflow");
1316        };
1317        if shape.n < self.shape.n {
1318            self.resize_with_transition(shape.n, EpochTransition::ExplicitResize);
1319        }
1320    }
1321
1322    fn clear(&mut self) {
1323        for slot in 0..self.shape.n {
1324            if self.storage.control_at(slot) == CTRL_TOMBSTONE {
1325                self.storage.set_control(slot, CTRL_EMPTY);
1326            }
1327        }
1328        self.tombstones = 0;
1329        let len = &mut self.len;
1330        self.storage.clear_occupied_slots_with(|slot| {
1331            *len -= 1;
1332            unsafe { core::ptr::drop_in_place(slot) };
1333        });
1334        debug_assert_eq!(self.len, 0);
1335        self.exceptional_placement = false;
1336        self.clear_membership();
1337        self.epoch.start(EpochTransition::Clear, 0);
1338    }
1339
1340    fn wipe_all(&mut self) {
1341        self.storage.clear_all_controls();
1342        self.len = 0;
1343        self.tombstones = 0;
1344        self.exceptional_placement = false;
1345        self.clear_membership();
1346        self.epoch.start(EpochTransition::Clear, 0);
1347    }
1348
1349    fn clone_table(&self) -> Self
1350    where
1351        K: Clone,
1352        V: Clone,
1353        S: Clone,
1354    {
1355        let mut cloned = Self::try_from_shape(
1356            self.shape.clone(),
1357            self.reserve_fraction,
1358            self.hash_builder.clone(),
1359            self.alloc.clone(),
1360        )
1361        .unwrap_or_else(|error| panic!("Funnel clone allocation failed: {error}"));
1362        for slot in 0..self.shape.n {
1363            let ctrl = self.storage.control_at(slot);
1364            if ctrl.is_occupied() {
1365                let entry = unsafe { self.storage.get_ref(slot) }.clone();
1366                cloned.storage.write_with_control(slot, entry, ctrl);
1367            } else if ctrl == CTRL_TOMBSTONE {
1368                cloned.storage.mark_tombstone(slot);
1369            }
1370        }
1371        cloned.len = self.len;
1372        cloned.tombstones = self.tombstones;
1373        cloned.epoch = self.epoch;
1374        cloned.exceptional_placement = self.exceptional_placement;
1375        // Slots are copied rather than reinserted, so the filter comes with them.
1376        cloned.copy_membership_from(self);
1377        cloned
1378    }
1379}
1380
1381#[cfg(test)]
1382mod tests {
1383    use core::hash::{BuildHasher, Hasher};
1384    use core::mem::ManuallyDrop;
1385    use core::num::NonZeroU32;
1386    use core::ptr::NonNull;
1387    use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
1388
1389    use alloc::sync::Arc;
1390    use allocator_api2::alloc::AllocError as RawAllocError;
1391    use std::panic::{AssertUnwindSafe, catch_unwind};
1392
1393    use super::*;
1394    use crate::common::exact::probe;
1395    use crate::common::exact::reference::{ScalarFunnel, ScalarFunnelInsert};
1396
1397    #[derive(Clone, Copy, Default)]
1398    struct IdentityBuildHasher;
1399
1400    struct IdentityHasher(u64);
1401
1402    struct PanicOnFirstDrop {
1403        drops: Arc<AtomicUsize>,
1404    }
1405
1406    struct PanicHashKey {
1407        id: u64,
1408        armed: Arc<AtomicBool>,
1409        drops: Arc<AtomicUsize>,
1410    }
1411
1412    #[derive(Clone)]
1413    struct ToggleAllocator {
1414        fail: Arc<AtomicBool>,
1415        allocations: Arc<AtomicUsize>,
1416        deallocations: Arc<AtomicUsize>,
1417    }
1418
1419    unsafe impl Allocator for ToggleAllocator {
1420        fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, RawAllocError> {
1421            if self.fail.load(Ordering::SeqCst) {
1422                Err(RawAllocError)
1423            } else {
1424                let allocation = Global.allocate(layout)?;
1425                self.allocations.fetch_add(1, Ordering::SeqCst);
1426                Ok(allocation)
1427            }
1428        }
1429
1430        unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
1431            self.deallocations.fetch_add(1, Ordering::SeqCst);
1432            unsafe { Global.deallocate(ptr, layout) };
1433        }
1434    }
1435
1436    impl PartialEq for PanicHashKey {
1437        fn eq(&self, other: &Self) -> bool {
1438            self.id == other.id
1439        }
1440    }
1441
1442    impl Eq for PanicHashKey {}
1443
1444    impl Hash for PanicHashKey {
1445        fn hash<H: Hasher>(&self, state: &mut H) {
1446            assert!(!self.armed.load(Ordering::SeqCst), "armed key hash");
1447            state.write_u64(self.id);
1448        }
1449    }
1450
1451    impl Drop for PanicHashKey {
1452        fn drop(&mut self) {
1453            self.drops.fetch_add(1, Ordering::SeqCst);
1454        }
1455    }
1456
1457    impl Drop for PanicOnFirstDrop {
1458        fn drop(&mut self) {
1459            assert!(
1460                self.drops.fetch_add(1, Ordering::SeqCst) != 0,
1461                "first value drop"
1462            );
1463        }
1464    }
1465
1466    impl Hasher for IdentityHasher {
1467        fn finish(&self) -> u64 {
1468            self.0
1469        }
1470
1471        fn write(&mut self, bytes: &[u8]) {
1472            let mut value = 0_u64;
1473            for (index, byte) in bytes.iter().take(8).enumerate() {
1474                value |= u64::from(*byte) << (index * 8);
1475            }
1476            self.0 = value;
1477        }
1478
1479        fn write_u64(&mut self, value: u64) {
1480            self.0 = value;
1481        }
1482    }
1483
1484    impl BuildHasher for IdentityBuildHasher {
1485        type Hasher = IdentityHasher;
1486
1487        fn build_hasher(&self) -> Self::Hasher {
1488            IdentityHasher(0)
1489        }
1490    }
1491
1492    fn raw_table(n: usize, d: u32) -> FunnelTable<u64, u64, IdentityBuildHasher> {
1493        let reserve = ReserveFraction::from_exponent(d).unwrap();
1494        FunnelTable::try_from_shape(
1495            FunnelShape::from_slots(n, reserve).unwrap(),
1496            reserve,
1497            IdentityBuildHasher,
1498            Global,
1499        )
1500        .unwrap()
1501    }
1502
1503    #[test]
1504    #[cfg_attr(miri, ignore)]
1505    fn clean_and_dirty_search_modes_agree_without_tombstones() {
1506        let mut table = raw_table(344, 4);
1507        for key in 0..128_u64 {
1508            assert!(!table.insert_unique(key, key));
1509        }
1510        assert_eq!(table.tombstones, 0);
1511
1512        for key in 0..256_u64 {
1513            let hash = table.hash_builder.hash_one(key);
1514            let fingerprint = control::control_fingerprint(hash);
1515            let probe = FunnelPrf::new(FUNNEL_PROBE_SEED).prepare(hash);
1516            let clean = table.search_exact_mode::<_, true>(&key, probe, fingerprint);
1517            let dirty = table.search_exact_mode::<_, false>(&key, probe, fingerprint);
1518            assert_eq!(clean, dirty, "key={key}");
1519        }
1520    }
1521
1522    #[test]
1523    #[cfg_attr(miri, ignore)]
1524    fn selector_finds_known_minimum_exact_geometries() {
1525        for &(d, expected) in &[
1526            (3, 144),
1527            (4, 344),
1528            (6, 1_372),
1529            (8, 5_472),
1530            (9, 10_924),
1531            (10, 21_856),
1532        ] {
1533            let reserve = ReserveFraction::from_exponent(d).unwrap();
1534            let shape = FunnelShape::for_insert_budget(1, reserve).unwrap();
1535            assert_eq!(shape.n, expected, "d={d}");
1536            assert!(PaperConfig::new(shape.n, d).unwrap().funnel_plan().is_ok());
1537        }
1538    }
1539
1540    #[test]
1541    #[cfg_attr(miri, ignore)]
1542    fn selector_reuses_a_geometry_at_its_reported_capacity() {
1543        let reserve = ReserveFraction::from_exponent(3).unwrap();
1544        for requested in 1..512 {
1545            let selected = FunnelShape::for_insert_budget(requested, reserve).unwrap();
1546            let at_capacity =
1547                FunnelShape::for_insert_budget(selected.max_insertions, reserve).unwrap();
1548            assert_eq!(at_capacity.n, selected.n, "request={requested}");
1549            assert_eq!(at_capacity.max_insertions, selected.max_insertions);
1550        }
1551        let headline = FunnelShape::for_insert_budget(28_672, reserve).unwrap();
1552        assert_eq!(headline.n, 32_767);
1553        assert_eq!(headline.max_insertions, 28_672);
1554    }
1555
1556    #[test]
1557    #[cfg_attr(miri, ignore)]
1558    fn funnel_locations_match_the_independent_scalar_oracle() {
1559        for &(n, d) in &[(144, 3), (344, 4), (1_372, 6), (5_472, 8), (21_856, 10)] {
1560            let config = PaperConfig::new(n, d).unwrap();
1561            let mut scalar = ScalarFunnel::new(
1562                config,
1563                FunnelPrf::new(FUNNEL_PROBE_SEED),
1564                NonZeroU32::new(RANGE_WORD_CAP).unwrap(),
1565            );
1566            let mut table = raw_table(n, d);
1567            let mut locations = Vec::with_capacity(config.target_insertions());
1568            for identity in 0..config.target_insertions() as u64 {
1569                let (result, _) = scalar.insert(identity).unwrap();
1570                let ScalarFunnelInsert::Inserted(expected) = result else {
1571                    panic!("scalar insertion failed at {identity}");
1572                };
1573                assert_eq!(
1574                    <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::insert(
1575                        &mut table,
1576                        identity,
1577                        identity ^ 0x55,
1578                        identity,
1579                    ),
1580                    None
1581                );
1582                let actual = table
1583                    .find_location(&identity, identity, control::control_fingerprint(identity))
1584                    .unwrap();
1585                assert_eq!(actual, expected.global_slot(), "n={n} key={identity}");
1586                locations.push((identity, actual, table.storage.slot_ptr(actual) as usize));
1587                assert_eq!(
1588                    table.find_location(
1589                        &identity,
1590                        identity,
1591                        control::control_fingerprint(identity),
1592                    ),
1593                    Some(actual)
1594                );
1595            }
1596            assert_eq!(table.len, scalar.len());
1597            for &(identity, location, pointer) in &locations {
1598                assert_eq!(
1599                    table.find_location(
1600                        &identity,
1601                        identity,
1602                        control::control_fingerprint(identity),
1603                    ),
1604                    Some(location),
1605                    "moved n={n} key={identity}"
1606                );
1607                assert_eq!(table.storage.slot_ptr(location) as usize, pointer);
1608            }
1609
1610            let duplicate = config.target_insertions() as u64 / 2;
1611            let len = table.len;
1612            assert_eq!(
1613                <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::insert(
1614                    &mut table,
1615                    duplicate,
1616                    u64::MAX,
1617                    duplicate,
1618                ),
1619                Some(duplicate ^ 0x55)
1620            );
1621            assert_eq!(table.len, len);
1622            assert_eq!(
1623                table.find_location(
1624                    &duplicate,
1625                    duplicate,
1626                    control::control_fingerprint(duplicate),
1627                ),
1628                Some(locations[duplicate as usize].1)
1629            );
1630
1631            let absent = config.target_insertions() as u64 + 1_000_000;
1632            assert_eq!(
1633                table.find_location(&absent, absent, control::control_fingerprint(absent)),
1634                None
1635            );
1636        }
1637    }
1638
1639    #[test]
1640    #[allow(clippy::too_many_lines)]
1641    fn funnel_search_reaches_primary_and_alternating_fallback_in_order() {
1642        fn sample(identity: u64, domain: ProbeDomain, logical: u64, upper: usize) -> usize {
1643            probe::unbiased_probe_index(
1644                &FunnelPrf::new(FUNNEL_PROBE_SEED),
1645                identity,
1646                domain,
1647                logical,
1648                upper,
1649                RANGE_WORD_CAP,
1650            )
1651            .unwrap()
1652            .index
1653        }
1654
1655        fn occupy(
1656            table: &mut FunnelTable<u64, u64, IdentityBuildHasher>,
1657            slot: usize,
1658            key: u64,
1659            fingerprint: u8,
1660        ) {
1661            if table.storage.control_at(slot).is_free() {
1662                table
1663                    .storage
1664                    .write_with_control(slot, SlotEntry { key, value: key }, fingerprint);
1665                // Placing a slot directly still owes the filter its record.
1666                table.record_membership(key);
1667                table.len += 1;
1668            }
1669        }
1670
1671        let mut table = raw_table(32_768, 3);
1672        let fallback_count = table.shape.fallback_bucket_range.upper();
1673        let identity = (0..10_000_u64)
1674            .find(|&identity| {
1675                sample(
1676                    identity,
1677                    ProbeDomain::FunnelSpecialFallbackChoiceA,
1678                    0,
1679                    fallback_count,
1680                ) != sample(
1681                    identity,
1682                    ProbeDomain::FunnelSpecialFallbackChoiceB,
1683                    0,
1684                    fallback_count,
1685                )
1686            })
1687            .unwrap();
1688        let fingerprint = control::control_fingerprint(identity);
1689        let dummy_fingerprint = if fingerprint == 1 { 2 } else { 1 };
1690
1691        for level_index in 0..table.shape.levels.len() {
1692            let level = table.shape.levels[level_index];
1693            let bucket = sample(
1694                identity,
1695                ProbeDomain::FunnelOrdinary {
1696                    level: level_index as u64,
1697                },
1698                0,
1699                level.bucket_range.upper(),
1700            );
1701            let start = level.offset + bucket * table.shape.beta;
1702            for lane in 0..table.shape.beta {
1703                occupy(
1704                    &mut table,
1705                    start + lane,
1706                    u64::MAX - (start + lane) as u64,
1707                    dummy_fingerprint,
1708                );
1709            }
1710        }
1711
1712        let first_primary = sample(
1713            identity,
1714            ProbeDomain::FunnelSpecialPrimary,
1715            0,
1716            table.shape.primary_range.upper(),
1717        );
1718        assert_eq!(
1719            table.search_exact_for_insert(&identity, identity, fingerprint),
1720            SearchResult::Vacant(table.shape.primary_offset + first_primary)
1721        );
1722        for logical in 0..table.shape.loglog_ceiling {
1723            let local = sample(
1724                identity,
1725                ProbeDomain::FunnelSpecialPrimary,
1726                u64::try_from(logical).unwrap(),
1727                table.shape.primary_range.upper(),
1728            );
1729            let slot = table.shape.primary_offset + local;
1730            occupy(&mut table, slot, u64::MAX - local as u64, dummy_fingerprint);
1731        }
1732
1733        let first_fallback_bucket = sample(
1734            identity,
1735            ProbeDomain::FunnelSpecialFallbackChoiceA,
1736            0,
1737            fallback_count,
1738        );
1739        let second_fallback_bucket = sample(
1740            identity,
1741            ProbeDomain::FunnelSpecialFallbackChoiceB,
1742            0,
1743            fallback_count,
1744        );
1745        assert_ne!(first_fallback_bucket, second_fallback_bucket);
1746        let width = table.shape.fallback_bucket_width;
1747        let first_bucket_slot_zero = table.shape.fallback_offset + first_fallback_bucket * width;
1748        let second_bucket_slot_zero = table.shape.fallback_offset + second_fallback_bucket * width;
1749        assert_eq!(
1750            table.search_exact_for_insert(&identity, identity, fingerprint),
1751            SearchResult::Vacant(first_bucket_slot_zero)
1752        );
1753
1754        occupy(
1755            &mut table,
1756            first_bucket_slot_zero,
1757            u64::MAX - 1,
1758            dummy_fingerprint,
1759        );
1760        occupy(
1761            &mut table,
1762            second_bucket_slot_zero,
1763            u64::MAX - 2,
1764            dummy_fingerprint,
1765        );
1766        let first_bucket_slot_one = first_bucket_slot_zero + 1;
1767        let second_bucket_slot_one = second_bucket_slot_zero + 1;
1768        assert_eq!(
1769            table.search_exact_for_insert(&identity, identity, fingerprint),
1770            SearchResult::Vacant(first_bucket_slot_one)
1771        );
1772        occupy(
1773            &mut table,
1774            first_bucket_slot_one,
1775            u64::MAX - 3,
1776            dummy_fingerprint,
1777        );
1778        assert_eq!(
1779            table.search_exact_for_insert(&identity, identity, fingerprint),
1780            SearchResult::Vacant(second_bucket_slot_one)
1781        );
1782
1783        occupy(&mut table, second_bucket_slot_one, identity, fingerprint);
1784        let direct = core::ptr::from_ref(
1785            table
1786                .find_entry_ref(&identity, identity, fingerprint)
1787                .unwrap(),
1788        );
1789        assert_eq!(
1790            direct,
1791            table.storage.slot_ptr(second_bucket_slot_one).cast_const()
1792        );
1793    }
1794
1795    #[test]
1796    fn membership_filter_records_every_inserted_key() {
1797        let mut table = raw_table(512, 3);
1798        for key in 0..200_u64 {
1799            table.insert_for_vacant_entry(key, key, key);
1800        }
1801        for key in 0..200_u64 {
1802            assert!(
1803                table.membership_maybe_contains(key),
1804                "inserted key {key} must pass its own filter"
1805            );
1806            assert_eq!(
1807                table.find_location(&key, key, control::control_fingerprint(key)),
1808                Some(
1809                    table
1810                        .find_by_full_scan(&key, control::control_fingerprint(key))
1811                        .unwrap()
1812                ),
1813                "filter must not hide key {key}"
1814            );
1815        }
1816        // Identity hashing here, so absent keys must carry their own spread the
1817        // way a real hasher's output would.
1818        let absent = (1..=500_u64).map(|i| i.wrapping_mul(0x9E37_79B9_7F4A_7C15));
1819        let rejected = absent
1820            .filter(|&key| !table.membership_maybe_contains(key))
1821            .count();
1822        assert!(
1823            rejected > 400,
1824            "filter rejected only {rejected} of 500 absent keys"
1825        );
1826    }
1827
1828    #[test]
1829    fn membership_filter_keeps_survivors_after_removals() {
1830        let mut table = raw_table(512, 3);
1831        for key in 0..200_u64 {
1832            table.insert_for_vacant_entry(key, key, key);
1833        }
1834        for key in (0..200_u64).step_by(2) {
1835            let slot = table
1836                .find_location(&key, key, control::control_fingerprint(key))
1837                .expect("inserted key is present");
1838            <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::remove(&mut table, slot);
1839        }
1840        for key in (1..200_u64).step_by(2) {
1841            assert!(
1842                table
1843                    .find_location(&key, key, control::control_fingerprint(key))
1844                    .is_some(),
1845                "removals must not clear the filter bits of surviving key {key}"
1846            );
1847        }
1848    }
1849
1850    #[test]
1851    fn membership_filter_survives_growth_and_clone() {
1852        let mut table = raw_table(512, 3);
1853        let start_slots = table.shape.n;
1854        let mut key = 0_u64;
1855        while table.shape.n == start_slots {
1856            table.insert_for_vacant_entry(key, key, key);
1857            key += 1;
1858        }
1859        assert!(table.shape.n > start_slots, "growth must have happened");
1860        let cloned = <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::clone_table(&table);
1861        for probe in 0..key {
1862            assert!(
1863                table.membership_maybe_contains(probe),
1864                "growth must rebuild the filter for key {probe}"
1865            );
1866            assert!(
1867                cloned
1868                    .find_location(&probe, probe, control::control_fingerprint(probe))
1869                    .is_some(),
1870                "clone must carry the filter for key {probe}"
1871            );
1872        }
1873    }
1874
1875    #[test]
1876    fn membership_filter_is_reset_by_clear_and_wipe() {
1877        for wipe in [false, true] {
1878            let mut table = raw_table(512, 3);
1879            for key in 0..200_u64 {
1880                table.insert_for_vacant_entry(key, key, key);
1881            }
1882            if wipe {
1883                <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::wipe_all(&mut table);
1884            } else {
1885                <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::clear(&mut table);
1886            }
1887            let recorded = (0..200_u64)
1888                .filter(|&key| table.membership_maybe_contains(key))
1889                .count();
1890            assert_eq!(recorded, 0, "reset must clear every filter word");
1891        }
1892    }
1893
1894    #[test]
1895    fn exact_geometry_sums_to_n() {
1896        let mut table = raw_table(512, 3);
1897        for key in 0..128_u64 {
1898            table.insert_for_vacant_entry(key, key, key);
1899        }
1900        let ordinary_slots = table
1901            .shape
1902            .levels
1903            .iter()
1904            .map(|level| level.bucket_range.upper() * table.shape.beta)
1905            .sum::<usize>();
1906        assert_eq!(
1907            ordinary_slots
1908                + table.shape.primary_range.upper()
1909                + table.shape.fallback_bucket_range.upper() * table.shape.fallback_bucket_width,
1910            table.shape.n
1911        );
1912        assert_eq!(table.storage.capacity(), table.shape.n);
1913    }
1914
1915    #[test]
1916    fn tombstones_never_hide_survivors_and_are_reused() {
1917        let mut table = raw_table(512, 3);
1918        for key in 0..200_u64 {
1919            table.insert_for_vacant_entry(key, key, key);
1920        }
1921        let removed = table
1922            .find_location(&7, 7, control::control_fingerprint(7))
1923            .unwrap();
1924        let (key, value) =
1925            <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::remove(&mut table, removed);
1926        assert_eq!((key, value), (7, 7));
1927        for key in 0..200_u64 {
1928            if key != 7 {
1929                assert!(
1930                    table
1931                        .find_location(&key, key, control::control_fingerprint(key))
1932                        .is_some(),
1933                    "lost key {key}"
1934                );
1935            }
1936        }
1937        let tombstones = table.tombstones;
1938        let replacement = table.insert_for_vacant_entry(10_000, 1, 7);
1939        assert_eq!(replacement, removed);
1940        assert_eq!(table.tombstones, tombstones - 1);
1941        assert_eq!(table.len, 200);
1942    }
1943
1944    #[test]
1945    fn tombstone_before_a_duplicate_does_not_hide_the_duplicate() {
1946        let mut table = raw_table(144, 3);
1947        let first = table.insert_for_vacant_entry(100, 1, 42);
1948        let second = table.insert_for_vacant_entry(200, 2, 42);
1949        assert_ne!(first, second);
1950        let _ = <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::remove(&mut table, first);
1951
1952        assert_eq!(
1953            <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::insert(&mut table, 200, 20, 42,),
1954            Some(2)
1955        );
1956        assert_eq!(table.len, 1);
1957        assert_eq!(table.tombstones, 1);
1958        assert_eq!(
1959            table.find_location(&200, 42, control::control_fingerprint(42)),
1960            Some(second)
1961        );
1962
1963        let reused = table.insert_for_vacant_entry(300, 3, 42);
1964        assert_eq!(reused, first);
1965        assert_eq!(table.tombstones, 0);
1966    }
1967
1968    #[test]
1969    fn clear_marks_each_slot_empty_before_dropping_its_value() {
1970        let drops = Arc::new(AtomicUsize::new(0));
1971        let reserve = ReserveFraction::from_exponent(3).unwrap();
1972        let shape = FunnelShape::from_slots(144, reserve).unwrap();
1973        let mut table = ManuallyDrop::new(
1974            FunnelTable::<u64, PanicOnFirstDrop, IdentityBuildHasher>::try_from_shape(
1975                shape,
1976                reserve,
1977                IdentityBuildHasher,
1978                Global,
1979            )
1980            .unwrap(),
1981        );
1982        for key in 0..3 {
1983            table.insert_for_vacant_entry(
1984                key,
1985                PanicOnFirstDrop {
1986                    drops: drops.clone(),
1987                },
1988                key,
1989            );
1990        }
1991        let first_occupied = (0..table.shape.n)
1992            .find(|&slot| table.storage.control_at(slot).is_occupied())
1993            .unwrap();
1994
1995        let result = catch_unwind(AssertUnwindSafe(|| {
1996            <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::clear(&mut table);
1997        }));
1998        assert!(result.is_err());
1999        assert_eq!(table.storage.control_at(first_occupied), CTRL_EMPTY);
2000        assert_eq!(
2001            table.len,
2002            (0..table.shape.n)
2003                .filter(|&slot| table.storage.control_at(slot).is_occupied())
2004                .count()
2005        );
2006
2007        <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::clear(&mut table);
2008        assert_eq!(table.len, 0);
2009        assert_eq!(drops.load(Ordering::SeqCst), 3);
2010        unsafe { ManuallyDrop::drop(&mut table) };
2011    }
2012
2013    #[test]
2014    fn failed_fallible_resize_leaves_a_valid_table() {
2015        let armed = Arc::new(AtomicBool::new(false));
2016        let drops = Arc::new(AtomicUsize::new(0));
2017        let reserve = ReserveFraction::from_exponent(3).unwrap();
2018        let shape = FunnelShape::from_slots(144, reserve).unwrap();
2019        let mut table = FunnelTable::<PanicHashKey, u64, IdentityBuildHasher>::try_from_shape(
2020            shape,
2021            reserve,
2022            IdentityBuildHasher,
2023            Global,
2024        )
2025        .unwrap();
2026        for id in 0..3 {
2027            let key = PanicHashKey {
2028                id,
2029                armed: armed.clone(),
2030                drops: drops.clone(),
2031            };
2032            let hash = id;
2033            assert_eq!(
2034                <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::insert(
2035                    &mut table, key, id, hash,
2036                ),
2037                None
2038            );
2039        }
2040        let next_shape = FunnelShape::for_insert_budget(256, reserve).unwrap();
2041
2042        armed.store(true, Ordering::SeqCst);
2043        let result = catch_unwind(AssertUnwindSafe(|| {
2044            table.try_resize_exact(next_shape.n).unwrap();
2045        }));
2046        assert!(result.is_err());
2047        assert_eq!(
2048            table.len,
2049            (0..table.shape.n)
2050                .filter(|&slot| table.storage.control_at(slot).is_occupied())
2051                .count()
2052        );
2053
2054        armed.store(false, Ordering::SeqCst);
2055        let replacement = PanicHashKey {
2056            id: 99,
2057            armed: armed.clone(),
2058            drops: drops.clone(),
2059        };
2060        assert_eq!(
2061            <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::insert(
2062                &mut table,
2063                replacement,
2064                99,
2065                99,
2066            ),
2067            None
2068        );
2069        assert_eq!(
2070            table.len,
2071            (0..table.shape.n)
2072                .filter(|&slot| table.storage.control_at(slot).is_occupied())
2073                .count()
2074        );
2075        drop(table);
2076        assert_eq!(drops.load(Ordering::SeqCst), 4);
2077    }
2078
2079    #[test]
2080    fn allocator_failure_is_typed_and_leaves_populated_resize_unchanged() {
2081        let fail = Arc::new(AtomicBool::new(true));
2082        let allocations = Arc::new(AtomicUsize::new(0));
2083        let deallocations = Arc::new(AtomicUsize::new(0));
2084        let alloc = ToggleAllocator {
2085            fail: fail.clone(),
2086            allocations: allocations.clone(),
2087            deallocations: deallocations.clone(),
2088        };
2089        let reserve = ReserveFraction::from_exponent(3).unwrap();
2090        let shape = FunnelShape::from_slots(144, reserve).unwrap();
2091        assert!(matches!(
2092            FunnelTable::<u64, u64, IdentityBuildHasher, _>::try_from_shape(
2093                shape.clone(),
2094                reserve,
2095                IdentityBuildHasher,
2096                alloc.clone(),
2097            ),
2098            Err(TryReserveError::AllocError)
2099        ));
2100        assert_eq!(allocations.load(Ordering::SeqCst), 0);
2101
2102        fail.store(false, Ordering::SeqCst);
2103        let mut table = FunnelTable::<u64, u64, IdentityBuildHasher, _>::try_from_shape(
2104            shape,
2105            reserve,
2106            IdentityBuildHasher,
2107            alloc,
2108        )
2109        .unwrap();
2110        for key in 0..32_u64 {
2111            assert_eq!(
2112                <FunnelTable<_, _, _, _> as map::TableBackend<_, _>>::insert(
2113                    &mut table, key, key, key,
2114                ),
2115                None
2116            );
2117        }
2118        let arena_bytes = table.arena.layout_size();
2119        let locations: Vec<_> = (0..32_u64)
2120            .map(|key| {
2121                (
2122                    key,
2123                    table
2124                        .find_location(&key, key, control::control_fingerprint(key))
2125                        .unwrap(),
2126                )
2127            })
2128            .collect();
2129        let next = FunnelShape::for_insert_budget(512, reserve).unwrap();
2130        fail.store(true, Ordering::SeqCst);
2131        assert_eq!(
2132            table.try_resize_exact(next.n),
2133            Err(TryReserveError::AllocError)
2134        );
2135        assert_eq!(table.len, 32);
2136        assert_eq!(table.shape.n, 144);
2137        assert_eq!(table.arena.layout_size(), arena_bytes);
2138        for (key, location) in locations {
2139            assert_eq!(
2140                table.find_location(&key, key, control::control_fingerprint(key)),
2141                Some(location)
2142            );
2143        }
2144
2145        fail.store(false, Ordering::SeqCst);
2146        drop(table);
2147        assert_eq!(allocations.load(Ordering::SeqCst), 1);
2148        assert_eq!(deallocations.load(Ordering::SeqCst), 1);
2149    }
2150
2151    #[test]
2152    #[cfg_attr(miri, ignore)]
2153    fn vector_bucket_scan_matches_scalar_order_for_every_default_pattern() {
2154        const FP: u8 = 7;
2155        const OTHER: u8 = 1;
2156        const WIDTH: usize = 6;
2157        const PATTERN_COUNT: usize = 4_usize.pow(6);
2158        let states = [CTRL_EMPTY, CTRL_TOMBSTONE, OTHER, FP];
2159
2160        for encoded in 0..PATTERN_COUNT {
2161            let mut controls = [FP; WIDTH + crate::common::config::GROUP_SIZE - 1];
2162            let mut value = encoded;
2163            for control in &mut controls[..WIDTH] {
2164                *control = states[value & 3];
2165                value >>= 2;
2166            }
2167            for hit_lane in 0..=WIDTH {
2168                let expected_hit = (hit_lane < WIDTH).then_some(hit_lane);
2169                let mut expected_first_tombstone = None;
2170                let mut expected_compared = Vec::new();
2171                let mut expected = BucketScanResult::Full;
2172                for (lane, &control) in controls[..WIDTH].iter().enumerate() {
2173                    if control == CTRL_TOMBSTONE {
2174                        expected_first_tombstone.get_or_insert(lane);
2175                    } else if control == CTRL_EMPTY {
2176                        expected =
2177                            BucketScanResult::Empty(expected_first_tombstone.unwrap_or(lane));
2178                        break;
2179                    } else if control == FP {
2180                        expected_compared.push(lane);
2181                        if Some(lane) == expected_hit {
2182                            expected = BucketScanResult::Hit(lane);
2183                            break;
2184                        }
2185                    }
2186                }
2187
2188                let mut actual_first_tombstone = None;
2189                let mut actual_compared = Vec::new();
2190                let actual = unsafe {
2191                    scan_funnel_bucket(
2192                        controls.as_ptr(),
2193                        0,
2194                        WIDTH,
2195                        FP,
2196                        &mut actual_first_tombstone,
2197                        |slot| {
2198                            actual_compared.push(slot);
2199                            (Some(slot) == expected_hit).then_some(slot)
2200                        },
2201                    )
2202                };
2203                assert_eq!(actual, expected, "pattern={encoded} hit={hit_lane}");
2204                assert_eq!(
2205                    actual_first_tombstone, expected_first_tombstone,
2206                    "pattern={encoded} hit={hit_lane}"
2207                );
2208                assert_eq!(
2209                    actual_compared, expected_compared,
2210                    "pattern={encoded} hit={hit_lane}"
2211                );
2212
2213                if !controls[..WIDTH].contains(&CTRL_TOMBSTONE) {
2214                    let mut clean_compared = Vec::new();
2215                    let clean = unsafe {
2216                        scan_clean_funnel_bucket(controls.as_ptr(), 0, WIDTH, FP, |slot| {
2217                            clean_compared.push(slot);
2218                            (Some(slot) == expected_hit).then_some(slot)
2219                        })
2220                    };
2221                    assert_eq!(clean, expected, "clean pattern={encoded} hit={hit_lane}");
2222                    assert_eq!(
2223                        clean_compared, expected_compared,
2224                        "clean pattern={encoded} hit={hit_lane}"
2225                    );
2226                }
2227            }
2228        }
2229    }
2230
2231    #[test]
2232    #[allow(clippy::too_many_lines)]
2233    fn vector_bucket_scan_preserves_order_across_group_boundaries_and_padding() {
2234        const FP: u8 = 7;
2235        const OTHER: u8 = 1;
2236
2237        fn scalar(
2238            controls: &[u8],
2239            start: usize,
2240            width: usize,
2241            hit_slot: Option<usize>,
2242            mut first_tombstone: Option<usize>,
2243        ) -> (BucketScanResult<usize>, Option<usize>, Vec<usize>) {
2244            let mut compared = Vec::new();
2245            for (slot, &control) in controls.iter().enumerate().skip(start).take(width) {
2246                match control {
2247                    CTRL_TOMBSTONE => {
2248                        first_tombstone.get_or_insert(slot);
2249                    }
2250                    CTRL_EMPTY => {
2251                        return (
2252                            BucketScanResult::Empty(first_tombstone.unwrap_or(slot)),
2253                            first_tombstone,
2254                            compared,
2255                        );
2256                    }
2257                    FP => {
2258                        compared.push(slot);
2259                        if Some(slot) == hit_slot {
2260                            return (BucketScanResult::Hit(slot), first_tombstone, compared);
2261                        }
2262                    }
2263                    _ => {}
2264                }
2265            }
2266            (BucketScanResult::Full, first_tombstone, compared)
2267        }
2268
2269        let start = 3;
2270        for width in [10, 15, 16, 18, 32, 34] {
2271            for existing_tombstone in [None, Some(1)] {
2272                let mut controls = vec![OTHER; start + width + GROUP_SIZE - 1];
2273                let logical_end = start + width;
2274                for (index, control) in controls[logical_end..].iter_mut().enumerate() {
2275                    *control = if index.is_multiple_of(2) {
2276                        FP
2277                    } else {
2278                        CTRL_EMPTY
2279                    };
2280                }
2281                let tombstone = start + width / 4;
2282                let collision = start + (GROUP_SIZE - 1).min(width - 2);
2283                let hit = start + GROUP_SIZE.min(width - 2);
2284                controls[tombstone] = CTRL_TOMBSTONE;
2285                controls[collision] = FP;
2286                controls[hit] = FP;
2287                controls[logical_end - 1] = CTRL_EMPTY;
2288
2289                let expected = scalar(&controls, start, width, Some(hit), existing_tombstone);
2290                let mut actual_first_tombstone = existing_tombstone;
2291                let mut actual_compared = Vec::new();
2292                let actual = unsafe {
2293                    scan_funnel_bucket(
2294                        controls.as_ptr(),
2295                        start,
2296                        width,
2297                        FP,
2298                        &mut actual_first_tombstone,
2299                        |slot| {
2300                            actual_compared.push(slot);
2301                            (slot == hit).then_some(slot)
2302                        },
2303                    )
2304                };
2305                assert_eq!(actual, expected.0, "width={width}");
2306                assert_eq!(actual_first_tombstone, expected.1, "width={width}");
2307                assert_eq!(actual_compared, expected.2, "width={width}");
2308
2309                controls.fill(OTHER);
2310                controls[start + width / 2] = CTRL_EMPTY;
2311                controls[logical_end - 1] = FP;
2312                controls[logical_end..].fill(FP);
2313                let expected = scalar(
2314                    &controls,
2315                    start,
2316                    width,
2317                    Some(logical_end - 1),
2318                    existing_tombstone,
2319                );
2320                let mut actual_first_tombstone = existing_tombstone;
2321                let mut actual_compared = Vec::new();
2322                let actual = unsafe {
2323                    scan_funnel_bucket(
2324                        controls.as_ptr(),
2325                        start,
2326                        width,
2327                        FP,
2328                        &mut actual_first_tombstone,
2329                        |slot| {
2330                            actual_compared.push(slot);
2331                            (slot == logical_end - 1).then_some(slot)
2332                        },
2333                    )
2334                };
2335                assert_eq!(actual, expected.0, "empty width={width}");
2336                assert_eq!(actual_first_tombstone, expected.1, "empty width={width}");
2337                assert_eq!(actual_compared, expected.2, "empty width={width}");
2338            }
2339        }
2340    }
2341}