Skip to main content

solverforge_solver/builder/context/scalar/group/
member.rs

1use std::fmt;
2
3use solverforge_core::domain::{DynamicScalarVariableSlot, EntityClassId, VariableId};
4
5use crate::heuristic::r#move::CompoundScalarEdit;
6
7use super::super::variable::ScalarVariableSlot;
8
9enum ScalarGroupMemberAccess<S> {
10    Static(ScalarVariableSlot<S>),
11    Dynamic(DynamicScalarVariableSlot<S>),
12}
13
14impl<S> Clone for ScalarGroupMemberAccess<S> {
15    fn clone(&self) -> Self {
16        match self {
17            Self::Static(slot) => Self::Static(*slot),
18            Self::Dynamic(slot) => Self::Dynamic(slot.clone()),
19        }
20    }
21}
22
23/// One scalar variable participating in a scalar group.
24///
25/// Typed members retain direct function pointers. Dynamic members retain the
26/// existing dynamic slot adapter; both are consumed through this one semantic
27/// surface by grouped construction and local search.
28pub struct ScalarGroupMemberBinding<S> {
29    pub descriptor_index: usize,
30    pub variable_index: usize,
31    pub entity_type_name: &'static str,
32    pub variable_name: &'static str,
33    pub allows_unassigned: bool,
34    construction_binding_index: Option<usize>,
35    access: ScalarGroupMemberAccess<S>,
36}
37
38impl<S> Clone for ScalarGroupMemberBinding<S> {
39    fn clone(&self) -> Self {
40        Self {
41            descriptor_index: self.descriptor_index,
42            variable_index: self.variable_index,
43            entity_type_name: self.entity_type_name,
44            variable_name: self.variable_name,
45            allows_unassigned: self.allows_unassigned,
46            construction_binding_index: self.construction_binding_index,
47            access: self.access.clone(),
48        }
49    }
50}
51
52impl<S> ScalarGroupMemberBinding<S> {
53    pub fn from_scalar_slot(slot: ScalarVariableSlot<S>) -> Self {
54        Self {
55            descriptor_index: slot.descriptor_index,
56            variable_index: slot.variable_index,
57            entity_type_name: slot.entity_type_name,
58            variable_name: slot.variable_name,
59            allows_unassigned: slot.allows_unassigned,
60            construction_binding_index: None,
61            access: ScalarGroupMemberAccess::Static(slot),
62        }
63    }
64
65    pub fn from_dynamic_slot(slot: DynamicScalarVariableSlot<S>) -> Self {
66        Self {
67            // Dynamic group targets are canonicalized only by RuntimeModel
68            // compilation. This avoids accepting a stale prebound descriptor
69            // index before both entity and variable identity are validated.
70            descriptor_index: usize::MAX,
71            variable_index: usize::MAX,
72            entity_type_name: slot.entity_type_name,
73            variable_name: slot.variable_name,
74            allows_unassigned: slot.allows_unassigned,
75            construction_binding_index: None,
76            access: ScalarGroupMemberAccess::Dynamic(slot),
77        }
78    }
79
80    pub(crate) fn is_dynamic(&self) -> bool {
81        matches!(self.access, ScalarGroupMemberAccess::Dynamic(_))
82    }
83
84    pub(crate) fn dynamic_identity(&self) -> Option<(EntityClassId, VariableId)> {
85        match &self.access {
86            ScalarGroupMemberAccess::Static(_) => None,
87            ScalarGroupMemberAccess::Dynamic(slot) => Some((slot.entity, slot.variable)),
88        }
89    }
90
91    pub(crate) fn canonicalize_dynamic_slot(
92        &mut self,
93        slot: DynamicScalarVariableSlot<S>,
94    ) -> Result<(), String> {
95        let Some((entity, variable)) = self.dynamic_identity() else {
96            return Ok(());
97        };
98        if slot.entity != entity || slot.variable != variable {
99            return Err(format!(
100                "dynamic scalar group target {}.{} does not match the registered dynamic scalar slot {}.{}",
101                self.entity_type_name,
102                self.variable_name,
103                slot.entity_type_name,
104                slot.variable_name
105            ));
106        }
107        // RuntimeModel resolves every registered dynamic slot before it
108        // canonicalizes group members, so both descriptor indexes are now
109        // available even when the original registration only prebound the
110        // entity descriptor index.
111        self.descriptor_index = slot.descriptor_index();
112        self.variable_index = slot.descriptor_variable_index();
113        self.entity_type_name = slot.entity_type_name;
114        self.variable_name = slot.variable_name;
115        self.allows_unassigned = slot.allows_unassigned;
116        self.access = ScalarGroupMemberAccess::Dynamic(slot);
117        Ok(())
118    }
119
120    pub(crate) fn set_construction_binding_index(&mut self, binding_index: usize) {
121        self.construction_binding_index = Some(binding_index);
122    }
123
124    pub(crate) fn construction_binding_index(&self) -> usize {
125        self.construction_binding_index.unwrap_or_else(|| {
126            panic!(
127                "scalar group member {}.{} was not bound to a construction slot",
128                self.entity_type_name, self.variable_name
129            )
130        })
131    }
132}
133
134impl<S> ScalarGroupMemberBinding<S> {
135    pub fn current_value(&self, solution: &S, entity_index: usize) -> Option<usize> {
136        match &self.access {
137            ScalarGroupMemberAccess::Static(slot) => slot.current_value(solution, entity_index),
138            ScalarGroupMemberAccess::Dynamic(slot) => slot.current_value(solution, entity_index),
139        }
140    }
141
142    pub fn set_value(&self, solution: &mut S, entity_index: usize, value: Option<usize>) {
143        match &self.access {
144            ScalarGroupMemberAccess::Static(slot) => slot.set_value(solution, entity_index, value),
145            ScalarGroupMemberAccess::Dynamic(slot) => slot.set_value(solution, entity_index, value),
146        }
147    }
148
149    pub fn value_is_legal(
150        &self,
151        solution: &S,
152        entity_index: usize,
153        candidate: Option<usize>,
154    ) -> bool {
155        match &self.access {
156            ScalarGroupMemberAccess::Static(slot) => {
157                slot.value_is_legal(solution, entity_index, candidate)
158            }
159            ScalarGroupMemberAccess::Dynamic(slot) => {
160                slot.value_is_legal(solution, entity_index, candidate)
161            }
162        }
163    }
164
165    pub fn entity_count(&self, solution: &S) -> usize {
166        match &self.access {
167            ScalarGroupMemberAccess::Static(slot) => (slot.entity_count)(solution),
168            ScalarGroupMemberAccess::Dynamic(slot) => slot.entity_count(solution),
169        }
170    }
171
172    pub fn candidate_values(
173        &self,
174        solution: &S,
175        entity_index: usize,
176        value_candidate_limit: Option<usize>,
177    ) -> Vec<usize> {
178        match &self.access {
179            ScalarGroupMemberAccess::Static(slot) => {
180                slot.candidate_values_for_entity(solution, entity_index, value_candidate_limit)
181            }
182            ScalarGroupMemberAccess::Dynamic(slot) => {
183                let values = slot.candidate_values(solution, entity_index);
184                match value_candidate_limit {
185                    Some(limit) => values.iter().copied().take(limit).collect(),
186                    None => values.to_vec(),
187                }
188            }
189        }
190    }
191
192    pub(crate) fn compound_edit(
193        &self,
194        entity_index: usize,
195        to_value: Option<usize>,
196    ) -> CompoundScalarEdit<S> {
197        match &self.access {
198            ScalarGroupMemberAccess::Static(slot) => CompoundScalarEdit::static_edit(
199                self.descriptor_index,
200                entity_index,
201                self.variable_index,
202                self.variable_name,
203                to_value,
204                slot.getter,
205                slot.setter,
206                None,
207            ),
208            ScalarGroupMemberAccess::Dynamic(slot) => CompoundScalarEdit::dynamic_edit(
209                self.descriptor_index,
210                entity_index,
211                self.variable_index,
212                self.variable_name,
213                to_value,
214                slot.clone(),
215            ),
216        }
217    }
218}
219
220impl<S> fmt::Debug for ScalarGroupMemberBinding<S> {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.debug_struct("ScalarGroupMemberBinding")
223            .field("descriptor_index", &self.descriptor_index)
224            .field("variable_index", &self.variable_index)
225            .field("entity_type_name", &self.entity_type_name)
226            .field("variable_name", &self.variable_name)
227            .field("allows_unassigned", &self.allows_unassigned)
228            .field(
229                "construction_binding_index",
230                &self.construction_binding_index,
231            )
232            .field("dynamic", &self.is_dynamic())
233            .finish()
234    }
235}