1use 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 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
219type MembershipWord = u64;
221
222#[derive(Clone, Copy)]
226struct MembershipGate {
227 word: MembershipWord,
228 bits: u64,
229}
230
231impl MembershipGate {
232 #[inline]
234 const fn passes(self) -> bool {
235 self.word & self.bits == self.bits
236 }
237}
238
239struct 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#[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 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#[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
365unsafe 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#[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#[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
478unsafe 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
517pub 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 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 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 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 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 #[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 unsafe { *self.membership_ptr().add(word) & bits == bits }
869 }
870
871 #[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 word: unsafe { *self.membership_ptr().add(word) },
886 bits: MembershipKey::from_signature(key_hash).bits(),
887 }
888 }
889
890 #[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 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 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 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 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 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}