Skip to main content

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}