Skip to main content

presolve_compiler/
context_ownership.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    ComponentNode, ConsumerEntity, ConsumerId, ContextEntity, ContextId, ExpressionGraph,
5    ProviderEntity, ProviderId, SemanticId, SourceProvenance,
6};
7
8/// Typed node identity for the immutable G6 Context ownership projection.
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
10pub enum ContextOwnershipNodeId {
11    Component(SemanticId),
12    Context(ContextId),
13    Provider(ProviderId),
14    Consumer(ConsumerId),
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub enum ContextOwnershipNodeKind {
19    Component,
20    Context,
21    Provider,
22    Consumer,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct ContextOwnershipNode {
27    pub id: ContextOwnershipNodeId,
28    pub kind: ContextOwnershipNodeKind,
29    pub provenance: SourceProvenance,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
33pub enum ContextOwnershipOwnerId {
34    Component(SemanticId),
35    Context(ContextId),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
39pub enum ContextOwnershipTargetId {
40    Context(ContextId),
41    Provider(ProviderId),
42    Consumer(ConsumerId),
43    Expression(SemanticId),
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub enum ContextOwnershipEdgeKind {
48    ComponentOwnsContext,
49    ComponentOwnsProvider,
50    ComponentOwnsConsumer,
51    ContextOwnsDefaultExpression,
52}
53
54/// One directed ownership fact. Edges always point owner to owned target.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ContextOwnershipEdge {
57    pub owner: ContextOwnershipOwnerId,
58    pub owned: ContextOwnershipTargetId,
59    pub kind: ContextOwnershipEdgeKind,
60    pub provenance: SourceProvenance,
61}
62
63/// Retained component-local ownership groups for later read-only consumers.
64#[derive(Debug, Clone, PartialEq, Eq, Default)]
65pub struct ContextOwnedEntities {
66    pub contexts: Vec<ContextId>,
67    pub providers: Vec<ProviderId>,
68    pub consumers: Vec<ConsumerId>,
69}
70
71/// Immutable G6 projection of canonical Context semantic ownership.
72///
73/// It deliberately does not contain component ancestry, Context request or
74/// Provider selection relations. Those remain owned by G4 products.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ContextOwnershipGraph {
77    pub nodes: Vec<ContextOwnershipNode>,
78    pub edges: Vec<ContextOwnershipEdge>,
79    contexts_by_component: BTreeMap<SemanticId, Vec<ContextId>>,
80    providers_by_component: BTreeMap<SemanticId, Vec<ProviderId>>,
81    consumers_by_component: BTreeMap<SemanticId, Vec<ConsumerId>>,
82    context_owner: BTreeMap<ContextId, SemanticId>,
83    provider_owner: BTreeMap<ProviderId, SemanticId>,
84    consumer_owner: BTreeMap<ConsumerId, SemanticId>,
85    default_by_context: BTreeMap<ContextId, SemanticId>,
86}
87
88impl ContextOwnershipGraph {
89    #[must_use]
90    pub fn owned_contexts(&self, component: &SemanticId) -> &[ContextId] {
91        self.contexts_by_component
92            .get(component)
93            .map_or(&[], Vec::as_slice)
94    }
95
96    #[must_use]
97    pub fn owned_providers(&self, component: &SemanticId) -> &[ProviderId] {
98        self.providers_by_component
99            .get(component)
100            .map_or(&[], Vec::as_slice)
101    }
102
103    #[must_use]
104    pub fn owned_consumers(&self, component: &SemanticId) -> &[ConsumerId] {
105        self.consumers_by_component
106            .get(component)
107            .map_or(&[], Vec::as_slice)
108    }
109
110    #[must_use]
111    pub fn owner_of_context(&self, context: &ContextId) -> Option<&SemanticId> {
112        self.context_owner.get(context)
113    }
114
115    #[must_use]
116    pub fn owner_of_provider(&self, provider: &ProviderId) -> Option<&SemanticId> {
117        self.provider_owner.get(provider)
118    }
119
120    #[must_use]
121    pub fn owner_of_consumer(&self, consumer: &ConsumerId) -> Option<&SemanticId> {
122        self.consumer_owner.get(consumer)
123    }
124
125    #[must_use]
126    pub fn context_default_expression(&self, context: &ContextId) -> Option<&SemanticId> {
127        self.default_by_context.get(context)
128    }
129
130    #[must_use]
131    pub fn context_owned_entities(&self, component: &SemanticId) -> ContextOwnedEntities {
132        ContextOwnedEntities {
133            contexts: self.owned_contexts(component).to_vec(),
134            providers: self.owned_providers(component).to_vec(),
135            consumers: self.owned_consumers(component).to_vec(),
136        }
137    }
138}
139
140/// Projects canonical G1--G3 ownership facts without interpreting source,
141/// scope ancestry, resolution, compatibility, or runtime state.
142///
143/// # Panics
144///
145/// Panics when a canonical component owner lacks source provenance.
146#[allow(clippy::too_many_lines)]
147#[must_use]
148pub fn collect_context_ownership_graph(
149    components: &[ComponentNode],
150    contexts: &BTreeMap<ContextId, ContextEntity>,
151    providers: &BTreeMap<ProviderId, ProviderEntity>,
152    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
153    expression_graph: &ExpressionGraph,
154    provenance: &BTreeMap<SemanticId, SourceProvenance>,
155) -> ContextOwnershipGraph {
156    let owner_components = contexts
157        .values()
158        .filter_map(|context| context.owner.entity_id().cloned())
159        .chain(
160            providers
161                .values()
162                .filter_map(|provider| provider.owner.entity_id().cloned()),
163        )
164        .chain(
165            consumers
166                .values()
167                .filter_map(|consumer| consumer.owner.entity_id().cloned()),
168        )
169        .collect::<BTreeSet<_>>();
170    let mut nodes = components
171        .iter()
172        .filter(|component| owner_components.contains(&component.id))
173        .map(|component| ContextOwnershipNode {
174            id: ContextOwnershipNodeId::Component(component.id.clone()),
175            kind: ContextOwnershipNodeKind::Component,
176            provenance: provenance
177                .get(&component.id)
178                .cloned()
179                .expect("canonical component owners should retain source provenance"),
180        })
181        .collect::<Vec<_>>();
182    let mut edges = Vec::new();
183    let mut contexts_by_component = BTreeMap::new();
184    let mut providers_by_component = BTreeMap::new();
185    let mut consumers_by_component = BTreeMap::new();
186    let mut context_owner = BTreeMap::new();
187    let mut provider_owner = BTreeMap::new();
188    let mut consumer_owner = BTreeMap::new();
189    let mut default_by_context = BTreeMap::new();
190
191    for context in contexts.values() {
192        let Some(owner) = context.owner.entity_id().cloned() else {
193            continue;
194        };
195        nodes.push(ContextOwnershipNode {
196            id: ContextOwnershipNodeId::Context(context.id.clone()),
197            kind: ContextOwnershipNodeKind::Context,
198            provenance: context.provenance.clone(),
199        });
200        contexts_by_component
201            .entry(owner.clone())
202            .or_insert_with(Vec::new)
203            .push(context.id.clone());
204        context_owner.insert(context.id.clone(), owner.clone());
205        edges.push(ContextOwnershipEdge {
206            owner: ContextOwnershipOwnerId::Component(owner),
207            owned: ContextOwnershipTargetId::Context(context.id.clone()),
208            kind: ContextOwnershipEdgeKind::ComponentOwnsContext,
209            provenance: context.provenance.clone(),
210        });
211        if let Some(expression) = &context.default_expression {
212            default_by_context.insert(context.id.clone(), expression.clone());
213            edges.push(ContextOwnershipEdge {
214                owner: ContextOwnershipOwnerId::Context(context.id.clone()),
215                owned: ContextOwnershipTargetId::Expression(expression.clone()),
216                kind: ContextOwnershipEdgeKind::ContextOwnsDefaultExpression,
217                provenance: expression_graph.node(expression).map_or_else(
218                    || context.provenance.clone(),
219                    |node| node.provenance.clone(),
220                ),
221            });
222        }
223    }
224    for provider in providers.values() {
225        let Some(owner) = provider.owner.entity_id().cloned() else {
226            continue;
227        };
228        nodes.push(ContextOwnershipNode {
229            id: ContextOwnershipNodeId::Provider(provider.id.clone()),
230            kind: ContextOwnershipNodeKind::Provider,
231            provenance: provider.provenance.clone(),
232        });
233        providers_by_component
234            .entry(owner.clone())
235            .or_insert_with(Vec::new)
236            .push(provider.id.clone());
237        provider_owner.insert(provider.id.clone(), owner.clone());
238        edges.push(ContextOwnershipEdge {
239            owner: ContextOwnershipOwnerId::Component(owner),
240            owned: ContextOwnershipTargetId::Provider(provider.id.clone()),
241            kind: ContextOwnershipEdgeKind::ComponentOwnsProvider,
242            provenance: provider.provenance.clone(),
243        });
244    }
245    for consumer in consumers.values() {
246        let Some(owner) = consumer.owner.entity_id().cloned() else {
247            continue;
248        };
249        nodes.push(ContextOwnershipNode {
250            id: ContextOwnershipNodeId::Consumer(consumer.id.clone()),
251            kind: ContextOwnershipNodeKind::Consumer,
252            provenance: consumer.provenance.clone(),
253        });
254        consumers_by_component
255            .entry(owner.clone())
256            .or_insert_with(Vec::new)
257            .push(consumer.id.clone());
258        consumer_owner.insert(consumer.id.clone(), owner.clone());
259        edges.push(ContextOwnershipEdge {
260            owner: ContextOwnershipOwnerId::Component(owner),
261            owned: ContextOwnershipTargetId::Consumer(consumer.id.clone()),
262            kind: ContextOwnershipEdgeKind::ComponentOwnsConsumer,
263            provenance: consumer.provenance.clone(),
264        });
265    }
266    nodes.sort_by(|left, right| left.id.cmp(&right.id));
267    edges.sort_by(|left, right| {
268        (&left.owner, left.kind, &left.owned).cmp(&(&right.owner, right.kind, &right.owned))
269    });
270    ContextOwnershipGraph {
271        nodes,
272        edges,
273        contexts_by_component,
274        providers_by_component,
275        consumers_by_component,
276        context_owner,
277        provider_owner,
278        consumer_owner,
279        default_by_context,
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use crate::{
286        build_application_semantic_model, build_application_semantic_model_for_unit,
287        validate_application_semantic_model, CompilationUnit, ConsumerId, ContextOwnershipEdgeKind,
288        ContextOwnershipTargetId, ProviderId,
289    };
290
291    #[test]
292    fn projects_foreign_context_users_by_declaring_component() {
293        let unit = CompilationUnit::parse_sources([
294            (
295                "src/app-shell.tsx",
296                r#"
297@component("x-app-shell")
298class AppShell extends Component {
299  @context()
300  theme: string = "light";
301  render() { return <main />; }
302}
303export { AppShell };
304"#,
305            ),
306            (
307                "src/theme-boundary.tsx",
308                r#"
309import { AppShell } from "./app-shell";
310@component("x-theme-boundary")
311class ThemeBoundary extends Component {
312  @provide(AppShell.theme)
313  providedTheme: string = "dark";
314  render() { return <main />; }
315}
316"#,
317            ),
318            (
319                "src/toolbar.tsx",
320                r#"
321import { AppShell } from "./app-shell";
322@component("x-toolbar")
323class Toolbar extends Component {
324  @consume(AppShell.theme)
325  theme!: string;
326  render() { return <main />; }
327}
328"#,
329            ),
330        ]);
331        let asm = build_application_semantic_model_for_unit(&unit);
332        let graph = &asm.context_ownership;
333        let app_shell = &asm.components[0].id;
334        let boundary = &asm.components[1].id;
335        let toolbar = &asm.components[2].id;
336        let context = asm.contexts()[0].id.clone();
337        let provider = ProviderId::for_component(boundary, "providedTheme");
338        let consumer = ConsumerId::for_component(toolbar, "theme");
339
340        assert_eq!(graph.owner_of_context(&context), Some(app_shell));
341        assert_eq!(graph.owner_of_provider(&provider), Some(boundary));
342        assert_eq!(graph.owner_of_consumer(&consumer), Some(toolbar));
343        assert_eq!(graph.owned_providers(app_shell), &[]);
344        assert_eq!(graph.owned_consumers(app_shell), &[]);
345        assert!(graph.context_default_expression(&context).is_some());
346        assert!(graph.edges.iter().all(|edge| !matches!(
347            edge.owned,
348            ContextOwnershipTargetId::Expression(_) if edge.kind != ContextOwnershipEdgeKind::ContextOwnsDefaultExpression
349        )));
350        assert!(validate_application_semantic_model(&asm).is_empty());
351    }
352
353    #[test]
354    fn retains_unresolved_consumers_and_is_deterministic() {
355        let parsed = presolve_parser::parse_file(
356            "src/toolbar.tsx",
357            r#"
358@component("x-app-shell")
359class AppShell extends Component {
360  @context()
361  theme!: string;
362  render() { return <main />; }
363}
364@component("x-toolbar")
365class Toolbar extends Component {
366  @consume(AppShell.theme)
367  theme!: string;
368  render() { return <main />; }
369}
370"#,
371        );
372        let first = build_application_semantic_model(&parsed);
373        let second = build_application_semantic_model(&parsed);
374        let consumer = ConsumerId::for_component(&first.components[1].id, "theme");
375
376        assert_eq!(first.context_ownership, second.context_ownership);
377        assert_eq!(
378            first.context_ownership.owner_of_consumer(&consumer),
379            Some(&first.components[1].id)
380        );
381        assert_eq!(first.context_ownership.edges.len(), 2);
382        let diagnostics = validate_application_semantic_model(&first);
383        assert!(diagnostics.is_empty(), "{diagnostics:#?}");
384    }
385}