mnemosyne_heap/heap.rs
1use crate::brand::{BrandedBlock, InvariantLifetime};
2use crate::raw_heap::RawHeap;
3use core::alloc::Layout;
4use core::marker::PhantomData;
5use core::ptr::NonNull;
6use melinoe::{ReadPermit, WritePermit};
7use mnemosyne_core::AllocPolicy;
8use mnemosyne_local::LocalAllocatorSelector;
9use mnemosyne_local::internal::HasSegmentPool;
10
11/// The reason a branded reallocation could not produce a replacement block.
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13#[non_exhaustive]
14pub enum ReallocFailure {
15 /// `new_size` and the existing alignment cannot form a valid `Layout`.
16 InvalidLayout {
17 /// The requested new size.
18 new_size: usize,
19 /// The existing block alignment the new size had to satisfy.
20 alignment: usize,
21 },
22 /// The supplied source layout exceeds the block's usable capacity or
23 /// requires an alignment the source pointer does not satisfy.
24 InvalidSourceLayout {
25 /// Size named by the caller-supplied source layout.
26 requested_size: usize,
27 /// Alignment named by that layout.
28 alignment: usize,
29 /// Capacity the block actually provides.
30 usable_size: usize,
31 },
32 /// The requested replacement allocation could not be obtained.
33 AllocationFailed,
34}
35
36/// A failed branded reallocation that retains ownership of the source block.
37///
38/// The source block is returned inside the error so allocation failure cannot
39/// leak it. Call [`Self::into_block`] to recover it, or inspect [`Self::reason`]
40/// before deciding how to handle the failure; the recovered block remains
41/// explicitly owned and must be released through the heap when no longer
42/// needed.
43#[must_use]
44pub struct ReallocError<'brand, T: ?Sized> {
45 block: BrandedBlock<'brand, T>,
46 reason: ReallocFailure,
47}
48
49impl<'brand, T: ?Sized> ReallocError<'brand, T> {
50 #[inline]
51 fn new(block: BrandedBlock<'brand, T>, reason: ReallocFailure) -> Self {
52 Self { block, reason }
53 }
54
55 /// Returns the failure classification without consuming the source block.
56 #[inline]
57 #[must_use]
58 pub fn reason(&self) -> ReallocFailure {
59 self.reason
60 }
61
62 /// Recovers the original source block without dropping or deallocating it.
63 #[inline]
64 #[must_use]
65 pub fn into_block(self) -> BrandedBlock<'brand, T> {
66 self.block
67 }
68}
69
70impl<'brand, T: ?Sized> core::fmt::Debug for ReallocError<'brand, T> {
71 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72 f.debug_struct("ReallocError")
73 .field("block", &self.block)
74 .field("reason", &self.reason)
75 .finish()
76 }
77}
78
79/// A scoped, lifetime-branded memory heap.
80///
81/// `Heap` is the single public heap surface. It statically validates local
82/// block ownership through the scoped brand lifetime while delegating all
83/// allocation mechanics to the monomorphized `RawHeap` core.
84pub struct Heap<
85 'brand,
86 P: AllocPolicy,
87 B: HasSegmentPool + LocalAllocatorSelector<B> = mnemosyne_backend::MemoryBackendWrapper,
88> {
89 pub(crate) raw: RawHeap<P, B>,
90 pub(crate) _phantom: InvariantLifetime<'brand>,
91}
92
93// SAFETY: `Heap<'brand, P, B>` wraps a `RawHeap<P, B>` whose only interior
94// state is a `core::cell::UnsafeCell<ThreadAllocator<B>>` accessed exclusively
95// through `&self` methods that assume single-threaded ownership and perform no
96// internal synchronization. A Melinoe permit proves branded access, but it
97// does not make the allocator state concurrently shareable: `Heap` remains
98// the sole owner and must stay on its originating thread. The `RawHeap` core
99// already carries the matching `unsafe impl<P, B: HasSegmentPool> Send for
100// RawHeap<P, B>` (see `raw_heap.rs`) for the same ownership-transfer reason.
101// `sync_scope` therefore transfers only `BrandedCell` handles and a
102// `SyncRegionToken`; it never shares this heap across workers.
103unsafe impl<'brand, P: AllocPolicy, B: HasSegmentPool + LocalAllocatorSelector<B>> Send
104 for Heap<'brand, P, B>
105{
106}
107
108/// Returns one block to its heap when dropped, on the normal path and on an
109/// unwind alike.
110///
111/// A value's destructor may panic. `drop_in_place` followed by a plain free
112/// call then unwinds past the free and leaks the block — `Vec` frees regardless,
113/// and the branded containers must match that guarantee. Holding the block here
114/// for the duration of the element drop puts the free on both exit paths.
115///
116/// This is the single home for that pairing: every `drop_in_place` + free site
117/// in this crate goes through it rather than repeating the guard.
118pub(crate) struct BlockFreeGuard<'guard, 'brand, P, B>
119where
120 P: AllocPolicy,
121 B: HasSegmentPool + LocalAllocatorSelector<B>,
122{
123 heap: &'guard Heap<'brand, P, B>,
124 ptr: *mut u8,
125}
126
127impl<'guard, 'brand, P, B> BlockFreeGuard<'guard, 'brand, P, B>
128where
129 P: AllocPolicy,
130 B: HasSegmentPool + LocalAllocatorSelector<B>,
131{
132 /// Takes ownership of `ptr` for the guard's scope.
133 ///
134 /// # Safety
135 ///
136 /// `ptr` must be a non-ZST block previously allocated by `heap` and not yet
137 /// freed, and it must not be freed by anything else: the guard frees it
138 /// exactly once when dropped.
139 #[inline(always)]
140 pub(crate) const unsafe fn new(heap: &'guard Heap<'brand, P, B>, ptr: *mut u8) -> Self {
141 Self { heap, ptr }
142 }
143}
144
145impl<P, B> Drop for BlockFreeGuard<'_, '_, P, B>
146where
147 P: AllocPolicy,
148 B: HasSegmentPool + LocalAllocatorSelector<B>,
149{
150 #[inline(always)]
151 fn drop(&mut self) {
152 // SAFETY: `new`'s contract makes `self.ptr` a live non-ZST block of
153 // `self.heap` that nothing else frees, which is exactly `free_raw`'s
154 // requirement. The guard is consumed by this drop, so the free happens
155 // exactly once.
156 unsafe { self.heap.free_raw(self.ptr) };
157 }
158}
159
160impl<'brand, P: AllocPolicy, B: HasSegmentPool + LocalAllocatorSelector<B>> Heap<'brand, P, B> {
161 /// Allocates a block of memory from this heap.
162 ///
163 /// The block is tied to the heap's unique `'brand` lifetime. Returns `None`
164 /// if the allocation fails. The read permit proves that the request belongs
165 /// to this brand without adding runtime state.
166 #[inline(always)]
167 pub fn alloc<Permit>(&self, _permit: Permit, layout: Layout) -> Option<BrandedBlock<'brand, u8>>
168 where
169 Permit: ReadPermit<'brand>,
170 {
171 let ptr = self.raw.alloc(layout);
172 NonNull::new(ptr).map(|ptr| BrandedBlock {
173 ptr,
174 _marker: PhantomData,
175 })
176 }
177
178 #[cfg(test)]
179 #[inline(always)]
180 pub(crate) fn stats(&self) -> mnemosyne_local::ThreadAllocatorStats {
181 self.raw.stats()
182 }
183
184 /// Internal raw deallocation function.
185 ///
186 /// # Safety
187 /// `ptr` must be a non-ZST block previously allocated by this heap's
188 /// `raw` core and not yet freed; passing a foreign, dangling, or
189 /// double-freed pointer is undefined behavior.
190 #[inline(always)]
191 pub(crate) unsafe fn free_raw(&self, ptr: *mut u8) {
192 // SAFETY: by this function's own contract `ptr` is a non-ZST block
193 // previously allocated by `self.raw` and not yet freed, satisfying
194 // `free_owned_unchecked`'s requirement of an owned, live allocation.
195 unsafe { self.raw.free_owned_unchecked(ptr) };
196 }
197
198 /// Frees a block of memory back to this heap, dropping the value in-place first.
199 ///
200 /// Because the block is branded with the heap's unique `'brand` lifetime,
201 /// it is statically guaranteed to have been allocated by this heap. The
202 /// write permit proves exclusive access while the value is dropped.
203 ///
204 /// The block is returned to the heap even if `T`'s destructor panics.
205 #[inline(always)]
206 pub fn free<T: ?Sized, Permit>(&self, _permit: &mut Permit, block: BrandedBlock<'brand, T>)
207 where
208 for<'token> &'token mut Permit: WritePermit<'brand>,
209 {
210 let ptr = block.ptr.as_ptr();
211 // SAFETY: `block` is a `BrandedBlock<'brand, T>`, and the matching
212 // `WritePermit<'brand>` carried by `_permit` proves exclusive access for this
213 // brand, so `ptr` points to a live, fully-initialized `T` uniquely
214 // owned here. Reading the runtime layout before dropping is valid for
215 // sized and unsized values; using the pointer after `drop_in_place`
216 // would not be. `drop_in_place` runs `T::drop` exactly once; the block
217 // is consumed by value so the pointer is never reused. A non-ZST block
218 // was allocated by this same heap, satisfying the guard's contract; a
219 // ZST was never allocated, so its branch frees nothing.
220 unsafe {
221 if core::mem::size_of_val(&*ptr) == 0 {
222 core::ptr::drop_in_place(ptr);
223 } else {
224 let _free = BlockFreeGuard::new(self, ptr as *mut u8);
225 core::ptr::drop_in_place(ptr);
226 }
227 }
228 }
229
230 /// Frees a block of memory back to this heap without dropping the value.
231 ///
232 /// Useful for uninitialized memory or manual drop management.
233 #[inline(always)]
234 pub fn free_uninit<T: ?Sized, Permit>(
235 &self,
236 _permit: &mut Permit,
237 block: BrandedBlock<'brand, T>,
238 ) where
239 for<'token> &'token mut Permit: WritePermit<'brand>,
240 {
241 let ptr = block.ptr.as_ptr();
242 // SAFETY: `block` is a `BrandedBlock<'brand, T>` consumed by value with
243 // the matching exclusive `WritePermit<'brand>`, so `ptr` is a
244 // live block uniquely owned here. The value is intentionally not
245 // dropped (uninitialized / manually-managed memory). `size_of_val`
246 // reads only `T`'s layout, and a non-ZST block was allocated by this
247 // heap, satisfying `free_raw`'s contract.
248 unsafe {
249 if core::mem::size_of_val(&*ptr) != 0 {
250 self.free_raw(ptr as *mut u8);
251 }
252 }
253 }
254
255 /// Allocates and initializes a value directly in a branded memory block.
256 ///
257 /// The block is guaranteed to contain a fully initialized value of type `T`.
258 #[inline(always)]
259 pub fn alloc_init<T, Permit>(&self, permit: Permit, val: T) -> Option<BrandedBlock<'brand, T>>
260 where
261 Permit: ReadPermit<'brand>,
262 {
263 if core::mem::size_of::<T>() == 0 {
264 let ptr: NonNull<T> = NonNull::dangling();
265 // SAFETY: `T` is a ZST (`size_of::<T>() == 0`), so a properly
266 // aligned dangling pointer is a valid place for a write of zero
267 // bytes; `write` moves `val` without reading the destination and
268 // does not dereference real storage.
269 unsafe {
270 ptr.as_ptr().write(val);
271 }
272 return Some(BrandedBlock {
273 ptr,
274 _marker: PhantomData,
275 });
276 }
277
278 let block = self.alloc(permit, Layout::new::<T>())?;
279 // SAFETY: `block` is freshly allocated for `Layout::new::<T>()`, so it
280 // is sized and aligned for `T` (cast layout contract), and it is
281 // uninitialized `T` storage that is written with a valid `T`
282 // immediately below — before any path can read or drop it as a `T`
283 // (cast initialization/drop contract).
284 let casted = unsafe { block.cast::<T>() };
285 // SAFETY: `block` was just allocated by `self.alloc` for
286 // `Layout::new::<T>()`, so `casted.as_ptr()` is non-null, sized and
287 // aligned for `T` and points to uninitialized owned storage;
288 // `write` initializes it by moving `val` in without dropping the
289 // (uninitialized) previous contents.
290 unsafe {
291 casted.as_ptr().write(val);
292 }
293 Some(casted)
294 }
295
296 /// Reallocates a memory block from this heap.
297 ///
298 /// The source block is returned inside [`ReallocError`] when the requested
299 /// layout is invalid or replacement allocation fails, so failure does not
300 /// leak or invalidate the original allocation. A `new_size` of zero frees
301 /// the source block and returns `Ok(None)`. `layout` must describe the
302 /// source allocation; the boundary rejects sizes beyond the allocator's
303 /// usable capacity and source-pointer alignment mismatches.
304 ///
305 /// # Errors
306 ///
307 /// Returns [`ReallocError`] with the original source block when the new
308 /// layout is invalid or replacement allocation fails.
309 #[inline(always)]
310 pub fn realloc<T: ?Sized, Permit>(
311 &self,
312 permit: &mut Permit,
313 block: BrandedBlock<'brand, T>,
314 layout: Layout,
315 new_size: usize,
316 ) -> Result<Option<BrandedBlock<'brand, u8>>, ReallocError<'brand, T>>
317 where
318 for<'token> &'token mut Permit: WritePermit<'brand>,
319 {
320 let ptr = block.ptr.as_ptr() as *mut u8;
321 if new_size == 0 {
322 self.free(permit, block);
323 return Ok(None);
324 }
325
326 let new_layout = match Layout::from_size_align(new_size, layout.align()) {
327 Ok(new_layout) => new_layout,
328 Err(_) => {
329 return Err(ReallocError::new(
330 block,
331 ReallocFailure::InvalidLayout {
332 new_size,
333 alignment: layout.align(),
334 },
335 ));
336 }
337 };
338
339 // ZSTs have no allocator-owned source mapping. The source value stays
340 // recoverable if the replacement allocation fails, just like a normal
341 // block below.
342 // SAFETY: `block` is a live branded block and the matching permit
343 // proves exclusive access, so reading its value layout is valid.
344 let is_zst = unsafe { core::mem::size_of_val(&*block.ptr.as_ptr()) == 0 };
345 if is_zst {
346 if layout.size() != 0 {
347 return Err(ReallocError::new(
348 block,
349 ReallocFailure::InvalidSourceLayout {
350 requested_size: layout.size(),
351 alignment: layout.align(),
352 usable_size: 0,
353 },
354 ));
355 }
356 return self
357 .alloc(permit, new_layout)
358 .map(Some)
359 .ok_or_else(|| ReallocError::new(block, ReallocFailure::AllocationFailed));
360 }
361
362 // SAFETY: non-ZST `block` is a live allocation returned by this heap;
363 // the brand preserves that ownership and `usable_size` only reads its
364 // originating segment metadata.
365 let usable_size = unsafe { mnemosyne_local::usable_size(ptr) };
366 if layout.size() == 0
367 || layout.size() > usable_size
368 || ptr.align_offset(layout.align()) != 0
369 {
370 return Err(ReallocError::new(
371 block,
372 ReallocFailure::InvalidSourceLayout {
373 requested_size: layout.size(),
374 alignment: layout.align(),
375 usable_size,
376 },
377 ));
378 }
379
380 let marker = block._marker;
381 // SAFETY: the ZST/zero-size cases returned above, so `ptr` is a non-ZST
382 // block previously allocated by `self.raw` and not yet freed, and
383 // `layout` is its current layout; `&mut permit` proves exclusive brand
384 // access. This satisfies `realloc_owned_unchecked`'s contract, which
385 // either grows/shrinks in place or moves the bytes and frees the old
386 // allocation, returning the (possibly relocated) block.
387 let new_ptr = unsafe { self.raw.realloc_owned_unchecked(ptr, layout, new_layout) };
388 match NonNull::new(new_ptr) {
389 Some(ptr) => Ok(Some(BrandedBlock {
390 ptr,
391 _marker: marker,
392 })),
393 None => Err(ReallocError::new(block, ReallocFailure::AllocationFailed)),
394 }
395 }
396}