1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
//! Grow-only scratch-buffer pool for allocation-free Chernoff hot loops (Wave 1, ADR-0041).
//!
//! ## Design
//!
//! [`ScratchPool<F>`] owns a free-list of `Vec<F>` buffers. When [`ScratchPool::borrow_vec`]
//! is called it takes the smallest free buffer with sufficient capacity (best fit,
//! ADR-0215; or allocates a fresh one),
//! fills it to `len` with `F::zero()`, and returns a RAII handle [`ScratchVec<'_, F>`].
//!
//! On [`Drop`], `ScratchVec` calls `buf.clear()` (sets `len = 0`, keeps `capacity`) then
//! pushes the buffer back onto the free-list. This makes re-use zero-cost for the common
//! steady-state: every `apply_into` call borrows, fills, and returns the same buffer.
//!
//! ## Aliasing safety
//!
//! `borrow_vec` requires `&'a mut ScratchPool<F>`, so the borrow checker enforces
//! single-borrow exclusivity. No `RefCell`, no `unsafe`.
//!
//! ## `no_std` + `alloc`
//!
//! Uses only `alloc::vec::Vec` — no `std` imports.
use alloc::vec::Vec;
use crate::float::SemiflowFloat;
// ---------------------------------------------------------------------------
// ScratchPool
// ---------------------------------------------------------------------------
/// Grow-only pool of reusable `Vec<F>` scratch buffers.
///
/// Call [`Self::borrow_vec`] to obtain a `ScratchVec<'_, F>` whose lifetime is
/// tied to the pool borrow, statically preventing double-borrow.
///
/// # Thread safety
///
/// `ScratchPool<F>` is `Send` (can move to another thread) but not `Sync`
/// (exclusively owned; single-threaded use only). The `&mut self` requirement on
/// `borrow_vec` enforces this at compile time.
///
/// # Example
///
/// ```rust
/// use semiflow::scratch::ScratchPool;
/// let mut pool: ScratchPool<f64> = ScratchPool::new();
/// {
/// let mut v = pool.borrow_vec(8);
/// v.iter_mut().enumerate().for_each(|(i, x)| *x = i as f64);
/// assert_eq!(v[3], 3.0);
/// } // ScratchVec dropped here → buffer returned to pool
/// // pool can lend it out again
/// let v2 = pool.borrow_vec(4);
/// assert_eq!(v2.len(), 4);
/// ```
pub struct ScratchPool<F: SemiflowFloat> {
free: Vec<Vec<F>>,
/// Largest capacity ever borrowed from this pool.
pub high_water_capacity: usize,
}
impl<F: SemiflowFloat> Default for ScratchPool<F> {
fn default() -> Self {
Self::new()
}
}
impl<F: SemiflowFloat> ScratchPool<F> {
/// Create an empty pool with no pre-allocated buffers.
#[must_use]
pub fn new() -> Self {
Self {
free: Vec::new(),
high_water_capacity: 0,
}
}
/// Create a pool pre-populated with `count` buffers, each with `cap` capacity.
///
/// Suitable as the initial scratch for `ChernoffSemigroup::evolve` when the
/// node count is known ahead of time.
#[must_use]
pub fn with_capacity(count: usize, cap: usize) -> Self {
let mut pool = Self::new();
for _ in 0..count {
pool.free.push(Vec::with_capacity(cap));
}
if cap > 0 {
pool.high_water_capacity = cap;
}
pool
}
/// Borrow a `Vec<F>` of length `len`, filled with `F::zero()`.
///
/// Selects the smallest free buffer with sufficient capacity (best fit,
/// ADR-0215); allocates a new one if none fit. The returned [`ScratchVec`] is tied to this pool's
/// lifetime — the borrow checker prevents two live handles on the same pool.
///
/// # Panics
///
/// Does not panic. All allocations use the global allocator.
pub fn borrow_vec(&mut self, len: usize) -> ScratchVec<'_, F> {
if len > self.high_water_capacity {
self.high_water_capacity = len;
}
// Smallest buffer with enough capacity (best fit, ADR-0215).
let pos = self.best_fit(len);
let mut buf = match pos {
Some(idx) => self.free.swap_remove(idx),
None => Vec::with_capacity(len),
};
// Resize to exactly `len`, zero-filling new slots (or truncating).
buf.resize(len, F::zero());
ScratchVec { buf, pool: self }
}
/// Take ownership of a scratch `Vec<F>` of length `len`, filled with `F::zero()`.
///
/// Unlike [`Self::borrow_vec`], the returned `Vec` is fully owned by the caller.
/// The caller MUST return it via [`Self::return_vec`] to reclaim capacity.
/// Use this when multiple buffers must be live simultaneously (e.g. the
/// K=4 g-grid chain in `TruncatedExp` `apply_into`). Selection is best fit
/// (the smallest free buffer with sufficient capacity, ADR-0215).
///
/// # Panics
///
/// Does not panic. All allocations use the global allocator.
pub fn take_vec(&mut self, len: usize) -> Vec<F> {
if len > self.high_water_capacity {
self.high_water_capacity = len;
}
let pos = self.best_fit(len);
let mut buf = match pos {
Some(idx) => self.free.swap_remove(idx),
None => Vec::with_capacity(len),
};
buf.resize(len, F::zero());
buf
}
/// Index of the SMALLEST free buffer with capacity `>= len` (best fit), so a
/// large buffer (a step plan, a 2-D state) is not handed to a small request
/// while a later large request has to allocate: with a fixed request sequence
/// the steady state allocates nothing (ADR-0215). Contents never depend on the
/// choice (every taken buffer is zero-filled).
fn best_fit(&self, len: usize) -> Option<usize> {
self.free
.iter()
.enumerate()
.filter(|(_, b)| b.capacity() >= len)
.min_by_key(|(_, b)| b.capacity())
.map(|(i, _)| i)
}
/// Return a previously taken `Vec` to the pool (capacity preserved).
///
/// Pair with [`Self::take_vec`]. After this call the `Vec` must not be used.
/// Calling this with a buffer that was never taken is harmless (it just adds
/// a free buffer to the pool).
pub fn return_vec(&mut self, mut buf: Vec<F>) {
buf.clear();
self.free.push(buf);
}
/// Number of buffers currently in the free list.
///
/// Useful for tests that verify pool drain/refill semantics.
/// Not part of the stable v1.0.0 public API; subject to change.
#[doc(hidden)]
#[must_use]
pub fn free_len(&self) -> usize {
self.free.len()
}
// -----------------------------------------------------------------------
// Graph-signal arena (R4 mitigation — Wave 2.1B)
// -----------------------------------------------------------------------
/// Take ownership of an `N`-length `Vec<F>` for use as a graph-signal
/// scratch buffer (capacity `n`).
///
/// Separate from the regular `take_vec` / `return_vec` pool so that the
/// caller can distinguish "scalar scratch" from "graph-signal scratch"
/// (useful for zero-alloc tests). Returns the first free buffer with
/// sufficient capacity, or allocates a fresh one.
///
/// Pair with [`Self::return_graph_buf`] to reclaim capacity.
pub fn take_graph_buf(&mut self, n: usize) -> Vec<F> {
if n > self.high_water_capacity {
self.high_water_capacity = n;
}
// Reuse a free buf that already has enough capacity.
let pos = self.free.iter().position(|b| b.capacity() >= n);
let mut buf = match pos {
Some(idx) => self.free.swap_remove(idx),
None => Vec::with_capacity(n),
};
buf.resize(n, F::zero());
buf
}
/// Return a graph-signal scratch buffer to the pool (capacity preserved).
///
/// Pair with [`Self::take_graph_buf`].
pub fn return_graph_buf(&mut self, mut buf: Vec<F>) {
buf.clear();
self.free.push(buf);
}
}
// ---------------------------------------------------------------------------
// ScratchVec
// ---------------------------------------------------------------------------
/// RAII handle for a buffer borrowed from a [`ScratchPool`].
///
/// Derefs to `&[F]` / `&mut [F]`. On drop, the buffer is returned to the
/// pool (capacity preserved, length zeroed).
pub struct ScratchVec<'a, F: SemiflowFloat> {
buf: Vec<F>,
pool: &'a mut ScratchPool<F>,
}
impl<F: SemiflowFloat> core::ops::Deref for ScratchVec<'_, F> {
type Target = [F];
#[inline]
fn deref(&self) -> &[F] {
&self.buf
}
}
impl<F: SemiflowFloat> core::ops::DerefMut for ScratchVec<'_, F> {
#[inline]
fn deref_mut(&mut self) -> &mut [F] {
&mut self.buf
}
}
impl<F: SemiflowFloat> Drop for ScratchVec<'_, F> {
fn drop(&mut self) {
// Swap out the buffer so we can push it back without a clone.
let mut buf = Vec::new();
core::mem::swap(&mut buf, &mut self.buf);
buf.clear(); // len → 0; capacity preserved
self.pool.free.push(buf);
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn borrow_return_cycle() {
let mut pool: ScratchPool<f64> = ScratchPool::new();
{
let v = pool.borrow_vec(8);
assert_eq!(v.len(), 8);
assert!(v.iter().all(|&x| x == 0.0));
}
// Buffer should be back in the pool now.
assert_eq!(pool.free.len(), 1);
assert!(pool.free[0].capacity() >= 8);
}
#[test]
fn reuse_preserves_capacity() {
let mut pool: ScratchPool<f64> = ScratchPool::with_capacity(1, 64);
let cap_before = pool.free[0].capacity();
{
let mut v = pool.borrow_vec(64);
v[0] = 42.0;
}
// Reuse same buffer.
let v2 = pool.borrow_vec(32);
assert_eq!(v2.len(), 32);
// All zero after borrow (resize fills with zero).
assert!(v2.iter().all(|&x| x == 0.0));
drop(v2);
assert!(pool.free[0].capacity() >= cap_before);
}
#[test]
fn with_capacity_preallocates() {
let pool: ScratchPool<f64> = ScratchPool::with_capacity(3, 128);
assert_eq!(pool.free.len(), 3);
assert!(pool.free.iter().all(|b| b.capacity() >= 128));
}
#[test]
fn high_water_mark_tracks() {
let mut pool: ScratchPool<f64> = ScratchPool::new();
{
let _v = pool.borrow_vec(16);
}
assert_eq!(pool.high_water_capacity, 16);
{
let _v = pool.borrow_vec(32);
}
assert_eq!(pool.high_water_capacity, 32);
{
let _v = pool.borrow_vec(8); // smaller — no change
}
assert_eq!(pool.high_water_capacity, 32);
}
}