Skip to main content

Region

Struct Region 

Source
pub struct Region<T> { /* private fields */ }
Expand description

A handle-addressed store of T.

A thin typed membrane over slotmap::SlotMap<slotmap::DefaultKey, T>. SlotMap keeps values in a contiguous slot array resolved by a single indirection (the lookup/churn axis it was benchmarked to win; see https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/BENCHMARKS.md), but it leaves tombstone holes after removals — it is NOT always-compact, and iteration walks the slot array skipping holes (~30 % slower than a DenseSlotMap, which packs live values for dense iteration). Every operation delegates to slotmap while exposing only typed Handle<T> values (raw DefaultKeys never escape as usable values through the API — Debug output renders the underlying key for diagnostics only, it cannot be turned back into a functioning handle through this crate’s public surface). Individual lookup and removal are O(1); insertion is amortized O(1) (may reallocate the slot array on growth); iteration and clear are linear in the slot-array length; reserve may reallocate.

§Invariants upheld

  • I1 — resolution: a fresh handle resolves via Region::get to the inserted value until it is Region::removed.
  • I2 — tombstone: after remove(h), get(h) returns None for roughly 2^31 reuse cycles of that slot (a stale handle that has survived that many insert/remove cycles may wrap and spuriously resolve to a later value). A second remove(h) is a no-op None.
  • I3 — no ABA: a stale handle — one whose slot has since been reused — does not resolve to a live value for roughly 2^31 reuse cycles of that slot. slotmap’s DefaultKey carries a 32-bit generation (odd = occupied, even = vacant): insert sets the low bit, remove increments via wrapping_add(1), so a full occupy/free cycle advances the generation by 2 — after 2^31 such cycles it wraps and a very old handle may alias a later value. Memory safety is never affected — slotmap guarantees this even after wrap.
  • I4 — accounting: Region::len equals the number of live entries and Region::is_empty agrees.
  • I5 — drop-once: every live value is dropped exactly once. Successful remove transfers ownership to the caller without calling Drop; values still owned when a normally-destroyed Region drops are dropped. The crate never duplicates or internally forgets values.
  • I6 — slot reuse and bounded growth: freed slots are reused by insert; capacity grows to a historical high-water mark of live entries and does not increase further under steady-state churn (slotmap does not physically compact — tombstone slots remain in the backing store; I6 guarantees only reuse and bounded growth, not physical density).
  • I7 — instance isolation: a Handle<T> resolves only through the Region<T> instance that minted it. Every accessor stamps its region_id at construction and checks it before touching the backing slotmap; a mismatch is treated exactly like a stale handle. Two Region<T>s can never alias each other’s values through a shared DefaultKey, even when that key collides (as it commonly does — the first insert into any fresh Region tends to produce the same key).

region_id is minted from a process-wide counter (NEXT_REGION_ID, AtomicUsize) that is incremented once per Region::new/with_capacity call and never reused. Once the counter is exhausted – at the 2^{pointer_width}-th Region construction attempt (when it would wrap from usize::MAX to 0), it transitions to a permanent exhausted state (0) and all future Region constructions panic. No region_id is ever reused, even after exhaustion — the value 0 is reserved as a sentinel that never transitions back to a positive value. See the # Panics sections on new and with_capacity. On a 64-bit host this is a theoretical guard only. On a 32-bit host (e.g. thumbv7em-none-eabi, i686-*) the bound is 2^32 (about 4.29 billion), which is reachable, not just theoretical, for a long-lived 32-bit server or embedded process that mints a fresh Region per request/session over its lifetime rather than reusing one — the same honest register as the I2/I3 generation-wrap disclosure below, just a much larger and process-lifetime-scoped count rather than a per-slot reuse count.

§Generation saturation

slotmap::DefaultKey uses a 32-bit generation counter stored alongside each slot. The exact encoding (odd = occupied, even = vacant), the LIFO freelist behavior, and the measured “~12 seconds” bound for 2^31 - 1 insert/remove cycles on a hot slot are implementation details of the resolved slotmap 1.1.1 snapshot — slotmap 1.x reserves the right to change these.

