Skip to main content

presolve_compiler/
context_lifetime.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    ComponentNode, ComponentScopeGraph, ComputedValue, ConsumerEntity, ConsumerId,
5    ContextDependencyEdgeKind, ContextDependencyGraph, ContextDependencyNodeId, ContextEntity,
6    ContextId, ContextOwnershipGraph, ContextResolution, ContextResolutionResult, ProviderEntity,
7    ProviderId, SemanticId, SourceProvenance,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub struct ContextLifetimeId {
12    pub component: SemanticId,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16pub enum ContextLifetimeEntityId {
17    Context(ContextId),
18    Provider(ProviderId),
19    Consumer(ConsumerId),
20    ContextDefault(ContextId),
21    State(SemanticId),
22    Computed(SemanticId),
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum LifetimeCompatibilityStatus {
27    Compatible,
28    Incompatible,
29    Unknown,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ContextBindingLifetimeStatus {
34    Compatible,
35    Incompatible,
36    Unknown,
37    Unresolved,
38    Ambiguous,
39    InvalidContextReference,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ContextEntityLifetimeRecord {
44    pub entity: ContextLifetimeEntityId,
45    pub owner_component: SemanticId,
46    pub lifetime: ContextLifetimeId,
47    pub provenance: SourceProvenance,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct ContextDependencyLifetimeRecord {
52    pub dependent: ContextDependencyNodeId,
53    pub dependency: ContextDependencyNodeId,
54    pub dependent_owner: Option<SemanticId>,
55    pub dependency_owner: Option<SemanticId>,
56    pub compatibility: LifetimeCompatibilityStatus,
57    pub provenance: SourceProvenance,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ProviderLifetimeRecord {
62    pub provider: ProviderId,
63    pub owner_component: SemanticId,
64    pub lifetime: ContextLifetimeId,
65    pub compatibility: LifetimeCompatibilityStatus,
66    pub provenance: SourceProvenance,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ContextDefaultLifetimeRecord {
71    pub context: ContextId,
72    pub owner_component: SemanticId,
73    pub lifetime: ContextLifetimeId,
74    pub compatibility: LifetimeCompatibilityStatus,
75    pub provenance: SourceProvenance,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum ContextBindingLifetimeSource {
80    Provider(ProviderId),
81    ContextDefault(ContextId),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct ContextBindingLifetimeRecord {
86    pub consumer: ConsumerId,
87    pub consumer_owner: Option<SemanticId>,
88    pub resolution: ContextResolutionResult,
89    pub source: Option<ContextBindingLifetimeSource>,
90    pub source_owner: Option<SemanticId>,
91    pub compatibility: ContextBindingLifetimeStatus,
92    pub distance: Option<u32>,
93    pub provenance: SourceProvenance,
94}
95
96/// Immutable G8 component-scope availability facts.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ContextLifetimeAnalysis {
99    pub entities: BTreeMap<ContextLifetimeEntityId, ContextEntityLifetimeRecord>,
100    pub provider_lifetimes: BTreeMap<ProviderId, ProviderLifetimeRecord>,
101    pub context_default_lifetimes: BTreeMap<ContextId, ContextDefaultLifetimeRecord>,
102    pub dependency_lifetimes: Vec<ContextDependencyLifetimeRecord>,
103    pub binding_lifetimes: BTreeMap<ConsumerId, ContextBindingLifetimeRecord>,
104    dependency_index: BTreeMap<ContextDependencyNodeId, Vec<ContextDependencyLifetimeRecord>>,
105}
106
107impl ContextLifetimeAnalysis {
108    #[must_use]
109    pub fn context_entity_lifetime(
110        &self,
111        entity: &ContextLifetimeEntityId,
112    ) -> Option<&ContextEntityLifetimeRecord> {
113        self.entities.get(entity)
114    }
115
116    #[must_use]
117    pub fn provider_lifetime(&self, provider: &ProviderId) -> Option<&ProviderLifetimeRecord> {
118        self.provider_lifetimes.get(provider)
119    }
120
121    #[must_use]
122    pub fn context_default_lifetime(
123        &self,
124        context: &ContextId,
125    ) -> Option<&ContextDefaultLifetimeRecord> {
126        self.context_default_lifetimes.get(context)
127    }
128
129    #[must_use]
130    pub fn context_binding_lifetime(
131        &self,
132        consumer: &ConsumerId,
133    ) -> Option<&ContextBindingLifetimeRecord> {
134        self.binding_lifetimes.get(consumer)
135    }
136
137    #[must_use]
138    pub fn lifetime_dependencies_of(
139        &self,
140        node: &ContextDependencyNodeId,
141    ) -> &[ContextDependencyLifetimeRecord] {
142        self.dependency_index.get(node).map_or(&[], Vec::as_slice)
143    }
144
145    #[must_use]
146    pub fn runtime_lifetime_eligible(&self, consumer: &ConsumerId) -> bool {
147        self.context_binding_lifetime(consumer)
148            .is_some_and(|record| record.compatibility == ContextBindingLifetimeStatus::Compatible)
149    }
150}
151
152/// Derives G8 availability strictly from canonical ownership, G4 scope and
153/// resolution, and G7 direct dependencies.
154#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
155#[must_use]
156pub fn collect_context_lifetime_analysis(
157    components: &[ComponentNode],
158    contexts: &BTreeMap<ContextId, ContextEntity>,
159    providers: &BTreeMap<ProviderId, ProviderEntity>,
160    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
161    computed_values: &BTreeMap<SemanticId, ComputedValue>,
162    ownership: &ContextOwnershipGraph,
163    scope: &ComponentScopeGraph,
164    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
165    dependencies: &ContextDependencyGraph,
166    provenance: &BTreeMap<SemanticId, SourceProvenance>,
167) -> ContextLifetimeAnalysis {
168    let mut entities = BTreeMap::new();
169    for context in contexts.values() {
170        insert_entity_lifetime(
171            &mut entities,
172            ContextLifetimeEntityId::Context(context.id.clone()),
173            ownership.owner_of_context(&context.id).cloned(),
174            context.provenance.clone(),
175        );
176        if let Some(default) = &context.default_expression {
177            let default_provenance = default_provenance(context, dependencies, provenance, default);
178            insert_entity_lifetime(
179                &mut entities,
180                ContextLifetimeEntityId::ContextDefault(context.id.clone()),
181                ownership.owner_of_context(&context.id).cloned(),
182                default_provenance,
183            );
184        }
185    }
186    for provider in providers.values() {
187        insert_entity_lifetime(
188            &mut entities,
189            ContextLifetimeEntityId::Provider(provider.id.clone()),
190            ownership.owner_of_provider(&provider.id).cloned(),
191            provider.provenance.clone(),
192        );
193    }
194    for consumer in consumers.values() {
195        insert_entity_lifetime(
196            &mut entities,
197            ContextLifetimeEntityId::Consumer(consumer.id.clone()),
198            ownership.owner_of_consumer(&consumer.id).cloned(),
199            consumer.provenance.clone(),
200        );
201    }
202    for node in &dependencies.nodes {
203        match &node.id {
204            ContextDependencyNodeId::State(id) => {
205                if let Some(provenance) = provenance.get(id).cloned() {
206                    insert_entity_lifetime(
207                        &mut entities,
208                        ContextLifetimeEntityId::State(id.clone()),
209                        state_owner(components, id),
210                        provenance,
211                    );
212                }
213            }
214            ContextDependencyNodeId::Computed(id) => {
215                if let Some(computed) = computed_values.get(id) {
216                    insert_entity_lifetime(
217                        &mut entities,
218                        ContextLifetimeEntityId::Computed(id.clone()),
219                        computed.owner.entity_id().cloned(),
220                        computed.provenance.clone(),
221                    );
222                }
223            }
224            _ => {}
225        }
226    }
227
228    let mut dependency_lifetimes = dependencies
229        .edges
230        .iter()
231        .filter(|edge| {
232            matches!(
233                edge.kind,
234                ContextDependencyEdgeKind::ProviderReadsState
235                    | ContextDependencyEdgeKind::ProviderReadsComputed
236                    | ContextDependencyEdgeKind::ContextDefaultReadsState
237                    | ContextDependencyEdgeKind::ContextDefaultReadsComputed
238            )
239        })
240        .map(|edge| {
241            let dependent_owner = lifetime_owner_for_node(&entities, &edge.dependent);
242            let dependency_owner = lifetime_owner_for_node(&entities, &edge.dependency);
243            ContextDependencyLifetimeRecord {
244                dependent: edge.dependent.clone(),
245                dependency: edge.dependency.clone(),
246                dependent_owner: dependent_owner.clone(),
247                dependency_owner: dependency_owner.clone(),
248                compatibility: compare_owners(scope, dependency_owner, dependent_owner),
249                provenance: edge.provenance.clone(),
250            }
251        })
252        .collect::<Vec<_>>();
253    dependency_lifetimes.sort_by(|left, right| {
254        (&left.dependent, &left.dependency).cmp(&(&right.dependent, &right.dependency))
255    });
256
257    let provider_lifetimes = providers
258        .values()
259        .filter_map(|provider| {
260            let owner = ownership.owner_of_provider(&provider.id)?.clone();
261            let status = aggregate_lifetime(dependency_lifetimes.iter().filter(|record| {
262                record.dependent == ContextDependencyNodeId::Provider(provider.id.clone())
263            }));
264            Some((
265                provider.id.clone(),
266                ProviderLifetimeRecord {
267                    provider: provider.id.clone(),
268                    owner_component: owner.clone(),
269                    lifetime: ContextLifetimeId { component: owner },
270                    compatibility: status,
271                    provenance: provider.provenance.clone(),
272                },
273            ))
274        })
275        .collect::<BTreeMap<_, _>>();
276    let context_default_lifetimes = contexts
277        .values()
278        .filter_map(|context| {
279            let default = context.default_expression.as_ref()?;
280            let owner = ownership.owner_of_context(&context.id)?.clone();
281            let status = aggregate_lifetime(dependency_lifetimes.iter().filter(|record| {
282                record.dependent == ContextDependencyNodeId::ContextDefault(context.id.clone())
283            }));
284            Some((
285                context.id.clone(),
286                ContextDefaultLifetimeRecord {
287                    context: context.id.clone(),
288                    owner_component: owner.clone(),
289                    lifetime: ContextLifetimeId { component: owner },
290                    compatibility: status,
291                    provenance: default_provenance(context, dependencies, provenance, default),
292                },
293            ))
294        })
295        .collect::<BTreeMap<_, _>>();
296    let binding_lifetimes = consumers
297        .values()
298        .filter_map(|consumer| {
299            let resolution = resolutions.get(&consumer.id)?;
300            let consumer_owner = ownership.owner_of_consumer(&consumer.id).cloned();
301            let record = binding_lifetime_record(
302                consumer,
303                resolution,
304                consumer_owner,
305                &provider_lifetimes,
306                &context_default_lifetimes,
307                scope,
308            );
309            Some((consumer.id.clone(), record))
310        })
311        .collect::<BTreeMap<_, _>>();
312    let mut dependency_index = BTreeMap::new();
313    for record in &dependency_lifetimes {
314        dependency_index
315            .entry(record.dependent.clone())
316            .or_insert_with(Vec::new)
317            .push(record.clone());
318    }
319    ContextLifetimeAnalysis {
320        entities,
321        provider_lifetimes,
322        context_default_lifetimes,
323        dependency_lifetimes,
324        binding_lifetimes,
325        dependency_index,
326    }
327}
328
329fn default_provenance(
330    context: &ContextEntity,
331    dependencies: &ContextDependencyGraph,
332    provenance: &BTreeMap<SemanticId, SourceProvenance>,
333    default: &SemanticId,
334) -> SourceProvenance {
335    dependencies
336        .edges
337        .iter()
338        .find(|edge| {
339            edge.kind == ContextDependencyEdgeKind::ContextDefaultSuppliesContext
340                && edge.dependent == ContextDependencyNodeId::ContextDefault(context.id.clone())
341        })
342        .map_or_else(
343            || {
344                provenance
345                    .get(default)
346                    .cloned()
347                    .unwrap_or_else(|| context.provenance.clone())
348            },
349            |edge| edge.provenance.clone(),
350        )
351}
352
353fn insert_entity_lifetime(
354    entities: &mut BTreeMap<ContextLifetimeEntityId, ContextEntityLifetimeRecord>,
355    entity: ContextLifetimeEntityId,
356    owner_component: Option<SemanticId>,
357    provenance: SourceProvenance,
358) {
359    let Some(owner_component) = owner_component else {
360        return;
361    };
362    entities.insert(
363        entity.clone(),
364        ContextEntityLifetimeRecord {
365            entity,
366            lifetime: ContextLifetimeId {
367                component: owner_component.clone(),
368            },
369            owner_component,
370            provenance,
371        },
372    );
373}
374
375fn state_owner(components: &[ComponentNode], state: &SemanticId) -> Option<SemanticId> {
376    components.iter().find_map(|component| {
377        component
378            .state_fields
379            .iter()
380            .any(|field| field.id == *state)
381            .then_some(component.id.clone())
382    })
383}
384
385fn lifetime_owner_for_node(
386    entities: &BTreeMap<ContextLifetimeEntityId, ContextEntityLifetimeRecord>,
387    node: &ContextDependencyNodeId,
388) -> Option<SemanticId> {
389    let entity = match node {
390        ContextDependencyNodeId::State(id) => ContextLifetimeEntityId::State(id.clone()),
391        ContextDependencyNodeId::Computed(id) => ContextLifetimeEntityId::Computed(id.clone()),
392        ContextDependencyNodeId::Context(id) => ContextLifetimeEntityId::Context(id.clone()),
393        ContextDependencyNodeId::ContextDefault(id) => {
394            ContextLifetimeEntityId::ContextDefault(id.clone())
395        }
396        ContextDependencyNodeId::Provider(id) => ContextLifetimeEntityId::Provider(id.clone()),
397        ContextDependencyNodeId::Consumer(id) => ContextLifetimeEntityId::Consumer(id.clone()),
398    };
399    entities
400        .get(&entity)
401        .map(|record| record.owner_component.clone())
402}
403
404fn compare_owners(
405    scope: &ComponentScopeGraph,
406    source: Option<SemanticId>,
407    dependent: Option<SemanticId>,
408) -> LifetimeCompatibilityStatus {
409    let (Some(source), Some(dependent)) = (source, dependent) else {
410        return LifetimeCompatibilityStatus::Unknown;
411    };
412    if !scope.components.contains(&source) || !scope.components.contains(&dependent) {
413        return LifetimeCompatibilityStatus::Unknown;
414    }
415    if scope.ancestor_chain(&dependent).contains(&source) {
416        LifetimeCompatibilityStatus::Compatible
417    } else {
418        LifetimeCompatibilityStatus::Incompatible
419    }
420}
421
422fn aggregate_lifetime<'a>(
423    records: impl Iterator<Item = &'a ContextDependencyLifetimeRecord>,
424) -> LifetimeCompatibilityStatus {
425    let mut status = LifetimeCompatibilityStatus::Compatible;
426    for record in records {
427        match record.compatibility {
428            LifetimeCompatibilityStatus::Incompatible => {
429                return LifetimeCompatibilityStatus::Incompatible
430            }
431            LifetimeCompatibilityStatus::Unknown => status = LifetimeCompatibilityStatus::Unknown,
432            LifetimeCompatibilityStatus::Compatible => {}
433        }
434    }
435    status
436}
437
438fn binding_lifetime_record(
439    consumer: &ConsumerEntity,
440    resolution: &ContextResolution,
441    consumer_owner: Option<SemanticId>,
442    providers: &BTreeMap<ProviderId, ProviderLifetimeRecord>,
443    defaults: &BTreeMap<ContextId, ContextDefaultLifetimeRecord>,
444    scope: &ComponentScopeGraph,
445) -> ContextBindingLifetimeRecord {
446    let (source, source_owner, source_status, distance, compatibility) = match &resolution.result {
447        ContextResolutionResult::Provider {
448            provider, distance, ..
449        } => {
450            let source = providers.get(provider);
451            let owner = source.map(|record| record.owner_component.clone());
452            let scope_status = compare_owners(scope, owner.clone(), consumer_owner.clone());
453            (
454                Some(ContextBindingLifetimeSource::Provider(provider.clone())),
455                owner,
456                source.map(|record| record.compatibility),
457                Some(*distance),
458                combine_binding_status(scope_status, source.map(|record| record.compatibility)),
459            )
460        }
461        ContextResolutionResult::ContextDefault { context, .. } => {
462            let source = defaults.get(context);
463            let owner = source.map(|record| record.owner_component.clone());
464            let scope_status = compare_owners(scope, owner.clone(), consumer_owner.clone());
465            let distance = owner.as_ref().and_then(|owner| {
466                consumer_owner.as_ref().and_then(|consumer_owner| {
467                    scope
468                        .ancestor_chain(consumer_owner)
469                        .iter()
470                        .position(|candidate| candidate == owner)
471                        .and_then(|index| u32::try_from(index).ok())
472                })
473            });
474            (
475                Some(ContextBindingLifetimeSource::ContextDefault(
476                    context.clone(),
477                )),
478                owner,
479                source.map(|record| record.compatibility),
480                distance,
481                combine_binding_status(scope_status, source.map(|record| record.compatibility)),
482            )
483        }
484        ContextResolutionResult::Unresolved => (
485            None,
486            None,
487            None,
488            None,
489            ContextBindingLifetimeStatus::Unresolved,
490        ),
491        ContextResolutionResult::Ambiguous { .. } => (
492            None,
493            None,
494            None,
495            None,
496            ContextBindingLifetimeStatus::Ambiguous,
497        ),
498        ContextResolutionResult::InvalidContextReference => (
499            None,
500            None,
501            None,
502            None,
503            ContextBindingLifetimeStatus::InvalidContextReference,
504        ),
505    };
506    let _ = source_status;
507    ContextBindingLifetimeRecord {
508        consumer: consumer.id.clone(),
509        consumer_owner,
510        resolution: resolution.result.clone(),
511        source,
512        source_owner,
513        compatibility,
514        distance,
515        provenance: consumer.context_designator.provenance.clone(),
516    }
517}
518
519fn combine_binding_status(
520    scope_status: LifetimeCompatibilityStatus,
521    source_status: Option<LifetimeCompatibilityStatus>,
522) -> ContextBindingLifetimeStatus {
523    let source_status = source_status.unwrap_or(LifetimeCompatibilityStatus::Unknown);
524    if scope_status == LifetimeCompatibilityStatus::Incompatible
525        || source_status == LifetimeCompatibilityStatus::Incompatible
526    {
527        ContextBindingLifetimeStatus::Incompatible
528    } else if scope_status == LifetimeCompatibilityStatus::Unknown
529        || source_status == LifetimeCompatibilityStatus::Unknown
530    {
531        ContextBindingLifetimeStatus::Unknown
532    } else {
533        ContextBindingLifetimeStatus::Compatible
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use std::collections::BTreeMap;
540
541    use super::compare_owners;
542    use crate::{
543        build_application_semantic_model, build_component_graph, ComponentScopeGraph, ConsumerId,
544        ContextBindingLifetimeStatus, ContextDependencyNodeId, LifetimeCompatibilityStatus,
545        ProviderId,
546    };
547
548    #[test]
549    fn retains_type_incompatible_but_lifetime_compatible_selected_bindings() {
550        let asm = build_application_semantic_model(&presolve_parser::parse_file(
551            "src/app.tsx",
552            r#"
553@component("x-app")
554class App extends Component {
555  selected = state("dark");
556  @context()
557  theme!: number;
558  @provide(App.theme)
559  providedTheme: string = this.selected;
560  @consume(App.theme)
561  themeValue!: number;
562  render() { return <main />; }
563}
564"#,
565        ));
566        let component = &asm.components[0].id;
567        let provider = ProviderId::for_component(component, "providedTheme");
568        let consumer = ConsumerId::for_component(component, "themeValue");
569        let analysis = asm.context_lifetime_analysis();
570
571        assert_eq!(
572            analysis.provider_lifetime(&provider).unwrap().compatibility,
573            LifetimeCompatibilityStatus::Compatible
574        );
575        assert_eq!(
576            analysis
577                .lifetime_dependencies_of(&ContextDependencyNodeId::Provider(provider.clone()))
578                .first()
579                .unwrap()
580                .compatibility,
581            LifetimeCompatibilityStatus::Compatible
582        );
583        assert_eq!(
584            analysis
585                .context_binding_lifetime(&consumer)
586                .unwrap()
587                .compatibility,
588            ContextBindingLifetimeStatus::Compatible
589        );
590        assert!(analysis.runtime_lifetime_eligible(&consumer));
591    }
592
593    #[test]
594    fn retains_default_and_unresolved_binding_statuses() {
595        let asm = build_application_semantic_model(&presolve_parser::parse_file(
596            "src/app.tsx",
597            r#"
598@component("x-app")
599class App extends Component {
600  @context()
601  locale: string = "en";
602  @context()
603  mode!: string;
604  @consume(App.locale)
605  selectedLocale!: string;
606  render() { return <main />; }
607}
608@component("x-toolbar")
609class Toolbar extends Component {
610  @consume(App.mode)
611  mode!: string;
612  render() { return <main />; }
613}
614"#,
615        ));
616        let selected = ConsumerId::for_component(&asm.components[0].id, "selectedLocale");
617        let unresolved = ConsumerId::for_component(&asm.components[1].id, "mode");
618        let analysis = asm.context_lifetime_analysis();
619
620        assert_eq!(
621            analysis
622                .context_binding_lifetime(&selected)
623                .unwrap()
624                .compatibility,
625            ContextBindingLifetimeStatus::Compatible
626        );
627        assert_eq!(
628            analysis
629                .context_binding_lifetime(&unresolved)
630                .unwrap()
631                .compatibility,
632            ContextBindingLifetimeStatus::Unresolved
633        );
634    }
635
636    #[test]
637    fn uses_only_canonical_scope_ancestry_for_outlives() {
638        let parsed = presolve_parser::parse_file(
639            "src/components.tsx",
640            r#"
641@component("x-parent")
642class Parent extends Component { render() { return <main />; } }
643@component("x-child")
644class Child extends Component { render() { return <main />; } }
645"#,
646        );
647        let components = build_component_graph(&parsed).components;
648        let parent = components[0].id.clone();
649        let child = components[1].id.clone();
650        let scope = ComponentScopeGraph::with_parent_relations(
651            &components,
652            BTreeMap::from([(child.clone(), parent.clone())]),
653        );
654
655        assert_eq!(
656            compare_owners(&scope, Some(parent.clone()), Some(child.clone())),
657            LifetimeCompatibilityStatus::Compatible
658        );
659        assert_eq!(
660            compare_owners(&scope, Some(child), Some(parent)),
661            LifetimeCompatibilityStatus::Incompatible
662        );
663    }
664}