Skip to main content

presolve_compiler/
context_typing.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    boundary_compatibility, is_assignable, serialization_compatibility, ConsumerEntity, ConsumerId,
5    ContextEntity, ContextId, ContextResolution, ContextResolutionResult, ExecutionBoundary,
6    ExpressionGraph, ProviderEntity, ProviderId, SemanticType, SemanticTypeId, SemanticTypeModel,
7    SerializationCompatibility, SourceProvenance,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CompatibilityStatus {
12    Compatible,
13    Incompatible,
14    Unknown,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ContextSerializationCompatibility {
19    Serializable,
20    NonSerializable,
21    Unknown,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ContextBindingCompatibility {
26    Compatible,
27    Incompatible,
28    Unknown,
29    Unresolved,
30    Ambiguous,
31    InvalidContextReference,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ContextTypeRecord {
36    pub context: ContextId,
37    pub declared_type: SemanticTypeId,
38    pub normalized_type: SemanticTypeId,
39    pub default_type: Option<SemanticTypeId>,
40    pub default_compatibility: Option<CompatibilityStatus>,
41    pub serialization: ContextSerializationCompatibility,
42    pub boundary: ExecutionBoundary,
43    pub boundary_compatibility: CompatibilityStatus,
44    pub provenance: SourceProvenance,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ProviderTypeRecord {
49    pub provider: ProviderId,
50    pub context: Option<ContextId>,
51    pub inferred_value_type: SemanticTypeId,
52    pub declared_type: SemanticTypeId,
53    pub value_to_declaration: CompatibilityStatus,
54    pub declaration_to_context: CompatibilityStatus,
55    pub serialization: ContextSerializationCompatibility,
56    pub boundary: ExecutionBoundary,
57    pub boundary_compatibility: CompatibilityStatus,
58    pub provenance: SourceProvenance,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ConsumerTypeRecord {
63    pub consumer: ConsumerId,
64    pub context: Option<ContextId>,
65    pub requested_type: SemanticTypeId,
66    pub context_to_consumer: CompatibilityStatus,
67    pub boundary: ExecutionBoundary,
68    pub boundary_compatibility: CompatibilityStatus,
69    pub provenance: SourceProvenance,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct ContextBindingTypeRecord {
74    pub consumer: ConsumerId,
75    pub resolution: ContextResolutionResult,
76    pub provider: Option<ProviderId>,
77    pub context: Option<ContextId>,
78    pub source_type: Option<SemanticTypeId>,
79    pub context_type: Option<SemanticTypeId>,
80    pub consumer_type: SemanticTypeId,
81    pub source_to_context: CompatibilityStatus,
82    pub context_to_consumer: CompatibilityStatus,
83    pub overall: ContextBindingCompatibility,
84    pub serialization: ContextSerializationCompatibility,
85    pub boundary_compatibility: CompatibilityStatus,
86    pub provenance: SourceProvenance,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ContextTypeProducts {
91    pub contexts: BTreeMap<ContextId, ContextTypeRecord>,
92    pub providers: BTreeMap<ProviderId, ProviderTypeRecord>,
93    pub consumers: BTreeMap<ConsumerId, ConsumerTypeRecord>,
94    pub bindings: BTreeMap<ConsumerId, ContextBindingTypeRecord>,
95}
96
97#[allow(clippy::too_many_lines)]
98#[must_use]
99pub fn collect_context_type_products(
100    contexts: &BTreeMap<ContextId, ContextEntity>,
101    providers: &BTreeMap<ProviderId, ProviderEntity>,
102    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
103    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
104    expression_graph: &ExpressionGraph,
105    semantic_types: &SemanticTypeModel,
106) -> ContextTypeProducts {
107    let context_records = contexts
108        .values()
109        .map(|context| {
110            let default_root = context
111                .default_expression
112                .as_ref()
113                .and_then(|_| expression_graph.root_for(context.id.as_semantic_id()))
114                .cloned();
115            let default_type = default_root.as_ref().map(SemanticTypeId::for_subject);
116            let default_compatibility = default_root.as_ref().map(|default| {
117                compatibility_for_ids(semantic_types, default, context.id.as_semantic_id())
118            });
119            let mut serial_types = vec![context.id.as_semantic_id()];
120            if let Some(default) = &default_root {
121                serial_types.push(default);
122            }
123            let serialization = serialization_for_ids(semantic_types, &serial_types);
124            (
125                context.id.clone(),
126                ContextTypeRecord {
127                    context: context.id.clone(),
128                    declared_type: context.declared_type_id.clone(),
129                    normalized_type: context.declared_type_id.clone(),
130                    default_type,
131                    default_compatibility,
132                    serialization,
133                    boundary: context.execution_boundary,
134                    boundary_compatibility: boundary_for_id(
135                        semantic_types,
136                        context.id.as_semantic_id(),
137                        context.execution_boundary,
138                        context.execution_boundary,
139                    ),
140                    provenance: context.provenance.clone(),
141                },
142            )
143        })
144        .collect::<BTreeMap<_, _>>();
145    let provider_records = providers
146        .values()
147        .map(|provider| {
148            let value_root = expression_graph
149                .root_for(provider.id.as_semantic_id())
150                .cloned();
151            let inferred_value_type = value_root.as_ref().map_or_else(
152                || SemanticTypeId::for_subject(provider.id.as_semantic_id()),
153                SemanticTypeId::for_subject,
154            );
155            let value_to_declaration = compatibility_for_ids(
156                semantic_types,
157                value_root.as_ref().unwrap_or(provider.id.as_semantic_id()),
158                provider.id.as_semantic_id(),
159            );
160            let declaration_to_context = compatibility_for_ids(
161                semantic_types,
162                provider.id.as_semantic_id(),
163                provider.context.as_semantic_id(),
164            );
165            (
166                provider.id.clone(),
167                ProviderTypeRecord {
168                    provider: provider.id.clone(),
169                    context: Some(provider.context.clone()),
170                    inferred_value_type: inferred_value_type.clone(),
171                    declared_type: provider.declared_type_id.clone(),
172                    value_to_declaration,
173                    declaration_to_context,
174                    serialization: serialization_for_ids(
175                        semantic_types,
176                        &[
177                            value_root.as_ref().unwrap_or(provider.id.as_semantic_id()),
178                            provider.id.as_semantic_id(),
179                        ],
180                    ),
181                    boundary: provider.execution_boundary,
182                    boundary_compatibility: boundary_for_id(
183                        semantic_types,
184                        value_root.as_ref().unwrap_or(provider.id.as_semantic_id()),
185                        provider.execution_boundary,
186                        provider.execution_boundary,
187                    ),
188                    provenance: provider.provenance.clone(),
189                },
190            )
191        })
192        .collect::<BTreeMap<_, _>>();
193    let consumer_records = consumers
194        .values()
195        .map(|consumer| {
196            let context_to_consumer =
197                consumer
198                    .context()
199                    .map_or(CompatibilityStatus::Unknown, |context| {
200                        compatibility_for_ids(
201                            semantic_types,
202                            context.as_semantic_id(),
203                            consumer.id.as_semantic_id(),
204                        )
205                    });
206            (
207                consumer.id.clone(),
208                ConsumerTypeRecord {
209                    consumer: consumer.id.clone(),
210                    context: consumer.context().cloned(),
211                    requested_type: consumer.requested_type_id.clone(),
212                    context_to_consumer,
213                    boundary: consumer.execution_boundary,
214                    boundary_compatibility: boundary_for_id(
215                        semantic_types,
216                        consumer.id.as_semantic_id(),
217                        consumer.execution_boundary,
218                        consumer.execution_boundary,
219                    ),
220                    provenance: consumer.provenance.clone(),
221                },
222            )
223        })
224        .collect::<BTreeMap<_, _>>();
225    let bindings = consumers
226        .values()
227        .filter_map(|consumer| {
228            let resolution = resolutions.get(&consumer.id)?;
229            let consumer_record = consumer_records.get(&consumer.id)?;
230            let context_record = consumer
231                .context()
232                .and_then(|context| context_records.get(context));
233            let (provider, source_type, source_to_context, serialization, boundary) =
234                match &resolution.result {
235                    ContextResolutionResult::Provider { provider, .. } => {
236                        let record = provider_records.get(provider)?;
237                        (
238                            Some(provider.clone()),
239                            Some(record.inferred_value_type.clone()),
240                            combine_compatibility(
241                                record.value_to_declaration,
242                                record.declaration_to_context,
243                            ),
244                            record.serialization,
245                            record.boundary_compatibility,
246                        )
247                    }
248                    ContextResolutionResult::ContextDefault {
249                        context,
250                        expression,
251                    } => (
252                        None,
253                        Some(SemanticTypeId::for_subject(expression)),
254                        context_records
255                            .get(context)
256                            .and_then(|record| record.default_compatibility)
257                            .unwrap_or(CompatibilityStatus::Unknown),
258                        serialization_for_ids(semantic_types, &[expression]),
259                        boundary_for_id(
260                            semantic_types,
261                            expression,
262                            ExecutionBoundary::Client,
263                            ExecutionBoundary::Client,
264                        ),
265                    ),
266                    _ => (
267                        None,
268                        None,
269                        CompatibilityStatus::Unknown,
270                        ContextSerializationCompatibility::Unknown,
271                        CompatibilityStatus::Unknown,
272                    ),
273                };
274            let context_to_consumer = consumer_record.context_to_consumer;
275            let overall = binding_overall(
276                &resolution.result,
277                source_to_context,
278                context_to_consumer,
279                serialization,
280                boundary,
281            );
282            Some((
283                consumer.id.clone(),
284                ContextBindingTypeRecord {
285                    consumer: consumer.id.clone(),
286                    resolution: resolution.result.clone(),
287                    provider,
288                    context: consumer.context().cloned(),
289                    source_type,
290                    context_type: context_record.map(|record| record.declared_type.clone()),
291                    consumer_type: consumer.requested_type_id.clone(),
292                    source_to_context,
293                    context_to_consumer,
294                    overall,
295                    serialization,
296                    boundary_compatibility: boundary,
297                    provenance: resolution.provenance.clone(),
298                },
299            ))
300        })
301        .collect::<BTreeMap<_, _>>();
302    ContextTypeProducts {
303        contexts: context_records,
304        providers: provider_records,
305        consumers: consumer_records,
306        bindings,
307    }
308}
309
310fn compatibility_for_ids(
311    types: &SemanticTypeModel,
312    source: &crate::SemanticId,
313    target: &crate::SemanticId,
314) -> CompatibilityStatus {
315    let Some(source) = types
316        .assignments
317        .get(source)
318        .map(|record| &record.semantic_type)
319    else {
320        return CompatibilityStatus::Unknown;
321    };
322    let Some(target) = types
323        .assignments
324        .get(target)
325        .map(|record| &record.semantic_type)
326    else {
327        return CompatibilityStatus::Unknown;
328    };
329    if matches!(source, SemanticType::Unknown) || matches!(target, SemanticType::Unknown) {
330        CompatibilityStatus::Unknown
331    } else if is_assignable(source, target) {
332        CompatibilityStatus::Compatible
333    } else {
334        CompatibilityStatus::Incompatible
335    }
336}
337
338fn serialization_for_ids(
339    types: &SemanticTypeModel,
340    ids: &[&crate::SemanticId],
341) -> ContextSerializationCompatibility {
342    let mut result = ContextSerializationCompatibility::Serializable;
343    for id in ids {
344        let Some(semantic_type) = types
345            .assignments
346            .get(*id)
347            .map(|record| &record.semantic_type)
348        else {
349            return ContextSerializationCompatibility::Unknown;
350        };
351        if matches!(semantic_type, SemanticType::Unknown) {
352            return ContextSerializationCompatibility::Unknown;
353        }
354        if serialization_compatibility(semantic_type) == SerializationCompatibility::NotSerializable
355        {
356            result = ContextSerializationCompatibility::NonSerializable;
357        }
358    }
359    result
360}
361
362fn boundary_for_id(
363    types: &SemanticTypeModel,
364    id: &crate::SemanticId,
365    source: ExecutionBoundary,
366    target: ExecutionBoundary,
367) -> CompatibilityStatus {
368    let Some(semantic_type) = types
369        .assignments
370        .get(id)
371        .map(|record| &record.semantic_type)
372    else {
373        return CompatibilityStatus::Unknown;
374    };
375    if matches!(semantic_type, SemanticType::Unknown) {
376        return CompatibilityStatus::Unknown;
377    }
378    match boundary_compatibility(semantic_type, source, target) {
379        crate::BoundaryCompatibility::Compatible => CompatibilityStatus::Compatible,
380        crate::BoundaryCompatibility::Incompatible => CompatibilityStatus::Incompatible,
381    }
382}
383
384fn combine_compatibility(
385    left: CompatibilityStatus,
386    right: CompatibilityStatus,
387) -> CompatibilityStatus {
388    if left == CompatibilityStatus::Incompatible || right == CompatibilityStatus::Incompatible {
389        CompatibilityStatus::Incompatible
390    } else if left == CompatibilityStatus::Unknown || right == CompatibilityStatus::Unknown {
391        CompatibilityStatus::Unknown
392    } else {
393        CompatibilityStatus::Compatible
394    }
395}
396
397fn binding_overall(
398    resolution: &ContextResolutionResult,
399    source_to_context: CompatibilityStatus,
400    context_to_consumer: CompatibilityStatus,
401    serialization: ContextSerializationCompatibility,
402    boundary: CompatibilityStatus,
403) -> ContextBindingCompatibility {
404    match resolution {
405        ContextResolutionResult::Unresolved => ContextBindingCompatibility::Unresolved,
406        ContextResolutionResult::Ambiguous { .. } => ContextBindingCompatibility::Ambiguous,
407        ContextResolutionResult::InvalidContextReference => {
408            ContextBindingCompatibility::InvalidContextReference
409        }
410        ContextResolutionResult::Provider { .. }
411        | ContextResolutionResult::ContextDefault { .. } => {
412            if source_to_context == CompatibilityStatus::Incompatible
413                || context_to_consumer == CompatibilityStatus::Incompatible
414                || boundary == CompatibilityStatus::Incompatible
415                || serialization == ContextSerializationCompatibility::NonSerializable
416            {
417                ContextBindingCompatibility::Incompatible
418            } else if source_to_context == CompatibilityStatus::Unknown
419                || context_to_consumer == CompatibilityStatus::Unknown
420                || boundary == CompatibilityStatus::Unknown
421                || serialization == ContextSerializationCompatibility::Unknown
422            {
423                ContextBindingCompatibility::Unknown
424            } else {
425                ContextBindingCompatibility::Compatible
426            }
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use crate::{
434        build_application_semantic_model, validate_application_semantic_model, CompatibilityStatus,
435        ConsumerId, ContextBindingCompatibility::*, ProviderId,
436    };
437
438    #[test]
439    fn retains_the_directed_compatible_provider_chain() {
440        let asm = build_application_semantic_model(&presolve_parser::parse_file(
441            "src/components.tsx",
442            r#"
443@component("x-app")
444class App extends Component {
445  @context()
446  theme!: string;
447  @provide(App.theme)
448  providedTheme: string = "dark";
449  @consume(App.theme)
450  toolbarTheme!: string;
451  render() { return <main />; }
452}
453"#,
454        ));
455        let component = &asm.components[0].id;
456        let provider = ProviderId::for_component(component, "providedTheme");
457        let consumer = ConsumerId::for_component(component, "toolbarTheme");
458        let provider_type = asm.provider_type(&provider).unwrap();
459        let binding = asm.context_binding_type(&consumer).unwrap();
460
461        assert_eq!(
462            provider_type.value_to_declaration,
463            CompatibilityStatus::Compatible
464        );
465        assert_eq!(
466            provider_type.declaration_to_context,
467            CompatibilityStatus::Compatible
468        );
469        assert_eq!(binding.context_to_consumer, CompatibilityStatus::Compatible);
470        assert_eq!(binding.overall, Compatible, "{binding:#?}");
471        assert!(asm.runtime_eligible_context_binding(&consumer));
472        assert!(validate_application_semantic_model(&asm).is_empty());
473    }
474
475    #[test]
476    fn retains_incompatible_selected_provider_without_reselecting() {
477        let asm = build_application_semantic_model(&presolve_parser::parse_file(
478            "src/components.tsx",
479            r#"
480@component("x-app")
481class App extends Component {
482  @context()
483  theme!: number;
484  @provide(App.theme)
485  providedTheme: string = "dark";
486  @consume(App.theme)
487  toolbarTheme!: number;
488  render() { return <main />; }
489}
490"#,
491        ));
492        let component = &asm.components[0].id;
493        let provider = ProviderId::for_component(component, "providedTheme");
494        let consumer = ConsumerId::for_component(component, "toolbarTheme");
495        let binding = asm.context_binding_type(&consumer).unwrap();
496
497        assert_eq!(asm.resolved_provider(&consumer), Some(&provider));
498        assert_eq!(
499            asm.provider_type(&provider).unwrap().declaration_to_context,
500            CompatibilityStatus::Incompatible
501        );
502        assert_eq!(binding.overall, Incompatible);
503        assert!(!asm.runtime_eligible_context_binding(&consumer));
504    }
505
506    #[test]
507    fn types_context_defaults_as_distinct_fallback_sources() {
508        let asm = build_application_semantic_model(&presolve_parser::parse_file(
509            "src/components.tsx",
510            r#"
511@component("x-app")
512class App extends Component {
513  @context()
514  locale: string = "en";
515  @consume(App.locale)
516  toolbarLocale!: string;
517  render() { return <main />; }
518}
519"#,
520        ));
521        let consumer = ConsumerId::for_component(&asm.components[0].id, "toolbarLocale");
522        let binding = asm.context_binding_type(&consumer).unwrap();
523
524        assert!(binding.provider.is_none());
525        assert!(binding.source_type.is_some());
526        assert_eq!(binding.overall, Compatible, "{binding:#?}");
527    }
528
529    #[test]
530    fn preserves_unresolved_and_invalid_binding_states() {
531        let unresolved = build_application_semantic_model(&presolve_parser::parse_file(
532            "src/unresolved.tsx",
533            r#"
534@component("x-app")
535class App extends Component {
536  @context()
537  locale!: string;
538  @consume(App.locale)
539  toolbarLocale!: string;
540  render() { return <main />; }
541}
542"#,
543        ));
544        let unresolved_consumer =
545            ConsumerId::for_component(&unresolved.components[0].id, "toolbarLocale");
546        assert_eq!(
547            unresolved
548                .context_binding_type(&unresolved_consumer)
549                .unwrap()
550                .overall,
551            Unresolved
552        );
553
554        let invalid = build_application_semantic_model(&presolve_parser::parse_file(
555            "src/invalid.tsx",
556            r#"
557@component("x-app")
558class App extends Component {
559  @consume(Missing.locale)
560  toolbarLocale!: string;
561  render() { return <main />; }
562}
563"#,
564        ));
565        let invalid_consumer =
566            ConsumerId::for_component(&invalid.components[0].id, "toolbarLocale");
567        assert!(invalid.context_binding_type(&invalid_consumer).is_none());
568        assert_eq!(
569            invalid
570                .context_declaration_candidates()
571                .invalid_candidates()
572                .len(),
573            1
574        );
575    }
576}