Skip to main content

solverforge_solver/builder/context/provider/
resolver.rs

1use std::collections::HashSet;
2use std::sync::Arc;
3
4use super::super::{RuntimeScalarSlot, RuntimeScalarSlotId};
5use super::types::ProviderCandidateReason;
6use super::{
7    ProviderNormalizationState, ProviderReasonArena, ProviderResolutionError, RawProviderCandidate,
8    RawProviderEdit, ResolvedProviderCandidate, ResolvedProviderEdit,
9};
10use crate::{RepairCandidate, ScalarCandidate, ScalarEdit};
11
12#[derive(Debug)]
13struct RuntimeProviderSlot<S> {
14    id: RuntimeScalarSlotId,
15    slot: RuntimeScalarSlot<S>,
16}
17
18trait ProviderEditAccess {
19    fn entity_index(&self) -> usize;
20    fn to_value(&self) -> Option<usize>;
21}
22
23impl ProviderEditAccess for RawProviderEdit {
24    fn entity_index(&self) -> usize {
25        self.entity_index
26    }
27
28    fn to_value(&self) -> Option<usize> {
29        self.to_value
30    }
31}
32
33impl<S> ProviderEditAccess for ScalarEdit<S> {
34    fn entity_index(&self) -> usize {
35        self.entity_index()
36    }
37
38    fn to_value(&self) -> Option<usize> {
39        self.to_value()
40    }
41}
42
43impl<S> Clone for RuntimeProviderSlot<S> {
44    fn clone(&self) -> Self {
45        Self {
46            id: self.id.clone(),
47            slot: self.slot.clone(),
48        }
49    }
50}
51
52/// Canonical first-match resolver for raw callback names.
53///
54/// Slot order is exactly `RuntimeModel.variables()` scalar order. An
55/// unqualified edit therefore resolves in the allowed model universe rather
56/// than by scanning mutable host schema state.
57#[derive(Debug)]
58pub struct RuntimeProviderSlotResolver<S> {
59    slots: Vec<RuntimeProviderSlot<S>>,
60}
61
62impl<S> Clone for RuntimeProviderSlotResolver<S> {
63    fn clone(&self) -> Self {
64        Self {
65            slots: self.slots.clone(),
66        }
67    }
68}
69
70impl<S> RuntimeProviderSlotResolver<S> {
71    pub fn new(slots: Vec<RuntimeScalarSlot<S>>) -> Result<Self, String> {
72        let mut seen = HashSet::new();
73        let mut resolved = Vec::with_capacity(slots.len());
74        for slot in slots {
75            let id = slot.id();
76            if !seen.insert((id.descriptor_index, id.variable_index)) {
77                return Err(format!(
78                    "runtime provider registry has duplicate scalar slot {}.{}",
79                    id.entity_class, id.variable_name
80                ));
81            }
82            resolved.push(RuntimeProviderSlot { id, slot });
83        }
84        Ok(Self { slots: resolved })
85    }
86
87    pub fn slot_ids(&self) -> impl Iterator<Item = &RuntimeScalarSlotId> {
88        self.slots.iter().map(|slot| &slot.id)
89    }
90
91    fn resolve_raw_index(
92        &self,
93        edit: &RawProviderEdit,
94        allowed_slots: &[RuntimeScalarSlotId],
95    ) -> Result<usize, ProviderResolutionError> {
96        let matches = |slot: &RuntimeProviderSlot<S>| {
97            edit.entity_class
98                .as_deref()
99                .is_none_or(|entity| entity == slot.id.entity_class.as_ref())
100                && edit.variable_name.as_ref() == slot.id.variable_name.as_ref()
101        };
102        let Some(first_matching_index) = self.slots.iter().position(matches) else {
103            return Err(ProviderResolutionError::UnknownSlot {
104                entity_class: edit.entity_class.clone(),
105                variable_name: Arc::clone(&edit.variable_name),
106            });
107        };
108        let is_allowed = |slot: &RuntimeProviderSlot<S>| {
109            allowed_slots.iter().any(|candidate| {
110                candidate.descriptor_index == slot.id.descriptor_index
111                    && candidate.variable_index == slot.id.variable_index
112            })
113        };
114        let Some(index) = self
115            .slots
116            .iter()
117            .position(|slot| matches(slot) && is_allowed(slot))
118        else {
119            let first_matching_slot = &self.slots[first_matching_index];
120            return Err(ProviderResolutionError::SlotOutsideSelector {
121                entity_class: Arc::clone(&first_matching_slot.id.entity_class),
122                variable_name: Arc::clone(&first_matching_slot.id.variable_name),
123            });
124        };
125        Ok(index)
126    }
127
128    fn resolve_static_index(
129        &self,
130        edit: &ScalarEdit<S>,
131        allowed_slots: &[RuntimeScalarSlotId],
132    ) -> Result<usize, ProviderResolutionError> {
133        let matches = |slot: &RuntimeProviderSlot<S>| {
134            slot.id.descriptor_index == edit.descriptor_index()
135                && slot.id.variable_name.as_ref() == edit.variable_name()
136        };
137        let Some(first_matching_index) = self.slots.iter().position(matches) else {
138            return Err(ProviderResolutionError::UnknownSlot {
139                entity_class: None,
140                variable_name: Arc::from(edit.variable_name()),
141            });
142        };
143        let is_allowed = |slot: &RuntimeProviderSlot<S>| {
144            allowed_slots.iter().any(|candidate| {
145                candidate.descriptor_index == slot.id.descriptor_index
146                    && candidate.variable_index == slot.id.variable_index
147            })
148        };
149        let Some(index) = self
150            .slots
151            .iter()
152            .position(|slot| matches(slot) && is_allowed(slot))
153        else {
154            let first_matching_slot = &self.slots[first_matching_index];
155            return Err(ProviderResolutionError::SlotOutsideSelector {
156                entity_class: Arc::clone(&first_matching_slot.id.entity_class),
157                variable_name: Arc::clone(&first_matching_slot.id.variable_name),
158            });
159        };
160        Ok(index)
161    }
162
163    pub fn resolve_and_normalize(
164        &self,
165        solution: &S,
166        candidates: Vec<RawProviderCandidate>,
167        allowed_slots: &[RuntimeScalarSlotId],
168        reasons: &mut ProviderReasonArena,
169    ) -> Result<Vec<ResolvedProviderCandidate<S>>, ProviderResolutionError> {
170        self.resolve_and_normalize_with_state(
171            solution,
172            candidates,
173            allowed_slots,
174            &mut ProviderNormalizationState::default(),
175            reasons,
176        )
177    }
178
179    /// Resolves raw candidates in returned order while applying deduplication
180    /// in the explicit caller-owned scope.
181    pub fn resolve_and_normalize_with_state(
182        &self,
183        solution: &S,
184        candidates: Vec<RawProviderCandidate>,
185        allowed_slots: &[RuntimeScalarSlotId],
186        state: &mut ProviderNormalizationState,
187        reasons: &mut ProviderReasonArena,
188    ) -> Result<Vec<ResolvedProviderCandidate<S>>, ProviderResolutionError> {
189        self.normalize_candidates(
190            solution,
191            candidates,
192            allowed_slots,
193            state,
194            reasons,
195            |candidate| {
196                (
197                    ProviderCandidateReason::Host(candidate.reason),
198                    candidate.edits,
199                )
200            },
201            |resolver, edit, allowed| resolver.resolve_raw_index(edit, allowed),
202        )
203    }
204
205    pub(super) fn resolve_static_group_and_normalize_with_state(
206        &self,
207        solution: &S,
208        candidates: Vec<ScalarCandidate<S>>,
209        allowed_slots: &[RuntimeScalarSlotId],
210        state: &mut ProviderNormalizationState,
211        reasons: &mut ProviderReasonArena,
212    ) -> Result<Vec<ResolvedProviderCandidate<S>>, ProviderResolutionError> {
213        self.normalize_candidates(
214            solution,
215            candidates,
216            allowed_slots,
217            state,
218            reasons,
219            |candidate| {
220                (
221                    ProviderCandidateReason::Static(candidate.reason()),
222                    candidate.into_edits(),
223                )
224            },
225            |resolver, edit, allowed| resolver.resolve_static_index(edit, allowed),
226        )
227    }
228
229    pub(super) fn resolve_static_repair_and_normalize_with_state(
230        &self,
231        solution: &S,
232        candidates: Vec<RepairCandidate<S>>,
233        allowed_slots: &[RuntimeScalarSlotId],
234        state: &mut ProviderNormalizationState,
235        reasons: &mut ProviderReasonArena,
236    ) -> Result<Vec<ResolvedProviderCandidate<S>>, ProviderResolutionError> {
237        self.normalize_candidates(
238            solution,
239            candidates,
240            allowed_slots,
241            state,
242            reasons,
243            |candidate| {
244                (
245                    ProviderCandidateReason::Static(candidate.reason()),
246                    candidate.into_edits(),
247                )
248            },
249            |resolver, edit, allowed| resolver.resolve_static_index(edit, allowed),
250        )
251    }
252
253    #[allow(clippy::too_many_arguments)]
254    fn normalize_candidates<C, E, Split, Resolve>(
255        &self,
256        solution: &S,
257        candidates: Vec<C>,
258        allowed_slots: &[RuntimeScalarSlotId],
259        state: &mut ProviderNormalizationState,
260        reasons: &mut ProviderReasonArena,
261        mut split: Split,
262        mut resolve: Resolve,
263    ) -> Result<Vec<ResolvedProviderCandidate<S>>, ProviderResolutionError>
264    where
265        E: ProviderEditAccess,
266        Split: FnMut(C) -> (ProviderCandidateReason, Vec<E>),
267        Resolve: FnMut(
268            &RuntimeProviderSlotResolver<S>,
269            &E,
270            &[RuntimeScalarSlotId],
271        ) -> Result<usize, ProviderResolutionError>,
272    {
273        let mut normalized = Vec::new();
274        for candidate in candidates {
275            let (reason, candidate_edits) = split(candidate);
276            if candidate_edits.is_empty() {
277                continue;
278            }
279            let mut edits = Vec::with_capacity(candidate_edits.len());
280            let mut seen_targets = HashSet::new();
281            let mut duplicate_target = false;
282            for edit in candidate_edits {
283                let slot_index = resolve(self, &edit, allowed_slots)?;
284                let slot = &self.slots[slot_index];
285                // Resolve every edit to its frozen numeric target before
286                // duplicate testing. Raw host aliases can therefore never
287                // bypass the same-target rule.
288                if !seen_targets.insert((
289                    slot.id.descriptor_index,
290                    slot.id.variable_index,
291                    edit.entity_index(),
292                )) {
293                    duplicate_target = true;
294                    break;
295                }
296                let entity_index = edit.entity_index();
297                let to_value = edit.to_value();
298                if entity_index >= slot.slot.entity_count(solution) {
299                    return Err(ProviderResolutionError::EntityIndexOutOfBounds {
300                        entity_class: Arc::clone(&slot.id.entity_class),
301                        variable_name: Arc::clone(&slot.id.variable_name),
302                        entity_index,
303                    });
304                }
305                if !slot.slot.value_is_legal(solution, entity_index, to_value) {
306                    return Err(ProviderResolutionError::IllegalValue {
307                        entity_class: Arc::clone(&slot.id.entity_class),
308                        variable_name: Arc::clone(&slot.id.variable_name),
309                        entity_index,
310                        to_value,
311                    });
312                }
313                edits.push(ResolvedProviderEdit {
314                    descriptor_index: slot.id.descriptor_index,
315                    variable_index: slot.id.variable_index,
316                    slot: slot.slot.clone(),
317                    entity_index,
318                    to_value,
319                });
320            }
321            if duplicate_target {
322                continue;
323            }
324            let dedup_edits = edits
325                .iter()
326                .map(|edit| {
327                    (
328                        edit.descriptor_index,
329                        edit.variable_index,
330                        edit.entity_index,
331                        edit.to_value,
332                    )
333                })
334                .collect::<Vec<_>>();
335            let reason = reasons.intern_candidate(reason);
336            if !state.seen_candidates.insert((reason, dedup_edits)) {
337                continue;
338            }
339            normalized.push(ResolvedProviderCandidate { reason, edits });
340        }
341        Ok(normalized)
342    }
343}