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}