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}