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::getto the inserted value until it isRegion::removed. - I2 — tombstone: after
remove(h),get(h)returnsNonefor roughly2^31reuse 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 secondremove(h)is a no-opNone. - I3 — no ABA: a stale handle — one whose slot has since been reused —
does not resolve to a live value for roughly
2^31reuse cycles of that slot.slotmap’sDefaultKeycarries a 32-bit generation (odd = occupied, even = vacant):insertsets the low bit,removeincrements viawrapping_add(1), so a full occupy/free cycle advances the generation by 2 — after2^31such cycles it wraps and a very old handle may alias a later value. Memory safety is never affected —slotmapguarantees this even after wrap. - I4 — accounting:
Region::lenequals the number of live entries andRegion::is_emptyagrees. - I5 — drop-once: every live value is dropped exactly once. Successful
removetransfers ownership to the caller without callingDrop; values still owned when a normally-destroyedRegiondrops 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 (slotmapdoes 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 theRegion<T>instance that minted it. Every accessor stamps itsregion_idat construction and checks it before touching the backing slotmap; a mismatch is treated exactly like a stale handle. TwoRegion<T>s can never alias each other’s values through a sharedDefaultKey, even when that key collides (as it commonly does — the first insert into any freshRegiontends 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 unsafety — slotmap 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>
impl<T> Region<T>
Sourcepub fn try_new() -> Result<Self, TryReserveError>
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.
Sourcepub fn new() -> Self
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.
Sourcepub fn try_with_capacity(capacity: usize) -> Result<Self, TryReserveError>
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 { .. })ifcapacity > 2^32 - 3(slotmap’s maximum live-entry limit is2^32 - 2; reserving for sentinel gives2^32 - 3) — this is the guard that fires for any out-of-domaincapacity, 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 overflowedusize(defense-in-depth, not currently reachable in practice). - Returns
Err(TryReserveError::RegionIdExhausted(...))if the process-wideregion_idcounter has been exhausted — seetry_new’s# Errorssection and the I7 doc block above. Once the counter is exhausted, all futureRegionconstructions 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.
Sourcepub fn with_capacity(capacity: usize) -> Self
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.
Sourcepub fn capacity(&self) -> usize
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).
Sourcepub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError>
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)iflen() + additionalwould overflowusize. - Returns
Err(TryReserveError::CapacityExceeded { .. })iflen() + 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.
Sourcepub fn reserve(&mut self, additional: usize)
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.
Sourcepub fn insert(&mut self, value: T) -> Handle<T>
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).
Sourcepub fn get(&self, handle: Handle<T>) -> Option<&T>
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).
Sourcepub fn get_mut(&mut self, handle: Handle<T>) -> Option<&mut T>
pub fn get_mut(&mut self, handle: Handle<T>) -> Option<&mut T>
Mutably borrows the value for handle, or None if stale/removed.
Sourcepub fn contains(&self, handle: Handle<T>) -> bool
pub fn contains(&self, handle: Handle<T>) -> bool
Whether handle currently resolves to a live value.
Sourcepub fn remove(&mut self, handle: Handle<T>) -> Option<T>
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).
Sourcepub fn iter(&self) -> Iter<'_, T> ⓘ
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.
Sourcepub fn iter_mut(&mut self) -> IterMut<'_, T> ⓘ
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.
Sourcepub fn clear(&mut self)
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.
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§impl<T> Default for Region<T>
impl<T> Default for Region<T>
Source§fn default() -> Self
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.
impl<T> From<Region<T>> for SyncRegion<T>
std only.Source§fn from(region: Region<T>) -> Self
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.
impl<T> From<SyncRegion<T>> for Region<T>
std only.