mnemosyne_arena/scratch/pool/borrow.rs
1//! Borrow entry points for [`super::ScratchPool`]: the depth-tracked
2//! `borrow_slot` dispatch, the safe `with_scratch`/`with_scratch_bounded`
3//! wrappers, and the unsafe `with_scratch_uninit` raw-pointer variant.
4//!
5//! All three forms share the RAII `BorrowGuard` defined at module level
6//! (SSOT) to restore `borrow_depth` on unwind. The divergence point is the
7//! `PROVISION` const-param that controls whether the high-water mark is
8//! updated.
9
10use super::super::aligned_vec::AlignedVec;
11use super::super::element::ScratchElement;
12use super::{MAX_POOL_SLOTS, ScratchPool};
13use core::cell::Cell;
14
15/// RAII guard that restores `borrow_depth` to its pre-borrow value on drop.
16///
17/// Using a named type rather than a closure (a) documents the invariant at the
18/// definition site, (b) is the SSOT for the restore logic shared by
19/// `borrow_slot` and `with_scratch_uninit`, and (c) unwind-safe: `drop` runs
20/// whether the closure returns normally or panics, matching the expected
21/// "depth is always restored" guarantee.
22struct BorrowGuard<'a> {
23 depth: &'a Cell<u8>,
24 original: u8,
25}
26
27impl Drop for BorrowGuard<'_> {
28 #[inline(always)]
29 fn drop(&mut self) {
30 self.depth.set(self.original);
31 }
32}
33
34impl<T: ScratchElement> ScratchPool<T> {
35 /// Provides a mutable aligned scratch slice of **exactly** `n` elements
36 /// to the closure. Borrow depth is released when the closure returns.
37 ///
38 /// If a pool slot is available, the closure receives a direct `&mut [T]`
39 /// into the pooled buffer (zero-copy). If all slots are exhausted (nested
40 /// recursive calls), a temporary buffer is allocated instead.
41 #[inline]
42 pub fn with_scratch<R>(&self, n: usize, f: impl FnOnce(&mut [T]) -> R) -> R {
43 self.borrow_slot::<false, R>(n, f)
44 }
45
46 /// Like [`with_scratch`](Self::with_scratch), but records the request for
47 /// [`release`](Self::release).
48 ///
49 /// Each depth's largest-ever request becomes that slot's provision; a
50 /// later [`release`] may reclaim everything a slot holds above it. The
51 /// two forms share the slot storage, so a pool can be driven through
52 /// either (or both) — only the provisions differ.
53 ///
54 /// # Panics
55 ///
56 /// Panics if `f` panics and leaves `self.borrow_depth` at `u8::MAX`, where
57 /// the depth increment would wrap; [`with_scratch`] has the same bound via
58 /// slot exhaustion, so this is not a new failure mode.
59 ///
60 /// [`release`]: Self::release
61 /// [`with_scratch`]: Self::with_scratch
62 #[inline]
63 pub fn with_scratch_bounded<R>(&self, n: usize, f: impl FnOnce(&mut [T]) -> R) -> R {
64 self.borrow_slot::<true, R>(n, f)
65 }
66
67 /// Shared implementation for [`with_scratch`] and [`with_scratch_bounded`].
68 ///
69 /// `PROVISION` is a compile-time constant: when `false` the provision
70 /// tracking branch is eliminated by the optimizer and the two public forms
71 /// have identical hot-path machine code, differing only in the
72 /// cold-provision-update path.
73 #[inline]
74 pub(super) fn borrow_slot<const PROVISION: bool, R>(
75 &self,
76 n: usize,
77 f: impl FnOnce(&mut [T]) -> R,
78 ) -> R {
79 let depth = self.borrow_depth.get();
80 if depth < MAX_POOL_SLOTS as u8 {
81 self.borrow_depth.set(depth + 1);
82 let _guard = BorrowGuard {
83 depth: &self.borrow_depth,
84 original: depth,
85 };
86 let idx = depth as usize;
87 if PROVISION {
88 // Record this depth's high-water request so `release` can
89 // distinguish the requested size from growth-policy headroom.
90 let provision = &self.provisions[idx];
91 provision.set(provision.get().max(n));
92 }
93 // SAFETY: exclusive access guaranteed by borrow_depth tracking.
94 // Each nesting level gets its own slot index.
95 let vec = unsafe { &mut *self.slots[idx].get() };
96 if n > vec.len() {
97 // Both paths use geometric growth so scratch reuse stays
98 // amortized (MN-SCRATCH-GROWTH-COST-2026-09-04: an
99 // exact-growth variant for the bounded path was rejected —
100 // it drops amortized doubling). The bounded path's
101 // provision is enforced by quiescent `release`
102 // (`shrink_to(provision)`), not by narrowing growth here.
103 vec.ensure_len(n);
104 // Republish this slot's capacity to its mirror. Reading it
105 // back through the live exclusive `vec` is the reborrow the
106 // accessors themselves must not perform, so every slot keeps a
107 // figure readable from outside the `UnsafeCell`.
108 self.slot_capacities[idx].set(vec.capacity());
109 }
110 debug_assert!(
111 self.slot_capacities[idx].get() == vec.capacity(),
112 "slot capacity mirror drifted from the slot's actual capacity"
113 );
114 debug_assert_eq!(
115 vec.as_mut_ptr() as usize % T::ALIGN_BYTES,
116 0,
117 "Scratch buffer not aligned to {} bytes",
118 T::ALIGN_BYTES
119 );
120 // Return exactly `n` elements (not the full buffer).
121 let slice = &mut vec.as_mut_slice()[..n];
122 f(slice)
123 } else {
124 // All slots exhausted; allocate owned fallback.
125 let mut owned = AlignedVec::with_capacity(n);
126 owned.ensure_len(n);
127 f(owned.as_mut_slice())
128 }
129 }
130
131 /// Like [`with_scratch`][Self::with_scratch] but provides uninitialized
132 /// memory via a raw pointer. The caller must initialize all elements.
133 ///
134 /// # Safety
135 ///
136 /// Every element of the returned slice must be initialized before any
137 /// safe read on the same allocation.
138 pub unsafe fn with_scratch_uninit<R>(&self, n: usize, f: impl FnOnce(*mut [T]) -> R) -> R {
139 let depth = self.borrow_depth.get();
140 if depth < MAX_POOL_SLOTS as u8 {
141 self.borrow_depth.set(depth + 1);
142 let _guard = BorrowGuard {
143 depth: &self.borrow_depth,
144 original: depth,
145 };
146 // SAFETY: borrow_depth tracking ensures exclusive access to this slot.
147 let vec = unsafe { &mut *self.slots[depth as usize].get() };
148 if vec.capacity() < n {
149 vec.ensure_len(n);
150 self.slot_capacities[depth as usize].set(vec.capacity());
151 }
152 let raw = core::ptr::slice_from_raw_parts_mut(vec.as_mut_ptr(), n);
153 // The length is published only after `f` returns normally. When the
154 // slot already has spare capacity no `ensure_len` runs, so
155 // `[len, n)` is uninitialized while `f` executes; publishing `n`
156 // first would leave that length behind on an unwind, and the next
157 // safe `with_scratch(n, ..)` — seeing `n <= len` — would skip
158 // `ensure_len` and hand out a slice over uninitialized elements.
159 let result = f(raw);
160 // SAFETY: `f` returned normally, discharging the caller's contract
161 // to initialize `[0, n)`; capacity >= n was established above.
162 unsafe { vec.set_len_unchecked(n) };
163 result
164 } else {
165 let mut owned = AlignedVec::with_capacity(n);
166 // SAFETY: caller initializes before safe reads.
167 unsafe { owned.set_len_unchecked(n) };
168 let raw = core::ptr::slice_from_raw_parts_mut(owned.as_mut_ptr(), n);
169 f(raw)
170 }
171 }
172}