In the current version: SlotMap::insert sets the low bit on reuse (version | 1); SlotMap::remove advances it past that with version.wrapping_add(1) (odd -> even). So one full occupy/free cycle of a slot advances its generation by 2, and after approximately 2^31 such cycles the generation wraps around to its starting value, and a sufficiently stale handle may then resolve to (or remove) a different live value that now occupies the same slot.

This is a logic/aliasing issue, not memory unsafetyslotmap guarantees that its internal data structure never becomes corrupt, even when a handle wraps. The worst case for reaching wrap quickly is a hot single-slot churn pattern (repeatedly inserting and removing at the same slot index while nothing else is live). This was empirically confirmed on slotmap 1.1.1: a tight insert/remove loop on one slot for 2^31 - 1 cycles took ~12 seconds on one development machine in release mode; treat this as an order-of-magnitude sense for that version, not a guaranteed bound for all slotmap 1.x.

Applications that need a stronger guarantee (e.g. to reuse handles without ever risking alias) must add their own wrapper layer that tracks generation wrap on a hot slot; cross-instance confusion is already handled by I7 and needs no wrapper.

Implementations§

Source§

impl<T> Region<T>

Source

pub fn try_new() -> Result<Self, TryReserveError>

Creates an empty region that allocates nothing until first use.

§Errors

Returns Err(TryReserveError::RegionIdExhausted(...)) if the process-wide region_id counter has been exhausted — i.e. this would be the 2^{pointer_width}-th Region construction attempt (via try_new/try_with_capacity) in this process. Once the counter is exhausted, all future Region constructions in this process will fail, and no region_id is ever reused. See the I7 doc block above for the exhaustion bound and why it is reachable, not just theoretical, on a 32-bit host.

Source

pub fn new() -> Self

Creates an empty region that allocates nothing until first use.

§Panics

Panics if the process-wide region_id counter has been exhausted — i.e. this would be the 2^{pointer_width}-th Region construction attempt (via new/with_capacity/Default) in this process. Once the counter is exhausted, all future Region constructions in this process will panic, and no region_id is ever reused. See the I7 doc block above for the exhaustion bound and why it is reachable, not just theoretical, on a 32-bit host.

Source

pub fn try_with_capacity(capacity: usize) -> Result<Self, TryReserveError>

Creates an empty region with space pre-reserved for capacity entries.

§Errors
  • Returns Err(TryReserveError::CapacityExceeded { .. }) if capacity > 2^32 - 3 (slotmap’s maximum live-entry limit is 2^32 - 2; reserving for sentinel gives 2^32 - 3) — this is the guard that fires for any out-of-domain capacity, on both 32-bit and 64-bit hosts; on 64-bit this is a theoretical guard only (realistic workloads never approach this limit), but on a 32-bit host it is reachable.
  • Returns Err(TryReserveError::Overflow) if an internal capacity computation overflowed usize (defense-in-depth, not currently reachable in practice).
  • Returns Err(TryReserveError::RegionIdExhausted(...)) if the process-wide region_id counter has been exhausted — see try_new’s # Errors section and the I7 doc block above. Once the counter is exhausted, all future Region constructions in this process will fail, and no region_id is ever reused.
§Note on allocation failure

As with any Vec-backed container, allocation failure for a capacity whose slot array would exceed isize::MAX bytes (roughly usize::MAX / size_of::<Slot<T>>()) aborts rather than returning an error — this is not a recoverable error in standard Rust’s memory model.

Source

pub fn with_capacity(capacity: usize) -> Self

Creates an empty region with space pre-reserved for capacity entries.

§Panics

