Skip to main content

presolve_compiler/
instance_context.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    ComponentInstanceId, ComponentInstanceScopeGraph, ComponentRootId, ConsumerEntity, ConsumerId,
7    ContextEntity, ContextId, ProviderEntity, ProviderId, SemanticId, SourceProvenance,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11pub struct ProviderInstanceId {
12    pub provider: ProviderId,
13    pub component_instance: ComponentInstanceId,
14}
15
16impl ProviderInstanceId {
17    #[must_use]
18    pub fn new(provider: ProviderId, component_instance: ComponentInstanceId) -> Self {
19        Self {
20            provider,
21            component_instance,
22        }
23    }
24}
25
26impl std::fmt::Display for ProviderInstanceId {
27    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(
29            formatter,
30            "provider-instance:{}@{}",
31            self.provider, self.component_instance
32        )
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
37pub struct ConsumerInstanceId {
38    pub consumer: ConsumerId,
39    pub component_instance: ComponentInstanceId,
40}
41
42impl ConsumerInstanceId {
43    #[must_use]
44    pub fn new(consumer: ConsumerId, component_instance: ComponentInstanceId) -> Self {
45        Self {
46            consumer,
47            component_instance,
48        }
49    }
50}
51
52impl std::fmt::Display for ConsumerInstanceId {
53    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(
55            formatter,
56            "consumer-instance:{}@{}",
57            self.consumer, self.component_instance
58        )
59    }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63pub struct ContextDefaultSourceInstanceId {
64    pub context: ContextId,
65    pub owner_root: ComponentRootId,
66}
67
68impl std::fmt::Display for ContextDefaultSourceInstanceId {
69    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        write!(
71            formatter,
72            "context-default-instance:{}@{}",
73            self.context, self.owner_root
74        )
75    }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
79pub enum ContextSourceInstanceOwner {
80    Provider(ProviderInstanceId),
81    RootDefault(ContextDefaultSourceInstanceId),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85pub struct ContextSourceInstanceId {
86    pub context: ContextId,
87    pub owner: ContextSourceInstanceOwner,
88}
89
90impl std::fmt::Display for ContextSourceInstanceId {
91    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        write!(formatter, "context-source:{}@", self.context)?;
93        match &self.owner {
94            ContextSourceInstanceOwner::Provider(provider) => provider.fmt(formatter),
95            ContextSourceInstanceOwner::RootDefault(default) => default.fmt(formatter),
96        }
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
101#[serde(transparent)]
102pub struct InstanceContextValueSlotId(String);
103
104impl InstanceContextValueSlotId {
105    #[must_use]
106    pub fn for_source(source: &ContextSourceInstanceId) -> Self {
107        Self(format!("context-instance-slot:{source}"))
108    }
109
110    #[must_use]
111    pub fn as_str(&self) -> &str {
112        &self.0
113    }
114}
115
116impl std::fmt::Display for InstanceContextValueSlotId {
117    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        self.0.fmt(formatter)
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ProviderInstanceRecord {
124    pub id: ProviderInstanceId,
125    pub context: ContextId,
126    pub component: SemanticId,
127    pub provenance: SourceProvenance,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ConsumerInstanceRecord {
132    pub id: ConsumerInstanceId,
133    pub context: Option<ContextId>,
134    pub component: SemanticId,
135    pub provenance: SourceProvenance,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum InstanceContextResolutionStatus {
140    ProviderSelected,
141    ContextDefaultSelected,
142    Unresolved,
143    Ambiguous,
144    InvalidContextReference,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct InstanceContextResolution {
149    pub consumer_instance: ConsumerInstanceId,
150    pub context: Option<ContextId>,
151    pub selected_source: Option<ContextSourceInstanceId>,
152    pub provider_instance: Option<ProviderInstanceId>,
153    pub default_source: Option<ContextDefaultSourceInstanceId>,
154    pub value_slot: Option<InstanceContextValueSlotId>,
155    pub ancestry_distance: Option<u32>,
156    pub candidate_provider_instances: Vec<ProviderInstanceId>,
157    pub status: InstanceContextResolutionStatus,
158    pub provenance: SourceProvenance,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
162pub struct InstanceContextRegistry {
163    pub provider_instances: BTreeMap<ProviderInstanceId, ProviderInstanceRecord>,
164    pub consumer_instances: BTreeMap<ConsumerInstanceId, ConsumerInstanceRecord>,
165    pub default_sources: BTreeSet<ContextDefaultSourceInstanceId>,
166    pub resolutions: BTreeMap<ConsumerInstanceId, InstanceContextResolution>,
167}
168
169impl InstanceContextRegistry {
170    #[must_use]
171    pub fn resolution(&self, consumer: &ConsumerInstanceId) -> Option<&InstanceContextResolution> {
172        self.resolutions.get(consumer)
173    }
174
175    #[must_use]
176    pub fn resolutions_for_declaration(
177        &self,
178        consumer: &ConsumerId,
179    ) -> Vec<&InstanceContextResolution> {
180        self.resolutions
181            .values()
182            .filter(|resolution| &resolution.consumer_instance.consumer == consumer)
183            .collect()
184    }
185}
186
187/// Reproject exact Context declarations through canonical H5 instance ancestry.
188#[must_use]
189pub fn collect_instance_context_registry(
190    scope: &ComponentInstanceScopeGraph,
191    contexts: &BTreeMap<ContextId, ContextEntity>,
192    providers: &BTreeMap<ProviderId, ProviderEntity>,
193    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
194) -> InstanceContextRegistry {
195    let providers_by_component = provider_index(providers);
196    let consumers_by_component = consumer_index(consumers);
197    let mut registry = InstanceContextRegistry::default();
198
199    for node in scope.nodes.values() {
200        for provider in providers.values().filter(|provider| {
201            provider
202                .owner
203                .entity_id()
204                .is_some_and(|owner| owner == &node.component)
205        }) {
206            let id = ProviderInstanceId::new(provider.id.clone(), node.id.clone());
207            registry.provider_instances.insert(
208                id.clone(),
209                ProviderInstanceRecord {
210                    id,
211                    context: provider.context.clone(),
212                    component: node.component.clone(),
213                    provenance: provider.provenance.clone(),
214                },
215            );
216        }
217    }
218
219    for node in scope.nodes.values() {
220        let Some(component_consumers) = consumers_by_component.get(&node.component) else {
221            continue;
222        };
223        for consumer in component_consumers {
224            let id = ConsumerInstanceId::new(consumer.id.clone(), node.id.clone());
225            registry.consumer_instances.insert(
226                id.clone(),
227                ConsumerInstanceRecord {
228                    id: id.clone(),
229                    context: consumer.context().cloned(),
230                    component: node.component.clone(),
231                    provenance: consumer.context_designator.provenance.clone(),
232                },
233            );
234            let resolution = resolve_consumer_instance(
235                id.clone(),
236                consumer,
237                scope,
238                contexts,
239                &providers_by_component,
240            );
241            if let Some(default) = &resolution.default_source {
242                registry.default_sources.insert(default.clone());
243            }
244            registry.resolutions.insert(id, resolution);
245        }
246    }
247
248    registry
249}
250
251#[derive(Debug, Clone)]
252struct IndexedProvider {
253    provider: ProviderId,
254}
255
256fn provider_index(
257    providers: &BTreeMap<ProviderId, ProviderEntity>,
258) -> BTreeMap<(SemanticId, ContextId), Vec<IndexedProvider>> {
259    let mut index = BTreeMap::<(SemanticId, ContextId), Vec<IndexedProvider>>::new();
260    for provider in providers.values() {
261        let Some(component) = provider.owner.entity_id() else {
262            continue;
263        };
264        index
265            .entry((component.clone(), provider.context.clone()))
266            .or_default()
267            .push(IndexedProvider {
268                provider: provider.id.clone(),
269            });
270    }
271    for providers in index.values_mut() {
272        providers.sort_by(|left, right| left.provider.cmp(&right.provider));
273    }
274    index
275}
276
277fn consumer_index(
278    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
279) -> BTreeMap<SemanticId, Vec<&ConsumerEntity>> {
280    let mut index = BTreeMap::<SemanticId, Vec<&ConsumerEntity>>::new();
281    for consumer in consumers.values() {
282        if let Some(component) = consumer.owner.entity_id() {
283            index.entry(component.clone()).or_default().push(consumer);
284        }
285    }
286    for consumers in index.values_mut() {
287        consumers.sort_by(|left, right| left.id.cmp(&right.id));
288    }
289    index
290}
291
292#[allow(clippy::too_many_lines)]
293fn resolve_consumer_instance(
294    consumer_instance: ConsumerInstanceId,
295    consumer: &ConsumerEntity,
296    scope: &ComponentInstanceScopeGraph,
297    contexts: &BTreeMap<ContextId, ContextEntity>,
298    providers_by_component: &BTreeMap<(SemanticId, ContextId), Vec<IndexedProvider>>,
299) -> InstanceContextResolution {
300    let provenance = consumer.context_designator.provenance.clone();
301    let Some(context) = consumer.context().cloned() else {
302        return InstanceContextResolution {
303            consumer_instance,
304            context: None,
305            selected_source: None,
306            provider_instance: None,
307            default_source: None,
308            value_slot: None,
309            ancestry_distance: None,
310            candidate_provider_instances: Vec::new(),
311            status: InstanceContextResolutionStatus::InvalidContextReference,
312            provenance,
313        };
314    };
315    let mut scopes = vec![&consumer_instance.component_instance];
316    scopes.extend(scope.ancestors(&consumer_instance.component_instance));
317
318    for (distance, instance_id) in scopes.into_iter().enumerate() {
319        let Some(node) = scope.node(instance_id) else {
320            continue;
321        };
322        let candidates = providers_by_component
323            .get(&(node.component.clone(), context.clone()))
324            .into_iter()
325            .flatten()
326            .map(|provider| ProviderInstanceId::new(provider.provider.clone(), instance_id.clone()))
327            .collect::<Vec<_>>();
328        match candidates.as_slice() {
329            [] => {}
330            [provider_instance] => {
331                let selected_source = ContextSourceInstanceId {
332                    context: context.clone(),
333                    owner: ContextSourceInstanceOwner::Provider(provider_instance.clone()),
334                };
335                return InstanceContextResolution {
336                    consumer_instance,
337                    context: Some(context),
338                    selected_source: Some(selected_source.clone()),
339                    provider_instance: Some(provider_instance.clone()),
340                    default_source: None,
341                    value_slot: Some(InstanceContextValueSlotId::for_source(&selected_source)),
342                    ancestry_distance: Some(
343                        u32::try_from(distance).expect("instance ancestry depth fits in u32"),
344                    ),
345                    candidate_provider_instances: candidates,
346                    status: InstanceContextResolutionStatus::ProviderSelected,
347                    provenance,
348                };
349            }
350            _ => {
351                return InstanceContextResolution {
352                    consumer_instance,
353                    context: Some(context),
354                    selected_source: None,
355                    provider_instance: None,
356                    default_source: None,
357                    value_slot: None,
358                    ancestry_distance: Some(
359                        u32::try_from(distance).expect("instance ancestry depth fits in u32"),
360                    ),
361                    candidate_provider_instances: candidates,
362                    status: InstanceContextResolutionStatus::Ambiguous,
363                    provenance,
364                };
365            }
366        }
367    }
368
369    let owner_root = scope
370        .node(&consumer_instance.component_instance)
371        .expect("consumer instances belong to scope nodes")
372        .owner_root
373        .clone();
374    if contexts
375        .get(&context)
376        .is_some_and(|context| context.default_expression.is_some())
377    {
378        let default_source = ContextDefaultSourceInstanceId {
379            context: context.clone(),
380            owner_root,
381        };
382        let selected_source = ContextSourceInstanceId {
383            context: context.clone(),
384            owner: ContextSourceInstanceOwner::RootDefault(default_source.clone()),
385        };
386        return InstanceContextResolution {
387            consumer_instance,
388            context: Some(context),
389            selected_source: Some(selected_source.clone()),
390            provider_instance: None,
391            default_source: Some(default_source),
392            value_slot: Some(InstanceContextValueSlotId::for_source(&selected_source)),
393            ancestry_distance: None,
394            candidate_provider_instances: Vec::new(),
395            status: InstanceContextResolutionStatus::ContextDefaultSelected,
396            provenance,
397        };
398    }
399
400    InstanceContextResolution {
401        consumer_instance,
402        context: Some(context),
403        selected_source: None,
404        provider_instance: None,
405        default_source: None,
406        value_slot: None,
407        ancestry_distance: None,
408        candidate_provider_instances: Vec::new(),
409        status: InstanceContextResolutionStatus::Unresolved,
410        provenance,
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use crate::{
417        build_application_semantic_model, validate_application_semantic_model,
418        ContextResolutionResult, InstanceContextResolutionStatus,
419    };
420
421    #[test]
422    fn resolves_repeated_consumer_definitions_to_distinct_provider_instances() {
423        let asm = build_application_semantic_model(&presolve_parser::parse_file(
424            "src/InstanceContext.tsx",
425            r#"
426@component("x-theme") class Theme extends Component {
427  @context() color!: string;
428  render() { return <div />; }
429}
430@component("x-leaf") class Leaf extends Component {
431  @consume(Theme.color) color!: string;
432  render() { return <span />; }
433}
434@component("x-light") class Light extends Component {
435  @provide(Theme.color) color: string = "light";
436  render() { return <Leaf />; }
437}
438@component("x-dark") class Dark extends Component {
439  @provide(Theme.color) color: string = "dark";
440  render() { return <Leaf />; }
441}
442@component("x-page") @route("/") class Page extends Component {
443  render() { return <main><Light /><Dark /></main>; }
444}
445"#,
446        ));
447        let consumer = asm.consumers().first().unwrap().id.clone();
448        let resolutions = asm.instance_context.resolutions_for_declaration(&consumer);
449
450        assert_eq!(resolutions.len(), 2);
451        assert!(resolutions.iter().all(|resolution| {
452            resolution.status == InstanceContextResolutionStatus::ProviderSelected
453                && resolution.ancestry_distance == Some(1)
454                && resolution.value_slot.is_some()
455        }));
456        assert_ne!(
457            resolutions[0].provider_instance,
458            resolutions[1].provider_instance
459        );
460        assert_ne!(
461            resolutions[0].selected_source,
462            resolutions[1].selected_source
463        );
464        assert_ne!(resolutions[0].value_slot, resolutions[1].value_slot);
465        assert!(matches!(
466            asm.context_resolution(&consumer).unwrap().result,
467            ContextResolutionResult::Unresolved
468        ));
469    }
470
471    #[test]
472    fn uses_root_qualified_defaults_and_never_reselects_for_type_failure() {
473        let default_asm = build_application_semantic_model(&presolve_parser::parse_file(
474            "src/Defaults.tsx",
475            r#"
476@component("x-theme") class Theme extends Component {
477  @context() color: string = "blue";
478  render() { return <div />; }
479}
480@component("x-leaf") class Leaf extends Component {
481  @consume(Theme.color) color!: string;
482  render() { return <span />; }
483}
484@component("x-a") @route("/a") class A extends Component { render() { return <Leaf />; } }
485@component("x-b") @route("/b") class B extends Component { render() { return <Leaf />; } }
486"#,
487        ));
488        let defaults = default_asm
489            .instance_context
490            .resolutions
491            .values()
492            .filter_map(|resolution| resolution.default_source.as_ref())
493            .collect::<Vec<_>>();
494        assert_eq!(defaults.len(), 2);
495        assert_ne!(defaults[0].owner_root, defaults[1].owner_root);
496
497        let incompatible = build_application_semantic_model(&presolve_parser::parse_file(
498            "src/Incompatible.tsx",
499            r#"
500@component("x-theme") class Theme extends Component {
501  @context() color!: string;
502  render() { return <div />; }
503}
504@component("x-leaf") class Leaf extends Component {
505  @consume(Theme.color) color!: string;
506  render() { return <span />; }
507}
508@component("x-page") @route("/") class Page extends Component {
509  @provide(Theme.color) color: number = 1;
510  render() { return <Leaf />; }
511}
512"#,
513        ));
514        let resolution = incompatible
515            .instance_context
516            .resolutions
517            .values()
518            .next()
519            .unwrap();
520        assert_eq!(
521            resolution.status,
522            InstanceContextResolutionStatus::ProviderSelected
523        );
524        assert!(resolution.provider_instance.is_some());
525        assert!(resolution.value_slot.is_some());
526    }
527
528    #[test]
529    fn asm_validation_rejects_noncanonical_instance_context_resolution() {
530        let mut asm = build_application_semantic_model(&presolve_parser::parse_file(
531            "src/Validate.tsx",
532            r#"
533@component("x-theme") class Theme extends Component {
534  @context() color: string = "blue";
535  render() { return <div />; }
536}
537@component("x-page")
538@route("/")
539class Page extends Component {
540  @consume(Theme.color) color!: string;
541  render() { return <main />; }
542}
543"#,
544        ));
545        assert!(validate_application_semantic_model(&asm).is_empty());
546
547        asm.instance_context
548            .resolutions
549            .values_mut()
550            .next()
551            .unwrap()
552            .status = InstanceContextResolutionStatus::Unresolved;
553        let diagnostics = validate_application_semantic_model(&asm);
554        assert!(diagnostics.iter().any(|diagnostic| {
555            diagnostic.code == "PSASM1194"
556                && diagnostic.message
557                    == "instance Context registry does not match canonical declarations and H5 ancestry"
558        }));
559    }
560}