Skip to main content

sefer_region/
region.rs

1//! [`Region`] — a handle-addressed store of `T` backed by `slotmap`.
2
3use crate::Handle;
4use core::num::NonZeroUsize;
5use core::sync::atomic::AtomicUsize;
6
7/// Process-wide counter for minting unique `region_id` values.
8///
9/// This counter starts at 1 and is incremented once per `Region::new`/`with_capacity`
10/// call. The value 0 is reserved as a permanent "exhausted" sentinel — once the counter
11/// wraps to 0, all future `Region` constructions panic forever, ensuring no region_id
12/// is ever reused.
13static NEXT_REGION_ID: AtomicUsize = AtomicUsize::new(1);
14
15/// Domain limits for slotmap backing store.
16///
17/// `slotmap` reserves one slot as a sentinel; `try_with_capacity` therefore
18/// rejects `capacity > 2^32 - 3` while `try_reserve` rejects `len() + additional > 2^32 - 2`
19/// (the extra slot can be filled after construction via `insert`).
20const SLOTMAP_MAX_RESERVE: usize = ((1u64 << 32) - 3) as usize;
21const SLOTMAP_MAX_LIVE: usize = ((1u64 << 32) - 2) as usize;
22
23/// Error type returned when the process-wide `region_id` counter is exhausted.
24///
25/// This error is returned by the internal ID-issuance helper when the counter
26/// has reached `usize::MAX` and transitioned to the exhausted sentinel (0).
27/// After this point, all future `Region::new`/`with_capacity` calls will fail.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct RegionIdExhaustedError;
30
31impl core::fmt::Display for RegionIdExhaustedError {
32    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
33        f.write_str("process-wide region_id counter exhausted")
34    }
35}
36
37impl core::error::Error for RegionIdExhaustedError {}
38
39/// Error returned by fallible `Region<T>` constructors and capacity operations.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum TryReserveError {
42    /// The requested capacity/length exceeds slotmap's maximum live-entry domain.
43    CapacityExceeded {
44        /// The capacity or length that was requested
45        requested: usize,
46        /// Slotmap's maximum live-entry limit (`2^32 - 2` for reserve operations,
47        /// `2^32 - 3` for `with_capacity` since one slot is reserved as a sentinel)
48        limit: usize,
49    },
50    /// An internal capacity computation overflowed `usize`.
51    Overflow,
52    /// The process-wide `region_id` counter has been exhausted. Only ever
53    /// returned by `Region::try_new`/`try_with_capacity` (constructors mint a
54    /// new region_id); `Region::try_reserve` on an existing `Region` never
55    /// produces this variant, since it does not mint a new region_id.
56    RegionIdExhausted(RegionIdExhaustedError),
57}
58
59impl core::fmt::Display for TryReserveError {
60    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61        match self {
62            // Deliberately method-agnostic: this variant is returned by both
63            // `try_with_capacity` and `try_reserve`. Their infallible wrappers
64            // (`with_capacity`/`reserve`) prefix the method name themselves
65            // when panicking, so this text must not bake in either name.
66            Self::CapacityExceeded { requested, limit } => {
67                write!(f, "capacity {} exceeds slotmap limit {}", requested, limit)
68            }
69            Self::Overflow => f.write_str("capacity overflow"),
70            Self::RegionIdExhausted(inner) => inner.fmt(f),
71        }
72    }
73}
74
75impl core::error::Error for TryReserveError {
76    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
77        match self {
78            Self::RegionIdExhausted(inner) => Some(inner),
79            _ => None,
80        }
81    }
82}
83
84impl From<RegionIdExhaustedError> for TryReserveError {
85    fn from(err: RegionIdExhaustedError) -> Self {
86        Self::RegionIdExhausted(err)
87    }
88}
89
90/// Attempts to mint a unique `region_id` from the given atomic counter.
91///
92/// # Returns
93///
94/// - `Ok(NonZeroUsize)` — the newly minted region_id
95/// - `Err(RegionIdExhaustedError)` — the counter has been exhausted (transitioned
96///   to the permanent sentinel 0)
97///
98/// # Exhaustion semantics
99///
100/// This function uses `fetch_update` to ensure atomic exhaustion semantics:
101/// - If the current value is 0: immediately returns an error (already exhausted)
102/// - If the current value is `usize::MAX`: returns `MAX` (the last valid ID) and
103///   transitions the counter to 0 (permanent exhausted sentinel)
104/// - Otherwise: returns the current value and increments by 1
105///
106/// Once the counter transitions to 0, it will never transition back to a positive
107/// value — all future calls will fail with `RegionIdExhaustedError`. This ensures
108/// that no region_id is ever reused, even after exhaustion.
109#[inline]
110fn try_mint_region_id(counter: &AtomicUsize) -> Result<NonZeroUsize, RegionIdExhaustedError> {
111    use core::sync::atomic::Ordering;
112
113    match counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
114        if current == 0 {
115            // Already exhausted: stay at 0 forever
116            None
117        } else if current == usize::MAX {
118            // Last valid ID is MAX; transition to exhausted sentinel
119            Some(0)
120        } else {
121            // Normal case: increment
122            Some(current + 1)
123        }
124    }) {
125        // Unreachable in practice: the closure above only ever returns
126        // `Some(0)` when the *previous* value was `usize::MAX` (never 0
127        // itself), so `fetch_update`'s `Ok(previous)` can never be `Ok(0)`.
128        // Kept as a defensive match arm, not a reachable path.
129        Ok(0) => Err(RegionIdExhaustedError),
130        Ok(value) => match NonZeroUsize::new(value) {
131            Some(nz) => Ok(nz),
132            None => Err(RegionIdExhaustedError),
133        },
134        Err(_) => Err(RegionIdExhaustedError),
135    }
136}
137
138/// Test-only forwarder exposing [`try_mint_region_id`] to integration tests
139/// under `tests/`, which — unlike unit tests inside this module — can only
140/// see items re-exported from the crate root. Not part of the public API;
141/// `#[doc(hidden)]` keeps it out of rendered docs (see the "doc-hidden
142/// test-only forwarders" convention in this repo's `CLAUDE.md`). Takes an
143/// explicit `&AtomicUsize` rather than reaching for the real
144/// `NEXT_REGION_ID` static so boundary/exhaustion tests can drive a local
145/// counter without mutating process-wide state shared with other tests.
146#[doc(hidden)]
147pub fn dbg_try_mint_region_id(
148    counter: &AtomicUsize,
149) -> Result<NonZeroUsize, RegionIdExhaustedError> {
150    try_mint_region_id(counter)
151}
152
153/// A handle-addressed store of `T`.
154///
155/// A thin typed membrane over `slotmap::SlotMap<slotmap::DefaultKey, T>`.
156/// `SlotMap` keeps values in a contiguous slot array resolved by a single
157/// indirection (the lookup/churn axis it was benchmarked to win; see
158/// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/BENCHMARKS.md>), but it leaves tombstone holes after removals — it is
159/// NOT always-compact, and iteration walks the slot array skipping holes
160/// (~30 % slower than a `DenseSlotMap`, which packs live values for dense
161/// iteration). Every operation delegates to `slotmap` while exposing only typed
162/// [`Handle<T>`] values (raw `DefaultKey`s never escape as usable values
163/// through the API — Debug output renders the underlying key for diagnostics
164/// only, it cannot be turned back into a functioning handle through this crate's
165/// public surface). Individual lookup and
166/// removal are `O(1)`; insertion is amortized `O(1)` (may reallocate the slot
167/// array on growth); iteration and [`clear`](Self::clear) are linear in the
168/// slot-array length; [`reserve`](Self::reserve) may reallocate.
169///
170/// ## Invariants upheld
171///
172#[doc = include_str!("invariants.md")]
173///
174/// `region_id` is minted from a process-wide counter (`NEXT_REGION_ID`,
175/// `AtomicUsize`) that is incremented once per `Region::new`/`with_capacity`
176/// call and never reused. Once the counter is exhausted -- at the
177/// `2^{pointer_width}`-th `Region` construction attempt (when it would wrap
178/// from `usize::MAX` to 0), it transitions to a permanent exhausted state (0)
179/// and all future `Region` constructions panic. No region_id is ever reused,
180/// even after exhaustion — the value 0 is reserved as a sentinel that never
181/// transitions back to a positive value. See the `# Panics` sections on
182/// [`new`](Self::new) and [`with_capacity`](Self::with_capacity). On a
183/// 64-bit host this is a theoretical guard only. On a **32-bit host**
184/// (e.g. `thumbv7em-none-eabi`, `i686-*`) the bound is `2^32` (about 4.29
185/// billion), which is *reachable*, not just theoretical, for a long-lived
186/// 32-bit server or embedded process that mints a fresh `Region` per
187/// request/session over its lifetime rather than reusing one — the same
188/// honest register as the I2/I3 generation-wrap disclosure below, just a
189/// much larger and process-lifetime-scoped count rather than a per-slot
190/// reuse count.
191///
192/// ## Generation saturation
193///
194/// `slotmap::DefaultKey` uses a 32-bit generation counter stored alongside each
195/// slot. The exact encoding (odd = occupied, even = vacant), the LIFO freelist
196/// behavior, and the measured "~12 seconds" bound for `2^31 - 1` insert/remove
197/// cycles on a hot slot are **implementation details of the resolved
198/// slotmap 1.1.1 snapshot** — slotmap 1.x reserves the right to change these.
199///
200/// In the current version: `SlotMap::insert` sets the low bit on reuse
201/// (`version | 1`); `SlotMap::remove` advances it past that with
202/// `version.wrapping_add(1)` (odd -> even). So one full occupy/free cycle of a
203/// slot advances its generation by 2, and after approximately `2^31` such cycles
204/// the generation wraps around to its starting value, and a sufficiently stale
205/// handle may then resolve to (or remove) a different live value that now
206/// occupies the same slot.
207///
208/// This is a **logic/aliasing issue, not memory unsafety** — `slotmap` guarantees
209/// that its internal data structure never becomes corrupt, even when a handle wraps.
210/// The worst case for reaching wrap quickly is a hot single-slot churn pattern
211/// (repeatedly inserting and removing at the same slot index while nothing else
212/// is live). This was empirically confirmed on slotmap 1.1.1: a tight insert/remove
213/// loop on one slot for `2^31 - 1` cycles took ~12 seconds on one development
214/// machine in release mode; treat this as an order-of-magnitude sense for that
215/// version, not a guaranteed bound for all slotmap 1.x.
216///
217/// Applications that need a stronger guarantee (e.g. to reuse handles without
218/// ever risking alias) must add their own wrapper layer that tracks generation
219/// wrap on a hot slot; cross-instance confusion is already handled by I7 and
220/// needs no wrapper.
221pub struct Region<T> {
222    region_id: NonZeroUsize,
223    inner: slotmap::SlotMap<slotmap::DefaultKey, T>,
224}
225
226impl<T> Region<T> {
227    /// Checks if a handle belongs to this region (I7). Returns `Some(key)` if it does,
228    /// `None` otherwise.
229    #[inline]
230    fn owned_key(&self, handle: Handle<T>) -> Option<slotmap::DefaultKey> {
231        (handle.region_id == self.region_id).then_some(handle.key)
232    }
233
234    /// Creates an empty region that allocates nothing until first use.
235    ///
236    /// # Errors
237    ///
238    /// Returns `Err(TryReserveError::RegionIdExhausted(...))` if the process-wide
239    /// `region_id` counter has been exhausted — i.e. this would be the
240    /// `2^{pointer_width}`-th `Region` construction attempt (via `try_new`/`try_with_capacity`)
241    /// in this process. Once the counter is exhausted, **all** future `Region`
242    /// constructions in this process will fail, and no region_id is ever reused.
243    /// See the I7 doc block above for the exhaustion bound and why it is reachable,
244    /// not just theoretical, on a 32-bit host.
245    pub fn try_new() -> Result<Self, TryReserveError> {
246        let region_id = try_mint_region_id(&NEXT_REGION_ID)?;
247        Ok(Self {
248            region_id,
249            inner: slotmap::SlotMap::new(),
250        })
251    }
252
253    /// Creates an empty region that allocates nothing until first use.
254    ///
255    /// # Panics
256    ///
257    /// Panics if the process-wide `region_id` counter has been exhausted —
258    /// i.e. this would be the `2^{pointer_width}`-th `Region` construction attempt
259    /// (via `new`/`with_capacity`/`Default`) in this process. Once the counter
260    /// is exhausted, **all** future `Region` constructions in this process will
261    /// panic, and no region_id is ever reused. See the I7 doc block above for
262    /// the exhaustion bound and why it is reachable, not just theoretical, on
263    /// a 32-bit host.
264    #[must_use]
265    pub fn new() -> Self {
266        Self::try_new().unwrap_or_else(|e| panic!("Region::new: {e}"))
267    }
268
269    /// Creates an empty region with space pre-reserved for `capacity` entries.
270    ///
271    /// # Errors
272    ///
273    /// - Returns `Err(TryReserveError::CapacityExceeded { .. })` if `capacity > 2^32 - 3`
274    ///   (slotmap's maximum live-entry limit is `2^32 - 2`; reserving for sentinel gives `2^32 - 3`)
275    ///   — this is the guard that fires for any out-of-domain `capacity`, on both 32-bit
276    ///   and 64-bit hosts; on 64-bit this is a theoretical guard only (realistic workloads
277    ///   never approach this limit), but on a 32-bit host it is reachable.
278    /// - Returns `Err(TryReserveError::Overflow)` if an internal capacity computation
279    ///   overflowed `usize` (defense-in-depth, not currently reachable in practice).
280    /// - Returns `Err(TryReserveError::RegionIdExhausted(...))` if the process-wide
281    ///   `region_id` counter has been exhausted — see [`try_new`](Self::try_new)'s
282    ///   `# Errors` section and the I7 doc block above. Once the counter is exhausted,
283    ///   **all** future `Region` constructions in this process will fail, and no region_id
284    ///   is ever reused.
285    ///
286    /// # Note on allocation failure
287    ///
288    /// As with any `Vec`-backed container, allocation failure for a capacity whose slot array
289    /// would exceed `isize::MAX` bytes (roughly `usize::MAX / size_of::<Slot<T>>()`) aborts
290    /// rather than returning an error — this is not a recoverable error in standard Rust's
291    /// memory model.
292    pub fn try_with_capacity(capacity: usize) -> Result<Self, TryReserveError> {
293        // Reject capacity that would overflow slotmap's limit: max live entries is 2^32 - 2.
294        // With one sentinel slot, the maximum reserve is 2^32 - 3.
295        if capacity > SLOTMAP_MAX_RESERVE {
296            return Err(TryReserveError::CapacityExceeded {
297                requested: capacity,
298                limit: SLOTMAP_MAX_RESERVE,
299            });
300        }
301        // Tripwire: confirmed unreachable on both 32- and 64-bit targets
302        // (see `tests/coverage_gaps.rs`). Stays so a future slotmap change can't
303        // silently reintroduce overflow.
304        debug_assert!(capacity.checked_add(1).is_some());
305        let region_id = try_mint_region_id(&NEXT_REGION_ID)?;
306        Ok(Self {
307            region_id,
308            inner: slotmap::SlotMap::with_capacity(capacity),
309        })
310    }
311
312    /// Creates an empty region with space pre-reserved for `capacity` entries.
313    ///
314    /// # Panics
315    ///
316    /// Panics if `capacity > 2^32 - 3` (slotmap's maximum live-entry limit is
317    /// `2^32 - 2`; reserving for sentinel gives `2^32 - 3`) — this is the
318    /// guard that actually fires for any out-of-domain `capacity`, on both
319    /// 32-bit and 64-bit hosts; on 64-bit this is a theoretical guard only
320    /// (realistic workloads never approach this limit), but on a 32-bit host
321    /// it is reachable. Also panics (as any `Vec`-backed container does) for
322    /// any `capacity` whose slot array would exceed `isize::MAX` bytes —
323    /// roughly `usize::MAX / size_of::<Slot<T>>()`; allocation failure beyond
324    /// that aborts rather than panicking. Also panics if the process-wide
325    /// `region_id` counter has been exhausted — see [`new`](Self::new)'s
326    /// `# Panics` section and the I7 doc block above. Once the counter is
327    /// exhausted, **all** future `Region` constructions in this process will
328    /// panic, and no region_id is ever reused.
329    #[must_use]
330    pub fn with_capacity(capacity: usize) -> Self {
331        Self::try_with_capacity(capacity).unwrap_or_else(|e| panic!("Region::with_capacity: {e}"))
332    }
333
334    /// Number of live values (I4).
335    #[must_use]
336    pub fn len(&self) -> usize {
337        self.inner.len()
338    }
339
340    /// Whether the region holds no live values (I4).
341    #[must_use]
342    pub fn is_empty(&self) -> bool {
343        self.inner.is_empty()
344    }
345
346    /// Current value-storage capacity, in entries.
347    ///
348    /// Note: the underlying `slotmap` provides no shrink/compact operation of any kind.
349    /// Capacity — and therefore per-sweep iteration cost — is permanently bounded BELOW
350    /// by the historical high-water mark of live entries. The only way to reclaim that
351    /// cost is to build a fresh `Region` and re-insert (which invalidates every
352    /// outstanding handle from the old one).
353    #[must_use]
354    pub fn capacity(&self) -> usize {
355        self.inner.capacity()
356    }
357
358    /// Reserves capacity for at least `additional` more insertions.
359    ///
360    /// Does nothing if the backing store already has room. After a churn that
361    /// removes entries, the freed slots live on the free list, so re-inserting
362    /// reuses existing capacity and does not grow unboundedly (the backing
363    /// stays bounded by the high-water mark of live entries). Delegates to
364    /// `slotmap`'s `reserve`; may allocate more than asked to avoid frequent
365    /// reallocations.
366    ///
367    /// # Errors
368    ///
369    /// - Returns `Err(TryReserveError::Overflow)` if `len() + additional` would overflow `usize`.
370    /// - Returns `Err(TryReserveError::CapacityExceeded { .. })` if `len() + additional > 2^32 - 2`
371    ///   (slotmap's maximum live-entry limit).
372    ///
373    /// # Note on allocation failure
374    ///
375    /// As with any `Vec`-backed container, allocation failure for a capacity whose slot array
376    /// would exceed `isize::MAX` bytes (roughly `usize::MAX / size_of::<Slot<T>>()`) aborts
377    /// rather than returning an error — this is not a recoverable error in standard Rust's
378    /// memory model.
379    pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError> {
380        let target = self
381            .inner
382            .len()
383            .checked_add(additional)
384            .ok_or(TryReserveError::Overflow)?;
385        if target > SLOTMAP_MAX_LIVE {
386            return Err(TryReserveError::CapacityExceeded {
387                requested: target,
388                limit: SLOTMAP_MAX_LIVE,
389            });
390        }
391        self.inner.reserve(additional);
392        Ok(())
393    }
394
395    /// Reserves capacity for at least `additional` more insertions.
396    ///
397    /// Does nothing if the backing store already has room. After a churn that
398    /// removes entries, the freed slots live on the free list, so re-inserting
399    /// reuses existing capacity and does not grow unboundedly (the backing
400    /// stays bounded by the high-water mark of live entries). Delegates to
401    /// `slotmap`'s `reserve`; may allocate more than asked to avoid frequent
402    /// reallocations.
403    ///
404    /// # Panics
405    ///
406    /// Panics if `len() + additional` overflows `usize`, in both debug and
407    /// release builds — checked up front, before delegating to `slotmap`.
408    /// Panics if `len() + additional > 2^32 - 2` (slotmap's maximum live-entry limit).
409    /// Additionally panics (as any `Vec`-backed container does) for any
410    /// `len() + additional` whose slot array would exceed `isize::MAX` bytes
411    /// — roughly `usize::MAX / size_of::<Slot<T>>()`; allocation failure
412    /// beyond that aborts rather than panicking.
413    pub fn reserve(&mut self, additional: usize) {
414        if let Err(e) = self.try_reserve(additional) {
415            panic!("Region::reserve: {e}");
416        }
417    }
418
419    /// Inserts `value`, returning a fresh handle that resolves to it (I1).
420    ///
421    /// # Panics
422    ///
423    /// Panics if the backing `slotmap` is full (2^32 - 2 live entries).
424    #[must_use]
425    pub fn insert(&mut self, value: T) -> Handle<T> {
426        Handle::from_key_and_region(self.region_id, self.inner.insert(value))
427    }
428
429    /// Borrows the value for `handle`, or `None` if the handle is stale or
430    /// removed (I1, I2, I3).
431    #[must_use]
432    pub fn get(&self, handle: Handle<T>) -> Option<&T> {
433        self.inner.get(self.owned_key(handle)?)
434    }
435
436    /// Mutably borrows the value for `handle`, or `None` if stale/removed.
437    #[must_use]
438    pub fn get_mut(&mut self, handle: Handle<T>) -> Option<&mut T> {
439        self.inner.get_mut(self.owned_key(handle)?)
440    }
441
442    /// Whether `handle` currently resolves to a live value.
443    #[must_use]
444    pub fn contains(&self, handle: Handle<T>) -> bool {
445        self.owned_key(handle)
446            .map(|key| self.inner.contains_key(key))
447            .unwrap_or(false)
448    }
449
450    /// Removes and returns the value for `handle`, or `None` if it is already
451    /// stale/removed. After this, `handle` resolves to `None` for roughly
452    /// `2^31` reuse cycles of that slot (I2 — see the struct-level doc for
453    /// the generation-wrap caveat).
454    pub fn remove(&mut self, handle: Handle<T>) -> Option<T> {
455        self.inner.remove(self.owned_key(handle)?)
456    }
457
458    /// Iterates the live values. The order is unspecified and changes as
459    /// elements are removed. Walks the underlying `SlotMap`'s slot array,
460    /// skipping tombstone holes — so this is NOT cache-dense over live values
461    /// (a `DenseSlotMap`-backed store would be); see
462    /// <https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/BENCHMARKS.md>.
463    ///
464    /// Note: iteration cost is proportional to the slot-array length, not to
465    /// the live-value count. Since the underlying `slotmap` provides no shrink
466    /// operation, the slot-array length is permanently bounded below by the
467    /// historical high-water mark of live entries — a post-churn region with
468    /// many holes pays iteration cost proportional to that high-water mark,
469    /// even if few values remain live. See `capacity()`'s documentation for
470    /// the full permanence semantics.
471    ///
472    /// The returned iterator implements `ExactSizeIterator`, `FusedIterator`,
473    /// and `Clone`.
474    #[must_use]
475    pub fn iter(&self) -> Iter<'_, T> {
476        Iter {
477            inner: self.inner.values(),
478        }
479    }
480
481    /// Mutably iterates the live values (same non-dense order caveat as
482    /// [`iter`](Self::iter)).
483    ///
484    /// The returned iterator implements `ExactSizeIterator` and `FusedIterator`.
485    #[must_use]
486    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
487        IterMut {
488            inner: self.inner.values_mut(),
489        }
490    }
491
492    /// Removes every value, invalidating all outstanding handles, while
493    /// retaining allocated capacity. The region is reusable afterwards.
494    ///
495    /// Note: `clear` does NOT shrink the underlying slot array; the capacity
496    /// remains at the historical high-water mark of live entries. See
497    /// `capacity()`'s documentation for the full permanence semantics.
498    ///
499    /// If a value's `Drop` impl panics mid-`clear`, the clear is partial:
500    /// the region stays fully consistent and reusable after unwinding, but
501    /// the exact set of survivors depends on the underlying `slotmap` version's
502    /// unwind cleanup (slotmap 1.x reserves the right to change this). What is
503    /// guaranteed is that: (1) no value is dropped twice, (2) no value is leaked
504    /// by the region itself (caller-side `mem::forget` of removed values is
505    /// outside this guarantee), and (3) the region's internal accounting remains
506    /// correct. See `tests/clear_partial_under_panic.rs`, which documents what
507    /// the CURRENT slotmap version actually does -- an observation of the
508    /// present dependency, not a stable contract this crate promises.
509    pub fn clear(&mut self) {
510        self.inner.clear();
511    }
512}
513
514impl<T> Default for Region<T> {
515    /// # Panics
516    ///
517    /// Panics under the same condition as [`Region::new`] (process-wide
518    /// `region_id` counter exhaustion) — this delegates to `new`.
519    fn default() -> Self {
520        Self::new()
521    }
522}
523
524/// Note: the `region_id` field shown by this impl is minted from a
525/// process-wide counter and is therefore NOT stable across separate runs or
526/// processes (its value depends on how many other `Region`/`SyncRegion`
527/// instances the process happened to construct first) — do not rely on it in
528/// snapshot/golden-output tests.
529impl<T> core::fmt::Debug for Region<T> {
530    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
531        f.debug_struct("Region")
532            .field("region_id", &self.region_id)
533            .field("len", &self.len())
534            .field("capacity", &self.capacity())
535            .finish()
536    }
537}
538
539// Note: consuming `IntoIterator for Region<T>` is not currently implemented.
540// This is not a technical impossibility — a wrapper could yield just `T`,
541// dropping the key internally — but has not been requested.
542// Iteration by reference (`&Region<T>` and `&mut Region<T>`) is provided below.
543
544impl<'a, T> IntoIterator for &'a Region<T> {
545    type Item = &'a T;
546    type IntoIter = Iter<'a, T>;
547
548    fn into_iter(self) -> Self::IntoIter {
549        self.iter()
550    }
551}
552
553impl<'a, T> IntoIterator for &'a mut Region<T> {
554    type Item = &'a mut T;
555    type IntoIter = IterMut<'a, T>;
556
557    fn into_iter(self) -> Self::IntoIter {
558        self.iter_mut()
559    }
560}
561
562/// Iterator over the live values in a [`Region<T>`], returned by
563/// [`Region::iter`] and `IntoIterator for &Region<T>`.
564///
565/// A thin wrapper over `slotmap`'s own values iterator — kept as a distinct
566/// named type (rather than re-exporting `slotmap`'s type directly) so this
567/// crate's public API surface never names a `slotmap` type, matching the
568/// rest of this crate's encapsulation of its backing store.
569pub struct Iter<'a, T> {
570    inner: slotmap::basic::Values<'a, slotmap::DefaultKey, T>,
571}
572
573impl<'a, T> Iterator for Iter<'a, T> {
574    type Item = &'a T;
575
576    fn next(&mut self) -> Option<Self::Item> {
577        self.inner.next()
578    }
579
580    fn size_hint(&self) -> (usize, Option<usize>) {
581        self.inner.size_hint()
582    }
583}
584
585impl<T> ExactSizeIterator for Iter<'_, T> {
586    fn len(&self) -> usize {
587        self.inner.len()
588    }
589}
590
591impl<T> core::iter::FusedIterator for Iter<'_, T> {}
592
593impl<T> Clone for Iter<'_, T> {
594    fn clone(&self) -> Self {
595        Self {
596            inner: self.inner.clone(),
597        }
598    }
599}
600
601impl<T> core::fmt::Debug for Iter<'_, T> {
602    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
603        f.debug_struct("Iter").field("len", &self.len()).finish()
604    }
605}
606
607/// Mutable iterator over the live values in a [`Region<T>`], returned by
608/// [`Region::iter_mut`] and `IntoIterator for &mut Region<T>`.
609///
610/// Same encapsulation rationale as [`Iter`] — not `Clone` (a mutable
611/// iterator cannot be duplicated without aliasing `&mut` references).
612pub struct IterMut<'a, T> {
613    inner: slotmap::basic::ValuesMut<'a, slotmap::DefaultKey, T>,
614}
615
616impl<'a, T> Iterator for IterMut<'a, T> {
617    type Item = &'a mut T;
618
619    fn next(&mut self) -> Option<Self::Item> {
620        self.inner.next()
621    }
622
623    fn size_hint(&self) -> (usize, Option<usize>) {
624        self.inner.size_hint()
625    }
626}
627
628impl<T> ExactSizeIterator for IterMut<'_, T> {
629    fn len(&self) -> usize {
630        self.inner.len()
631    }
632}
633
634impl<T> core::iter::FusedIterator for IterMut<'_, T> {}
635
636impl<T> core::fmt::Debug for IterMut<'_, T> {
637    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
638        f.debug_struct("IterMut").field("len", &self.len()).finish()
639    }
640}