mnemosyne_heap/branded_vec.rs
1use crate::Heap;
2use crate::brand::BrandedBlock;
3use crate::branded_box::BrandedBox;
4use core::alloc::Layout;
5use core::marker::PhantomData;
6use core::ptr::NonNull;
7use melinoe::{ReadPermit, WritePermit};
8use mnemosyne_core::AllocPolicy;
9use mnemosyne_local::LocalAllocatorSelector;
10use mnemosyne_local::internal::HasSegmentPool;
11
12/// Growth, indexing and element access for [`BrandedVec`].
13pub mod ops;
14/// Standard-trait impls (iteration, comparison, formatting) for
15/// [`BrandedVec`].
16pub mod traits;
17
18/// A dynamically growing array allocated from a `Heap`.
19///
20/// Automatically handles growth and reallocation, dropping all elements on drop.
21pub struct BrandedVec<
22 'brand,
23 'heap,
24 T,
25 P: AllocPolicy = mnemosyne_core::StandardPolicy,
26 B: HasSegmentPool + LocalAllocatorSelector<B> = mnemosyne_backend::MemoryBackendWrapper,
27> {
28 pub(crate) ptr: NonNull<T>,
29 pub(crate) cap: usize,
30 pub(crate) len: usize,
31 pub(crate) heap: &'heap Heap<'brand, P, B>,
32 pub(crate) _non_send_sync: core::marker::PhantomData<*mut ()>,
33}
34
35impl<'brand, 'heap, T, P: AllocPolicy, B: HasSegmentPool + LocalAllocatorSelector<B>>
36 BrandedVec<'brand, 'heap, T, P, B>
37{
38 /// Creates a new empty `BrandedVec` backed by the given `Heap`.
39 #[inline(always)]
40 pub fn new(heap: &'heap Heap<'brand, P, B>) -> Self {
41 Self {
42 ptr: NonNull::dangling(),
43 cap: if core::mem::size_of::<T>() == 0 {
44 usize::MAX
45 } else {
46 0
47 },
48 len: 0,
49 heap,
50 _non_send_sync: core::marker::PhantomData,
51 }
52 }
53
54 /// Creates a new `BrandedVec` with space for at least `capacity` elements.
55 #[inline]
56 pub fn with_capacity<Permit>(
57 heap: &'heap Heap<'brand, P, B>,
58 permit: Permit,
59 capacity: usize,
60 ) -> Option<Self>
61 where
62 Permit: ReadPermit<'brand>,
63 {
64 if capacity == 0 || core::mem::size_of::<T>() == 0 {
65 return Some(Self::new(heap));
66 }
67 let layout = Layout::array::<T>(capacity).ok()?;
68 let block = heap.alloc(permit, layout)?;
69 Some(Self {
70 ptr: block.ptr.cast(),
71 cap: capacity,
72 len: 0,
73 heap,
74 _non_send_sync: core::marker::PhantomData,
75 })
76 }
77
78 /// Converts this vector into a boxed slice, shrinking the memory allocation to fit.
79 #[inline]
80 pub fn into_boxed_slice<Permit>(
81 mut self,
82 token: &mut Permit,
83 ) -> BrandedBox<'brand, 'heap, [T], P, B>
84 where
85 for<'token> &'token mut Permit: WritePermit<'brand>,
86 {
87 if core::mem::size_of::<T>() == 0 {
88 // SAFETY: `T` is zero-sized, so a `[T]` of any length occupies no
89 // bytes; `NonNull::dangling()` is a valid, aligned base for a
90 // zero-sized slice of `self.len` ZST elements. `from_raw_parts_mut`
91 // requires the pointer be non-null and aligned (dangling satisfies
92 // both for a ZST) and the resulting fat pointer is never read/written
93 // for storage. The base pointer is non-null, so `new_unchecked` is
94 // sound. Ownership of the `len` logical elements transfers to the
95 // returned `BrandedBox` (`self` is forgotten below), so no element is
96 // dropped twice.
97 let slice_ptr = unsafe {
98 let raw_slice =
99 core::slice::from_raw_parts_mut(NonNull::<T>::dangling().as_ptr(), self.len);
100 NonNull::new_unchecked(raw_slice)
101 };
102 let heap = self.heap;
103 core::mem::forget(self);
104 return BrandedBox {
105 ptr: slice_ptr,
106 heap,
107 _non_send_sync: core::marker::PhantomData,
108 };
109 }
110
111 // Best-effort shrink to fit via the shared SSOT helper; a failed realloc
112 // leaves the (larger) block in place and the boxed slice still owns it
113 // correctly, so the `Err` is intentionally ignored here.
114 let _ = self.shrink_to_len(token);
115
116 // SAFETY: for non-ZST `T`, `self.ptr` addresses a live allocation of at
117 // least `self.len` initialized `T` (after the shrink above, `self.cap`
118 // is either unchanged or equal to `self.len`, and `[0, self.len)` is
119 // always the initialized prefix). `slice_from_raw_parts_mut` builds a fat
120 // pointer over exactly those `self.len` elements; `self.ptr` is non-null
121 // (`NonNull`), so `new_unchecked` is sound. Ownership of the elements and
122 // the backing block transfers to the returned `BrandedBox` (`self` is
123 // forgotten below), so the block is freed exactly once.
124 let slice_ptr = unsafe {
125 let raw_slice = core::ptr::slice_from_raw_parts_mut(self.ptr.as_ptr(), self.len);
126 NonNull::new_unchecked(raw_slice)
127 };
128
129 let heap = self.heap;
130 core::mem::forget(self);
131
132 BrandedBox {
133 ptr: slice_ptr,
134 heap,
135 _non_send_sync: core::marker::PhantomData,
136 }
137 }
138
139 /// Converts a `BrandedBox<'brand, 'heap, [T], P, B>` back into a `BrandedVec<'brand, 'heap, T, P, B>`.
140 ///
141 /// This does not allocate or copy.
142 #[inline]
143 pub fn from_boxed_slice(boxed_slice: BrandedBox<'brand, 'heap, [T], P, B>) -> Self {
144 let len = boxed_slice.len();
145 let heap = boxed_slice.heap;
146 let block = boxed_slice.into_raw();
147 Self {
148 // SAFETY: `block.ptr` originates from a `BrandedBox<[T]>`'s
149 // `NonNull<[T]>` and is therefore non-null; reinterpreting the slice
150 // base address as the element pointer `*mut T` preserves
151 // non-nullness (and, for non-ZST `T`, the original allocation's
152 // alignment for `T`), so `new_unchecked` is sound. Ownership of the
153 // block transfers from the consumed box to the new vector with no
154 // copy.
155 ptr: unsafe { NonNull::new_unchecked(block.ptr.as_ptr() as *mut T) },
156 cap: if core::mem::size_of::<T>() == 0 {
157 usize::MAX
158 } else {
159 len
160 },
161 len,
162 heap,
163 _non_send_sync: core::marker::PhantomData,
164 }
165 }
166
167 /// Grows the backing allocation to exactly `new_cap` elements and updates
168 /// `ptr`/`cap` on success. This is the single authoritative grow path shared
169 /// by [`push`](BrandedVec::push) and [`reserve`](BrandedVec::reserve), so the
170 /// alloc-when-empty / realloc-otherwise mechanics cannot drift between them;
171 /// each caller keeps only its own capacity *policy* (push's initial-4
172 /// doubling vs reserve's `max(cap*2, needed)`).
173 ///
174 /// Callers guarantee `T` is non-ZST and `new_cap > self.cap`. On layout
175 /// overflow or allocation failure the vector is left unchanged and `Err(())`
176 /// is returned.
177 #[inline]
178 fn grow_to<Permit>(&mut self, token: &mut Permit, new_cap: usize) -> Result<(), ()>
179 where
180 for<'token> &'token mut Permit: WritePermit<'brand>,
181 {
182 let new_layout = Layout::array::<T>(new_cap).map_err(|_| ())?;
183 if self.cap == 0 {
184 let block = self.heap.alloc(&mut *token, new_layout).ok_or(())?;
185 self.ptr = block.ptr.cast();
186 } else {
187 // `self.cap` was validated when the current allocation was made,
188 // but retain the fallible boundary so a corrupted or future-mutated
189 // capacity cannot turn an allocation failure into undefined behavior.
190 let old_layout = Layout::array::<T>(self.cap).map_err(|_| ())?;
191 let block = BrandedBlock {
192 ptr: self.ptr,
193 _marker: PhantomData,
194 };
195 match self
196 .heap
197 .realloc(token, block, old_layout, new_layout.size())
198 {
199 Ok(Some(new_block)) => self.ptr = new_block.ptr.cast(),
200 Ok(None) => {
201 debug_assert!(false, "vector growth must not request a zero-sized realloc");
202 return Err(());
203 }
204 Err(error) => {
205 self.ptr = error.into_block().ptr.cast();
206 return Err(());
207 }
208 }
209 }
210 self.cap = new_cap;
211 Ok(())
212 }
213
214 /// Reserves capacity for at least `additional` more elements to be inserted in the vector.
215 ///
216 /// # Errors
217 /// Returns `Err(())` if layout calculations overflow or allocation fails.
218 #[inline]
219 #[expect(clippy::result_unit_err)]
220 pub fn reserve<Permit>(&mut self, token: &mut Permit, additional: usize) -> Result<(), ()>
221 where
222 for<'token> &'token mut Permit: WritePermit<'brand>,
223 {
224 if core::mem::size_of::<T>() == 0 {
225 return Ok(());
226 }
227 let needed = match self.len.checked_add(additional) {
228 Some(n) => n,
229 None => return Err(()),
230 };
231 if needed <= self.cap {
232 return Ok(());
233 }
234 let new_cap = core::cmp::max(self.cap.checked_mul(2).unwrap_or(needed), needed);
235 self.grow_to(token, new_cap)
236 }
237
238 /// Shrinks the capacity of the vector as much as possible.
239 ///
240 /// # Errors
241 /// Returns `Err(())` if allocation fails.
242 #[inline]
243 #[expect(clippy::result_unit_err)]
244 pub fn shrink_to_fit<Permit>(&mut self, token: &mut Permit) -> Result<(), ()>
245 where
246 for<'token> &'token mut Permit: WritePermit<'brand>,
247 {
248 if core::mem::size_of::<T>() == 0 {
249 return Ok(());
250 }
251 self.shrink_to_len(token)
252 }
253
254 /// Shrinks the backing allocation so its capacity equals `self.len` — the
255 /// single authoritative shrink path shared by
256 /// [`shrink_to_fit`](BrandedVec::shrink_to_fit) and
257 /// [`into_boxed_slice`](BrandedVec::into_boxed_slice), so the
258 /// free-when-empty / realloc-to-len mechanics cannot drift between them.
259 ///
260 /// Callers guarantee `T` is non-ZST. A no-op when `cap <= len`; frees the
261 /// block when `len == 0`; otherwise reallocates down to `len` elements.
262 /// Returns `Err(())` only if the shrinking realloc fails, leaving the vector
263 /// valid and unchanged (the over-sized block is retained).
264 #[inline]
265 fn shrink_to_len<Permit>(&mut self, token: &mut Permit) -> Result<(), ()>
266 where
267 for<'token> &'token mut Permit: WritePermit<'brand>,
268 {
269 if self.cap <= self.len {
270 return Ok(());
271 }
272 if self.len == 0 {
273 // SAFETY: reached only with non-ZST `T` and `self.cap > self.len == 0`,
274 // so `self.cap > 0` and `self.ptr` is a live block from `self.heap`
275 // (not the dangling sentinel). No element is initialized, so freeing
276 // drops nothing; `self.ptr`/`self.cap` reset to the dangling sentinel
277 // right after, so the freed block is never reused.
278 unsafe {
279 self.heap.free_raw(self.ptr.as_ptr() as *mut u8);
280 }
281 self.ptr = NonNull::dangling();
282 self.cap = 0;
283 return Ok(());
284 }
285 let old_layout = Layout::array::<T>(self.cap).map_err(|_| ())?;
286 let block = BrandedBlock {
287 ptr: self.ptr,
288 _marker: PhantomData,
289 };
290 let new_size = core::mem::size_of::<T>() * self.len;
291 match self.heap.realloc(token, block, old_layout, new_size) {
292 Ok(Some(new_block)) => {
293 self.ptr = new_block.ptr.cast();
294 self.cap = self.len;
295 Ok(())
296 }
297 Ok(None) => {
298 debug_assert!(false, "nonzero shrink must not free its source block");
299 Err(())
300 }
301 Err(error) => {
302 self.ptr = error.into_block().ptr.cast();
303 Err(())
304 }
305 }
306 }
307}