Skip to main content

solverforge_solver/builder/context/
scalar_access.rs

1//! Shared typed/dynamic scalar access carrier for runtime graph compilation.
2//!
3//! This is deliberately below selector/provider policy.  A graph node carries
4//! one `RuntimeScalarSlot`; scalar leaves and generic provider moves use
5//! the same identity, legality, and metadata dispatch rather than rebuilding
6//! typed and dynamic selector trees.
7
8use std::fmt;
9use std::sync::Arc;
10
11use solverforge_core::domain::{DynamicScalarVariableSlot, EntityClassId, VariableId};
12
13use super::{ScalarVariableSlot, ValueSource};
14
15/// Immutable scalar slot identity used by compiler errors, provider
16/// normalization, candidate ownership, and trace provenance.
17#[derive(Clone, Debug, PartialEq, Eq, Hash)]
18pub struct RuntimeScalarSlotId {
19    pub descriptor_index: usize,
20    pub variable_index: usize,
21    pub entity_class: Arc<str>,
22    pub variable_name: Arc<str>,
23    pub dynamic_identity: Option<(EntityClassId, VariableId)>,
24}
25
26impl RuntimeScalarSlotId {
27    pub fn from_static_slot<S>(slot: &ScalarVariableSlot<S>) -> Self {
28        Self {
29            descriptor_index: slot.descriptor_index,
30            variable_index: slot.variable_index,
31            entity_class: Arc::from(slot.entity_type_name),
32            variable_name: Arc::from(slot.variable_name),
33            dynamic_identity: None,
34        }
35    }
36
37    pub fn from_dynamic_slot<S>(slot: &DynamicScalarVariableSlot<S>) -> Self {
38        Self {
39            descriptor_index: slot.descriptor_index(),
40            variable_index: slot.descriptor_variable_index(),
41            entity_class: Arc::from(slot.entity_type_name),
42            variable_name: Arc::from(slot.variable_name),
43            dynamic_identity: Some((slot.entity, slot.variable)),
44        }
45    }
46}
47
48impl fmt::Display for RuntimeScalarSlotId {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(
51            f,
52            "{}.{} (descriptor {}, variable {})",
53            self.entity_class, self.variable_name, self.descriptor_index, self.variable_index
54        )
55    }
56}
57
58/// Structural scalar capability.  It answers whether an immutable binding
59/// supplies a source, not whether a particular row produces a candidate.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum ScalarAccessCapability {
62    Candidates,
63    NearbyValue,
64    NearbyEntity,
65    ConstructionEntityOrder,
66    ConstructionValueOrder,
67    SearchEntityOrder,
68    SearchValueOrder,
69}
70
71/// Physical payload carrier.  This is the only typed/dynamic distinction
72/// retained by the shared scalar/provider kernels.
73pub enum RuntimeScalarSlot<S> {
74    Static(ScalarVariableSlot<S>),
75    Dynamic(DynamicScalarVariableSlot<S>),
76}
77
78impl<S> Clone for RuntimeScalarSlot<S> {
79    fn clone(&self) -> Self {
80        match self {
81            Self::Static(slot) => Self::Static(*slot),
82            Self::Dynamic(slot) => Self::Dynamic(slot.clone()),
83        }
84    }
85}
86
87impl<S> fmt::Debug for RuntimeScalarSlot<S> {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.debug_struct("RuntimeScalarSlot")
90            .field("id", &self.id())
91            .field("dynamic", &self.is_dynamic())
92            .finish()
93    }
94}
95
96impl<S> RuntimeScalarSlot<S> {
97    pub fn id(&self) -> RuntimeScalarSlotId {
98        match self {
99            Self::Static(slot) => RuntimeScalarSlotId::from_static_slot(slot),
100            Self::Dynamic(slot) => RuntimeScalarSlotId::from_dynamic_slot(slot),
101        }
102    }
103
104    pub fn matches_target(&self, entity_class: Option<&str>, variable_name: Option<&str>) -> bool {
105        match self {
106            Self::Static(slot) => slot.matches_target(entity_class, variable_name),
107            Self::Dynamic(slot) => slot.matches_target(entity_class, variable_name),
108        }
109    }
110
111    pub fn has_capability(&self, capability: ScalarAccessCapability) -> bool {
112        match (self, capability) {
113            // Every scalar binding exposes an ordinary stream, even when a
114            // row's stream is empty.
115            (_, ScalarAccessCapability::Candidates) => true,
116            (Self::Static(slot), ScalarAccessCapability::NearbyValue) => {
117                slot.nearby_value_candidates.is_some()
118            }
119            (Self::Static(slot), ScalarAccessCapability::NearbyEntity) => {
120                slot.nearby_entity_candidates.is_some()
121            }
122            (Self::Static(slot), ScalarAccessCapability::ConstructionEntityOrder) => {
123                slot.construction_entity_order_key.is_some()
124            }
125            (Self::Static(slot), ScalarAccessCapability::ConstructionValueOrder) => {
126                slot.construction_value_order_key.is_some()
127            }
128            (Self::Static(slot), ScalarAccessCapability::SearchEntityOrder) => {
129                slot.search_entity_order_key.is_some()
130            }
131            (Self::Static(slot), ScalarAccessCapability::SearchValueOrder) => {
132                slot.search_value_order_key.is_some()
133            }
134            (Self::Dynamic(slot), ScalarAccessCapability::NearbyValue) => {
135                slot.has_nearby_value_candidates()
136            }
137            (Self::Dynamic(slot), ScalarAccessCapability::NearbyEntity) => {
138                slot.has_nearby_entity_candidates()
139            }
140            // Dynamic scalar decreasing/queue construction requires immutable
141            // order metadata. It is intentionally not recreated by a wrapper
142            // callback or ad hoc model scan.
143            (Self::Dynamic(_), ScalarAccessCapability::ConstructionEntityOrder)
144            | (Self::Dynamic(_), ScalarAccessCapability::ConstructionValueOrder)
145            | (Self::Dynamic(_), ScalarAccessCapability::SearchEntityOrder)
146            | (Self::Dynamic(_), ScalarAccessCapability::SearchValueOrder) => false,
147        }
148    }
149
150    /// Descriptor coordinate without materializing an owned identity. Runtime
151    /// moves use this in their hot apply/undo and tabu paths.
152    pub(crate) fn descriptor_index(&self) -> usize {
153        match self {
154            Self::Static(slot) => slot.descriptor_index,
155            Self::Dynamic(slot) => slot.descriptor_index(),
156        }
157    }
158
159    /// Descriptor-local scalar coordinate without materializing an owned
160    /// identity.
161    pub(crate) fn variable_index(&self) -> usize {
162        match self {
163            Self::Static(slot) => slot.variable_index,
164            Self::Dynamic(slot) => slot.descriptor_variable_index(),
165        }
166    }
167
168    /// Stable slot name held by the frozen model. Callback reasons remain
169    /// owned separately; this accessor is only the planning-variable identity
170    /// needed by existing core telemetry/tabu primitives.
171    pub(crate) fn variable_name(&self) -> &'static str {
172        match self {
173            Self::Static(slot) => slot.variable_name,
174            Self::Dynamic(slot) => slot.variable_name,
175        }
176    }
177
178    pub(crate) fn entity_count(&self, solution: &S) -> usize {
179        match self {
180            Self::Static(slot) => (slot.entity_count)(solution),
181            Self::Dynamic(slot) => slot.entity_count(solution),
182        }
183    }
184
185    /// Whether a construction placement may retain an unassigned value. This
186    /// is immutable slot metadata, shared by the descriptor-placement and
187    /// global runtime-slot construction schedules.
188    pub(crate) fn allows_unassigned(&self) -> bool {
189        match self {
190            Self::Static(slot) => slot.allows_unassigned,
191            Self::Dynamic(slot) => slot.allows_unassigned,
192        }
193    }
194
195    pub(crate) fn current_value(&self, solution: &S, entity_index: usize) -> Option<usize> {
196        match self {
197            Self::Static(slot) => slot.current_value(solution, entity_index),
198            Self::Dynamic(slot) => slot.current_value(solution, entity_index),
199        }
200    }
201
202    pub(crate) fn set_value(&self, solution: &mut S, entity_index: usize, value: Option<usize>) {
203        match self {
204            Self::Static(slot) => slot.set_value(solution, entity_index, value),
205            Self::Dynamic(slot) => slot.set_value(solution, entity_index, value),
206        }
207    }
208
209    pub(crate) fn value_is_legal(
210        &self,
211        solution: &S,
212        entity_index: usize,
213        value: Option<usize>,
214    ) -> bool {
215        match self {
216            Self::Static(slot) => slot.value_is_legal(solution, entity_index, value),
217            Self::Dynamic(slot) => slot.value_is_legal(solution, entity_index, value),
218        }
219    }
220
221    /// Applies scalar-swap legality through the shared carrier. Native empty
222    /// value sources permit exchanging assigned values;
223    /// every other source uses its declared per-row legality predicate.
224    pub(crate) fn swap_destination_is_legal(
225        &self,
226        solution: &S,
227        entity_index: usize,
228        value: Option<usize>,
229    ) -> bool {
230        match self {
231            Self::Static(slot) if matches!(slot.value_source, ValueSource::Empty) => {
232                value.is_some()
233            }
234            Self::Static(slot) => slot.value_is_legal(solution, entity_index, value),
235            Self::Dynamic(slot) => slot.value_is_legal(solution, entity_index, value),
236        }
237    }
238
239    /// Visits ordinary candidates in their declared source order.  `limit`
240    /// limits source consumption, never a post-hoc result set, so lazy dynamic
241    /// bindings and static value ranges share one ordering contract.
242    pub(crate) fn visit_candidate_values(
243        &self,
244        solution: &S,
245        entity_index: usize,
246        limit: Option<usize>,
247        visit: &mut dyn FnMut(usize),
248    ) {
249        let limit = limit.unwrap_or(usize::MAX);
250        match self {
251            Self::Static(slot) => {
252                for value in slot.candidate_values_for_entity(solution, entity_index, Some(limit)) {
253                    visit(value);
254                }
255            }
256            Self::Dynamic(slot) => {
257                for &value in slot
258                    .candidate_values(solution, entity_index)
259                    .iter()
260                    .take(limit)
261                {
262                    visit(value);
263                }
264            }
265        }
266    }
267
268    /// Visits the declared nearby-value source, returning false only when the
269    /// binding has no such source and a caller may select its explicit normal
270    /// candidate behavior.  It never synthesizes a wrapper-local fallback.
271    pub(crate) fn visit_nearby_value_candidates(
272        &self,
273        solution: &S,
274        entity_index: usize,
275        limit: usize,
276        visit: &mut dyn FnMut(usize),
277    ) -> bool {
278        match self {
279            Self::Static(slot) => {
280                let Some(source) = slot.nearby_value_candidates else {
281                    return false;
282                };
283                for &value in source(solution, entity_index, slot.variable_index)
284                    .iter()
285                    .take(limit)
286                {
287                    visit(value);
288                }
289                true
290            }
291            Self::Dynamic(slot) => {
292                slot.visit_nearby_value_candidates(solution, entity_index, limit, visit)
293            }
294        }
295    }
296
297    pub(crate) fn nearby_value_distance(
298        &self,
299        solution: &S,
300        entity_index: usize,
301        candidate: usize,
302    ) -> Option<f64> {
303        match self {
304            Self::Static(slot) => slot.nearby_value_distance(solution, entity_index, candidate),
305            Self::Dynamic(slot) => slot.nearby_value_distance(solution, entity_index, candidate),
306        }
307    }
308
309    pub(crate) fn visit_nearby_entity_candidates(
310        &self,
311        solution: &S,
312        entity_index: usize,
313        limit: usize,
314        visit: &mut dyn FnMut(usize),
315    ) -> bool {
316        match self {
317            Self::Static(slot) => {
318                let Some(source) = slot.nearby_entity_candidates else {
319                    return false;
320                };
321                for &candidate in source(solution, entity_index, slot.variable_index)
322                    .iter()
323                    .take(limit)
324                {
325                    visit(candidate);
326                }
327                true
328            }
329            Self::Dynamic(slot) => {
330                slot.visit_nearby_entity_candidates(solution, entity_index, limit, visit)
331            }
332        }
333    }
334
335    pub(crate) fn nearby_entity_distance(
336        &self,
337        solution: &S,
338        left_entity_index: usize,
339        right_entity_index: usize,
340    ) -> Option<f64> {
341        match self {
342            Self::Static(slot) => {
343                slot.nearby_entity_distance(solution, left_entity_index, right_entity_index)
344            }
345            Self::Dynamic(slot) => {
346                slot.nearby_entity_distance(solution, left_entity_index, right_entity_index)
347            }
348        }
349    }
350
351    pub(crate) fn construction_entity_order_key(
352        &self,
353        solution: &S,
354        entity_index: usize,
355    ) -> Option<i64> {
356        match self {
357            Self::Static(slot) => slot.construction_entity_order_key(solution, entity_index),
358            Self::Dynamic(_) => None,
359        }
360    }
361
362    pub(crate) fn construction_value_order_key(
363        &self,
364        solution: &S,
365        entity_index: usize,
366        value: usize,
367    ) -> Option<i64> {
368        match self {
369            Self::Static(slot) => slot.construction_value_order_key(solution, entity_index, value),
370            Self::Dynamic(_) => None,
371        }
372    }
373
374    pub(crate) fn search_entity_order_key(&self, solution: &S, entity_index: usize) -> Option<i64> {
375        match self {
376            Self::Static(slot) => slot.search_entity_order_key(solution, entity_index),
377            Self::Dynamic(_) => None,
378        }
379    }
380
381    pub(crate) fn search_value_order_key(
382        &self,
383        solution: &S,
384        entity_index: usize,
385        value: usize,
386    ) -> Option<i64> {
387        match self {
388            Self::Static(slot) => slot.search_value_order_key(solution, entity_index, value),
389            Self::Dynamic(_) => None,
390        }
391    }
392
393    pub(crate) fn edit(
394        &self,
395        entity_index: usize,
396        to_value: Option<usize>,
397    ) -> RuntimeScalarEdit<S> {
398        RuntimeScalarEdit {
399            slot: self.clone(),
400            entity_index,
401            to_value,
402        }
403    }
404
405    pub fn is_dynamic(&self) -> bool {
406        matches!(self, Self::Dynamic(_))
407    }
408}
409
410/// A neutral scalar edit used by the shared provider/scalar kernels before it
411/// is lowered to the outer move payload.  The kind/reason policy lives above
412/// this type; apply/undo ownership never needs a second typed/dynamic edit
413/// implementation.
414pub struct RuntimeScalarEdit<S> {
415    pub slot: RuntimeScalarSlot<S>,
416    pub entity_index: usize,
417    pub to_value: Option<usize>,
418}
419
420impl<S> Clone for RuntimeScalarEdit<S> {
421    fn clone(&self) -> Self {
422        Self {
423            slot: self.slot.clone(),
424            entity_index: self.entity_index,
425            to_value: self.to_value,
426        }
427    }
428}
429
430impl<S> fmt::Debug for RuntimeScalarEdit<S> {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        f.debug_struct("RuntimeScalarEdit")
433            .field("slot", &self.slot)
434            .field("entity_index", &self.entity_index)
435            .field("to_value", &self.to_value)
436            .finish()
437    }
438}
439
440impl<S> RuntimeScalarEdit<S> {
441    pub fn id(&self) -> RuntimeScalarSlotId {
442        self.slot.id()
443    }
444
445    pub(crate) fn descriptor_index(&self) -> usize {
446        self.slot.descriptor_index()
447    }
448
449    pub(crate) fn variable_index(&self) -> usize {
450        self.slot.variable_index()
451    }
452
453    pub(crate) fn variable_name(&self) -> &'static str {
454        self.slot.variable_name()
455    }
456}