solverforge_solver/heuristic/move/arena.rs
1/* Arena allocator for moves.
2
3Provides O(1) step cleanup by reusing a pre-allocated buffer.
4Instead of allocating a new Vec each step, the arena is reset
5and reused.
6*/
7
8use std::fmt::Debug;
9use std::mem::MaybeUninit;
10use std::ptr;
11
12use rand::seq::SliceRandom;
13use rand::Rng;
14
15/// Arena allocator for moves with O(1) reset.
16///
17/// Instead of allocating a new Vec<M> each step and letting it drop,
18/// the arena maintains a reusable buffer. Calling `reset()` simply
19/// sets the length to 0 without running destructors (moves are Copy-like
20/// in practice since they contain only primitives and small inline data).
21///
22/// # Performance
23///
24/// | Operation | Vec per step | MoveArena |
25/// |-----------|--------------|-----------|
26/// | Alloc | O(n) heap | O(1) bump |
27/// | Cleanup | O(n) drop | O(1) reset|
28/// | Memory | n * size_of | Reused |
29///
30/// # Example
31///
32/// ```
33/// use solverforge_solver::heuristic::r#move::MoveArena;
34///
35/// let mut arena: MoveArena<i32> = MoveArena::new();
36///
37/// // Step 1: generate and evaluate moves
38/// arena.push(1);
39/// arena.push(2);
40/// arena.push(3);
41/// assert_eq!(arena.len(), 3);
42///
43/// // Step 2: reset and reuse (O(1)!)
44/// arena.reset();
45/// assert!(arena.is_empty());
46///
47/// arena.push(10);
48/// arena.push(20);
49/// for mov in arena.iter() {
50/// assert!(*mov >= 10);
51/// }
52/// ```
53pub struct MoveArena<M> {
54 // Storage for moves. We use MaybeUninit to avoid requiring Default.
55 storage: Vec<MaybeUninit<M>>,
56 // Number of valid moves currently in the arena.
57 len: usize,
58 // Index of the taken move (if any). Only one move can be taken per step.
59 taken: Option<usize>,
60}
61
62impl<M> MoveArena<M> {
63 #[inline]
64 pub fn new() -> Self {
65 Self {
66 storage: Vec::new(),
67 len: 0,
68 taken: None,
69 }
70 }
71
72 #[inline]
73 pub fn with_capacity(capacity: usize) -> Self {
74 Self {
75 storage: Vec::with_capacity(capacity),
76 len: 0,
77 taken: None,
78 }
79 }
80
81 /* Resets the arena, making it empty.
82
83 Drops all moves except the one that was taken (if any).
84 */
85 #[inline]
86 pub fn reset(&mut self) {
87 // Drop existing moves except the taken one (already moved out)
88 for i in 0..self.len {
89 if self.taken != Some(i) {
90 unsafe {
91 ptr::drop_in_place(self.storage[i].as_mut_ptr());
92 }
93 }
94 }
95 self.len = 0;
96 self.taken = None;
97 }
98
99 #[inline]
100 pub fn push(&mut self, m: M) {
101 if self.len < self.storage.len() {
102 // Reuse existing slot
103 self.storage[self.len] = MaybeUninit::new(m);
104 } else {
105 // Need to grow
106 self.storage.push(MaybeUninit::new(m));
107 }
108 self.len += 1;
109 }
110
111 #[inline]
112 pub fn len(&self) -> usize {
113 self.len
114 }
115
116 // Returns true if the arena is empty.
117 #[inline]
118 pub fn is_empty(&self) -> bool {
119 self.len == 0
120 }
121
122 // Returns an iterator over the moves.
123 #[inline]
124 pub fn iter(&self) -> impl Iterator<Item = &M> {
125 self.storage[..self.len]
126 .iter()
127 .map(|slot| unsafe { slot.assume_init_ref() })
128 }
129
130 // Returns a mutable iterator over the moves.
131 #[inline]
132 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut M> {
133 self.storage[..self.len]
134 .iter_mut()
135 .map(|slot| unsafe { slot.assume_init_mut() })
136 }
137
138 #[inline]
139 pub fn get(&self, index: usize) -> Option<&M> {
140 if index < self.len {
141 Some(unsafe { self.storage[index].assume_init_ref() })
142 } else {
143 None
144 }
145 }
146
147 /* Takes ownership of a move by index.
148
149 Only one move can be taken per step. Call `reset()` before taking another.
150
151 # Panics
152
153 Panics if `index >= len` or if a move was already taken.
154
155 # Example
156
157 ```
158 use solverforge_solver::heuristic::r#move::MoveArena;
159
160 let mut arena: MoveArena<String> = MoveArena::new();
161 arena.push("first".to_string());
162 arena.push("second".to_string());
163
164 // Take ownership of the move at index 1
165 let taken = arena.take(1);
166 assert_eq!(taken, "second");
167
168 // Reset before next step
169 arena.reset();
170 ```
171 */
172 #[inline]
173 pub fn take(&mut self, index: usize) -> M {
174 assert!(index < self.len, "index out of bounds");
175 assert!(self.taken.is_none(), "move already taken this step");
176 self.taken = Some(index);
177 unsafe { self.storage[index].assume_init_read() }
178 }
179
180 // Extends the arena from an iterator.
181 #[inline]
182 pub fn extend<I: IntoIterator<Item = M>>(&mut self, iter: I) {
183 for m in iter {
184 self.push(m);
185 }
186 }
187
188 /* Shuffles the initialized moves in-place using Fisher-Yates.
189
190 This avoids re-generating and re-collecting all moves each step.
191 The arena keeps its existing storage and just randomises evaluation order.
192 */
193 #[inline]
194 pub fn shuffle<R: Rng>(&mut self, rng: &mut R) {
195 if self.len < 2 {
196 return;
197 }
198 // Fisher-Yates on the initialized portion of storage
199 let slice = &mut self.storage[..self.len];
200 slice.shuffle(rng);
201 }
202
203 #[inline]
204 pub fn capacity(&self) -> usize {
205 self.storage.capacity()
206 }
207}
208
209impl<M> Default for MoveArena<M> {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215impl<M: Debug> Debug for MoveArena<M> {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.debug_struct("MoveArena")
218 .field("len", &self.len)
219 .field("capacity", &self.storage.capacity())
220 .finish()
221 }
222}
223
224impl<M> Drop for MoveArena<M> {
225 fn drop(&mut self) {
226 // Drop all initialized moves except taken one
227 for i in 0..self.len {
228 if self.taken != Some(i) {
229 unsafe {
230 ptr::drop_in_place(self.storage[i].as_mut_ptr());
231 }
232 }
233 }
234 }
235}