Skip to main content

forge_alloc/layout/
generational.rs

1//! `GenerationalSlab<T, B, G>` — typed allocator that returns stable
2//! [`Handle<T, G>`] values instead of raw pointers. Each slot carries a
3//! generation counter; a handle is only valid while its generation matches
4//! the slot's. This prevents ABA / use-after-free at the API level: a stale
5//! handle returns `None` rather than producing undefined behavior.
6//!
7//! Interleaved layout: generation + state live together in a single
8//! `GenerationalSlot<T>` struct, backed by one contiguous allocation.
9//! One cache-line load covers both the generation check and the value access.
10//!
11//! See `docs/ARCHITECTURE.md` for the generational-slab design.
12
13use core::marker::PhantomData;
14use core::ptr::NonNull;
15
16use forge_alloc_core::{AllocError, Allocator, FixedRange, NonZeroLayout};
17
18/// Width of the generation counter. Public so callers can pick `u32`
19/// (default — 2^32 reuses per slot before wraparound) or `u64` (effectively
20/// unbounded). Internal trait sealed via the empty trait pattern.
21pub trait GenerationInt: Copy + Eq + sealed::Sealed {
22    /// Initial generation value.
23    const ZERO: Self;
24    /// Increment, wrapping at the type's max value.
25    fn wrapping_inc(self) -> Self;
26}
27
28mod sealed {
29    pub trait Sealed {}
30    impl Sealed for u32 {}
31    impl Sealed for u64 {}
32}
33
34impl GenerationInt for u32 {
35    const ZERO: Self = 0;
36    #[inline]
37    fn wrapping_inc(self) -> Self {
38        self.wrapping_add(1)
39    }
40}
41
42impl GenerationInt for u64 {
43    const ZERO: Self = 0;
44    #[inline]
45    fn wrapping_inc(self) -> Self {
46        self.wrapping_add(1)
47    }
48}
49
50/// Stable, non-pointer handle to a slot in a [`GenerationalSlab`].
51///
52/// `Copy` (cheap to pass), `Eq` + `Hash` (works in maps), and notably does
53/// NOT carry a reference to the slab — handles do not extend the slab's
54/// lifetime. A handle outliving its slab is allowed and gives `None` on
55/// access.
56///
57/// # Generation wraparound — closed by slot retirement
58///
59/// The generation counter increments on every `remove` of a slot and is
60/// `wrapping_add(1)` (per `GenerationInt`). Without mitigation, after `2^G`
61/// reuses of the *same* slot the counter would return to its original value
62/// and a stale `Handle` could be re-accepted against a different `T` (the
63/// classic ABA problem at a long horizon).
64///
65/// This is **defended, not merely documented**: when a slot's generation
66/// wraps a full cycle (the increment reaches `ZERO`), `remove` **retires**
67/// the slot — it is never returned to the freelist, so it can never be
68/// re-issued at a generation a stale handle holds. The cost is one leaked
69/// slot per full wrap of that slot (4.3 billion removes for `u32`, which is
70/// negligible). The choice between `u32` and `u64` is therefore now about how
71/// soon a hot slot is retired, not about a UAF window:
72///
73/// - `G = u32` (default): wrap after **2^32 ≈ 4.3 billion** reuses of
74///   the same slot. Realistic for long-running servers with high churn
75///   on a small slab (e.g. a 1024-slot connection pool processing
76///   millions of connections per second).
77/// - `G = u64`: wrap after 2^64 reuses — effectively unreachable in
78///   any realistic deployment (>500 years at 1 GHz of pure slot churn).
79///   Recommended for long-lived servers; the per-handle cost is 8
80///   extra bytes (8 bytes for `u32`, 16 for `u64` — the wider counter
81///   forces 8-byte struct alignment and 4 bytes of tail padding).
82///
83/// `Copy` means a handle can outlive the slot's original lifetime
84/// arbitrarily — including past 2^G recycles. If your handles can
85/// realistically be stashed for that long (audit logs, persisted
86/// session records, snapshot indices), use `Handle<T, u64>`. For
87/// per-request handles that never escape the request scope, `u32` is
88/// fine.
89///
90/// # Known limitation — cross-pool handle confusion
91///
92/// `Handle<T, G>` is typed by `T` (and `G`) but **not** by which
93/// `GenerationalSlab<T, B, G>` issued it. Passing a handle returned
94/// by `pool_a.insert(...)` to `pool_b.get(...)` is currently a
95/// runtime concern, not a compile error: the slot index will be
96/// interpreted against `pool_b`'s slot array, and ABA-safety
97/// degenerates to "match-by-coincidence" on the generation counter.
98///
99/// Closing this gap requires runtime-unique branding — either an
100/// invariant-lifetime tag (`generativity` crate style) or a
101/// monotonic pool-id passed through `PhantomData` — and is
102/// **API-breaking** because every `Handle<T, G>` user signature
103/// would gain an extra type parameter. The v0.1 API ships without
104/// it; v2.0+ may revisit. See the "Generational-handle slab" recipe
105/// in `docs/COMPOSITION_RECIPES.md` for the documented pitfall.
106///
107/// Until branded, callers who keep multiple pools of the same `T`
108/// must NOT mix their handles. The naming convention recommended in
109/// the recipes doc is to type-alias each pool's handle:
110/// `type SessionHandle = Handle<Session, u32>` per pool, in
111/// different modules.
112#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
113pub struct Handle<T, G: GenerationInt = u32> {
114    index: u32,
115    generation: G,
116    _phantom: PhantomData<*const T>,
117}
118
119/// One slot in the generational slab — generation + value state colocated.
120enum SlotState<T> {
121    Occupied(T),
122    Free { next_free: Option<u32> },
123}
124
125struct GenerationalSlot<T, G: GenerationInt> {
126    generation: G,
127    state: SlotState<T>,
128}
129
130/// Generational slab.
131///
132/// `capacity` slots, each holding either a live `T` or a free-list link.
133/// Handles encode `(slot_index, generation_at_allocate)`; access via
134/// [`get`](Self::get) / [`get_mut`](Self::get_mut) / [`remove`](Self::remove)
135/// only succeeds when the slot's current generation equals the handle's.
136///
137/// # Thread safety
138///
139/// All mutation goes through `&mut self`. The slab is `Send + Sync` if `T`
140/// and `B` are, but concurrent mutation requires external synchronization
141/// (`Mutex<GenerationalSlab<T, B>>`).
142pub struct GenerationalSlab<T, B: Allocator + FixedRange, G: GenerationInt = u32> {
143    backing: B,
144    /// Byte offset from `backing.base()` to the start of the slot array.
145    ///
146    /// We deliberately do NOT store an absolute `NonNull<GenerationalSlot<T, G>>`
147    /// captured from `backing.allocate(...)` at construction. Backings whose
148    /// `base()` is structure-relative (e.g. `InlineBacked<N>` returns
149    /// `&self.storage`) report a DIFFERENT address before and after the
150    /// backing has been moved. An absolute pointer captured at construction
151    /// time would then point at the backing's OLD location after `Self {
152    /// backing, ... }` moves the backing into Self by-value — silently
153    /// corrupting every subsequent `insert` / `get` / `remove`. Matches the
154    /// Slab move-safety pattern (see `UntypedSlab::base_offset` in size_classed.rs).
155    slots_offset: usize,
156    capacity: u32,
157    /// Free-list head: `None` = empty (must carve from `len`).
158    free_head: Option<u32>,
159    /// First slot never yet allocated.
160    len: u32,
161    backing_layout: NonZeroLayout,
162    /// Ties `T` and `G` to the struct so the field-less slot region (we
163    /// store only an offset, not a typed pointer) still carries the right
164    /// type identity. `*const` keeps `Send`/`Sync` opt-in via the explicit
165    /// `unsafe impl`s below.
166    _phantom: PhantomData<(*const T, G)>,
167}
168
169impl<T, B: Allocator + FixedRange> GenerationalSlab<T, B, u32> {
170    /// Construct with default `u32` generation counter.
171    pub fn new(capacity: usize, backing: B) -> Result<Self, AllocError> {
172        Self::with_generation(capacity, backing)
173    }
174}
175
176impl<T, B: Allocator + FixedRange, G: GenerationInt> GenerationalSlab<T, B, G> {
177    /// Construct with an explicit generation-width parameter.
178    ///
179    /// Use `u64` for high-churn long-running servers where 2^32 reuses of
180    /// the same slot could realistically occur.
181    pub fn with_generation(capacity: usize, backing: B) -> Result<Self, AllocError> {
182        if capacity == 0 {
183            return Err(AllocError);
184        }
185        let cap_u32 = u32::try_from(capacity).map_err(|_| AllocError)?;
186        let layout = NonZeroLayout::array::<GenerationalSlot<T, G>>(capacity).ok_or(AllocError)?;
187        let block = backing.allocate(layout)?;
188        let slots_ptr = block.cast::<GenerationalSlot<T, G>>();
189        // Compute offset from backing.base() — see field docs on
190        // `slots_offset`. Absolute `slots_ptr` is only valid in the local
191        // frame; after `Self { backing, ... }` moves `backing` by-value,
192        // any structure-relative backing's base address changes.
193        let base_addr = backing.base().as_ptr() as usize;
194        let slots_addr = slots_ptr.as_ptr() as usize;
195        debug_assert!(
196            slots_addr >= base_addr,
197            "GenerationalSlab: allocate() returned a pointer below backing.base() — \
198             impossible if `B: FixedRange` honours the OsBacked / FixedRange contract"
199        );
200        let slots_offset = slots_addr - base_addr;
201        // Initialize every slot to Free with generation ZERO.
202        // SAFETY: `slots_ptr..slots_ptr+capacity` is uninitialized memory of
203        // the right size and alignment per the layout request. We write
204        // through the LOCAL (pre-move) pointer; the post-move recompute
205        // (`backing.base() + slots_offset`) hits the SAME bytes inside the
206        // moved backing's storage, since the bytes are a sub-region of the
207        // backing and travel with it on the move.
208        unsafe {
209            for i in 0..capacity {
210                let p = slots_ptr.as_ptr().add(i);
211                p.write(GenerationalSlot {
212                    generation: G::ZERO,
213                    state: SlotState::Free { next_free: None },
214                });
215            }
216        }
217        Ok(Self {
218            backing,
219            slots_offset,
220            capacity: cap_u32,
221            free_head: None,
222            len: 0,
223            backing_layout: layout,
224            _phantom: PhantomData,
225        })
226    }
227
228    /// Recompute the live slot-array pointer from the backing's *current*
229    /// base. Must be called every time the slab needs to dereference into
230    /// the slot region — never cached across calls.
231    ///
232    /// For backings with a structure-relative `base()` (e.g.
233    /// `InlineBacked<N>`), the backing's storage moves with the
234    /// `GenerationalSlab` struct itself. The offset captured at
235    /// construction stays valid (the relative position of the slot array
236    /// inside the backing's storage is invariant); recomputing through
237    /// the live `backing.base()` plus that offset always lands on the
238    /// correct bytes.
239    #[inline]
240    fn slots(&self) -> NonNull<GenerationalSlot<T, G>> {
241        let base = self.backing.base().as_ptr();
242        // SAFETY: `slots_offset` was the difference between
243        // `backing.allocate(layout)`'s result and `backing.base()` at
244        // construction; the slot region is a sub-range of the backing's
245        // storage and `slots_offset + capacity * size_of` <= region.
246        let p = unsafe { base.add(self.slots_offset) };
247        // SAFETY: derived from a non-null backing.base() plus an in-range
248        // offset; the result is non-null.
249        unsafe { NonNull::new_unchecked(p.cast::<GenerationalSlot<T, G>>()) }
250    }
251
252    /// Number of slots.
253    #[inline]
254    pub fn capacity(&self) -> usize {
255        self.capacity as usize
256    }
257
258    /// Insert a value and return its handle. Errors if the slab is full.
259    pub fn insert(&mut self, value: T) -> Result<Handle<T, G>, AllocError> {
260        // Try free list first. Resolve `slot_idx` AND `slot` pointer in a
261        // single branch so the freelist-pop path does ONE
262        // `slots.as_ptr().add(idx)` computation rather than two — the
263        // optimizer should CSE, but folding the access ourselves avoids
264        // relying on noalias deduction across the `self.free_head =
265        // *next_free` write that sits between the two derefs.
266        // Defense-in-depth: `free_head` is only ever set from `remove` with an
267        // index in `0..len <= capacity`, so it cannot legitimately be out of
268        // range. Guard the one unchecked index path so a corrupted `free_head`
269        // can never cause an out-of-bounds slot deref (UB) — abandon the link
270        // and carve fresh instead. (Not reachable via the safe API today.)
271        if let Some(idx) = self.free_head {
272            if idx >= self.capacity {
273                debug_assert!(
274                    false,
275                    "GenerationalSlab: corrupt free_head {idx} >= capacity {}",
276                    self.capacity,
277                );
278                self.free_head = None;
279            }
280        }
281        let (slot_idx, slot) = match self.free_head {
282            Some(idx) => {
283                // SAFETY: idx came from our free_head (set by remove), and the
284                // guard above ensured idx < capacity, so the slot is in bounds.
285                let slot = unsafe { &mut *self.slots().as_ptr().add(idx as usize) };
286                match &slot.state {
287                    SlotState::Free { next_free } => {
288                        self.free_head = *next_free;
289                    }
290                    SlotState::Occupied(_) => {
291                        // Inconsistent state — the free list pointed at an
292                        // occupied slot. Not reachable via the safe API; signal
293                        // it in debug (mirroring the corrupt-`free_head` guard
294                        // above) rather than silently masquerading as OOM.
295                        debug_assert!(
296                            false,
297                            "GenerationalSlab: free_head {idx} points at an occupied slot",
298                        );
299                        return Err(AllocError);
300                    }
301                }
302                (idx, slot)
303            }
304            None => {
305                if self.len >= self.capacity {
306                    return Err(AllocError);
307                }
308                let idx = self.len;
309                self.len += 1;
310                // SAFETY: idx < capacity (we just checked); slot is
311                // initialized by `with_generation`.
312                let slot = unsafe { &mut *self.slots().as_ptr().add(idx as usize) };
313                (idx, slot)
314            }
315        };
316        // Place the value and capture the current generation for the handle.
317        slot.state = SlotState::Occupied(value);
318        let generation = slot.generation;
319        // Defense-in-depth: the slot we just filled must lie within
320        // `[0, len)` — freelist-pop returns previously-allocated slots
321        // and the fresh-allocation branch above bumped `self.len`. A
322        // mismatch indicates either freelist corruption or a stale
323        // `len` (refactor regression). Compiled out in release.
324        debug_assert!(
325            (slot_idx as usize) < self.len as usize,
326            "GenerationalSlab::insert: filled slot {slot_idx} but len={} — \
327             freelist corruption or stale len",
328            self.len,
329        );
330        Ok(Handle {
331            index: slot_idx,
332            generation,
333            _phantom: PhantomData,
334        })
335    }
336
337    /// Borrow the value behind a handle, or `None` if stale / vacant.
338    pub fn get(&self, handle: Handle<T, G>) -> Option<&T> {
339        if handle.index >= self.capacity {
340            return None;
341        }
342        // SAFETY: index is in-range; slot is initialized.
343        let slot = unsafe { &*self.slots().as_ptr().add(handle.index as usize) };
344        if slot.generation != handle.generation {
345            return None;
346        }
347        match &slot.state {
348            SlotState::Occupied(v) => Some(v),
349            SlotState::Free { .. } => None,
350        }
351    }
352
353    /// Borrow mutably behind a handle, or `None` if stale / vacant.
354    pub fn get_mut(&mut self, handle: Handle<T, G>) -> Option<&mut T> {
355        if handle.index >= self.capacity {
356            return None;
357        }
358        // SAFETY: as above; &mut self gives exclusive access.
359        let slot = unsafe { &mut *self.slots().as_ptr().add(handle.index as usize) };
360        if slot.generation != handle.generation {
361            return None;
362        }
363        match &mut slot.state {
364            SlotState::Occupied(v) => Some(v),
365            SlotState::Free { .. } => None,
366        }
367    }
368
369    /// Remove the value behind a handle and return it, or `None` if stale.
370    /// Bumps the slot's generation so the handle (and any copies) become
371    /// invalid.
372    pub fn remove(&mut self, handle: Handle<T, G>) -> Option<T> {
373        if handle.index >= self.capacity {
374            return None;
375        }
376        // SAFETY: as above.
377        let slot = unsafe { &mut *self.slots().as_ptr().add(handle.index as usize) };
378        if slot.generation != handle.generation {
379            return None;
380        }
381        // Bump the generation so the old handle is now stale. `wrapping_inc`
382        // reaches `ZERO` only from the type's max value, so `next == ZERO`
383        // means this slot just completed a full 2^bits cycle.
384        let next_gen = slot.generation.wrapping_inc();
385        slot.generation = next_gen;
386        let prior = if next_gen == G::ZERO {
387            // The generation wrapped. Returning the slot to the freelist now
388            // would let it be reused at generation 0 (and onward), which is
389            // exactly when a stale handle minted earlier in the cycle would
390            // re-validate against a different `T` — the ABA / UAF this type
391            // exists to prevent. RETIRE the slot instead: extract the value
392            // but leave it OFF the freelist (a tombstone), so it is never
393            // handed out again. Costs one slot per full wrap (4.3B removes for
394            // `u32`) — negligible — and closes the wraparound window entirely
395            // rather than merely documenting it.
396            core::mem::replace(&mut slot.state, SlotState::Free { next_free: None })
397        } else {
398            // Normal path: link the slot onto the freelist for reuse.
399            let prior = core::mem::replace(
400                &mut slot.state,
401                SlotState::Free {
402                    next_free: self.free_head,
403                },
404            );
405            self.free_head = Some(handle.index);
406            prior
407        };
408        match prior {
409            SlotState::Occupied(v) => Some(v),
410            SlotState::Free { .. } => None, // can't happen; we checked above
411        }
412    }
413
414    /// True if the handle would currently access a live value.
415    #[inline]
416    pub fn contains(&self, handle: Handle<T, G>) -> bool {
417        self.get(handle).is_some()
418    }
419
420    /// Test-only: force a live slot's generation, so the wraparound-retirement
421    /// path can be reached without 2^bits real removes.
422    #[cfg(test)]
423    fn force_generation(&mut self, index: u32, generation: G) {
424        assert!(index < self.capacity);
425        // SAFETY: index < capacity; slot is initialized.
426        let slot = unsafe { &mut *self.slots().as_ptr().add(index as usize) };
427        slot.generation = generation;
428    }
429}
430
431impl<T, B: Allocator + FixedRange, G: GenerationInt> Drop for GenerationalSlab<T, B, G> {
432    fn drop(&mut self) {
433        // Drop every Occupied value, then return the backing chunk.
434        //
435        // **Panic-safety contract**: `T::drop` is allowed to panic. If it
436        // does:
437        //
438        // - During **normal** drop (no in-flight unwind): the panic
439        //   escapes this body. Slots at indices > i are left undropped
440        //   (Rust's loop unwind semantics — `mem::replace`'s temporary
441        //   is the unwinding value), and the `backing.deallocate` call
442        //   below this loop **does not run**, leaking the entire
443        //   backing region. This is strictly worse than Slab's pattern
444        //   (Slab deallocates the backing FIRST), but unlike Slab we
445        //   cannot reorder — we need the backing memory live to read
446        //   each `slot.state` pointer.
447        // - During **drop while unwinding** (a panic was already in
448        //   flight when `GenerationalSlab` started dropping): a second
449        //   panic in `T::drop` triggers **immediate process abort**
450        //   per Rust language rules (`drop-while-panicking → abort`).
451        //   This is unavoidable without `catch_unwind` (std-only) and
452        //   matches the standard library's contract everywhere.
453        //
454        // Production guidance: ensure `T::drop` is panic-free for any
455        // `T` stored in a `GenerationalSlab`. Wrap fallible cleanup in
456        // a separate `try_close()`-style API and run it before the slab
457        // drops.
458        //
459        // SAFETY: every slot 0..capacity was initialized in construction.
460        // Use the live `self.slots()` accessor so the post-move backing
461        // base is honoured — caching the construction-time pointer would
462        // double-bug the structure-relative-backing case at drop time too.
463        unsafe {
464            let slots = self.slots();
465            for i in 0..self.capacity as usize {
466                let slot = &mut *slots.as_ptr().add(i);
467                if let SlotState::Occupied(_) = &slot.state {
468                    // Replace with Free to run T's Drop.
469                    let _ =
470                        core::mem::replace(&mut slot.state, SlotState::Free { next_free: None });
471                }
472            }
473            self.backing
474                .deallocate(slots.cast::<u8>(), self.backing_layout);
475        }
476    }
477}
478
479// Send when T, B, G are Send. Sync when T, B are Sync; G is Copy and !Sync
480// is impossible for Copy types in practice. The slab itself permits &mut
481// access through methods, so cross-thread use via Arc<Mutex<...>> is the
482// standard pattern.
483unsafe impl<T: Send, B: Allocator + FixedRange + Send, G: GenerationInt + Send> Send
484    for GenerationalSlab<T, B, G>
485{
486}
487unsafe impl<T: Sync, B: Allocator + FixedRange + Sync, G: GenerationInt + Sync> Sync
488    for GenerationalSlab<T, B, G>
489{
490}
491
492// ============================================================================
493// Kani proof harnesses
494// ============================================================================
495
496#[cfg(kani)]
497mod kani_proofs {
498    use super::*;
499    use crate::backing::InlineBacked;
500
501    /// `insert` then `get` returns the inserted value via the issued handle.
502    #[kani::proof]
503    #[kani::unwind(5)]
504    fn insert_then_get_round_trips() {
505        let mut s: GenerationalSlab<u64, InlineBacked<512>> =
506            GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
507        let v: u64 = kani::any();
508        if let Ok(h) = s.insert(v) {
509            assert!(s.get(h) == Some(&v));
510        }
511    }
512
513    /// After `remove`, the old handle no longer resolves (`get` returns
514    /// `None`) — the generation counter on the slot mismatches the
515    /// handle's frozen generation.
516    #[kani::proof]
517    #[kani::unwind(5)]
518    fn remove_invalidates_old_handle() {
519        let mut s: GenerationalSlab<u64, InlineBacked<512>> =
520            GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
521        if let Ok(h) = s.insert(0xAA) {
522            let _ = s.remove(h);
523            // After remove, the handle's generation is stale.
524            assert!(s.get(h).is_none());
525        }
526    }
527
528    /// Reusing a slot via subsequent insert hands out a NEW handle whose
529    /// generation differs from any prior handle to the same slot.
530    #[kani::proof]
531    #[kani::unwind(5)]
532    fn reused_slot_gets_new_handle() {
533        let mut s: GenerationalSlab<u64, InlineBacked<512>> =
534            GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
535        if let Ok(h_old) = s.insert(0xAA) {
536            let _ = s.remove(h_old);
537            if let Ok(h_new) = s.insert(0xBB) {
538                // Same slot index could be reused, but generation must
539                // have moved — so the two handles are not interchangeable.
540                assert!(s.get(h_old).is_none());
541                assert!(s.get(h_new) == Some(&0xBB));
542            }
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use crate::backing::InlineBacked;
551
552    #[test]
553    fn insert_then_get_round_trip() {
554        let mut s: GenerationalSlab<u64, InlineBacked<1024>> =
555            GenerationalSlab::new(16, InlineBacked::<1024>::new()).unwrap();
556        let h = s.insert(42).unwrap();
557        assert_eq!(s.get(h), Some(&42));
558    }
559
560    /// A slot whose generation wraps a full cycle on `remove` must be RETIRED
561    /// (never returned to the freelist), closing the ABA/UAF window. With a
562    /// capacity-2 slab: slot 0 is forced to `u32::MAX`, removed (→ wraps →
563    /// retired), then the slab can only ever hand out slot 1 — a third insert
564    /// must OOM rather than reuse the retired slot 0.
565    #[test]
566    fn wrapped_generation_slot_is_retired() {
567        let mut s: GenerationalSlab<u64, InlineBacked<1024>> =
568            GenerationalSlab::with_generation(2, InlineBacked::<1024>::new()).unwrap();
569        // Force slot 0 (carved by the first insert) to MAX *before* inserting,
570        // so the handle is minted at MAX and the matching `remove` wraps to 0.
571        s.force_generation(0, u32::MAX);
572        let a = s.insert(1).unwrap();
573        assert_eq!(a.index, 0);
574        assert_eq!(a.generation, u32::MAX);
575        assert_eq!(s.remove(a), Some(1)); // gen matches → wraps → retires slot 0
576                                          // Slot 0 retired. The fresh carve gives slot 1; a third insert OOMs
577                                          // because slot 0 is off the freelist (without retirement it would be
578                                          // reused and `c` below would succeed at index 0).
579        let b = s.insert(2).unwrap();
580        assert_eq!(b.index, 1, "retired slot 0 must not be reused");
581        assert!(
582            s.insert(3).is_err(),
583            "retired slot must stay out of circulation (no reuse)",
584        );
585    }
586
587    #[test]
588    fn remove_invalidates_handle() {
589        let mut s: GenerationalSlab<u64, InlineBacked<1024>> =
590            GenerationalSlab::new(16, InlineBacked::<1024>::new()).unwrap();
591        let h = s.insert(42).unwrap();
592        assert_eq!(s.remove(h), Some(42));
593        // Second access through the same handle must return None.
594        assert_eq!(s.get(h), None);
595        assert_eq!(s.remove(h), None);
596    }
597
598    #[test]
599    fn reused_slot_returns_new_value_only_to_new_handle() {
600        let mut s: GenerationalSlab<u64, InlineBacked<512>> =
601            GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
602        let h1 = s.insert(1).unwrap();
603        s.remove(h1);
604        let h2 = s.insert(2).unwrap();
605        // ABA test: stale h1 must NOT see h2's value.
606        assert_eq!(s.get(h1), None, "stale handle must not access reused slot");
607        assert_eq!(s.get(h2), Some(&2));
608    }
609
610    #[test]
611    fn capacity_exhaustion_returns_err() {
612        let mut s: GenerationalSlab<u64, InlineBacked<256>> =
613            GenerationalSlab::new(4, InlineBacked::<256>::new()).unwrap();
614        for i in 0..4 {
615            s.insert(i).unwrap();
616        }
617        assert!(s.insert(5).is_err());
618    }
619
620    #[test]
621    fn get_mut_mutates() {
622        let mut s: GenerationalSlab<u64, InlineBacked<512>> =
623            GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
624        let h = s.insert(0).unwrap();
625        *s.get_mut(h).unwrap() = 99;
626        assert_eq!(s.get(h), Some(&99));
627    }
628
629    #[test]
630    fn handles_are_copy() {
631        let mut s: GenerationalSlab<u64, InlineBacked<256>> =
632            GenerationalSlab::new(2, InlineBacked::<256>::new()).unwrap();
633        let h = s.insert(7).unwrap();
634        let h2 = h; // Copy
635        assert_eq!(s.get(h), Some(&7));
636        assert_eq!(s.get(h2), Some(&7));
637    }
638
639    #[test]
640    #[cfg(feature = "std")]
641    fn drops_occupied_values() {
642        use std::sync::atomic::{AtomicUsize, Ordering};
643        use std::sync::Arc;
644        struct Counter(Arc<AtomicUsize>);
645        impl Drop for Counter {
646            fn drop(&mut self) {
647                self.0.fetch_add(1, Ordering::Relaxed);
648            }
649        }
650        let drops = Arc::new(AtomicUsize::new(0));
651        {
652            let mut s: GenerationalSlab<Counter, InlineBacked<512>> =
653                GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap();
654            s.insert(Counter(drops.clone())).unwrap();
655            s.insert(Counter(drops.clone())).unwrap();
656            // slab dropped here; both Counters should run Drop.
657        }
658        assert_eq!(drops.load(Ordering::Relaxed), 2);
659    }
660
661    /// Regression: `GenerationalSlab` used to
662    /// cache `slots: NonNull<GenerationalSlot<T, G>>` captured from
663    /// `backing.allocate(...)` BEFORE the backing was moved into Self. For
664    /// structure-relative backings like `InlineBacked<N>` (whose `base()`
665    /// returns `&self.storage`), the cached pointer aimed at the *pre-move*
666    /// location forever, silently corrupting every subsequent
667    /// `insert`/`get`/`remove`.
668    ///
669    /// This test constructs a slab inside a helper, returns it by value to
670    /// a caller frame, defeats NRVO with a large stack local, then exercises
671    /// the slab. Each access must land on the LIVE backing's slot region,
672    /// not the pre-move location.
673    #[test]
674    fn slab_survives_move_with_inline_backed_nrvo_defeating() {
675        #[inline(never)]
676        fn make() -> GenerationalSlab<u64, InlineBacked<512>> {
677            let mut s: GenerationalSlab<u64, InlineBacked<512>> =
678                GenerationalSlab::new(8, InlineBacked::<512>::new()).unwrap();
679            // Pre-populate from the constructor frame so the bug's window
680            // is exercised: writes here go through the construction-time
681            // backing location. The returned struct must still read the
682            // SAME values from the moved backing's storage.
683            for i in 0..4u64 {
684                let _ = s.insert(0xAA00 + i).unwrap();
685            }
686            s
687        }
688        // Force enough locals to defeat NRVO. The 4096-byte array sits
689        // on the caller's stack, pushing the returned `slab` to a
690        // different address than `make`'s local.
691        let _arr = [0u8; 4096];
692        let mut slab = make();
693        // The construction-time pointer (if cached) would now be stale.
694        let h_new = slab.insert(0xDEADBEEF).unwrap();
695        // Live read through the new handle — must round-trip.
696        assert_eq!(slab.get(h_new), Some(&0xDEADBEEF));
697        // Insert/get round trips for more values to stress the post-move
698        // slot region.
699        let mut handles = alloc::vec::Vec::new();
700        for v in 0..3u64 {
701            handles.push(slab.insert(0xCAFE_0000 + v).unwrap());
702        }
703        for (i, h) in handles.iter().enumerate() {
704            assert_eq!(slab.get(*h), Some(&(0xCAFE_0000 + i as u64)));
705        }
706        // Remove + insert: exercise the freelist path on the post-move
707        // storage. If the slots pointer were stale, the bumped generation
708        // would be written into the WRONG slot and the new handle's read
709        // would either return None or the stale value.
710        assert_eq!(slab.remove(h_new), Some(0xDEADBEEF));
711        let h_reuse = slab.insert(0x1234).unwrap();
712        assert_eq!(slab.get(h_reuse), Some(&0x1234));
713    }
714
715    /// Companion: assert that the slot region the slab dereferences
716    /// actually lies inside the LIVE backing's range after the move.
717    /// A stale-pointer bug would have the slab reading from a different
718    /// address range than `slab.backing.base()` reports.
719    #[test]
720    fn slab_slots_lie_inside_live_backing_after_move() {
721        #[inline(never)]
722        fn make() -> GenerationalSlab<u64, InlineBacked<512>> {
723            GenerationalSlab::new(4, InlineBacked::<512>::new()).unwrap()
724        }
725        let _arr = [0u8; 4096];
726        let mut slab = make();
727        // Insert and capture the address of the slot.
728        let h = slab.insert(0x55AA).unwrap();
729        let val_ref: &u64 = slab.get(h).unwrap();
730        let val_addr = val_ref as *const u64 as usize;
731        // Resolve the live backing base + size via FixedRange.
732        let base = FixedRange::base(&slab.backing).as_ptr() as usize;
733        let size = FixedRange::size(&slab.backing);
734        assert!(
735            val_addr >= base && val_addr < base + size,
736            "slot address {val_addr:#x} outside live backing range \
737             [{base:#x}, {:#x}) — stale-pointer bug",
738            base + size,
739        );
740    }
741}