1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 ComponentInstanceId, ComponentInstanceScopeGraph, ComponentRootId, ConsumerEntity, ConsumerId,
7 ContextEntity, ContextId, ProviderEntity, ProviderId, SemanticId, SourceProvenance,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11pub struct ProviderInstanceId {
12 pub provider: ProviderId,
13 pub component_instance: ComponentInstanceId,
14}
15
16impl ProviderInstanceId {
17 #[must_use]
18 pub fn new(provider: ProviderId, component_instance: ComponentInstanceId) -> Self {
19 Self {
20 provider,
21 component_instance,
22 }
23 }
24}
25
26impl std::fmt::Display for ProviderInstanceId {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(
29 formatter,
30 "provider-instance:{}@{}",
31 self.provider, self.component_instance
32 )
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
37pub struct ConsumerInstanceId {
38 pub consumer: ConsumerId,
39 pub component_instance: ComponentInstanceId,
40}
41
42impl ConsumerInstanceId {
43 #[must_use]
44 pub fn new(consumer: ConsumerId, component_instance: ComponentInstanceId) -> Self {
45 Self {
46 consumer,
47 component_instance,
48 }
49 }
50}
51
52impl std::fmt::Display for ConsumerInstanceId {
53 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(
55 formatter,
56 "consumer-instance:{}@{}",
57 self.consumer, self.component_instance
58 )
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63pub struct ContextDefaultSourceInstanceId {
64 pub context: ContextId,
65 pub owner_root: ComponentRootId,
66}
67
68impl std::fmt::Display for ContextDefaultSourceInstanceId {
69 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 write!(
71 formatter,
72 "context-default-instance:{}@{}",
73 self.context, self.owner_root
74 )
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
79pub enum ContextSourceInstanceOwner {
80 Provider(ProviderInstanceId),
81 RootDefault(ContextDefaultSourceInstanceId),
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85pub struct ContextSourceInstanceId {
86 pub context: ContextId,
87 pub owner: ContextSourceInstanceOwner,
88}
89
90impl std::fmt::Display for ContextSourceInstanceId {
91 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 write!(formatter, "context-source:{}@", self.context)?;
93 match &self.owner {
94 ContextSourceInstanceOwner::Provider(provider) => provider.fmt(formatter),
95 ContextSourceInstanceOwner::RootDefault(default) => default.fmt(formatter),
96 }
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
101#[serde(transparent)]
102pub struct InstanceContextValueSlotId(String);
103
104impl InstanceContextValueSlotId {
105 #[must_use]
106 pub fn for_source(source: &ContextSourceInstanceId) -> Self {
107 Self(format!("context-instance-slot:{source}"))
108 }
109
110 #[must_use]
111 pub fn as_str(&self) -> &str {
112 &self.0
113 }
114}
115
116impl std::fmt::Display for InstanceContextValueSlotId {
117 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 self.0.fmt(formatter)
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct ProviderInstanceRecord {
124 pub id: ProviderInstanceId,
125 pub context: ContextId,
126 pub component: SemanticId,
127 pub provenance: SourceProvenance,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ConsumerInstanceRecord {
132 pub id: ConsumerInstanceId,
133 pub context: Option<ContextId>,
134 pub component: SemanticId,
135 pub provenance: SourceProvenance,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum InstanceContextResolutionStatus {
140 ProviderSelected,
141 ContextDefaultSelected,
142 Unresolved,
143 Ambiguous,
144 InvalidContextReference,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct InstanceContextResolution {
149 pub consumer_instance: ConsumerInstanceId,
150 pub context: Option<ContextId>,
151 pub selected_source: Option<ContextSourceInstanceId>,
152 pub provider_instance: Option<ProviderInstanceId>,
153 pub default_source: Option<ContextDefaultSourceInstanceId>,
154 pub value_slot: Option<InstanceContextValueSlotId>,
155 pub ancestry_distance: Option<u32>,
156 pub candidate_provider_instances: Vec<ProviderInstanceId>,
157 pub status: InstanceContextResolutionStatus,
158 pub provenance: SourceProvenance,
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
162pub struct InstanceContextRegistry {
163 pub provider_instances: BTreeMap<ProviderInstanceId, ProviderInstanceRecord>,
164 pub consumer_instances: BTreeMap<ConsumerInstanceId, ConsumerInstanceRecord>,
165 pub default_sources: BTreeSet<ContextDefaultSourceInstanceId>,
166 pub resolutions: BTreeMap<ConsumerInstanceId, InstanceContextResolution>,
167}
168
169impl InstanceContextRegistry {
170 #[must_use]
171 pub fn resolution(&self, consumer: &ConsumerInstanceId) -> Option<&InstanceContextResolution> {
172 self.resolutions.get(consumer)
173 }
174
175 #[must_use]
176 pub fn resolutions_for_declaration(
177 &self,
178 consumer: &ConsumerId,
179 ) -> Vec<&InstanceContextResolution> {
180 self.resolutions
181 .values()
182 .filter(|resolution| &resolution.consumer_instance.consumer == consumer)
183 .collect()
184 }
185}
186
187#[must_use]
189pub fn collect_instance_context_registry(
190 scope: &ComponentInstanceScopeGraph,
191 contexts: &BTreeMap<ContextId, ContextEntity>,
192 providers: &BTreeMap<ProviderId, ProviderEntity>,
193 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
194) -> InstanceContextRegistry {
195 let providers_by_component = provider_index(providers);
196 let consumers_by_component = consumer_index(consumers);
197 let mut registry = InstanceContextRegistry::default();
198
199 for node in scope.nodes.values() {
200 for provider in providers.values().filter(|provider| {
201 provider
202 .owner
203 .entity_id()
204 .is_some_and(|owner| owner == &node.component)
205 }) {
206 let id = ProviderInstanceId::new(provider.id.clone(), node.id.clone());
207 registry.provider_instances.insert(
208 id.clone(),
209 ProviderInstanceRecord {
210 id,
211 context: provider.context.clone(),
212 component: node.component.clone(),
213 provenance: provider.provenance.clone(),
214 },
215 );
216 }
217 }
218
219 for node in scope.nodes.values() {
220 let Some(component_consumers) = consumers_by_component.get(&node.component) else {
221 continue;
222 };
223 for consumer in component_consumers {
224 let id = ConsumerInstanceId::new(consumer.id.clone(), node.id.clone());
225 registry.consumer_instances.insert(
226 id.clone(),
227 ConsumerInstanceRecord {
228 id: id.clone(),
229 context: consumer.context().cloned(),
230 component: node.component.clone(),
231 provenance: consumer.context_designator.provenance.clone(),
232 },
233 );
234 let resolution = resolve_consumer_instance(
235 id.clone(),
236 consumer,
237 scope,
238 contexts,
239 &providers_by_component,
240 );
241 if let Some(default) = &resolution.default_source {
242 registry.default_sources.insert(default.clone());
243 }
244 registry.resolutions.insert(id, resolution);
245 }
246 }
247
248 registry
249}
250
251#[derive(Debug, Clone)]
252struct IndexedProvider {
253 provider: ProviderId,
254}
255
256fn provider_index(
257 providers: &BTreeMap<ProviderId, ProviderEntity>,
258) -> BTreeMap<(SemanticId, ContextId), Vec<IndexedProvider>> {
259 let mut index = BTreeMap::<(SemanticId, ContextId), Vec<IndexedProvider>>::new();
260 for provider in providers.values() {
261 let Some(component) = provider.owner.entity_id() else {
262 continue;
263 };
264 index
265 .entry((component.clone(), provider.context.clone()))
266 .or_default()
267 .push(IndexedProvider {
268 provider: provider.id.clone(),
269 });
270 }
271 for providers in index.values_mut() {
272 providers.sort_by(|left, right| left.provider.cmp(&right.provider));
273 }
274 index
275}
276
277fn consumer_index(
278 consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
279) -> BTreeMap<SemanticId, Vec<&ConsumerEntity>> {
280 let mut index = BTreeMap::<SemanticId, Vec<&ConsumerEntity>>::new();
281 for consumer in consumers.values() {
282 if let Some(component) = consumer.owner.entity_id() {
283 index.entry(component.clone()).or_default().push(consumer);
284 }
285 }
286 for consumers in index.values_mut() {
287 consumers.sort_by(|left, right| left.id.cmp(&right.id));
288 }
289 index
290}
291
292#[allow(clippy::too_many_lines)]
293fn resolve_consumer_instance(
294 consumer_instance: ConsumerInstanceId,
295 consumer: &ConsumerEntity,
296 scope: &ComponentInstanceScopeGraph,
297 contexts: &BTreeMap<ContextId, ContextEntity>,
298 providers_by_component: &BTreeMap<(SemanticId, ContextId), Vec<IndexedProvider>>,
299) -> InstanceContextResolution {
300 let provenance = consumer.context_designator.provenance.clone();
301 let Some(context) = consumer.context().cloned() else {
302 return InstanceContextResolution {
303 consumer_instance,
304 context: None,
305 selected_source: None,
306 provider_instance: None,
307 default_source: None,
308 value_slot: None,
309 ancestry_distance: None,
310 candidate_provider_instances: Vec::new(),
311 status: InstanceContextResolutionStatus::InvalidContextReference,
312 provenance,
313 };
314 };
315 let mut scopes = vec![&consumer_instance.component_instance];
316 scopes.extend(scope.ancestors(&consumer_instance.component_instance));
317
318 for (distance, instance_id) in scopes.into_iter().enumerate() {
319 let Some(node) = scope.node(instance_id) else {
320 continue;
321 };
322 let candidates = providers_by_component
323 .get(&(node.component.clone(), context.clone()))
324 .into_iter()
325 .flatten()
326 .map(|provider| ProviderInstanceId::new(provider.provider.clone(), instance_id.clone()))
327 .collect::<Vec<_>>();
328 match candidates.as_slice() {
329 [] => {}
330 [provider_instance] => {
331 let selected_source = ContextSourceInstanceId {
332 context: context.clone(),
333 owner: ContextSourceInstanceOwner::Provider(provider_instance.clone()),
334 };
335 return InstanceContextResolution {
336 consumer_instance,
337 context: Some(context),
338 selected_source: Some(selected_source.clone()),
339 provider_instance: Some(provider_instance.clone()),
340 default_source: None,
341 value_slot: Some(InstanceContextValueSlotId::for_source(&selected_source)),
342 ancestry_distance: Some(
343 u32::try_from(distance).expect("instance ancestry depth fits in u32"),
344 ),
345 candidate_provider_instances: candidates,
346 status: InstanceContextResolutionStatus::ProviderSelected,
347 provenance,
348 };
349 }
350 _ => {
351 return InstanceContextResolution {
352 consumer_instance,
353 context: Some(context),
354 selected_source: None,
355 provider_instance: None,
356 default_source: None,
357 value_slot: None,
358 ancestry_distance: Some(
359 u32::try_from(distance).expect("instance ancestry depth fits in u32"),
360 ),
361 candidate_provider_instances: candidates,
362 status: InstanceContextResolutionStatus::Ambiguous,
363 provenance,
364 };
365 }
366 }
367 }
368
369 let owner_root = scope
370 .node(&consumer_instance.component_instance)
371 .expect("consumer instances belong to scope nodes")
372 .owner_root
373 .clone();
374 if contexts
375 .get(&context)
376 .is_some_and(|context| context.default_expression.is_some())
377 {
378 let default_source = ContextDefaultSourceInstanceId {
379 context: context.clone(),
380 owner_root,
381 };
382 let selected_source = ContextSourceInstanceId {
383 context: context.clone(),
384 owner: ContextSourceInstanceOwner::RootDefault(default_source.clone()),
385 };
386 return InstanceContextResolution {
387 consumer_instance,
388 context: Some(context),
389 selected_source: Some(selected_source.clone()),
390 provider_instance: None,
391 default_source: Some(default_source),
392 value_slot: Some(InstanceContextValueSlotId::for_source(&selected_source)),
393 ancestry_distance: None,
394 candidate_provider_instances: Vec::new(),
395 status: InstanceContextResolutionStatus::ContextDefaultSelected,
396 provenance,
397 };
398 }
399
400 InstanceContextResolution {
401 consumer_instance,
402 context: Some(context),
403 selected_source: None,
404 provider_instance: None,
405 default_source: None,
406 value_slot: None,
407 ancestry_distance: None,
408 candidate_provider_instances: Vec::new(),
409 status: InstanceContextResolutionStatus::Unresolved,
410 provenance,
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use crate::{
417 build_application_semantic_model, validate_application_semantic_model,
418 ContextResolutionResult, InstanceContextResolutionStatus,
419 };
420
421 #[test]
422 fn resolves_repeated_consumer_definitions_to_distinct_provider_instances() {
423 let asm = build_application_semantic_model(&presolve_parser::parse_file(
424 "src/InstanceContext.tsx",
425 r#"
426@component("x-theme") class Theme extends Component {
427 @context() color!: string;
428 render() { return <div />; }
429}
430@component("x-leaf") class Leaf extends Component {
431 @consume(Theme.color) color!: string;
432 render() { return <span />; }
433}
434@component("x-light") class Light extends Component {
435 @provide(Theme.color) color: string = "light";
436 render() { return <Leaf />; }
437}
438@component("x-dark") class Dark extends Component {
439 @provide(Theme.color) color: string = "dark";
440 render() { return <Leaf />; }
441}
442@component("x-page") @route("/") class Page extends Component {
443 render() { return <main><Light /><Dark /></main>; }
444}
445"#,
446 ));
447 let consumer = asm.consumers().first().unwrap().id.clone();
448 let resolutions = asm.instance_context.resolutions_for_declaration(&consumer);
449
450 assert_eq!(resolutions.len(), 2);
451 assert!(resolutions.iter().all(|resolution| {
452 resolution.status == InstanceContextResolutionStatus::ProviderSelected
453 && resolution.ancestry_distance == Some(1)
454 && resolution.value_slot.is_some()
455 }));
456 assert_ne!(
457 resolutions[0].provider_instance,
458 resolutions[1].provider_instance
459 );
460 assert_ne!(
461 resolutions[0].selected_source,
462 resolutions[1].selected_source
463 );
464 assert_ne!(resolutions[0].value_slot, resolutions[1].value_slot);
465 assert!(matches!(
466 asm.context_resolution(&consumer).unwrap().result,
467 ContextResolutionResult::Unresolved
468 ));
469 }
470
471 #[test]
472 fn uses_root_qualified_defaults_and_never_reselects_for_type_failure() {
473 let default_asm = build_application_semantic_model(&presolve_parser::parse_file(
474 "src/Defaults.tsx",
475 r#"
476@component("x-theme") class Theme extends Component {
477 @context() color: string = "blue";
478 render() { return <div />; }
479}
480@component("x-leaf") class Leaf extends Component {
481 @consume(Theme.color) color!: string;
482 render() { return <span />; }
483}
484@component("x-a") @route("/a") class A extends Component { render() { return <Leaf />; } }
485@component("x-b") @route("/b") class B extends Component { render() { return <Leaf />; } }
486"#,
487 ));
488 let defaults = default_asm
489 .instance_context
490 .resolutions
491 .values()
492 .filter_map(|resolution| resolution.default_source.as_ref())
493 .collect::<Vec<_>>();
494 assert_eq!(defaults.len(), 2);
495 assert_ne!(defaults[0].owner_root, defaults[1].owner_root);
496
497 let incompatible = build_application_semantic_model(&presolve_parser::parse_file(
498 "src/Incompatible.tsx",
499 r#"
500@component("x-theme") class Theme extends Component {
501 @context() color!: string;
502 render() { return <div />; }
503}
504@component("x-leaf") class Leaf extends Component {
505 @consume(Theme.color) color!: string;
506 render() { return <span />; }
507}
508@component("x-page") @route("/") class Page extends Component {
509 @provide(Theme.color) color: number = 1;
510 render() { return <Leaf />; }
511}
512"#,
513 ));
514 let resolution = incompatible
515 .instance_context
516 .resolutions
517 .values()
518 .next()
519 .unwrap();
520 assert_eq!(
521 resolution.status,
522 InstanceContextResolutionStatus::ProviderSelected
523 );
524 assert!(resolution.provider_instance.is_some());
525 assert!(resolution.value_slot.is_some());
526 }
527
528 #[test]
529 fn asm_validation_rejects_noncanonical_instance_context_resolution() {
530 let mut asm = build_application_semantic_model(&presolve_parser::parse_file(
531 "src/Validate.tsx",
532 r#"
533@component("x-theme") class Theme extends Component {
534 @context() color: string = "blue";
535 render() { return <div />; }
536}
537@component("x-page")
538@route("/")
539class Page extends Component {
540 @consume(Theme.color) color!: string;
541 render() { return <main />; }
542}
543"#,
544 ));
545 assert!(validate_application_semantic_model(&asm).is_empty());
546
547 asm.instance_context
548 .resolutions
549 .values_mut()
550 .next()
551 .unwrap()
552 .status = InstanceContextResolutionStatus::Unresolved;
553 let diagnostics = validate_application_semantic_model(&asm);
554 assert!(diagnostics.iter().any(|diagnostic| {
555 diagnostic.code == "PSASM1194"
556 && diagnostic.message
557 == "instance Context registry does not match canonical declarations and H5 ancestry"
558 }));
559 }
560}