Skip to main content

solverforge_solver/builder/context/scalar/
group.rs

1use std::fmt;
2use std::sync::Arc;
3
4use solverforge_core::domain::{DynamicScalarAssignmentMetadata, DynamicScalarVariableSlot};
5
6use crate::planning::{ScalarCandidateProvider, ScalarGroup, ScalarGroupKind, ScalarGroupLimits};
7
8use super::variable::ScalarVariableSlot;
9
10mod assignment;
11mod member;
12
13pub use assignment::ScalarAssignmentBinding;
14pub use member::ScalarGroupMemberBinding;
15
16pub struct ScalarGroupBinding<S> {
17    pub group_name: &'static str,
18    pub members: Vec<ScalarGroupMemberBinding<S>>,
19    pub kind: ScalarGroupBindingKind<S>,
20    pub limits: ScalarGroupLimits,
21}
22
23// Keep assignment metadata inline so the canonical grouped construction and
24// local-search path remains allocation-free after model compilation.
25#[allow(clippy::large_enum_variant)]
26pub enum ScalarGroupBindingKind<S> {
27    Candidates {
28        candidate_provider: ScalarCandidateProvider<S>,
29    },
30    Assignment(ScalarAssignmentBinding<S>),
31}
32
33impl<S> Clone for ScalarGroupBindingKind<S> {
34    fn clone(&self) -> Self {
35        match self {
36            Self::Candidates { candidate_provider } => Self::Candidates {
37                candidate_provider: *candidate_provider,
38            },
39            Self::Assignment(assignment) => Self::Assignment(assignment.clone()),
40        }
41    }
42}
43
44impl<S> ScalarGroupBinding<S> {
45    pub fn bind(group: ScalarGroup<S>, scalar_slots: &[ScalarVariableSlot<S>]) -> Self {
46        let members = group
47            .targets()
48            .iter()
49            .map(|target| {
50                let descriptor_index = target.descriptor_index();
51                let variable_name = target.variable_name();
52                let slot = scalar_slots
53                    .iter()
54                    .copied()
55                    .find(|slot| {
56                        slot.descriptor_index == descriptor_index
57                            && slot.variable_name == variable_name
58                    })
59                    .unwrap_or_else(|| {
60                        panic!(
61                            "scalar group `{}` targets unknown scalar variable `{}` on descriptor {}",
62                            group.group_name(),
63                            variable_name,
64                            descriptor_index
65                        )
66                    });
67                ScalarGroupMemberBinding::from_scalar_slot(slot)
68            })
69            .collect::<Vec<_>>();
70
71        let kind = match group.kind() {
72            ScalarGroupKind::Assignment(declaration) => ScalarGroupBindingKind::Assignment(
73                ScalarAssignmentBinding::bind(group.group_name(), &members, declaration),
74            ),
75            ScalarGroupKind::Candidates { candidate_provider } => {
76                ScalarGroupBindingKind::Candidates { candidate_provider }
77            }
78        };
79
80        Self {
81            group_name: group.group_name(),
82            members,
83            kind,
84            limits: group.limits(),
85        }
86    }
87
88    pub fn dynamic_assignment(
89        group_name: &'static str,
90        target: DynamicScalarVariableSlot<S>,
91        metadata: Arc<dyn DynamicScalarAssignmentMetadata<S>>,
92        limits: ScalarGroupLimits,
93    ) -> Self {
94        let member = ScalarGroupMemberBinding::from_dynamic_slot(target);
95        let assignment = ScalarAssignmentBinding::dynamic(group_name, member.clone(), metadata);
96        Self {
97            group_name,
98            members: vec![member],
99            kind: ScalarGroupBindingKind::Assignment(assignment),
100            limits,
101        }
102    }
103
104    pub fn member_for_edit(
105        &self,
106        edit: &crate::planning::ScalarEdit<S>,
107    ) -> Option<ScalarGroupMemberBinding<S>> {
108        self.members
109            .iter()
110            .find(|member| {
111                member.descriptor_index == edit.descriptor_index()
112                    && member.variable_name == edit.variable_name()
113            })
114            .cloned()
115    }
116
117    pub fn assignment(&self) -> Option<&ScalarAssignmentBinding<S>> {
118        match &self.kind {
119            ScalarGroupBindingKind::Assignment(assignment) => Some(assignment),
120            ScalarGroupBindingKind::Candidates { .. } => None,
121        }
122    }
123
124    pub(crate) fn assignment_mut(&mut self) -> Option<&mut ScalarAssignmentBinding<S>> {
125        match &mut self.kind {
126            ScalarGroupBindingKind::Assignment(assignment) => Some(assignment),
127            ScalarGroupBindingKind::Candidates { .. } => None,
128        }
129    }
130
131    pub(crate) fn canonicalize_dynamic_members(
132        &mut self,
133        scalar_slots: &[DynamicScalarVariableSlot<S>],
134    ) -> Result<(), String> {
135        for member in &mut self.members {
136            let Some((entity, variable)) = member.dynamic_identity() else {
137                continue;
138            };
139            let slot = scalar_slots
140                .iter()
141                .find(|slot| slot.entity == entity && slot.variable == variable)
142                .cloned()
143                .ok_or_else(|| {
144                    format!(
145                        "dynamic scalar group `{}` targets unregistered scalar variable {}.{}",
146                        self.group_name, member.entity_type_name, member.variable_name
147                    )
148                })?;
149            member.canonicalize_dynamic_slot(slot)?;
150        }
151
152        let group_name = self.group_name;
153        let Some(assignment) = self.assignment_mut() else {
154            return Ok(());
155        };
156        let Some((entity, variable)) = assignment.target().dynamic_identity() else {
157            return Ok(());
158        };
159        let slot = scalar_slots
160            .iter()
161            .find(|slot| slot.entity == entity && slot.variable == variable)
162            .cloned()
163            .ok_or_else(|| {
164                format!(
165                    "dynamic scalar group `{}` targets an unregistered assignment variable",
166                    group_name
167                )
168            })?;
169        assignment.target_mut().canonicalize_dynamic_slot(slot)
170    }
171
172    pub fn is_assignment(&self) -> bool {
173        matches!(self.kind, ScalarGroupBindingKind::Assignment(_))
174    }
175
176    pub fn is_candidate_group(&self) -> bool {
177        matches!(self.kind, ScalarGroupBindingKind::Candidates { .. })
178    }
179
180    pub fn has_sequence_metadata(&self) -> bool {
181        self.assignment()
182            .is_some_and(ScalarAssignmentBinding::has_sequence_metadata)
183    }
184
185    pub fn has_position_metadata(&self) -> bool {
186        self.assignment()
187            .is_some_and(ScalarAssignmentBinding::has_position_metadata)
188    }
189
190    pub fn default_max_moves_per_step(&self) -> Option<usize> {
191        self.limits.max_moves_per_step
192    }
193}
194
195impl<S> Clone for ScalarGroupBinding<S> {
196    fn clone(&self) -> Self {
197        Self {
198            group_name: self.group_name,
199            members: self.members.clone(),
200            kind: self.kind.clone(),
201            limits: self.limits,
202        }
203    }
204}
205
206impl<S> fmt::Debug for ScalarGroupBinding<S> {
207    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        f.debug_struct("ScalarGroupBinding")
209            .field("group_name", &self.group_name)
210            .field("members", &self.members)
211            .finish_non_exhaustive()
212    }
213}
214
215pub fn bind_scalar_groups<S>(
216    groups: Vec<ScalarGroup<S>>,
217    scalar_slots: &[ScalarVariableSlot<S>],
218) -> Vec<ScalarGroupBinding<S>> {
219    groups
220        .into_iter()
221        .map(|group| ScalarGroupBinding::bind(group, scalar_slots))
222        .collect()
223}