Skip to main content

solverforge_solver/heuristic/move/
change.rs

1/* ChangeMove - assigns a value to a planning variable.
2
3This is the most fundamental move type. It takes a value and assigns
4it to a planning variable on an entity.
5
6# Zero-Erasure Design
7
8This move stores typed function pointers that operate directly on
9the solution. No `Arc<dyn>`, no `Box<dyn Any>`, no `downcast_ref`.
10*/
11
12use std::fmt::Debug;
13
14use smallvec::smallvec;
15use solverforge_core::domain::PlanningSolution;
16use solverforge_scoring::Director;
17
18use super::metadata::{encode_option_debug, encode_usize, hash_str, MoveTabuScope};
19use super::{Move, MoveTabuSignature};
20
21/// A move that assigns a value to an entity's variable.
22///
23/// Stores typed function pointers for zero-erasure execution.
24/// No trait objects, no boxing - all operations are fully typed at compile time.
25///
26/// # Type Parameters
27/// * `S` - The planning solution type
28/// * `V` - The variable value type
29pub struct ChangeMove<S, V> {
30    entity_index: usize,
31    to_value: Option<V>,
32    getter: fn(&S, usize, usize) -> Option<V>,
33    setter: fn(&mut S, usize, usize, Option<V>),
34    variable_index: usize,
35    variable_name: &'static str,
36    descriptor_index: usize,
37}
38
39impl<S, V: Clone> Clone for ChangeMove<S, V> {
40    fn clone(&self) -> Self {
41        Self {
42            entity_index: self.entity_index,
43            to_value: self.to_value.clone(),
44            getter: self.getter,
45            setter: self.setter,
46            variable_index: self.variable_index,
47            variable_name: self.variable_name,
48            descriptor_index: self.descriptor_index,
49        }
50    }
51}
52
53impl<S, V: Copy> Copy for ChangeMove<S, V> {}
54
55impl<S, V: Debug> Debug for ChangeMove<S, V> {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("ChangeMove")
58            .field("entity_index", &self.entity_index)
59            .field("descriptor_index", &self.descriptor_index)
60            .field("variable_index", &self.variable_index)
61            .field("variable_name", &self.variable_name)
62            .field("to_value", &self.to_value)
63            .finish()
64    }
65}
66
67impl<S, V> ChangeMove<S, V> {
68    /// Creates a new change move with typed function pointers.
69    ///
70    /// # Arguments
71    /// * `entity_index` - Index of the entity in its collection
72    /// * `to_value` - The value to assign (None to unassign)
73    /// * `getter` - Function pointer to get current value from solution
74    /// * `setter` - Function pointer to set value on solution
75    /// * `variable_name` - Name of the variable (for debugging)
76    /// * `descriptor_index` - Index of the entity descriptor
77    pub fn new(
78        entity_index: usize,
79        to_value: Option<V>,
80        getter: fn(&S, usize, usize) -> Option<V>,
81        setter: fn(&mut S, usize, usize, Option<V>),
82        variable_index: usize,
83        variable_name: &'static str,
84        descriptor_index: usize,
85    ) -> Self {
86        Self {
87            entity_index,
88            to_value,
89            getter,
90            setter,
91            variable_index,
92            variable_name,
93            descriptor_index,
94        }
95    }
96
97    pub fn entity_index(&self) -> usize {
98        self.entity_index
99    }
100
101    pub fn to_value(&self) -> Option<&V> {
102        self.to_value.as_ref()
103    }
104
105    pub fn getter(&self) -> fn(&S, usize, usize) -> Option<V> {
106        self.getter
107    }
108
109    pub fn setter(&self) -> fn(&mut S, usize, usize, Option<V>) {
110        self.setter
111    }
112
113    pub fn variable_index(&self) -> usize {
114        self.variable_index
115    }
116}
117
118impl<S, V> Move<S> for ChangeMove<S, V>
119where
120    S: PlanningSolution,
121    V: Clone + PartialEq + Send + Sync + Debug + 'static,
122{
123    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
124        // Get current value using typed getter - no boxing, no downcast
125        let current = (self.getter)(
126            score_director.working_solution(),
127            self.entity_index,
128            self.variable_index,
129        );
130
131        // Compare directly - fully typed comparison
132        match (&current, &self.to_value) {
133            (None, None) => false,                      // Both unassigned
134            (Some(cur), Some(target)) => cur != target, // Different values
135            _ => true,                                  // One assigned, one not
136        }
137    }
138
139    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
140        // Capture old value using typed getter - zero erasure
141        let old_value = (self.getter)(
142            score_director.working_solution(),
143            self.entity_index,
144            self.variable_index,
145        );
146
147        // Notify before change
148        score_director.before_variable_changed(self.descriptor_index, self.entity_index);
149
150        // Set value using typed setter - no boxing
151        (self.setter)(
152            score_director.working_solution_mut(),
153            self.entity_index,
154            self.variable_index,
155            self.to_value.clone(),
156        );
157
158        // Notify after change
159        score_director.after_variable_changed(self.descriptor_index, self.entity_index);
160
161        // Register typed undo closure - zero erasure
162        let setter = self.setter;
163        let idx = self.entity_index;
164        let variable_index = self.variable_index;
165        score_director.register_undo(Box::new(move |s: &mut S| {
166            setter(s, idx, variable_index, old_value);
167        }));
168    }
169
170    fn descriptor_index(&self) -> usize {
171        self.descriptor_index
172    }
173
174    fn entity_indices(&self) -> &[usize] {
175        std::slice::from_ref(&self.entity_index)
176    }
177
178    fn variable_name(&self) -> &str {
179        self.variable_name
180    }
181
182    fn tabu_signature<D: Director<S>>(&self, score_director: &D) -> MoveTabuSignature {
183        let current = (self.getter)(
184            score_director.working_solution(),
185            self.entity_index,
186            self.variable_index,
187        );
188        let from_id = encode_option_debug(current.as_ref());
189        let to_id = encode_option_debug(self.to_value.as_ref());
190        let entity_id = encode_usize(self.entity_index);
191        let variable_id = hash_str(self.variable_name);
192        let scope = MoveTabuScope::new(self.descriptor_index, self.variable_name);
193
194        MoveTabuSignature::new(
195            scope,
196            smallvec![
197                encode_usize(self.descriptor_index),
198                variable_id,
199                entity_id,
200                from_id,
201                to_id
202            ],
203            smallvec![
204                encode_usize(self.descriptor_index),
205                variable_id,
206                entity_id,
207                to_id,
208                from_id
209            ],
210        )
211        .with_entity_tokens([scope.entity_token(entity_id)])
212        .with_destination_value_tokens([scope.value_token(to_id)])
213    }
214}