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