Panics if capacity > 2^32 - 3 (slotmap’s maximum live-entry limit is 2^32 - 2; reserving for sentinel gives 2^32 - 3) — this is the guard that actually fires for any out-of-domain capacity, on both 32-bit and 64-bit hosts; on 64-bit this is a theoretical guard only (realistic workloads never approach this limit), but on a 32-bit host it is reachable. Also panics (as any Vec-backed container does) for any capacity whose slot array would exceed isize::MAX bytes — roughly usize::MAX / size_of::<Slot<T>>(); allocation failure beyond that aborts rather than panicking. Also panics if the process-wide region_id counter has been exhausted — see new’s # Panics section and the I7 doc block above. Once the counter is exhausted, all future Region constructions in this process will panic, and no region_id is ever reused.

Source

pub fn len(&self) -> usize

Number of live values (I4).

Source

pub fn is_empty(&self) -> bool

Whether the region holds no live values (I4).

Source

pub fn capacity(&self) -> usize

Current value-storage capacity, in entries.

Note: the underlying slotmap provides no shrink/compact operation of any kind. Capacity — and therefore per-sweep iteration cost — is permanently bounded BELOW by the historical high-water mark of live entries. The only way to reclaim that cost is to build a fresh Region and re-insert (which invalidates every outstanding handle from the old one).

Source

pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError>

Reserves capacity for at least additional more insertions.

Does nothing if the backing store already has room. After a churn that removes entries, the freed slots live on the free list, so re-inserting reuses existing capacity and does not grow unboundedly (the backing stays bounded by the high-water mark of live entries). Delegates to slotmap’s reserve; may allocate more than asked to avoid frequent reallocations.

§Errors
  • Returns Err(TryReserveError::Overflow) if len() + additional would overflow usize.
  • Returns Err(TryReserveError::CapacityExceeded { .. }) if len() + additional > 2^32 - 2 (slotmap’s maximum live-entry limit).
§Note on allocation failure

As with any Vec-backed container, allocation failure for a capacity whose slot array would exceed isize::MAX bytes (roughly usize::MAX / size_of::<Slot<T>>()) aborts rather than returning an error — this is not a recoverable error in standard Rust’s memory model.

Source

pub fn reserve(&mut self, additional: usize)

Reserves capacity for at least additional more insertions.

Does nothing if the backing store already has room. After a churn that removes entries, the freed slots live on the free list, so re-inserting reuses existing capacity and does not grow unboundedly (the backing stays bounded by the high-water mark of live entries). Delegates to slotmap’s reserve; may allocate more than asked to avoid frequent reallocations.

§Panics

Panics if len() + additional overflows usize, in both debug and release builds — checked up front, before delegating to slotmap. Panics if len() + additional > 2^32 - 2 (slotmap’s maximum live-entry limit). Additionally panics (as any Vec-backed container does) for any len() + additional whose slot array would exceed isize::MAX bytes — roughly usize::MAX / size_of::<Slot<T>>(); allocation failure beyond that aborts rather than panicking.

Source

pub fn insert(&mut self, value: T) -> Handle<T>

Inserts value, returning a fresh handle that resolves to it (I1).

§Panics

Panics if the backing slotmap is full (2^32 - 2 live entries).

Source

pub fn get(&self, handle: Handle<T>) -> Option<&T>

Borrows the value for handle, or None if the handle is stale or removed (I1, I2, I3).

Source

pub fn get_mut(&mut self, handle: Handle<T>) -> Option<&mut T>

Mutably borrows the value for handle, or None if stale/removed.

Source

pub fn contains(&self, handle: Handle<T>) -> bool

Whether handle currently resolves to a live value.

Source

pub fn remove(&mut self, handle: Handle<T>) -> Option<T>

Removes and returns the value for handle, or None if it is already stale/removed. After this, handle resolves to None for roughly 2^31 reuse cycles of that slot (I2 — see the struct-level doc for the generation-wrap caveat).

Source

pub fn iter(&self) -> Iter<'_, T>

