Skip to main content

presolve_compiler/
context_evaluation.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::{
4    CompatibilityStatus, ConsumerId, ContextBindingCompatibility, ContextBindingLifetimeStatus,
5    ContextBindingTypeRecord, ContextDefaultLifetimeRecord, ContextDependencyNodeId, ContextEntity,
6    ContextId, ContextLifetimeAnalysis, ContextResolution, ContextResolutionResult,
7    ContextSerializationCompatibility, ContextTypeRecord, IrReactiveGraph, IrReactiveNode,
8    IrReactiveNodeKind, IrUpdateScheduler, ProviderId, ProviderLifetimeRecord, ProviderTypeRecord,
9    SemanticId, SourceProvenance,
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13pub enum ContextEvaluationPlanId {
14    Initial,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct ContextEvaluationBatchId {
19    pub plan: ContextEvaluationPlanId,
20    pub index: u32,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
24pub enum ContextValueSourceId {
25    ContextDefault(ContextId),
26    Provider(ProviderId),
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ContextSourcePlanStatus {
31    Planned,
32    Unused,
33    BlockedType,
34    BlockedSerialization,
35    BlockedBoundary,
36    BlockedLifetime,
37    BlockedDependency,
38    BlockedUnsupportedExpression,
39    UnknownEligibility,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
43pub enum ContextSourceBlockReason {
44    TypeIncompatible,
45    TypeUnknown,
46    NonSerializable,
47    SerializationUnknown,
48    BoundaryIncompatible,
49    BoundaryUnknown,
50    LifetimeIncompatible,
51    LifetimeUnknown,
52    MissingStateDependency(SemanticId),
53    UnavailableComputedDependency(SemanticId),
54    UnsupportedExpression,
55    InvalidContextTarget,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ContextSourcePlanEntry {
60    pub source: ContextValueSourceId,
61    pub owner_component: SemanticId,
62    pub context: ContextId,
63    pub expression_root: SemanticId,
64    pub status: ContextSourcePlanStatus,
65    pub reasons: Vec<ContextSourceBlockReason>,
66    pub required_state: Vec<SemanticId>,
67    pub required_computed: Vec<SemanticId>,
68    pub prerequisite_computed_batches: Vec<u32>,
69    pub evaluation_batch: Option<u32>,
70    pub provenance: SourceProvenance,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct ContextEvaluationBatch {
75    pub id: ContextEvaluationBatchId,
76    pub sources: Vec<ContextValueSourceId>,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ContextConsumerAvailabilityStatus {
81    Available,
82    BlockedSource,
83    TypeIncompatible,
84    SerializationIncompatible,
85    BoundaryIncompatible,
86    LifetimeIncompatible,
87    UnknownEligibility,
88    Unresolved,
89    Ambiguous,
90    InvalidContextReference,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ContextConsumerAvailabilityEntry {
95    pub consumer: ConsumerId,
96    pub resolution: ContextResolutionResult,
97    pub selected_source: Option<ContextValueSourceId>,
98    pub status: ContextConsumerAvailabilityStatus,
99    pub source_batch: Option<u32>,
100    pub required_computed: Vec<SemanticId>,
101    pub provenance: SourceProvenance,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct ContextEvaluationPlan {
106    pub id: ContextEvaluationPlanId,
107    pub source_entries: BTreeMap<ContextValueSourceId, ContextSourcePlanEntry>,
108    pub consumer_entries: BTreeMap<ConsumerId, ContextConsumerAvailabilityEntry>,
109    pub evaluation_batches: Vec<ContextEvaluationBatch>,
110    pub blocked_entries: Vec<ContextValueSourceId>,
111    planned_sources: Vec<ContextValueSourceId>,
112    available_consumers: Vec<ConsumerId>,
113    unavailable_consumers: Vec<ConsumerId>,
114    batches_by_id: BTreeMap<ContextEvaluationBatchId, ContextEvaluationBatch>,
115}
116
117impl ContextEvaluationPlan {
118    #[must_use]
119    pub fn context_source_plan(
120        &self,
121        source: &ContextValueSourceId,
122    ) -> Option<&ContextSourcePlanEntry> {
123        self.source_entries.get(source)
124    }
125    #[must_use]
126    pub fn context_consumer_availability(
127        &self,
128        consumer: &ConsumerId,
129    ) -> Option<&ContextConsumerAvailabilityEntry> {
130        self.consumer_entries.get(consumer)
131    }
132    #[must_use]
133    pub fn context_evaluation_batch(
134        &self,
135        id: &ContextEvaluationBatchId,
136    ) -> Option<&ContextEvaluationBatch> {
137        self.batches_by_id.get(id)
138    }
139    #[must_use]
140    pub fn planned_context_sources(&self) -> &[ContextValueSourceId] {
141        &self.planned_sources
142    }
143    #[must_use]
144    pub fn blocked_context_sources(&self) -> &[ContextValueSourceId] {
145        &self.blocked_entries
146    }
147    #[must_use]
148    pub fn available_context_consumers(&self) -> &[ConsumerId] {
149        &self.available_consumers
150    }
151    #[must_use]
152    pub fn unavailable_context_consumers(&self) -> &[ConsumerId] {
153        &self.unavailable_consumers
154    }
155}
156
157#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
158#[must_use]
159pub fn collect_context_evaluation_plan(
160    contexts: &BTreeMap<ContextId, ContextEntity>,
161    providers: &BTreeMap<ProviderId, crate::ProviderEntity>,
162    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
163    context_types: &BTreeMap<ContextId, ContextTypeRecord>,
164    provider_types: &BTreeMap<ProviderId, ProviderTypeRecord>,
165    binding_types: &BTreeMap<ConsumerId, ContextBindingTypeRecord>,
166    lifetime: &ContextLifetimeAnalysis,
167    dependency: &crate::ContextDependencyGraph,
168    computed_plan: &crate::IrComputedEvaluationPlan,
169    scope: &crate::ComponentScopeGraph,
170) -> ContextEvaluationPlan {
171    let mut entries = BTreeMap::new();
172    for context in contexts.values() {
173        if let Some(expression_root) = &context.default_expression {
174            let source = ContextValueSourceId::ContextDefault(context.id.clone());
175            let owner = lifetime
176                .context_default_lifetime(&context.id)
177                .map(|record| record.owner_component.clone());
178            let (required_state, required_computed) = source_dependencies(dependency, &source);
179            let provenance = dependency
180                .edges
181                .iter()
182                .find(|edge| {
183                    edge.dependent == ContextDependencyNodeId::ContextDefault(context.id.clone())
184                        && edge.kind
185                            == crate::ContextDependencyEdgeKind::ContextDefaultSuppliesContext
186                })
187                .map_or_else(
188                    || context.provenance.clone(),
189                    |edge| edge.provenance.clone(),
190                );
191            let reasons = default_reasons(
192                context,
193                context_types.get(&context.id),
194                lifetime.context_default_lifetime(&context.id),
195                &required_state,
196                &required_computed,
197                computed_plan,
198            );
199            if let Some(owner_component) = owner {
200                entries.insert(
201                    source.clone(),
202                    ContextSourcePlanEntry {
203                        source,
204                        owner_component,
205                        context: context.id.clone(),
206                        expression_root: expression_root.clone(),
207                        status: primary_status(&reasons),
208                        reasons,
209                        required_state,
210                        required_computed: required_computed.clone(),
211                        prerequisite_computed_batches: computed_batches(
212                            &required_computed,
213                            computed_plan,
214                        ),
215                        evaluation_batch: None,
216                        provenance,
217                    },
218                );
219            }
220        }
221    }
222    for provider in providers.values() {
223        let source = ContextValueSourceId::Provider(provider.id.clone());
224        let owner = lifetime
225            .provider_lifetime(&provider.id)
226            .map(|record| record.owner_component.clone());
227        let (required_state, required_computed) = source_dependencies(dependency, &source);
228        let reasons = provider_reasons(
229            provider,
230            provider_types.get(&provider.id),
231            lifetime.provider_lifetime(&provider.id),
232            &required_state,
233            &required_computed,
234            computed_plan,
235        );
236        if let Some(owner_component) = owner {
237            entries.insert(
238                source.clone(),
239                ContextSourcePlanEntry {
240                    source,
241                    owner_component,
242                    context: provider.context.clone(),
243                    expression_root: provider.value_expression.clone(),
244                    status: primary_status(&reasons),
245                    reasons,
246                    required_state,
247                    required_computed: required_computed.clone(),
248                    prerequisite_computed_batches: computed_batches(
249                        &required_computed,
250                        computed_plan,
251                    ),
252                    evaluation_batch: None,
253                    provenance: provider.provenance.clone(),
254                },
255            );
256        }
257    }
258    let mut demand = BTreeSet::new();
259    let mut selected = BTreeSet::new();
260    for (consumer, resolution) in resolutions {
261        let Some(source) = selected_source(&resolution.result) else {
262            continue;
263        };
264        selected.insert(source.clone());
265        if binding_types
266            .get(consumer)
267            .is_some_and(|binding| binding.overall == ContextBindingCompatibility::Compatible)
268            && lifetime
269                .context_binding_lifetime(consumer)
270                .is_some_and(|record| {
271                    record.compatibility == ContextBindingLifetimeStatus::Compatible
272                })
273        {
274            demand.insert(source);
275        }
276    }
277    for (source, entry) in &mut entries {
278        if !selected.contains(source) {
279            entry.status = ContextSourcePlanStatus::Unused;
280        } else if entry.reasons.is_empty() && demand.contains(source) {
281            entry.status = ContextSourcePlanStatus::Planned;
282        } else if entry.reasons.is_empty() {
283            entry.status = ContextSourcePlanStatus::Unused;
284        }
285    }
286    let batches = schedule_sources(&entries, scope);
287    for batch in &batches {
288        for source in &batch.sources {
289            if let Some(entry) = entries.get_mut(source) {
290                entry.evaluation_batch = Some(batch.id.index);
291            }
292        }
293    }
294    let consumer_entries = resolutions
295        .iter()
296        .map(|(consumer, resolution)| {
297            let selected_source = selected_source(&resolution.result);
298            let source_entry = selected_source
299                .as_ref()
300                .and_then(|source| entries.get(source));
301            let status = consumer_status(
302                &resolution.result,
303                binding_types.get(consumer),
304                lifetime.context_binding_lifetime(consumer),
305                source_entry,
306            );
307            let required_computed =
308                source_entry.map_or_else(Vec::new, |entry| entry.required_computed.clone());
309            (
310                consumer.clone(),
311                ContextConsumerAvailabilityEntry {
312                    consumer: consumer.clone(),
313                    resolution: resolution.result.clone(),
314                    selected_source,
315                    status,
316                    source_batch: source_entry.and_then(|entry| entry.evaluation_batch),
317                    required_computed,
318                    provenance: resolution.provenance.clone(),
319                },
320            )
321        })
322        .collect::<BTreeMap<_, _>>();
323    let planned_sources = entries
324        .iter()
325        .filter_map(|(source, entry)| {
326            (entry.status == ContextSourcePlanStatus::Planned).then_some(source.clone())
327        })
328        .collect();
329    let blocked_entries = entries
330        .iter()
331        .filter_map(|(source, entry)| {
332            (!matches!(
333                entry.status,
334                ContextSourcePlanStatus::Planned | ContextSourcePlanStatus::Unused
335            ))
336            .then_some(source.clone())
337        })
338        .collect();
339    let available_consumers = consumer_entries
340        .iter()
341        .filter_map(|(consumer, entry)| {
342            (entry.status == ContextConsumerAvailabilityStatus::Available)
343                .then_some(consumer.clone())
344        })
345        .collect();
346    let unavailable_consumers = consumer_entries
347        .iter()
348        .filter_map(|(consumer, entry)| {
349            (entry.status != ContextConsumerAvailabilityStatus::Available)
350                .then_some(consumer.clone())
351        })
352        .collect();
353    let batches_by_id = batches
354        .iter()
355        .map(|batch| (batch.id.clone(), batch.clone()))
356        .collect();
357    ContextEvaluationPlan {
358        id: ContextEvaluationPlanId::Initial,
359        source_entries: entries,
360        consumer_entries,
361        evaluation_batches: batches,
362        blocked_entries,
363        planned_sources,
364        available_consumers,
365        unavailable_consumers,
366        batches_by_id,
367    }
368}
369
370fn selected_source(result: &ContextResolutionResult) -> Option<ContextValueSourceId> {
371    match result {
372        ContextResolutionResult::Provider { provider, .. } => {
373            Some(ContextValueSourceId::Provider(provider.clone()))
374        }
375        ContextResolutionResult::ContextDefault { context, .. } => {
376            Some(ContextValueSourceId::ContextDefault(context.clone()))
377        }
378        _ => None,
379    }
380}
381
382fn source_dependencies(
383    graph: &crate::ContextDependencyGraph,
384    source: &ContextValueSourceId,
385) -> (Vec<SemanticId>, Vec<SemanticId>) {
386    let node = match source {
387        ContextValueSourceId::Provider(id) => ContextDependencyNodeId::Provider(id.clone()),
388        ContextValueSourceId::ContextDefault(id) => {
389            ContextDependencyNodeId::ContextDefault(id.clone())
390        }
391    };
392    graph
393        .direct_dependencies(&node)
394        .iter()
395        .fold((Vec::new(), Vec::new()), |mut acc, dependency| {
396            match dependency {
397                ContextDependencyNodeId::State(id) => acc.0.push(id.clone()),
398                ContextDependencyNodeId::Computed(id) => acc.1.push(id.clone()),
399                _ => {}
400            }
401            acc
402        })
403}
404
405fn provider_reasons(
406    provider: &crate::ProviderEntity,
407    types: Option<&ProviderTypeRecord>,
408    lifetime: Option<&ProviderLifetimeRecord>,
409    states: &[SemanticId],
410    computed: &[SemanticId],
411    computed_plan: &crate::IrComputedEvaluationPlan,
412) -> Vec<ContextSourceBlockReason> {
413    let mut reasons = Vec::new();
414    let Some(types) = types else {
415        return vec![ContextSourceBlockReason::TypeUnknown];
416    };
417    status_reasons(types.value_to_declaration, &mut reasons);
418    status_reasons(types.declaration_to_context, &mut reasons);
419    serialization_reasons(types.serialization, &mut reasons);
420    compatibility_reasons(
421        types.boundary_compatibility,
422        ContextSourceBlockReason::BoundaryIncompatible,
423        ContextSourceBlockReason::BoundaryUnknown,
424        &mut reasons,
425    );
426    lifetime_reasons(lifetime.map(|record| record.compatibility), &mut reasons);
427    dependency_reasons(states, computed, computed_plan, &mut reasons);
428    if provider.value_expression.as_str().is_empty() {
429        reasons.push(ContextSourceBlockReason::UnsupportedExpression);
430    }
431    normalize_reasons(reasons)
432}
433
434fn default_reasons(
435    context: &ContextEntity,
436    types: Option<&ContextTypeRecord>,
437    lifetime: Option<&ContextDefaultLifetimeRecord>,
438    states: &[SemanticId],
439    computed: &[SemanticId],
440    computed_plan: &crate::IrComputedEvaluationPlan,
441) -> Vec<ContextSourceBlockReason> {
442    let mut reasons = Vec::new();
443    let Some(types) = types else {
444        return vec![ContextSourceBlockReason::TypeUnknown];
445    };
446    match types.default_compatibility {
447        Some(compatibility) => status_reasons(compatibility, &mut reasons),
448        None => reasons.push(ContextSourceBlockReason::TypeUnknown),
449    }
450    serialization_reasons(types.serialization, &mut reasons);
451    compatibility_reasons(
452        types.boundary_compatibility,
453        ContextSourceBlockReason::BoundaryIncompatible,
454        ContextSourceBlockReason::BoundaryUnknown,
455        &mut reasons,
456    );
457    lifetime_reasons(lifetime.map(|record| record.compatibility), &mut reasons);
458    dependency_reasons(states, computed, computed_plan, &mut reasons);
459    if context.default_expression.is_none() {
460        reasons.push(ContextSourceBlockReason::UnsupportedExpression);
461    }
462    normalize_reasons(reasons)
463}
464
465fn status_reasons(status: CompatibilityStatus, reasons: &mut Vec<ContextSourceBlockReason>) {
466    match status {
467        CompatibilityStatus::Compatible => {}
468        CompatibilityStatus::Incompatible => {
469            reasons.push(ContextSourceBlockReason::TypeIncompatible);
470        }
471        CompatibilityStatus::Unknown => reasons.push(ContextSourceBlockReason::TypeUnknown),
472    }
473}
474fn serialization_reasons(
475    status: ContextSerializationCompatibility,
476    reasons: &mut Vec<ContextSourceBlockReason>,
477) {
478    match status {
479        ContextSerializationCompatibility::Serializable => {}
480        ContextSerializationCompatibility::NonSerializable => {
481            reasons.push(ContextSourceBlockReason::NonSerializable);
482        }
483        ContextSerializationCompatibility::Unknown => {
484            reasons.push(ContextSourceBlockReason::SerializationUnknown);
485        }
486    }
487}
488fn compatibility_reasons(
489    status: CompatibilityStatus,
490    incompatible: ContextSourceBlockReason,
491    unknown: ContextSourceBlockReason,
492    reasons: &mut Vec<ContextSourceBlockReason>,
493) {
494    match status {
495        CompatibilityStatus::Compatible => {}
496        CompatibilityStatus::Incompatible => reasons.push(incompatible),
497        CompatibilityStatus::Unknown => reasons.push(unknown),
498    }
499}
500fn lifetime_reasons(
501    status: Option<crate::LifetimeCompatibilityStatus>,
502    reasons: &mut Vec<ContextSourceBlockReason>,
503) {
504    match status {
505        Some(crate::LifetimeCompatibilityStatus::Compatible) => {}
506        Some(crate::LifetimeCompatibilityStatus::Incompatible) => {
507            reasons.push(ContextSourceBlockReason::LifetimeIncompatible);
508        }
509        _ => reasons.push(ContextSourceBlockReason::LifetimeUnknown),
510    }
511}
512fn dependency_reasons(
513    states: &[SemanticId],
514    computed: &[SemanticId],
515    plan: &crate::IrComputedEvaluationPlan,
516    reasons: &mut Vec<ContextSourceBlockReason>,
517) {
518    for state in states {
519        if state.as_str().is_empty() {
520            reasons.push(ContextSourceBlockReason::MissingStateDependency(
521                state.clone(),
522            ));
523        }
524    }
525    for value in computed {
526        if !plan.evaluation_order.contains(&value.as_str().to_string())
527            || plan.unplanned.contains(&value.as_str().to_string())
528        {
529            reasons.push(ContextSourceBlockReason::UnavailableComputedDependency(
530                value.clone(),
531            ));
532        }
533    }
534}
535fn normalize_reasons(mut reasons: Vec<ContextSourceBlockReason>) -> Vec<ContextSourceBlockReason> {
536    reasons.sort();
537    reasons.dedup();
538    reasons
539}
540fn primary_status(reasons: &[ContextSourceBlockReason]) -> ContextSourcePlanStatus {
541    if reasons.iter().any(|reason| {
542        matches!(
543            reason,
544            ContextSourceBlockReason::TypeIncompatible | ContextSourceBlockReason::TypeUnknown
545        )
546    }) {
547        ContextSourcePlanStatus::BlockedType
548    } else if reasons.iter().any(|reason| {
549        matches!(
550            reason,
551            ContextSourceBlockReason::NonSerializable
552                | ContextSourceBlockReason::SerializationUnknown
553        )
554    }) {
555        ContextSourcePlanStatus::BlockedSerialization
556    } else if reasons.iter().any(|reason| {
557        matches!(
558            reason,
559            ContextSourceBlockReason::BoundaryIncompatible
560                | ContextSourceBlockReason::BoundaryUnknown
561        )
562    }) {
563        ContextSourcePlanStatus::BlockedBoundary
564    } else if reasons.iter().any(|reason| {
565        matches!(
566            reason,
567            ContextSourceBlockReason::LifetimeIncompatible
568                | ContextSourceBlockReason::LifetimeUnknown
569        )
570    }) {
571        ContextSourcePlanStatus::BlockedLifetime
572    } else if reasons.iter().any(|reason| {
573        matches!(
574            reason,
575            ContextSourceBlockReason::MissingStateDependency(_)
576                | ContextSourceBlockReason::UnavailableComputedDependency(_)
577        )
578    }) {
579        ContextSourcePlanStatus::BlockedDependency
580    } else if reasons
581        .iter()
582        .any(|reason| matches!(reason, ContextSourceBlockReason::UnsupportedExpression))
583    {
584        ContextSourcePlanStatus::BlockedUnsupportedExpression
585    } else {
586        ContextSourcePlanStatus::UnknownEligibility
587    }
588}
589fn computed_batches(values: &[SemanticId], plan: &crate::IrComputedEvaluationPlan) -> Vec<u32> {
590    plan.update_batches
591        .iter()
592        .enumerate()
593        .filter_map(|(index, batch)| {
594            values
595                .iter()
596                .any(|value| batch.contains(&value.as_str().to_string()))
597                .then_some(u32::try_from(index).ok())
598                .flatten()
599        })
600        .collect()
601}
602fn schedule_sources(
603    entries: &BTreeMap<ContextValueSourceId, ContextSourcePlanEntry>,
604    scope: &crate::ComponentScopeGraph,
605) -> Vec<ContextEvaluationBatch> {
606    let source_keys = entries
607        .values()
608        .filter(|entry| entry.status == ContextSourcePlanStatus::Planned)
609        .map(|entry| {
610            let depth = scope.ancestor_chain(&entry.owner_component).len();
611            (
612                format!("{depth:08}:{:?}", entry.source),
613                entry.source.clone(),
614            )
615        })
616        .collect::<BTreeMap<_, _>>();
617    let nodes = source_keys
618        .iter()
619        .filter_map(|(id, source)| {
620            entries.get(source).map(|entry| {
621                (
622                    id.clone(),
623                    IrReactiveNode {
624                        id: id.clone(),
625                        kind: IrReactiveNodeKind::Effect,
626                        provenance: entry.provenance.clone(),
627                    },
628                )
629            })
630        })
631        .collect();
632    let inspection = IrUpdateScheduler::new(IrReactiveGraph {
633        nodes,
634        edges: Vec::new(),
635    })
636    .inspect();
637    inspection
638        .batches
639        .into_iter()
640        .enumerate()
641        .filter_map(|(index, batch)| {
642            let index = u32::try_from(index).ok()?;
643            let sources = batch
644                .into_iter()
645                .filter_map(|key| source_keys.get(&key).cloned())
646                .collect::<Vec<_>>();
647            (!sources.is_empty()).then_some(ContextEvaluationBatch {
648                id: ContextEvaluationBatchId {
649                    plan: ContextEvaluationPlanId::Initial,
650                    index,
651                },
652                sources,
653            })
654        })
655        .collect()
656}
657fn consumer_status(
658    resolution: &ContextResolutionResult,
659    binding: Option<&ContextBindingTypeRecord>,
660    lifetime: Option<&crate::ContextBindingLifetimeRecord>,
661    source: Option<&ContextSourcePlanEntry>,
662) -> ContextConsumerAvailabilityStatus {
663    match resolution {
664        ContextResolutionResult::Unresolved => ContextConsumerAvailabilityStatus::Unresolved,
665        ContextResolutionResult::Ambiguous { .. } => ContextConsumerAvailabilityStatus::Ambiguous,
666        ContextResolutionResult::InvalidContextReference => {
667            ContextConsumerAvailabilityStatus::InvalidContextReference
668        }
669        _ => {
670            let Some(binding) = binding else {
671                return ContextConsumerAvailabilityStatus::UnknownEligibility;
672            };
673            if binding.overall == ContextBindingCompatibility::Incompatible {
674                if binding.serialization == ContextSerializationCompatibility::NonSerializable {
675                    return ContextConsumerAvailabilityStatus::SerializationIncompatible;
676                }
677                if binding.boundary_compatibility == CompatibilityStatus::Incompatible {
678                    return ContextConsumerAvailabilityStatus::BoundaryIncompatible;
679                }
680                return ContextConsumerAvailabilityStatus::TypeIncompatible;
681            }
682            if binding.overall == ContextBindingCompatibility::Unknown {
683                return ContextConsumerAvailabilityStatus::UnknownEligibility;
684            }
685            match lifetime.map(|record| record.compatibility) {
686                Some(ContextBindingLifetimeStatus::Incompatible) => {
687                    ContextConsumerAvailabilityStatus::LifetimeIncompatible
688                }
689                Some(ContextBindingLifetimeStatus::Unknown) | None => {
690                    ContextConsumerAvailabilityStatus::UnknownEligibility
691                }
692                Some(ContextBindingLifetimeStatus::Compatible)
693                    if source
694                        .is_some_and(|entry| entry.status == ContextSourcePlanStatus::Planned) =>
695                {
696                    ContextConsumerAvailabilityStatus::Available
697                }
698                _ => ContextConsumerAvailabilityStatus::BlockedSource,
699            }
700        }
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use crate::{
707        build_application_semantic_model, ConsumerId, ContextConsumerAvailabilityStatus,
708        ContextSourcePlanStatus, ProviderId,
709    };
710    #[test]
711    fn plans_one_shared_provider_for_eligible_consumers() {
712        let asm = build_application_semantic_model(&presolve_parser::parse_file(
713            "src/app.tsx",
714            r#"@component("x-app") class App extends Component { @context() theme!: string; @provide(App.theme) provided: string = "dark"; @consume(App.theme) a!: string; @consume(App.theme) b!: string; render() { return <main />; } }"#,
715        ));
716        let component = &asm.components[0].id;
717        let provider = ProviderId::for_component(component, "provided");
718        let plan = asm.context_evaluation_plan();
719        assert_eq!(
720            plan.context_source_plan(&crate::ContextValueSourceId::Provider(provider))
721                .unwrap()
722                .status,
723            ContextSourcePlanStatus::Planned
724        );
725        assert_eq!(plan.evaluation_batches.len(), 1);
726        assert_eq!(plan.available_context_consumers().len(), 2);
727    }
728    #[test]
729    fn retains_unused_and_incompatible_consumer_facts() {
730        let asm = build_application_semantic_model(&presolve_parser::parse_file(
731            "src/app.tsx",
732            r#"@component("x-app") class App extends Component { @context() theme!: number; @provide(App.theme) provided: string = "dark"; @consume(App.theme) value!: number; render() { return <main />; } }"#,
733        ));
734        let component = &asm.components[0].id;
735        let provider = ProviderId::for_component(component, "provided");
736        let consumer = ConsumerId::for_component(component, "value");
737        let plan = asm.context_evaluation_plan();
738        assert_eq!(
739            plan.context_source_plan(&crate::ContextValueSourceId::Provider(provider))
740                .unwrap()
741                .status,
742            ContextSourcePlanStatus::BlockedType
743        );
744        assert_eq!(
745            plan.context_consumer_availability(&consumer)
746                .unwrap()
747                .status,
748            ContextConsumerAvailabilityStatus::TypeIncompatible
749        );
750    }
751    #[test]
752    fn plans_a_selected_context_default_once() {
753        let asm = build_application_semantic_model(&presolve_parser::parse_file(
754            "src/app.tsx",
755            r#"@component("x-app") class App extends Component { @context() locale: string = "en"; @consume(App.locale) selected!: string; render() { return <main />; } }"#,
756        ));
757        let component = &asm.components[0].id;
758        let context = crate::ContextId::for_component(component, "locale");
759        let consumer = ConsumerId::for_component(component, "selected");
760        let plan = asm.context_evaluation_plan();
761        assert_eq!(
762            plan.context_source_plan(&crate::ContextValueSourceId::ContextDefault(context))
763                .unwrap()
764                .status,
765            ContextSourcePlanStatus::Planned
766        );
767        assert_eq!(
768            plan.context_consumer_availability(&consumer)
769                .unwrap()
770                .status,
771            ContextConsumerAvailabilityStatus::Available
772        );
773    }
774    #[test]
775    fn retains_an_unselected_provider_without_an_executable_batch() {
776        let asm = build_application_semantic_model(&presolve_parser::parse_file(
777            "src/app.tsx",
778            r#"@component("x-app") class App extends Component { @context() theme!: string; @provide(App.theme) provided: string = "dark"; render() { return <main />; } }"#,
779        ));
780        let component = &asm.components[0].id;
781        let provider = ProviderId::for_component(component, "provided");
782        let plan = asm.context_evaluation_plan();
783        assert_eq!(
784            plan.context_source_plan(&crate::ContextValueSourceId::Provider(provider))
785                .unwrap()
786                .status,
787            ContextSourcePlanStatus::Unused
788        );
789        assert!(plan.evaluation_batches.is_empty());
790    }
791    #[test]
792    fn orders_same_scope_sources_by_stable_source_identity() {
793        let asm = build_application_semantic_model(&presolve_parser::parse_file(
794            "src/app.tsx",
795            r#"@component("x-app") class App extends Component { @context() alpha!: string; @context() beta!: string; @provide(App.beta) second: string = "b"; @provide(App.alpha) first: string = "a"; @consume(App.beta) betaValue!: string; @consume(App.alpha) alphaValue!: string; render() { return <main />; } }"#,
796        ));
797        let component = &asm.components[0].id;
798        let plan = asm.context_evaluation_plan();
799        assert_eq!(plan.evaluation_batches.len(), 1);
800        assert_eq!(
801            plan.evaluation_batches[0].sources,
802            vec![
803                crate::ContextValueSourceId::Provider(ProviderId::for_component(
804                    component, "first"
805                )),
806                crate::ContextValueSourceId::Provider(ProviderId::for_component(
807                    component, "second"
808                )),
809            ]
810        );
811    }
812}