Skip to main content

solverforge_core/domain/
dynamic.rs

1//! Dynamic model contracts for host-language integrations.
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::domain::{EntityClassId, SolutionDescriptor, VariableId};
7use crate::score::Score;
8
9mod backend;
10mod resolution;
11
12use backend::{BackendListAccess, BackendScalarAccess};
13use resolution::{resolve_dynamic_descriptor_index, DynamicVariableKind};
14
15/// Rust-owned dynamic planning model backend.
16///
17/// Binding crates implement this trait on their concrete dynamic solution
18/// state. The trait is expressed in logical entity and variable IDs rather
19/// than Rust `TypeId`s.
20pub trait DynamicModelBackend: Clone + Send + Sync + 'static {
21    type Score: Score;
22
23    fn entity_count(&self, entity: EntityClassId) -> usize;
24
25    fn get_scalar(&self, entity: EntityClassId, row: usize, variable: VariableId) -> Option<usize>;
26
27    fn set_scalar(
28        &mut self,
29        entity: EntityClassId,
30        row: usize,
31        variable: VariableId,
32        value: Option<usize>,
33    );
34
35    fn list_len(&self, entity: EntityClassId, row: usize, variable: VariableId) -> usize;
36
37    fn list_get(
38        &self,
39        entity: EntityClassId,
40        row: usize,
41        variable: VariableId,
42        pos: usize,
43    ) -> Option<usize>;
44
45    fn list_insert(
46        &mut self,
47        entity: EntityClassId,
48        row: usize,
49        variable: VariableId,
50        pos: usize,
51        value: usize,
52    );
53
54    fn list_remove(
55        &mut self,
56        entity: EntityClassId,
57        row: usize,
58        variable: VariableId,
59        pos: usize,
60    ) -> Option<usize>;
61
62    fn candidate_values(&self, entity: EntityClassId, row: usize, variable: VariableId)
63        -> &[usize];
64
65    fn list_element_count(&self, _entity: EntityClassId, _variable: VariableId) -> usize {
66        0
67    }
68
69    fn list_element(
70        &self,
71        _entity: EntityClassId,
72        _variable: VariableId,
73        element_index: usize,
74    ) -> Option<usize> {
75        Some(element_index)
76    }
77
78    fn list_assigned_elements(&self, _entity: EntityClassId, _variable: VariableId) -> Vec<usize> {
79        Vec::new()
80    }
81}
82
83/// Object-safe dynamic scalar variable access.
84pub trait DynamicScalarAccess<S>: Send + Sync
85where
86    S: Clone + Send + Sync + 'static,
87{
88    fn entity_class(&self) -> EntityClassId;
89    fn variable(&self) -> VariableId;
90    fn entity_count(&self, solution: &S) -> usize;
91    fn get(&self, solution: &S, row: usize) -> Option<usize>;
92    fn set(&self, solution: &mut S, row: usize, value: Option<usize>);
93    fn candidate_values<'a>(&self, solution: &'a S, row: usize) -> &'a [usize];
94}
95
96/// Object-safe dynamic list variable access.
97pub trait DynamicListAccess<S>: Send + Sync
98where
99    S: Clone + Send + Sync + 'static,
100{
101    fn entity_class(&self) -> EntityClassId;
102    fn variable(&self) -> VariableId;
103    fn entity_count(&self, solution: &S) -> usize;
104    fn element_count(&self, solution: &S) -> usize;
105    fn element(&self, solution: &S, element_index: usize) -> Option<usize>;
106    fn assigned_elements(&self, solution: &S) -> Vec<usize>;
107    fn len(&self, solution: &S, row: usize) -> usize;
108    fn get(&self, solution: &S, row: usize, pos: usize) -> Option<usize>;
109    fn insert(&self, solution: &mut S, row: usize, pos: usize, value: usize);
110    fn remove(&self, solution: &mut S, row: usize, pos: usize) -> Option<usize>;
111}
112
113/// Public dynamic scalar variable slot.
114pub struct DynamicScalarVariableSlot<S> {
115    pub entity: EntityClassId,
116    pub variable: VariableId,
117    pub entity_type_name: &'static str,
118    pub variable_name: &'static str,
119    pub allows_unassigned: bool,
120    descriptor_index: Option<usize>,
121    access: Arc<dyn DynamicScalarAccess<S>>,
122}
123
124impl<S> Clone for DynamicScalarVariableSlot<S> {
125    fn clone(&self) -> Self {
126        Self {
127            entity: self.entity,
128            variable: self.variable,
129            entity_type_name: self.entity_type_name,
130            variable_name: self.variable_name,
131            allows_unassigned: self.allows_unassigned,
132            descriptor_index: self.descriptor_index,
133            access: Arc::clone(&self.access),
134        }
135    }
136}
137
138impl<S> fmt::Debug for DynamicScalarVariableSlot<S> {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        f.debug_struct("DynamicScalarVariableSlot")
141            .field("entity", &self.entity)
142            .field("variable", &self.variable)
143            .field("entity_type_name", &self.entity_type_name)
144            .field("variable_name", &self.variable_name)
145            .field("allows_unassigned", &self.allows_unassigned)
146            .field("descriptor_index", &self.descriptor_index)
147            .finish()
148    }
149}
150
151impl<S> PartialEq for DynamicScalarVariableSlot<S> {
152    fn eq(&self, other: &Self) -> bool {
153        self.entity == other.entity
154            && self.variable == other.variable
155            && self.entity_type_name == other.entity_type_name
156            && self.variable_name == other.variable_name
157            && self.allows_unassigned == other.allows_unassigned
158            && self.descriptor_index == other.descriptor_index
159    }
160}
161
162impl<S> Eq for DynamicScalarVariableSlot<S> {}
163
164impl<S> DynamicScalarVariableSlot<S> {
165    pub fn with_access(
166        entity: EntityClassId,
167        variable: VariableId,
168        entity_type_name: &'static str,
169        variable_name: &'static str,
170        allows_unassigned: bool,
171        access: Arc<dyn DynamicScalarAccess<S>>,
172    ) -> Self {
173        Self {
174            entity,
175            variable,
176            entity_type_name,
177            variable_name,
178            allows_unassigned,
179            descriptor_index: None,
180            access,
181        }
182    }
183
184    pub fn with_descriptor_index(mut self, descriptor_index: usize) -> Self {
185        self.descriptor_index = Some(descriptor_index);
186        self
187    }
188
189    pub fn resolve_descriptor_index(
190        &mut self,
191        descriptor: &SolutionDescriptor,
192    ) -> Result<(), String> {
193        let descriptor_index = resolve_dynamic_descriptor_index(
194            descriptor,
195            self.entity,
196            self.variable,
197            self.entity_type_name,
198            self.variable_name,
199            DynamicVariableKind::Scalar,
200        )?;
201        if let Some(existing) = self.descriptor_index {
202            if existing != descriptor_index {
203                return Err(format!(
204                    "dynamic scalar variable {}.{} was pre-bound to descriptor index {existing}, but logical entity ID {} resolves to descriptor index {descriptor_index}",
205                    self.entity_type_name, self.variable_name, self.entity.0
206                ));
207            }
208        }
209        self.descriptor_index = Some(descriptor_index);
210        Ok(())
211    }
212
213    pub fn resolved_against(mut self, descriptor: &SolutionDescriptor) -> Result<Self, String> {
214        self.resolve_descriptor_index(descriptor)?;
215        Ok(self)
216    }
217
218    pub fn is_descriptor_resolved(&self) -> bool {
219        self.descriptor_index.is_some()
220    }
221
222    pub fn descriptor_index(&self) -> usize {
223        self.descriptor_index.unwrap_or_else(|| {
224            panic!(
225                "dynamic scalar variable {}.{} has not been resolved against a SolutionDescriptor",
226                self.entity_type_name, self.variable_name
227            )
228        })
229    }
230
231    pub fn matches_target(&self, entity_class: Option<&str>, variable_name: Option<&str>) -> bool {
232        entity_class.is_none_or(|entity| entity == self.entity_type_name)
233            && variable_name.is_none_or(|variable| variable == self.variable_name)
234    }
235
236    pub fn entity_count(&self, solution: &S) -> usize
237    where
238        S: Clone + Send + Sync + 'static,
239    {
240        self.access.entity_count(solution)
241    }
242
243    pub fn current_value(&self, solution: &S, row: usize) -> Option<usize>
244    where
245        S: Clone + Send + Sync + 'static,
246    {
247        self.access.get(solution, row)
248    }
249
250    pub fn set_value(&self, solution: &mut S, row: usize, value: Option<usize>)
251    where
252        S: Clone + Send + Sync + 'static,
253    {
254        self.access.set(solution, row, value);
255    }
256
257    pub fn candidate_values<'a>(&self, solution: &'a S, row: usize) -> &'a [usize]
258    where
259        S: Clone + Send + Sync + 'static,
260    {
261        self.access.candidate_values(solution, row)
262    }
263
264    pub fn value_is_legal(&self, solution: &S, row: usize, value: Option<usize>) -> bool
265    where
266        S: Clone + Send + Sync + 'static,
267    {
268        let Some(value) = value else {
269            return self.allows_unassigned;
270        };
271        self.candidate_values(solution, row).contains(&value)
272    }
273}
274
275impl<S> DynamicScalarVariableSlot<S>
276where
277    S: DynamicModelBackend,
278{
279    pub fn new(
280        entity: EntityClassId,
281        variable: VariableId,
282        entity_type_name: &'static str,
283        variable_name: &'static str,
284        allows_unassigned: bool,
285    ) -> Self {
286        Self::with_access(
287            entity,
288            variable,
289            entity_type_name,
290            variable_name,
291            allows_unassigned,
292            Arc::new(BackendScalarAccess { entity, variable }),
293        )
294    }
295}
296
297/// Public dynamic list variable slot.
298pub struct DynamicListVariableSlot<S> {
299    pub entity: EntityClassId,
300    pub variable: VariableId,
301    pub entity_type_name: &'static str,
302    pub variable_name: &'static str,
303    descriptor_index: Option<usize>,
304    access: Arc<dyn DynamicListAccess<S>>,
305}
306
307impl<S> Clone for DynamicListVariableSlot<S> {
308    fn clone(&self) -> Self {
309        Self {
310            entity: self.entity,
311            variable: self.variable,
312            entity_type_name: self.entity_type_name,
313            variable_name: self.variable_name,
314            descriptor_index: self.descriptor_index,
315            access: Arc::clone(&self.access),
316        }
317    }
318}
319
320impl<S> fmt::Debug for DynamicListVariableSlot<S> {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        f.debug_struct("DynamicListVariableSlot")
323            .field("entity", &self.entity)
324            .field("variable", &self.variable)
325            .field("entity_type_name", &self.entity_type_name)
326            .field("variable_name", &self.variable_name)
327            .field("descriptor_index", &self.descriptor_index)
328            .finish()
329    }
330}
331
332impl<S> PartialEq for DynamicListVariableSlot<S> {
333    fn eq(&self, other: &Self) -> bool {
334        self.entity == other.entity
335            && self.variable == other.variable
336            && self.entity_type_name == other.entity_type_name
337            && self.variable_name == other.variable_name
338            && self.descriptor_index == other.descriptor_index
339    }
340}
341
342impl<S> Eq for DynamicListVariableSlot<S> {}
343
344impl<S> DynamicListVariableSlot<S> {
345    pub fn with_access(
346        entity: EntityClassId,
347        variable: VariableId,
348        entity_type_name: &'static str,
349        variable_name: &'static str,
350        access: Arc<dyn DynamicListAccess<S>>,
351    ) -> Self {
352        Self {
353            entity,
354            variable,
355            entity_type_name,
356            variable_name,
357            descriptor_index: None,
358            access,
359        }
360    }
361
362    pub fn with_descriptor_index(mut self, descriptor_index: usize) -> Self {
363        self.descriptor_index = Some(descriptor_index);
364        self
365    }
366
367    pub fn resolve_descriptor_index(
368        &mut self,
369        descriptor: &SolutionDescriptor,
370    ) -> Result<(), String> {
371        let descriptor_index = resolve_dynamic_descriptor_index(
372            descriptor,
373            self.entity,
374            self.variable,
375            self.entity_type_name,
376            self.variable_name,
377            DynamicVariableKind::List,
378        )?;
379        if let Some(existing) = self.descriptor_index {
380            if existing != descriptor_index {
381                return Err(format!(
382                    "dynamic list variable {}.{} was pre-bound to descriptor index {existing}, but logical entity ID {} resolves to descriptor index {descriptor_index}",
383                    self.entity_type_name, self.variable_name, self.entity.0
384                ));
385            }
386        }
387        self.descriptor_index = Some(descriptor_index);
388        Ok(())
389    }
390
391    pub fn resolved_against(mut self, descriptor: &SolutionDescriptor) -> Result<Self, String> {
392        self.resolve_descriptor_index(descriptor)?;
393        Ok(self)
394    }
395
396    pub fn is_descriptor_resolved(&self) -> bool {
397        self.descriptor_index.is_some()
398    }
399
400    pub fn descriptor_index(&self) -> usize {
401        self.descriptor_index.unwrap_or_else(|| {
402            panic!(
403                "dynamic list variable {}.{} has not been resolved against a SolutionDescriptor",
404                self.entity_type_name, self.variable_name
405            )
406        })
407    }
408
409    pub fn matches_target(&self, entity_class: Option<&str>, variable_name: Option<&str>) -> bool {
410        entity_class.is_none_or(|entity| entity == self.entity_type_name)
411            && variable_name.is_none_or(|variable| variable == self.variable_name)
412    }
413
414    pub fn entity_count(&self, solution: &S) -> usize
415    where
416        S: Clone + Send + Sync + 'static,
417    {
418        self.access.entity_count(solution)
419    }
420
421    pub fn element_count(&self, solution: &S) -> usize
422    where
423        S: Clone + Send + Sync + 'static,
424    {
425        self.access.element_count(solution)
426    }
427
428    pub fn element(&self, solution: &S, element_index: usize) -> Option<usize>
429    where
430        S: Clone + Send + Sync + 'static,
431    {
432        self.access.element(solution, element_index)
433    }
434
435    pub fn assigned_elements(&self, solution: &S) -> Vec<usize>
436    where
437        S: Clone + Send + Sync + 'static,
438    {
439        self.access.assigned_elements(solution)
440    }
441
442    pub fn list_len(&self, solution: &S, row: usize) -> usize
443    where
444        S: Clone + Send + Sync + 'static,
445    {
446        self.access.len(solution, row)
447    }
448
449    pub fn list_get(&self, solution: &S, row: usize, pos: usize) -> Option<usize>
450    where
451        S: Clone + Send + Sync + 'static,
452    {
453        self.access.get(solution, row, pos)
454    }
455
456    pub fn list_insert(&self, solution: &mut S, row: usize, pos: usize, value: usize)
457    where
458        S: Clone + Send + Sync + 'static,
459    {
460        self.access.insert(solution, row, pos, value);
461    }
462
463    pub fn list_remove(&self, solution: &mut S, row: usize, pos: usize) -> Option<usize>
464    where
465        S: Clone + Send + Sync + 'static,
466    {
467        self.access.remove(solution, row, pos)
468    }
469}
470
471impl<S> DynamicListVariableSlot<S>
472where
473    S: DynamicModelBackend,
474{
475    pub fn new(
476        entity: EntityClassId,
477        variable: VariableId,
478        entity_type_name: &'static str,
479        variable_name: &'static str,
480    ) -> Self {
481        Self::with_access(
482            entity,
483            variable,
484            entity_type_name,
485            variable_name,
486            Arc::new(BackendListAccess { entity, variable }),
487        )
488    }
489}