Iterates the live values. The order is unspecified and changes as elements are removed. Walks the underlying SlotMap’s slot array, skipping tombstone holes — so this is NOT cache-dense over live values (a DenseSlotMap-backed store would be); see https://github.com/PHPCraftdream/sefer-alloc/blob/main/docs/BENCHMARKS.md.

Note: iteration cost is proportional to the slot-array length, not to the live-value count. Since the underlying slotmap provides no shrink operation, the slot-array length is permanently bounded below by the historical high-water mark of live entries — a post-churn region with many holes pays iteration cost proportional to that high-water mark, even if few values remain live. See capacity()’s documentation for the full permanence semantics.

The returned iterator implements ExactSizeIterator, FusedIterator, and Clone.

Source

pub fn iter_mut(&mut self) -> IterMut<'_, T>

Mutably iterates the live values (same non-dense order caveat as iter).

The returned iterator implements ExactSizeIterator and FusedIterator.

Source

pub fn clear(&mut self)

Removes every value, invalidating all outstanding handles, while retaining allocated capacity. The region is reusable afterwards.

Note: clear does NOT shrink the underlying slot array; the capacity remains at the historical high-water mark of live entries. See capacity()’s documentation for the full permanence semantics.

If a value’s Drop impl panics mid-clear, the clear is partial: the region stays fully consistent and reusable after unwinding, but the exact set of survivors depends on the underlying slotmap version’s unwind cleanup (slotmap 1.x reserves the right to change this). What is guaranteed is that: (1) no value is dropped twice, (2) no value is leaked by the region itself (caller-side mem::forget of removed values is outside this guarantee), and (3) the region’s internal accounting remains correct. See tests/clear_partial_under_panic.rs, which documents what the CURRENT slotmap version actually does – an observation of the present dependency, not a stable contract this crate promises.

Trait Implementations§

Source§

impl<T> Debug for Region<T>

Note: the region_id field shown by this impl is minted from a process-wide counter and is therefore NOT stable across separate runs or processes (its value depends on how many other Region/SyncRegion instances the process happened to construct first) — do not rely on it in snapshot/golden-output tests.

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<T> Default for Region<T>

Source§

fn default() -> Self

§Panics

Panics under the same condition as Region::new (process-wide region_id counter exhaustion) — this delegates to new.

Source§

impl<T> From<Region<T>> for SyncRegion<T>

Available on crate feature std only.
Source§

fn from(region: Region<T>) -> Self

Wraps an existing Region<T> in a SyncRegion for safe concurrent access.

This provides a zero-copy conversion path from single-threaded to concurrent usage without invalidating existing handles — all Handle<T> values from the original Region remain valid and resolve correctly in the wrapped SyncRegion.

Source§

impl<T> From<SyncRegion<T>> for Region<T>

Available on crate feature std only.
Source§

fn from(sr: SyncRegion<T>) -> Self

Converts to this type from the input type.
Source§

impl<'a, T> IntoIterator for &'a Region<T>

Source§

type Item = &'a T

The type of the elements being iterated over.
Source§

type IntoIter = Iter<'a, T>

Which kind of iterator are we turning this into?
Source§

fn into_iter(self) -> Self::IntoIter

Creates an iterator from a value. Read more
Source§

impl<'a, T> IntoIterator for &'a mut Region<T>

Source§

type Item = &'a mut T

The type of the elements being iterated over.
Source§

type IntoIter = IterMut<'a, T>

Which kind of iterator are we turning this into?
Source§

fn into_iter(self) -> Self::IntoIter

Creates an iterator from a value. Read more

Auto Trait Implementations§

§

impl<T> Freeze for Region<T>

§

impl<T> RefUnwindSafe for Region<T>
where T: RefUnwindSafe,

§

impl<T> Send for Region<T>
where T: Send,

§

impl<T> Sync for Region<T>
where T: Sync,

§

impl<T> Unpin for Region<T>
where T: Unpin,

§

impl<T> UnsafeUnpin for Region<T>

§

impl<T> UnwindSafe for Region<T>
where T: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.