Skip to main content

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}