Skip to main content

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}