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