Skip to main content

presolve_compiler/
context_dependency.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    CompatibilityStatus, ComponentNode, ComputedValue, ConsumerEntity, ConsumerId,
5    ContextBindingCompatibility, ContextBindingTypeRecord, ContextEntity, ContextId,
6    ContextResolution, ContextResolutionResult, ContextTypeRecord, ExpressionGraph,
7    ExpressionNodeKind, ProviderEntity, ProviderId, ProviderTypeRecord, SemanticId,
8    SourceProvenance,
9};
10
11/// Typed direct-value node identity for the immutable G7 dependency graph.
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
13pub enum ContextDependencyNodeId {
14    State(SemanticId),
15    Computed(SemanticId),
16    Context(ContextId),
17    ContextDefault(ContextId),
18    Provider(ProviderId),
19    Consumer(ConsumerId),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
23pub enum ContextDependencyNodeKind {
24    State,
25    Computed,
26    Context,
27    ContextDefault,
28    Provider,
29    Consumer,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33pub struct ContextDependencyNode {
34    pub id: ContextDependencyNodeId,
35    pub kind: ContextDependencyNodeKind,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
39pub enum ContextDependencyEdgeKind {
40    ProviderReadsState,
41    ProviderReadsComputed,
42    ContextDefaultReadsState,
43    ContextDefaultReadsComputed,
44    ProviderSuppliesContext,
45    ContextDefaultSuppliesContext,
46    ConsumerReadsProvider,
47    ConsumerReadsContextDefault,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ContextDependencyCompatibility {
52    Compatible,
53    Incompatible,
54    Unknown,
55    NotApplicable,
56}
57
58/// One direct, compiler-resolved value-flow relation. The edge points from the
59/// dependent product to the prerequisite product.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ContextDependencyEdge {
62    pub dependent: ContextDependencyNodeId,
63    pub dependency: ContextDependencyNodeId,
64    pub kind: ContextDependencyEdgeKind,
65    pub compatibility: ContextDependencyCompatibility,
66    pub provenance: SourceProvenance,
67}
68
69/// Immutable compiler-owned direct Context dependency topology.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct ContextDependencyGraph {
72    pub nodes: Vec<ContextDependencyNode>,
73    pub edges: Vec<ContextDependencyEdge>,
74    dependencies_by_node: BTreeMap<ContextDependencyNodeId, Vec<ContextDependencyNodeId>>,
75    dependents_by_node: BTreeMap<ContextDependencyNodeId, Vec<ContextDependencyNodeId>>,
76    provider_value_dependencies: BTreeMap<ProviderId, Vec<ContextDependencyNodeId>>,
77    context_default_dependencies: BTreeMap<ContextId, Vec<ContextDependencyNodeId>>,
78    consumer_binding_dependencies: BTreeMap<ConsumerId, ContextDependencyNodeId>,
79    consumers_by_provider: BTreeMap<ProviderId, Vec<ConsumerId>>,
80    consumers_by_default: BTreeMap<ContextId, Vec<ConsumerId>>,
81}
82
83impl ContextDependencyGraph {
84    #[must_use]
85    pub fn direct_dependencies(
86        &self,
87        node: &ContextDependencyNodeId,
88    ) -> &[ContextDependencyNodeId] {
89        self.dependencies_by_node
90            .get(node)
91            .map_or(&[], Vec::as_slice)
92    }
93
94    #[must_use]
95    pub fn direct_dependents(&self, node: &ContextDependencyNodeId) -> &[ContextDependencyNodeId] {
96        self.dependents_by_node.get(node).map_or(&[], Vec::as_slice)
97    }
98
99    #[must_use]
100    pub fn provider_value_dependencies(&self, provider: &ProviderId) -> &[ContextDependencyNodeId] {
101        self.provider_value_dependencies
102            .get(provider)
103            .map_or(&[], Vec::as_slice)
104    }
105
106    #[must_use]
107    pub fn context_default_dependencies(&self, context: &ContextId) -> &[ContextDependencyNodeId] {
108        self.context_default_dependencies
109            .get(context)
110            .map_or(&[], Vec::as_slice)
111    }
112
113    #[must_use]
114    pub fn consumer_binding_dependency(
115        &self,
116        consumer: &ConsumerId,
117    ) -> Option<&ContextDependencyNodeId> {
118        self.consumer_binding_dependencies.get(consumer)
119    }
120
121    #[must_use]
122    pub fn consumers_bound_to_provider(&self, provider: &ProviderId) -> &[ConsumerId] {
123        self.consumers_by_provider
124            .get(provider)
125            .map_or(&[], Vec::as_slice)
126    }
127
128    #[must_use]
129    pub fn consumers_bound_to_default(&self, context: &ContextId) -> &[ConsumerId] {
130        self.consumers_by_default
131            .get(context)
132            .map_or(&[], Vec::as_slice)
133    }
134}
135
136/// Projects canonical G1--G5 products into direct Context value-flow facts.
137#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
138#[must_use]
139pub fn collect_context_dependency_graph(
140    components: &[ComponentNode],
141    contexts: &BTreeMap<ContextId, ContextEntity>,
142    providers: &BTreeMap<ProviderId, ProviderEntity>,
143    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
144    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
145    context_types: &BTreeMap<ContextId, ContextTypeRecord>,
146    provider_types: &BTreeMap<ProviderId, ProviderTypeRecord>,
147    binding_types: &BTreeMap<ConsumerId, ContextBindingTypeRecord>,
148    computed_values: &BTreeMap<SemanticId, ComputedValue>,
149    expression_graph: &ExpressionGraph,
150) -> ContextDependencyGraph {
151    let mut nodes = contexts
152        .keys()
153        .cloned()
154        .map(|id| ContextDependencyNode {
155            id: ContextDependencyNodeId::Context(id),
156            kind: ContextDependencyNodeKind::Context,
157        })
158        .chain(contexts.values().filter_map(|context| {
159            context
160                .default_expression
161                .as_ref()
162                .map(|_| ContextDependencyNode {
163                    id: ContextDependencyNodeId::ContextDefault(context.id.clone()),
164                    kind: ContextDependencyNodeKind::ContextDefault,
165                })
166        }))
167        .chain(providers.keys().cloned().map(|id| ContextDependencyNode {
168            id: ContextDependencyNodeId::Provider(id),
169            kind: ContextDependencyNodeKind::Provider,
170        }))
171        .chain(consumers.keys().cloned().map(|id| ContextDependencyNode {
172            id: ContextDependencyNodeId::Consumer(id),
173            kind: ContextDependencyNodeKind::Consumer,
174        }))
175        .collect::<Vec<_>>();
176    let mut edges = Vec::new();
177
178    for context in contexts.values() {
179        let Some(default) = &context.default_expression else {
180            continue;
181        };
182        let default_node = ContextDependencyNodeId::ContextDefault(context.id.clone());
183        edges.push(ContextDependencyEdge {
184            dependent: default_node.clone(),
185            dependency: ContextDependencyNodeId::Context(context.id.clone()),
186            kind: ContextDependencyEdgeKind::ContextDefaultSuppliesContext,
187            compatibility: context_types
188                .get(&context.id)
189                .and_then(|record| record.default_compatibility)
190                .map_or(
191                    ContextDependencyCompatibility::Unknown,
192                    compatibility_from_status,
193                ),
194            provenance: expression_graph.node(default).map_or_else(
195                || context.provenance.clone(),
196                |node| node.provenance.clone(),
197            ),
198        });
199        edges.extend(expression_read_edges(
200            &default_node,
201            context.owner.entity_id(),
202            expression_graph,
203            components,
204            computed_values,
205            ContextDependencyEdgeKind::ContextDefaultReadsState,
206            ContextDependencyEdgeKind::ContextDefaultReadsComputed,
207        ));
208    }
209    for provider in providers.values() {
210        let provider_node = ContextDependencyNodeId::Provider(provider.id.clone());
211        edges.push(ContextDependencyEdge {
212            dependent: provider_node.clone(),
213            dependency: ContextDependencyNodeId::Context(provider.context.clone()),
214            kind: ContextDependencyEdgeKind::ProviderSuppliesContext,
215            compatibility: provider_types
216                .get(&provider.id)
217                .map_or(ContextDependencyCompatibility::Unknown, |record| {
218                    compatibility_from_status(record.declaration_to_context)
219                }),
220            provenance: provider.context_designator.provenance.clone(),
221        });
222        edges.extend(expression_read_edges(
223            &provider_node,
224            provider.owner.entity_id(),
225            expression_graph,
226            components,
227            computed_values,
228            ContextDependencyEdgeKind::ProviderReadsState,
229            ContextDependencyEdgeKind::ProviderReadsComputed,
230        ));
231    }
232    for consumer in consumers.values() {
233        let Some(resolution) = resolutions.get(&consumer.id) else {
234            continue;
235        };
236        let dependent = ContextDependencyNodeId::Consumer(consumer.id.clone());
237        let compatibility = binding_types
238            .get(&consumer.id)
239            .map_or(ContextDependencyCompatibility::Unknown, |binding| {
240                compatibility_from_binding(binding.overall)
241            });
242        let edge = match &resolution.result {
243            ContextResolutionResult::Provider { provider, .. } => Some(ContextDependencyEdge {
244                dependent,
245                dependency: ContextDependencyNodeId::Provider(provider.clone()),
246                kind: ContextDependencyEdgeKind::ConsumerReadsProvider,
247                compatibility,
248                provenance: consumer.context_designator.provenance.clone(),
249            }),
250            ContextResolutionResult::ContextDefault { context, .. } => {
251                Some(ContextDependencyEdge {
252                    dependent,
253                    dependency: ContextDependencyNodeId::ContextDefault(context.clone()),
254                    kind: ContextDependencyEdgeKind::ConsumerReadsContextDefault,
255                    compatibility,
256                    provenance: consumer.context_designator.provenance.clone(),
257                })
258            }
259            ContextResolutionResult::Unresolved
260            | ContextResolutionResult::Ambiguous { .. }
261            | ContextResolutionResult::InvalidContextReference => None,
262        };
263        if let Some(edge) = edge {
264            edges.push(edge);
265        }
266    }
267
268    edges.sort_by(|left, right| {
269        (
270            &left.dependent,
271            left.kind,
272            &left.dependency,
273            left.provenance.span.start,
274        )
275            .cmp(&(
276                &right.dependent,
277                right.kind,
278                &right.dependency,
279                right.provenance.span.start,
280            ))
281    });
282    edges.dedup_by(|left, right| {
283        left.dependent == right.dependent
284            && left.kind == right.kind
285            && left.dependency == right.dependency
286    });
287
288    let referenced_values = edges
289        .iter()
290        .filter_map(|edge| match &edge.dependency {
291            ContextDependencyNodeId::State(id) => Some(ContextDependencyNode {
292                id: ContextDependencyNodeId::State(id.clone()),
293                kind: ContextDependencyNodeKind::State,
294            }),
295            ContextDependencyNodeId::Computed(id) => Some(ContextDependencyNode {
296                id: ContextDependencyNodeId::Computed(id.clone()),
297                kind: ContextDependencyNodeKind::Computed,
298            }),
299            _ => None,
300        })
301        .collect::<BTreeSet<_>>();
302    nodes.extend(referenced_values);
303    nodes.sort_by(|left, right| left.id.cmp(&right.id));
304    nodes.dedup_by(|left, right| left.id == right.id);
305
306    let mut dependencies_by_node = BTreeMap::new();
307    let mut dependents_by_node = BTreeMap::new();
308    let mut provider_value_dependencies = BTreeMap::new();
309    let mut context_default_dependencies = BTreeMap::new();
310    let mut consumer_binding_dependencies = BTreeMap::new();
311    let mut consumers_by_provider = BTreeMap::new();
312    let mut consumers_by_default = BTreeMap::new();
313    for edge in &edges {
314        dependencies_by_node
315            .entry(edge.dependent.clone())
316            .or_insert_with(Vec::new)
317            .push(edge.dependency.clone());
318        dependents_by_node
319            .entry(edge.dependency.clone())
320            .or_insert_with(Vec::new)
321            .push(edge.dependent.clone());
322        match (&edge.dependent, &edge.dependency) {
323            (ContextDependencyNodeId::Provider(provider), dependency)
324                if matches!(
325                    edge.kind,
326                    ContextDependencyEdgeKind::ProviderReadsState
327                        | ContextDependencyEdgeKind::ProviderReadsComputed
328                ) =>
329            {
330                provider_value_dependencies
331                    .entry(provider.clone())
332                    .or_insert_with(Vec::new)
333                    .push(dependency.clone());
334            }
335            (ContextDependencyNodeId::ContextDefault(context), dependency)
336                if matches!(
337                    edge.kind,
338                    ContextDependencyEdgeKind::ContextDefaultReadsState
339                        | ContextDependencyEdgeKind::ContextDefaultReadsComputed
340                ) =>
341            {
342                context_default_dependencies
343                    .entry(context.clone())
344                    .or_insert_with(Vec::new)
345                    .push(dependency.clone());
346            }
347            (
348                ContextDependencyNodeId::Consumer(consumer),
349                ContextDependencyNodeId::Provider(provider),
350            ) => {
351                consumer_binding_dependencies.insert(consumer.clone(), edge.dependency.clone());
352                consumers_by_provider
353                    .entry(provider.clone())
354                    .or_insert_with(Vec::new)
355                    .push(consumer.clone());
356            }
357            (
358                ContextDependencyNodeId::Consumer(consumer),
359                ContextDependencyNodeId::ContextDefault(context),
360            ) => {
361                consumer_binding_dependencies.insert(consumer.clone(), edge.dependency.clone());
362                consumers_by_default
363                    .entry(context.clone())
364                    .or_insert_with(Vec::new)
365                    .push(consumer.clone());
366            }
367            _ => {}
368        }
369    }
370    for dependencies in dependencies_by_node.values_mut() {
371        dependencies.sort();
372        dependencies.dedup();
373    }
374    for dependents in dependents_by_node.values_mut() {
375        dependents.sort();
376        dependents.dedup();
377    }
378    for dependencies in provider_value_dependencies.values_mut() {
379        dependencies.sort();
380        dependencies.dedup();
381    }
382    for dependencies in context_default_dependencies.values_mut() {
383        dependencies.sort();
384        dependencies.dedup();
385    }
386    for consumers in consumers_by_provider.values_mut() {
387        consumers.sort();
388        consumers.dedup();
389    }
390    for consumers in consumers_by_default.values_mut() {
391        consumers.sort();
392        consumers.dedup();
393    }
394    ContextDependencyGraph {
395        nodes,
396        edges,
397        dependencies_by_node,
398        dependents_by_node,
399        provider_value_dependencies,
400        context_default_dependencies,
401        consumer_binding_dependencies,
402        consumers_by_provider,
403        consumers_by_default,
404    }
405}
406
407#[allow(clippy::too_many_arguments)]
408fn expression_read_edges(
409    dependent: &ContextDependencyNodeId,
410    owner: Option<&SemanticId>,
411    expression_graph: &ExpressionGraph,
412    components: &[ComponentNode],
413    computed_values: &BTreeMap<SemanticId, ComputedValue>,
414    state_kind: ContextDependencyEdgeKind,
415    computed_kind: ContextDependencyEdgeKind,
416) -> Vec<ContextDependencyEdge> {
417    let Some(owner) = owner else {
418        return Vec::new();
419    };
420    let Some(component) = components.iter().find(|component| component.id == *owner) else {
421        return Vec::new();
422    };
423    expression_graph
424        .nodes_for(match dependent {
425            ContextDependencyNodeId::Provider(provider) => provider.as_semantic_id(),
426            ContextDependencyNodeId::ContextDefault(context) => context.as_semantic_id(),
427            _ => return Vec::new(),
428        })
429        .into_iter()
430        .filter_map(|node| {
431            let ExpressionNodeKind::ThisMember { name } = &node.kind else {
432                return None;
433            };
434            let (dependency, kind) = if let Some(state) = component
435                .state_fields
436                .iter()
437                .find(|field| field.name == *name)
438            {
439                (ContextDependencyNodeId::State(state.id.clone()), state_kind)
440            } else {
441                let computed = computed_values.get(&component.id.computed(name))?;
442                (
443                    ContextDependencyNodeId::Computed(computed.id.clone()),
444                    computed_kind,
445                )
446            };
447            Some(ContextDependencyEdge {
448                dependent: dependent.clone(),
449                dependency,
450                kind,
451                compatibility: ContextDependencyCompatibility::NotApplicable,
452                provenance: node.provenance.clone(),
453            })
454        })
455        .collect()
456}
457
458fn compatibility_from_status(status: CompatibilityStatus) -> ContextDependencyCompatibility {
459    match status {
460        CompatibilityStatus::Compatible => ContextDependencyCompatibility::Compatible,
461        CompatibilityStatus::Incompatible => ContextDependencyCompatibility::Incompatible,
462        CompatibilityStatus::Unknown => ContextDependencyCompatibility::Unknown,
463    }
464}
465
466fn compatibility_from_binding(
467    status: ContextBindingCompatibility,
468) -> ContextDependencyCompatibility {
469    match status {
470        ContextBindingCompatibility::Compatible => ContextDependencyCompatibility::Compatible,
471        ContextBindingCompatibility::Incompatible => ContextDependencyCompatibility::Incompatible,
472        ContextBindingCompatibility::Unknown => ContextDependencyCompatibility::Unknown,
473        ContextBindingCompatibility::Unresolved
474        | ContextBindingCompatibility::Ambiguous
475        | ContextBindingCompatibility::InvalidContextReference => {
476            ContextDependencyCompatibility::NotApplicable
477        }
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use crate::{
484        build_application_semantic_model, build_application_semantic_model_for_unit,
485        validate_application_semantic_model, CompilationUnit, ConsumerId,
486        ContextDependencyCompatibility, ContextDependencyEdgeKind, ContextDependencyNodeId,
487        ProviderId,
488    };
489
490    #[test]
491    fn projects_provider_reads_and_exact_selected_provider_bindings() {
492        let unit = CompilationUnit::parse_sources([
493            (
494                "src/app-shell.tsx",
495                r#"
496@component("x-app-shell")
497class AppShell extends Component {
498  @context()
499  theme!: string;
500  @context()
501  mode!: string;
502  render() { return <main />; }
503}
504export { AppShell };
505"#,
506            ),
507            (
508                "src/theme-boundary.tsx",
509                r#"
510import { AppShell } from "./app-shell";
511@component("x-theme-boundary")
512class ThemeBoundary extends Component {
513  selected = state("dark");
514  @computed()
515  get derivedTheme(): string { return this.selected; }
516  @provide(AppShell.theme)
517  providedTheme: string = this.selected;
518  @provide(AppShell.mode)
519  providedMode: string = this.derivedTheme;
520  @consume(AppShell.theme)
521  theme!: string;
522  render() { return <main />; }
523}
524"#,
525            ),
526        ]);
527        let asm = build_application_semantic_model_for_unit(&unit);
528        let component = &asm.components[1].id;
529        let provider = ProviderId::for_component(component, "providedTheme");
530        let computed_provider = ProviderId::for_component(component, "providedMode");
531        let consumer = ConsumerId::for_component(component, "theme");
532        let state = ContextDependencyNodeId::State(component.state_field("selected"));
533        let graph = asm.context_dependency_graph();
534
535        assert_eq!(graph.provider_value_dependencies(&provider), &[state]);
536        assert_eq!(
537            graph.provider_value_dependencies(&computed_provider),
538            &[ContextDependencyNodeId::Computed(
539                component.computed("derivedTheme")
540            )]
541        );
542        assert_eq!(
543            graph.consumer_binding_dependency(&consumer),
544            Some(&ContextDependencyNodeId::Provider(provider.clone()))
545        );
546        assert_eq!(graph.consumers_bound_to_provider(&provider), &[consumer]);
547        assert!(graph.edges.iter().any(|edge| {
548            edge.kind == ContextDependencyEdgeKind::ProviderSuppliesContext
549                && edge.compatibility == ContextDependencyCompatibility::Compatible
550        }));
551    }
552
553    #[test]
554    fn retains_default_and_unresolved_consumer_topology_without_hidden_providers() {
555        let asm = build_application_semantic_model(&presolve_parser::parse_file(
556            "src/app.tsx",
557            r#"
558@component("x-app")
559class App extends Component {
560  @context()
561  locale: string = "en";
562  @context()
563  mode!: string;
564  @consume(App.locale)
565  selectedLocale!: string;
566  render() { return <main />; }
567}
568@component("x-toolbar")
569class Toolbar extends Component {
570  @consume(App.mode)
571  mode!: string;
572  render() { return <main />; }
573}
574"#,
575        ));
576        let app = &asm.components[0].id;
577        let toolbar = &asm.components[1].id;
578        let context = asm.contexts()[0].id.clone();
579        let default = ContextDependencyNodeId::ContextDefault(context.clone());
580        let selected = ConsumerId::for_component(app, "selectedLocale");
581        let unresolved = ConsumerId::for_component(toolbar, "mode");
582        let graph = asm.context_dependency_graph();
583
584        assert_eq!(graph.consumer_binding_dependency(&selected), Some(&default));
585        assert_eq!(graph.consumers_bound_to_default(&context), &[selected]);
586        assert_eq!(graph.consumer_binding_dependency(&unresolved), None);
587        assert!(graph.edges.iter().any(|edge| {
588            edge.kind == ContextDependencyEdgeKind::ContextDefaultSuppliesContext
589                && edge.dependent == default
590        }));
591        assert!(validate_application_semantic_model(&asm).is_empty());
592    }
593
594    #[test]
595    fn preserves_incompatible_selected_provider_and_deterministic_ordering() {
596        let parsed = presolve_parser::parse_file(
597            "src/app.tsx",
598            r#"
599@component("x-app")
600class App extends Component {
601  @context()
602  theme!: number;
603  @provide(App.theme)
604  providedTheme: string = "dark";
605  @consume(App.theme)
606  themeValue!: number;
607  render() { return <main />; }
608}
609"#,
610        );
611        let first = build_application_semantic_model(&parsed);
612        let second = build_application_semantic_model(&parsed);
613        let component = &first.components[0].id;
614        let consumer = ConsumerId::for_component(component, "themeValue");
615        let provider = ProviderId::for_component(component, "providedTheme");
616        let edge = first
617            .context_dependency_graph()
618            .edges
619            .iter()
620            .find(|edge| edge.kind == ContextDependencyEdgeKind::ConsumerReadsProvider)
621            .unwrap();
622
623        assert_eq!(first.context_dependency, second.context_dependency);
624        assert_eq!(edge.dependent, ContextDependencyNodeId::Consumer(consumer));
625        assert_eq!(edge.dependency, ContextDependencyNodeId::Provider(provider));
626        assert_eq!(
627            edge.compatibility,
628            ContextDependencyCompatibility::Incompatible
629        );
630    }
631}