Skip to main content

presolve_compiler/
application_semantic_model.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::path::Path;
3
4use presolve_parser::ParsedFile;
5
6use crate::compilation_unit::CompilationUnit;
7use crate::component_composition::{analyze_component_composition, ComponentCompositionAnalysis};
8use crate::component_graph::{
9    build_component_graph_for_module, render_event_handlers, ComponentAction, ComponentDiagnostic,
10    ComponentDiagnosticSeverity, ComponentMethod, ComponentNode, ComputedExpression,
11    ComputedExpressionKind, MethodLocalVariable, RenderEventHandler, StateField,
12};
13use crate::component_initialization::{plan_component_initialization, ComponentInitializationPlan};
14use crate::component_instance::{
15    plan_component_instances, plan_component_instances_with_virtual_invocations,
16    BlockedComponentInstancePlan, ComponentInstance, ComponentInstancePlan,
17};
18use crate::component_instance_scope::{
19    build_component_instance_scope_graph, ComponentInstanceScopeGraph,
20};
21use crate::component_invocation::{collect_component_invocations, ComponentInvocationEntity};
22use crate::component_ir::{lower_component_ir, ComponentIrReport};
23use crate::component_ir_optimization::{optimize_component_ir, OptimizedComponentIrReport};
24use crate::component_scope::ComponentScopeGraph;
25use crate::composition_typing::{collect_composition_type_products, CompositionTypeProducts};
26use crate::computed_value::{collect_computed_values, ComputedDiagnosticCode, ComputedValue};
27use crate::consumer::{collect_consumer_entities, ConsumerEntity, ContextResolutionState};
28use crate::context::{collect_context_entities, ContextEntity};
29use crate::context_declaration_candidate::{
30    collect_context_declaration_candidates, ContextDeclarationCandidateRegistry,
31};
32use crate::context_dependency::{collect_context_dependency_graph, ContextDependencyGraph};
33use crate::context_evaluation::{collect_context_evaluation_plan, ContextEvaluationPlan};
34use crate::context_lifetime::{collect_context_lifetime_analysis, ContextLifetimeAnalysis};
35use crate::context_ownership::{collect_context_ownership_graph, ContextOwnershipGraph};
36use crate::context_resolution::{
37    collect_context_resolutions, ContextResolution, ContextResolutionResult,
38};
39use crate::context_typing::{
40    collect_context_type_products, ConsumerTypeRecord, ContextBindingCompatibility,
41    ContextBindingTypeRecord, ContextTypeRecord, ProviderTypeRecord,
42};
43use crate::expression_graph::{ExpressionGraph, ExpressionNode, ExpressionNodeKind};
44use crate::form::{collect_form_entities, FormEntity};
45use crate::form_binding::{
46    collect_form_field_binding_products, FormFieldBinding, FormFieldBindingCandidate,
47};
48use crate::form_field::{collect_form_field_products, FormFieldEntity};
49use crate::form_ir::{lower_form_ir, FormIrReport};
50use crate::form_ir_optimization::{optimize_form_ir, OptimizedFormIrReport};
51use crate::form_ownership::{collect_form_ownership_graph, FormOwnershipGraph};
52use crate::form_reset::{collect_reset_products, ResetProducts};
53use crate::form_serialization::{collect_serialization_products, SerializationProducts};
54use crate::form_submission::{collect_submission_products, SubmissionProducts};
55use crate::form_submission_host::{
56    collect_submission_host_products, SubmissionHost, SubmissionHostCandidate,
57};
58use crate::form_tracking::{collect_form_tracking_products, FormTrackingProducts};
59use crate::form_validation::{
60    collect_validation_graph, collect_validation_products, ValidationGraph, ValidationRule,
61    ValidationRuleCandidate,
62};
63use crate::form_validation_plan::{collect_validation_dependency_plans, ValidationDependencyPlans};
64use crate::instance_context::{collect_instance_context_registry, InstanceContextRegistry};
65use crate::intermediate_representation::{
66    IrComputedEvaluationPlan, IrReactiveCycleAnalysis, IrReactiveGraph,
67    IrReactiveTransitiveAnalysis,
68};
69use crate::provider::{collect_provider_entities, DuplicateProviderDeclaration, ProviderEntity};
70use crate::runtime_form_registry::{build_runtime_form_registry, RuntimeFormRegistry};
71use crate::semantic_id::{
72    ComponentInvocationId, ConsumerId, ContextId, FieldBindingId, FieldId, FormId, ProviderId,
73    ResourceActivationId, ResourceId, SemanticId, SemanticOwner, SlotBindingId,
74    SlotContentFragmentId, SlotId, SlotOutletId, ValidationRuleId,
75};
76use crate::semantic_provenance::SourceProvenance;
77use crate::semantic_reference::{SemanticReference, SemanticReferenceKind};
78use crate::semantic_type::{EffectStatementTypeRecord, SemanticTypeModel};
79use crate::slot::{collect_slot_entities, SlotEntity};
80use crate::slot_binding::{
81    collect_slot_bindings, collect_slot_bindings_with_virtual_invocations, SlotBinding,
82    SlotBindingRegistry,
83};
84use crate::slot_content::{collect_slot_composition, SlotContentFragment, SlotOutlet};
85use crate::template_graph::{build_template_graph, TemplateNode};
86use crate::template_semantics::{
87    build_template_semantic_entities, TemplateSemanticEntity, TemplateSemanticKind,
88    TemplateSemanticScope,
89};
90use crate::{
91    analyze_effect_reactivity, collect_effects, derive_effect_trigger_plan, lower_effect_bodies,
92    plan_effect_execution, validate_effects, Effect, EffectBody, EffectExecutionPlan,
93    EffectReactiveAnalysis, EffectStatement, EffectTriggerPlan,
94};
95
96/// Stable failure returned while assembling the compiler-owned semantic model
97/// for a conventional `app/routes` application.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct FileRouteApplicationModelErrorV1 {
100    pub code: &'static str,
101    pub message: String,
102}
103
104/// Application-level semantic data assembled from the compiler's existing graphs.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ApplicationSemanticModel {
107    pub expression_graph: ExpressionGraph,
108    pub semantic_types: SemanticTypeModel,
109    pub components: Vec<ComponentNode>,
110    pub contexts: BTreeMap<ContextId, ContextEntity>,
111    pub providers: BTreeMap<ProviderId, ProviderEntity>,
112    pub consumers: BTreeMap<ConsumerId, ConsumerEntity>,
113    pub forms: BTreeMap<FormId, FormEntity>,
114    /// Integrity-checked package endpoint selections for Resource declaration
115    /// and activation lowering.
116    pub resource_endpoint_resolutions: Vec<crate::ResourceEndpointResolution>,
117    /// Integrity-checked opaque terminal package selections. These are
118    /// resolution facts only until a later artifact/runtime lowering product
119    /// consumes them.
120    pub opaque_action_resolutions: Vec<crate::OpaqueActionResolution>,
121    /// Validated Resource declaration products projected into the canonical
122    /// runtime activation artifact when a host supplies exact module locations.
123    pub resource_declarations: BTreeMap<ResourceId, crate::ResourceDeclaration>,
124    pub resource_activations: BTreeMap<ResourceActivationId, crate::ResourceActivation>,
125    pub form_field_declaration_candidates: Vec<crate::FormFieldDeclarationCandidate>,
126    pub form_fields: BTreeMap<FieldId, FormFieldEntity>,
127    pub form_field_binding_candidates: Vec<FormFieldBindingCandidate>,
128    pub form_field_bindings: BTreeMap<FieldBindingId, FormFieldBinding>,
129    pub form_ownership: FormOwnershipGraph,
130    pub validation_rule_candidates: Vec<ValidationRuleCandidate>,
131    pub validation_rules: BTreeMap<ValidationRuleId, ValidationRule>,
132    pub validation_graph: ValidationGraph,
133    pub validation_dependency_plans: ValidationDependencyPlans,
134    pub form_tracking: FormTrackingProducts,
135    pub submissions: SubmissionProducts,
136    pub submission_host_candidates: Vec<SubmissionHostCandidate>,
137    pub submission_hosts: BTreeMap<crate::SubmissionHostId, SubmissionHost>,
138    pub serialization: SerializationProducts,
139    pub reset: ResetProducts,
140    pub form_ir: FormIrReport,
141    pub optimized_form_ir: OptimizedFormIrReport,
142    pub runtime_forms: RuntimeFormRegistry,
143    pub slots: BTreeMap<SlotId, SlotEntity>,
144    pub component_invocations: BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
145    pub component_instance_plan: ComponentInstancePlan,
146    pub component_instance_scope: ComponentInstanceScopeGraph,
147    pub component_composition: ComponentCompositionAnalysis,
148    pub component_initialization: ComponentInitializationPlan,
149    pub component_ir: ComponentIrReport,
150    pub component_ir_optimization: OptimizedComponentIrReport,
151    pub instance_context: InstanceContextRegistry,
152    pub slot_content_fragments: BTreeMap<SlotContentFragmentId, SlotContentFragment>,
153    pub slot_outlets: BTreeMap<SlotOutletId, SlotOutlet>,
154    pub slot_bindings: SlotBindingRegistry,
155    pub composition_types: CompositionTypeProducts,
156    pub context_declaration_candidates: ContextDeclarationCandidateRegistry,
157    pub context_ownership: ContextOwnershipGraph,
158    pub context_dependency: ContextDependencyGraph,
159    pub context_lifetime: ContextLifetimeAnalysis,
160    pub context_evaluation: ContextEvaluationPlan,
161    pub component_scope: ComponentScopeGraph,
162    pub context_resolutions: BTreeMap<ConsumerId, ContextResolution>,
163    pub context_types: BTreeMap<ContextId, ContextTypeRecord>,
164    pub provider_types: BTreeMap<ProviderId, ProviderTypeRecord>,
165    pub consumer_types: BTreeMap<ConsumerId, ConsumerTypeRecord>,
166    pub context_binding_types: BTreeMap<ConsumerId, ContextBindingTypeRecord>,
167    pub duplicate_provider_declarations: Vec<DuplicateProviderDeclaration>,
168    pub computed_values: BTreeMap<SemanticId, ComputedValue>,
169    pub effects: BTreeMap<SemanticId, Effect>,
170    pub effect_reactive_analysis: BTreeMap<SemanticId, EffectReactiveAnalysis>,
171    pub effect_trigger_plan: EffectTriggerPlan,
172    pub effect_execution_plan: EffectExecutionPlan,
173    pub effect_bodies: BTreeMap<SemanticId, EffectBody>,
174    pub effect_statements: BTreeMap<SemanticId, EffectStatement>,
175    pub reactive_graph: IrReactiveGraph,
176    pub reactive_transitive_analysis: IrReactiveTransitiveAnalysis,
177    pub reactive_cycle_analysis: IrReactiveCycleAnalysis,
178    pub computed_evaluation_plan: IrComputedEvaluationPlan,
179    pub templates: Vec<TemplateNode>,
180    pub template_entities: Vec<TemplateSemanticEntity>,
181    pub diagnostics: Vec<ComponentDiagnostic>,
182    pub ownership: BTreeMap<SemanticId, SemanticOwner>,
183    pub references: Vec<SemanticReference>,
184    pub provenance: BTreeMap<SemanticId, SourceProvenance>,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum SemanticEntity<'a> {
189    Component(&'a ComponentNode),
190    StateField(&'a StateField),
191    Method(&'a ComponentMethod),
192    Context(&'a ContextEntity),
193    Provider(&'a ProviderEntity),
194    Consumer(&'a ConsumerEntity),
195    Form(&'a FormEntity),
196    FormField(&'a FormFieldEntity),
197    FormFieldBinding(&'a FormFieldBinding),
198    ValidationRule(&'a ValidationRule),
199    Slot(&'a SlotEntity),
200    ComponentInvocation(&'a ComponentInvocationEntity),
201    ComponentInstance(&'a ComponentInstance),
202    BlockedComponentInstance(&'a BlockedComponentInstancePlan),
203    SlotContentFragment(&'a SlotContentFragment),
204    SlotOutlet(&'a SlotOutlet),
205    Computed(&'a ComputedValue),
206    Effect(&'a Effect),
207    Parameter(&'a crate::MethodParameter),
208    LocalVariable(&'a MethodLocalVariable),
209    Action(&'a ComponentAction),
210    EventHandler(&'a RenderEventHandler),
211    Template(&'a TemplateNode),
212    TemplateEntity(&'a TemplateSemanticEntity),
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum SemanticEntityKind {
217    Component,
218    StateField,
219    Method,
220    Context,
221    Provider,
222    Consumer,
223    Form,
224    FormField,
225    FormFieldBinding,
226    ValidationRule,
227    Slot,
228    ComponentInvocation,
229    ComponentInstance,
230    BlockedComponentInstance,
231    SlotContentFragment,
232    SlotOutlet,
233    Computed,
234    Effect,
235    Parameter,
236    LocalVariable,
237    Action,
238    EventHandler,
239    Template,
240    TemplateEntity,
241}
242
243impl SemanticEntity<'_> {
244    #[must_use]
245    pub fn kind(self) -> SemanticEntityKind {
246        match self {
247            Self::Component(_) => SemanticEntityKind::Component,
248            Self::StateField(_) => SemanticEntityKind::StateField,
249            Self::Method(_) => SemanticEntityKind::Method,
250            Self::Context(_) => SemanticEntityKind::Context,
251            Self::Provider(_) => SemanticEntityKind::Provider,
252            Self::Consumer(_) => SemanticEntityKind::Consumer,
253            Self::Form(_) => SemanticEntityKind::Form,
254            Self::FormField(_) => SemanticEntityKind::FormField,
255            Self::FormFieldBinding(_) => SemanticEntityKind::FormFieldBinding,
256            Self::ValidationRule(_) => SemanticEntityKind::ValidationRule,
257            Self::Slot(_) => SemanticEntityKind::Slot,
258            Self::ComponentInvocation(_) => SemanticEntityKind::ComponentInvocation,
259            Self::ComponentInstance(_) => SemanticEntityKind::ComponentInstance,
260            Self::BlockedComponentInstance(_) => SemanticEntityKind::BlockedComponentInstance,
261            Self::SlotContentFragment(_) => SemanticEntityKind::SlotContentFragment,
262            Self::SlotOutlet(_) => SemanticEntityKind::SlotOutlet,
263            Self::Computed(_) => SemanticEntityKind::Computed,
264            Self::Effect(_) => SemanticEntityKind::Effect,
265            Self::Parameter(_) => SemanticEntityKind::Parameter,
266            Self::LocalVariable(_) => SemanticEntityKind::LocalVariable,
267            Self::Action(_) => SemanticEntityKind::Action,
268            Self::EventHandler(_) => SemanticEntityKind::EventHandler,
269            Self::Template(_) => SemanticEntityKind::Template,
270            Self::TemplateEntity(_) => SemanticEntityKind::TemplateEntity,
271        }
272    }
273}
274
275impl ApplicationSemanticModel {
276    #[allow(clippy::too_many_lines)]
277    #[must_use]
278    pub fn entity(&self, id: &SemanticId) -> Option<SemanticEntity<'_>> {
279        for component in &self.components {
280            if component.id == *id {
281                return Some(SemanticEntity::Component(component));
282            }
283            if let Some(field) = component.state_fields.iter().find(|field| field.id == *id) {
284                return Some(SemanticEntity::StateField(field));
285            }
286            if let Some(method) = component.methods.iter().find(|method| method.id == *id) {
287                return Some(SemanticEntity::Method(method));
288            }
289            if let Some(parameter) = component
290                .methods
291                .iter()
292                .flat_map(|method| method.parameters.iter())
293                .find(|parameter| parameter.id == *id)
294            {
295                return Some(SemanticEntity::Parameter(parameter));
296            }
297            if let Some(local) = component
298                .methods
299                .iter()
300                .flat_map(|method| method.local_variables.iter())
301                .find(|local| local.id == *id)
302            {
303                return Some(SemanticEntity::LocalVariable(local));
304            }
305            if let Some(action) = component.actions.iter().find(|action| action.id == *id) {
306                return Some(SemanticEntity::Action(action));
307            }
308            if let Some(render) = &component.render {
309                if let Some(handler) = render_event_handlers(render)
310                    .into_iter()
311                    .find(|handler| handler.id == *id)
312                {
313                    return Some(SemanticEntity::EventHandler(handler));
314                }
315            }
316        }
317
318        if let Some(computed) = self.computed_values.get(id) {
319            return Some(SemanticEntity::Computed(computed));
320        }
321        if let Some(context) = self
322            .contexts
323            .values()
324            .find(|context| context.id.as_semantic_id() == id)
325        {
326            return Some(SemanticEntity::Context(context));
327        }
328        if let Some(provider) = self
329            .providers
330            .values()
331            .find(|provider| provider.id.as_semantic_id() == id)
332        {
333            return Some(SemanticEntity::Provider(provider));
334        }
335        if let Some(consumer) = self
336            .consumers
337            .values()
338            .find(|consumer| consumer.id.as_semantic_id() == id)
339        {
340            return Some(SemanticEntity::Consumer(consumer));
341        }
342        if let Some(form) = self
343            .forms
344            .values()
345            .find(|form| form.id.as_semantic_id() == id)
346        {
347            return Some(SemanticEntity::Form(form));
348        }
349        if let Some(field) = self
350            .form_fields
351            .values()
352            .find(|field| field.id.as_semantic_id() == id)
353        {
354            return Some(SemanticEntity::FormField(field));
355        }
356        if let Some(binding) = self
357            .form_field_bindings
358            .values()
359            .find(|binding| binding.id.as_semantic_id() == id)
360        {
361            return Some(SemanticEntity::FormFieldBinding(binding));
362        }
363        if let Some(rule) = self
364            .validation_rules
365            .values()
366            .find(|rule| rule.id.as_semantic_id() == id)
367        {
368            return Some(SemanticEntity::ValidationRule(rule));
369        }
370        if let Some(slot) = self
371            .slots
372            .values()
373            .find(|slot| slot.id.as_semantic_id() == id)
374        {
375            return Some(SemanticEntity::Slot(slot));
376        }
377        if let Some(invocation) = self
378            .component_invocations
379            .values()
380            .find(|invocation| invocation.id.as_semantic_id() == id)
381        {
382            return Some(SemanticEntity::ComponentInvocation(invocation));
383        }
384        if let Some(instance) = self
385            .component_instance_plan
386            .instances
387            .values()
388            .find(|instance| instance.id.as_semantic_id() == id)
389        {
390            return Some(SemanticEntity::ComponentInstance(instance));
391        }
392        if let Some(blocked) = self
393            .component_instance_plan
394            .blocked
395            .values()
396            .find(|blocked| blocked.id.as_semantic_id() == id)
397        {
398            return Some(SemanticEntity::BlockedComponentInstance(blocked));
399        }
400        if let Some(fragment) = self
401            .slot_content_fragments
402            .values()
403            .find(|fragment| fragment.id.as_semantic_id() == id)
404        {
405            return Some(SemanticEntity::SlotContentFragment(fragment));
406        }
407        if let Some(outlet) = self
408            .slot_outlets
409            .values()
410            .find(|outlet| outlet.id.as_semantic_id() == id)
411        {
412            return Some(SemanticEntity::SlotOutlet(outlet));
413        }
414        if let Some(effect) = self.effects.get(id) {
415            return Some(SemanticEntity::Effect(effect));
416        }
417
418        if let Some(template) = self.templates.iter().find(|template| template.id == *id) {
419            return Some(SemanticEntity::Template(template));
420        }
421
422        self.template_entities
423            .iter()
424            .find(|entity| entity.id == *id)
425            .map(SemanticEntity::TemplateEntity)
426    }
427
428    #[must_use]
429    pub fn component(&self, id: &SemanticId) -> Option<&ComponentNode> {
430        self.components.iter().find(|component| component.id == *id)
431    }
432
433    #[must_use]
434    pub fn template(&self, id: &SemanticId) -> Option<&TemplateNode> {
435        self.templates.iter().find(|template| template.id == *id)
436    }
437
438    #[must_use]
439    pub fn computed_value(&self, id: &SemanticId) -> Option<&ComputedValue> {
440        self.computed_values.get(id)
441    }
442
443    #[must_use]
444    pub fn contexts(&self) -> Vec<&ContextEntity> {
445        self.contexts.values().collect()
446    }
447
448    #[must_use]
449    pub fn context(&self, id: &ContextId) -> Option<&ContextEntity> {
450        self.contexts.get(id)
451    }
452
453    #[must_use]
454    pub fn forms(&self) -> Vec<&FormEntity> {
455        self.forms.values().collect()
456    }
457
458    #[must_use]
459    pub fn form(&self, id: &FormId) -> Option<&FormEntity> {
460        self.forms.get(id)
461    }
462
463    #[must_use]
464    pub fn form_fields(&self) -> Vec<&FormFieldEntity> {
465        let mut fields = self.form_fields.values().collect::<Vec<_>>();
466        fields.sort_by(|left, right| {
467            (&left.owner_form, left.declaration_order, &left.id).cmp(&(
468                &right.owner_form,
469                right.declaration_order,
470                &right.id,
471            ))
472        });
473        fields
474    }
475
476    #[must_use]
477    pub fn form_field(&self, id: &FieldId) -> Option<&FormFieldEntity> {
478        self.form_fields.get(id)
479    }
480
481    #[must_use]
482    pub fn form_field_declaration_candidates(&self) -> &[crate::FormFieldDeclarationCandidate] {
483        &self.form_field_declaration_candidates
484    }
485
486    #[must_use]
487    pub fn form_field_binding_candidates(&self) -> &[FormFieldBindingCandidate] {
488        &self.form_field_binding_candidates
489    }
490
491    #[must_use]
492    pub fn form_field_bindings(&self) -> Vec<&FormFieldBinding> {
493        let mut bindings = self.form_field_bindings.values().collect::<Vec<_>>();
494        bindings.sort_by(|left, right| {
495            (&left.owner_template, left.authored_order, &left.id).cmp(&(
496                &right.owner_template,
497                right.authored_order,
498                &right.id,
499            ))
500        });
501        bindings
502    }
503
504    #[must_use]
505    pub fn form_field_binding(&self, id: &FieldBindingId) -> Option<&FormFieldBinding> {
506        self.form_field_bindings.get(id)
507    }
508
509    #[must_use]
510    pub const fn form_ownership(&self) -> &FormOwnershipGraph {
511        &self.form_ownership
512    }
513
514    #[must_use]
515    pub fn validation_rule_candidates(&self) -> &[ValidationRuleCandidate] {
516        &self.validation_rule_candidates
517    }
518
519    #[must_use]
520    pub fn validation_candidates_of(&self, field: &FieldId) -> Vec<&ValidationRuleCandidate> {
521        self.validation_rule_candidates
522            .iter()
523            .filter(|candidate| candidate.target_field.as_ref() == Some(field))
524            .collect()
525    }
526
527    #[must_use]
528    pub fn validation_rule(&self, id: &ValidationRuleId) -> Option<&ValidationRule> {
529        self.validation_rules.get(id)
530    }
531
532    #[must_use]
533    pub fn validation_rules(&self) -> Vec<&ValidationRule> {
534        let mut rules = self.validation_rules.values().collect::<Vec<_>>();
535        rules.sort_by(|left, right| {
536            (
537                &left.owner_form,
538                left.field_authored_order,
539                left.rule_authored_order,
540                &left.id,
541            )
542                .cmp(&(
543                    &right.owner_form,
544                    right.field_authored_order,
545                    right.rule_authored_order,
546                    &right.id,
547                ))
548        });
549        rules
550    }
551
552    #[must_use]
553    pub const fn validation_graph(&self) -> &ValidationGraph {
554        &self.validation_graph
555    }
556
557    #[must_use]
558    pub const fn validation_dependency_plans(&self) -> &ValidationDependencyPlans {
559        &self.validation_dependency_plans
560    }
561
562    #[must_use]
563    pub const fn form_tracking(&self) -> &FormTrackingProducts {
564        &self.form_tracking
565    }
566
567    #[must_use]
568    pub const fn submissions(&self) -> &SubmissionProducts {
569        &self.submissions
570    }
571
572    #[must_use]
573    pub const fn serialization(&self) -> &SerializationProducts {
574        &self.serialization
575    }
576
577    #[must_use]
578    pub const fn reset(&self) -> &ResetProducts {
579        &self.reset
580    }
581
582    #[must_use]
583    pub fn form_declaration_candidates(&self) -> Vec<&crate::FormDeclarationCandidate> {
584        let mut candidates = self
585            .components
586            .iter()
587            .flat_map(|component| component.form_declaration_candidates.iter())
588            .collect::<Vec<_>>();
589        candidates.sort_by(|left, right| {
590            (
591                left.provenance.path.as_path(),
592                left.provenance.span.start,
593                left.provenance.span.end,
594                left.id.as_str(),
595            )
596                .cmp(&(
597                    right.provenance.path.as_path(),
598                    right.provenance.span.start,
599                    right.provenance.span.end,
600                    right.id.as_str(),
601                ))
602        });
603        candidates
604    }
605
606    #[must_use]
607    pub const fn context_declaration_candidates(&self) -> &ContextDeclarationCandidateRegistry {
608        &self.context_declaration_candidates
609    }
610
611    #[must_use]
612    pub fn contexts_owned_by(&self, component: &SemanticId) -> Vec<&ContextId> {
613        self.contexts
614            .iter()
615            .filter_map(|(id, context)| {
616                (context.owner.entity_id() == Some(component)).then_some(id)
617            })
618            .collect()
619    }
620
621    #[must_use]
622    pub fn context_for_authored_field(&self, field: &SemanticId) -> Option<&ContextId> {
623        self.contexts
624            .iter()
625            .find_map(|(id, context)| (&context.authored_field == field).then_some(id))
626    }
627
628    #[must_use]
629    pub fn providers(&self) -> Vec<&ProviderEntity> {
630        self.providers.values().collect()
631    }
632
633    #[must_use]
634    pub fn provider(&self, id: &ProviderId) -> Option<&ProviderEntity> {
635        self.providers.get(id)
636    }
637
638    #[must_use]
639    pub fn providers_owned_by(&self, component: &SemanticId) -> Vec<&ProviderId> {
640        self.providers
641            .iter()
642            .filter_map(|(id, provider)| {
643                (provider.owner.entity_id() == Some(component)).then_some(id)
644            })
645            .collect()
646    }
647
648    #[must_use]
649    pub fn providers_for_context(&self, context: &ContextId) -> Vec<&ProviderId> {
650        self.providers
651            .iter()
652            .filter_map(|(id, provider)| (&provider.context == context).then_some(id))
653            .collect()
654    }
655
656    #[must_use]
657    pub fn provider_for_authored_field(&self, field: &SemanticId) -> Option<&ProviderId> {
658        self.providers
659            .iter()
660            .find_map(|(id, provider)| (&provider.authored_field == field).then_some(id))
661    }
662
663    #[must_use]
664    pub fn consumers(&self) -> Vec<&ConsumerEntity> {
665        self.consumers.values().collect()
666    }
667
668    #[must_use]
669    pub fn consumer(&self, id: &ConsumerId) -> Option<&ConsumerEntity> {
670        self.consumers.get(id)
671    }
672
673    pub(crate) fn consumer_for_semantic_id(&self, id: &SemanticId) -> Option<&ConsumerEntity> {
674        self.consumers
675            .values()
676            .find(|consumer| consumer.id.as_semantic_id() == id)
677    }
678
679    #[must_use]
680    pub fn consumers_owned_by(&self, component: &SemanticId) -> Vec<&ConsumerId> {
681        self.consumers
682            .iter()
683            .filter_map(|(id, consumer)| {
684                (consumer.owner.entity_id() == Some(component)).then_some(id)
685            })
686            .collect()
687    }
688
689    #[must_use]
690    pub fn consumers_for_context(&self, context: &ContextId) -> Vec<&ConsumerId> {
691        self.consumers
692            .iter()
693            .filter_map(|(id, consumer)| (consumer.context() == Some(context)).then_some(id))
694            .collect()
695    }
696
697    #[must_use]
698    pub fn consumer_for_authored_field(&self, field: &SemanticId) -> Option<&ConsumerId> {
699        self.consumers
700            .iter()
701            .find_map(|(id, consumer)| (&consumer.authored_field == field).then_some(id))
702    }
703
704    #[must_use]
705    pub fn slots(&self) -> Vec<&SlotEntity> {
706        self.slots.values().collect()
707    }
708
709    #[must_use]
710    pub fn slot(&self, id: &SlotId) -> Option<&SlotEntity> {
711        self.slots.get(id)
712    }
713
714    #[must_use]
715    pub fn slots_owned_by(&self, component: &SemanticId) -> Vec<&SlotId> {
716        self.slots
717            .iter()
718            .filter_map(|(id, slot)| (&slot.owner == component).then_some(id))
719            .collect()
720    }
721
722    #[must_use]
723    pub fn slot_for_authored_field(&self, field: &SemanticId) -> Option<&SlotId> {
724        self.slots
725            .iter()
726            .find_map(|(id, slot)| (&slot.authored_field == field).then_some(id))
727    }
728
729    #[must_use]
730    pub fn component_invocations(&self) -> Vec<&ComponentInvocationEntity> {
731        self.component_invocations.values().collect()
732    }
733
734    #[must_use]
735    pub fn component_invocation(
736        &self,
737        id: &ComponentInvocationId,
738    ) -> Option<&ComponentInvocationEntity> {
739        self.component_invocations.get(id)
740    }
741
742    #[must_use]
743    pub fn component_invocations_owned_by(
744        &self,
745        component: &SemanticId,
746    ) -> Vec<&ComponentInvocationId> {
747        self.component_invocations
748            .iter()
749            .filter_map(|(id, invocation)| (&invocation.owner_component == component).then_some(id))
750            .collect()
751    }
752
753    #[must_use]
754    pub fn component_instance(
755        &self,
756        id: &crate::ComponentInstanceId,
757    ) -> Option<&ComponentInstance> {
758        self.component_instance_plan.instances.get(id)
759    }
760
761    #[must_use]
762    pub fn component_build_roots(&self) -> Vec<&crate::ComponentBuildRoot> {
763        self.component_instance_plan.roots.values().collect()
764    }
765
766    #[must_use]
767    pub fn component_instances(&self) -> Vec<&ComponentInstance> {
768        self.component_instance_plan.instances.values().collect()
769    }
770
771    #[must_use]
772    pub fn blocked_component_instances(&self) -> Vec<&BlockedComponentInstancePlan> {
773        self.component_instance_plan.blocked.values().collect()
774    }
775
776    #[must_use]
777    pub fn blocked_component_instance(
778        &self,
779        id: &crate::ComponentInstanceId,
780    ) -> Option<&BlockedComponentInstancePlan> {
781        self.component_instance_plan.blocked.get(id)
782    }
783
784    #[must_use]
785    pub fn component_instances_for_definition(
786        &self,
787        component: &SemanticId,
788    ) -> Vec<&ComponentInstance> {
789        self.component_instance_plan
790            .instances
791            .values()
792            .filter(|instance| &instance.component == component)
793            .collect()
794    }
795
796    #[must_use]
797    pub const fn component_instance_scope_graph(&self) -> &ComponentInstanceScopeGraph {
798        &self.component_instance_scope
799    }
800
801    #[must_use]
802    pub const fn component_composition_analysis(&self) -> &ComponentCompositionAnalysis {
803        &self.component_composition
804    }
805
806    #[must_use]
807    pub const fn component_initialization_plan(&self) -> &ComponentInitializationPlan {
808        &self.component_initialization
809    }
810
811    #[must_use]
812    pub const fn component_ir_report(&self) -> &ComponentIrReport {
813        &self.component_ir
814    }
815
816    #[must_use]
817    pub const fn optimized_component_ir_report(&self) -> &OptimizedComponentIrReport {
818        &self.component_ir_optimization
819    }
820
821    #[must_use]
822    pub const fn instance_context_registry(&self) -> &InstanceContextRegistry {
823        &self.instance_context
824    }
825
826    #[must_use]
827    pub fn slot_content_fragments(&self) -> Vec<&SlotContentFragment> {
828        self.slot_content_fragments.values().collect()
829    }
830
831    #[must_use]
832    pub const fn slot_binding_registry(&self) -> &SlotBindingRegistry {
833        &self.slot_bindings
834    }
835
836    #[must_use]
837    pub const fn composition_type_products(&self) -> &CompositionTypeProducts {
838        &self.composition_types
839    }
840
841    #[must_use]
842    pub fn slot_bindings(&self) -> Vec<&SlotBinding> {
843        self.slot_bindings.bindings.values().collect()
844    }
845
846    #[must_use]
847    pub fn slot_binding(&self, id: &SlotBindingId) -> Option<&SlotBinding> {
848        self.slot_bindings.binding(id)
849    }
850
851    #[must_use]
852    pub fn slot_bindings_for_callee(
853        &self,
854        callee: &crate::ComponentInstanceId,
855    ) -> Vec<&SlotBinding> {
856        self.slot_bindings.for_callee(callee)
857    }
858
859    #[must_use]
860    pub fn slot_content_fragment(
861        &self,
862        id: &SlotContentFragmentId,
863    ) -> Option<&SlotContentFragment> {
864        self.slot_content_fragments.get(id)
865    }
866
867    #[must_use]
868    pub fn slot_content_fragments_for(
869        &self,
870        invocation: &ComponentInvocationId,
871    ) -> Vec<&SlotContentFragment> {
872        self.slot_content_fragments
873            .values()
874            .filter(|fragment| &fragment.invocation == invocation)
875            .collect()
876    }
877
878    #[must_use]
879    pub fn slot_outlets(&self) -> Vec<&SlotOutlet> {
880        self.slot_outlets.values().collect()
881    }
882
883    #[must_use]
884    pub fn slot_outlet(&self, id: &SlotOutletId) -> Option<&SlotOutlet> {
885        self.slot_outlets.get(id)
886    }
887
888    #[must_use]
889    pub fn slot_outlets_owned_by(&self, component: &SemanticId) -> Vec<&SlotOutletId> {
890        self.slot_outlets
891            .iter()
892            .filter_map(|(id, outlet)| (&outlet.owner_component == component).then_some(id))
893            .collect()
894    }
895
896    #[must_use]
897    pub fn context_resolution(&self, consumer: &ConsumerId) -> Option<&ContextResolution> {
898        self.context_resolutions.get(consumer)
899    }
900
901    #[must_use]
902    pub const fn context_ownership_graph(&self) -> &ContextOwnershipGraph {
903        &self.context_ownership
904    }
905
906    #[must_use]
907    pub const fn context_dependency_graph(&self) -> &ContextDependencyGraph {
908        &self.context_dependency
909    }
910
911    #[must_use]
912    pub const fn context_lifetime_analysis(&self) -> &ContextLifetimeAnalysis {
913        &self.context_lifetime
914    }
915
916    #[must_use]
917    pub const fn context_evaluation_plan(&self) -> &ContextEvaluationPlan {
918        &self.context_evaluation
919    }
920
921    #[must_use]
922    pub fn resolved_provider(&self, consumer: &ConsumerId) -> Option<&ProviderId> {
923        let ContextResolutionResult::Provider { provider, .. } =
924            &self.context_resolution(consumer)?.result
925        else {
926            return None;
927        };
928        Some(provider)
929    }
930
931    #[must_use]
932    pub fn consumers_resolved_to(&self, provider: &ProviderId) -> Vec<&ConsumerId> {
933        self.context_resolutions
934            .iter()
935            .filter_map(|(consumer, resolution)| {
936                matches!(
937                    &resolution.result,
938                    ContextResolutionResult::Provider {
939                        provider: resolved, ..
940                    } if resolved == provider
941                )
942                .then_some(consumer)
943            })
944            .collect()
945    }
946
947    #[must_use]
948    pub fn consumers_using_default(&self, context: &ContextId) -> Vec<&ConsumerId> {
949        self.context_resolutions
950            .iter()
951            .filter_map(|(consumer, resolution)| {
952                matches!(
953                    &resolution.result,
954                    ContextResolutionResult::ContextDefault {
955                        context: resolved, ..
956                    } if resolved == context
957                )
958                .then_some(consumer)
959            })
960            .collect()
961    }
962
963    #[must_use]
964    pub fn unresolved_context_consumers(&self) -> Vec<&ConsumerId> {
965        self.context_resolutions
966            .iter()
967            .filter_map(|(consumer, resolution)| {
968                matches!(resolution.result, ContextResolutionResult::Unresolved).then_some(consumer)
969            })
970            .collect()
971    }
972
973    #[must_use]
974    pub fn context_type(&self, context: &ContextId) -> Option<&ContextTypeRecord> {
975        self.context_types.get(context)
976    }
977
978    #[must_use]
979    pub fn provider_type(&self, provider: &ProviderId) -> Option<&ProviderTypeRecord> {
980        self.provider_types.get(provider)
981    }
982
983    #[must_use]
984    pub fn consumer_type(&self, consumer: &ConsumerId) -> Option<&ConsumerTypeRecord> {
985        self.consumer_types.get(consumer)
986    }
987
988    #[must_use]
989    pub fn context_binding_type(&self, consumer: &ConsumerId) -> Option<&ContextBindingTypeRecord> {
990        self.context_binding_types.get(consumer)
991    }
992
993    #[must_use]
994    pub fn runtime_eligible_context_binding(&self, consumer: &ConsumerId) -> bool {
995        self.context_binding_type(consumer)
996            .is_some_and(|binding| binding.overall == ContextBindingCompatibility::Compatible)
997    }
998
999    #[must_use]
1000    pub fn effect(&self, id: &SemanticId) -> Option<&Effect> {
1001        self.effects.get(id)
1002    }
1003
1004    #[must_use]
1005    pub fn effect_body(&self, effect: &SemanticId) -> Option<&EffectBody> {
1006        self.effect_bodies.get(effect)
1007    }
1008
1009    #[must_use]
1010    pub fn effect_reactive_analysis(&self, effect: &SemanticId) -> Option<&EffectReactiveAnalysis> {
1011        self.effect_reactive_analysis.get(effect)
1012    }
1013
1014    #[must_use]
1015    pub const fn effect_trigger_plan(&self) -> &EffectTriggerPlan {
1016        &self.effect_trigger_plan
1017    }
1018
1019    #[must_use]
1020    pub const fn effect_execution_plan(&self) -> &EffectExecutionPlan {
1021        &self.effect_execution_plan
1022    }
1023
1024    /// Returns F4 typing facts for one canonical effect statement.
1025    #[must_use]
1026    pub fn effect_statement_type(
1027        &self,
1028        statement: &SemanticId,
1029    ) -> Option<&EffectStatementTypeRecord> {
1030        self.semantic_types.effect_statements.get(statement)
1031    }
1032
1033    #[must_use]
1034    pub fn effect_statement(&self, id: &SemanticId) -> Option<&EffectStatement> {
1035        self.effect_statements.get(id)
1036    }
1037
1038    #[must_use]
1039    pub fn template_entity(&self, id: &SemanticId) -> Option<&TemplateSemanticEntity> {
1040        self.template_entities
1041            .iter()
1042            .find(|entity| entity.id == *id)
1043    }
1044
1045    #[must_use]
1046    pub fn template_entities_for(&self, template: &SemanticId) -> Vec<&TemplateSemanticEntity> {
1047        self.children_of(template)
1048            .into_iter()
1049            .filter_map(|id| self.template_entity(id))
1050            .collect()
1051    }
1052
1053    #[must_use]
1054    pub fn owner(&self, id: &SemanticId) -> Option<&SemanticOwner> {
1055        self.ownership.get(id)
1056    }
1057
1058    #[must_use]
1059    pub fn parent_of(&self, id: &SemanticId) -> Option<&SemanticId> {
1060        self.owner(id).and_then(SemanticOwner::entity_id)
1061    }
1062
1063    #[must_use]
1064    pub fn ancestors_of(&self, id: &SemanticId) -> Vec<&SemanticId> {
1065        let mut ancestors = Vec::new();
1066        let mut seen = BTreeSet::from([id.clone()]);
1067        let mut current = id;
1068
1069        while let Some(parent) = self.parent_of(current) {
1070            if !seen.insert(parent.clone()) {
1071                break;
1072            }
1073            ancestors.push(parent);
1074            current = parent;
1075        }
1076
1077        ancestors
1078    }
1079
1080    #[must_use]
1081    pub fn provenance(&self, id: &SemanticId) -> Option<&SourceProvenance> {
1082        self.provenance.get(id)
1083    }
1084
1085    #[must_use]
1086    pub fn expression(&self, id: &SemanticId) -> Option<&ExpressionNode> {
1087        self.expression_graph.node(id)
1088    }
1089
1090    #[must_use]
1091    pub fn expression_root(&self, owner: &SemanticId) -> Option<&SemanticId> {
1092        self.expression_graph.root_for(owner)
1093    }
1094
1095    #[must_use]
1096    pub fn expressions_for(&self, owner: &SemanticId) -> Vec<&ExpressionNode> {
1097        self.expression_graph.nodes_for(owner)
1098    }
1099
1100    #[must_use]
1101    pub fn expression_dependencies(&self, id: &SemanticId) -> Vec<&SemanticId> {
1102        self.expression_graph.dependencies_of(id)
1103    }
1104
1105    #[must_use]
1106    pub fn expression_dependents(&self, id: &SemanticId) -> Vec<&ExpressionNode> {
1107        self.expression_graph.dependents_of(id)
1108    }
1109
1110    #[must_use]
1111    pub fn expression_owner(&self, id: &SemanticId) -> Option<&SemanticId> {
1112        self.expression_graph.owner_of(id)
1113    }
1114
1115    #[must_use]
1116    pub fn expression_provenance(&self, id: &SemanticId) -> Option<&SourceProvenance> {
1117        self.expression_graph.provenance_of(id)
1118    }
1119
1120    #[must_use]
1121    pub fn semantic_type_of(&self, id: &SemanticId) -> Option<&crate::SemanticType> {
1122        self.semantic_types
1123            .assignments
1124            .get(id)
1125            .map(|assignment| &assignment.semantic_type)
1126    }
1127
1128    #[must_use]
1129    pub fn expression_type(&self, id: &SemanticId) -> Option<&crate::SemanticType> {
1130        self.expression_graph
1131            .node(id)
1132            .and_then(|_| self.semantic_type_of(id))
1133    }
1134
1135    #[must_use]
1136    pub fn type_declarations(
1137        &self,
1138        semantic_type: &crate::SemanticType,
1139    ) -> Vec<&crate::SemanticTypeAssignment> {
1140        self.semantic_types
1141            .assignments
1142            .values()
1143            .filter(|assignment| {
1144                assignment.status == crate::SemanticTypeStatus::Declared
1145                    && assignment.semantic_type == *semantic_type
1146            })
1147            .collect()
1148    }
1149
1150    #[must_use]
1151    pub fn type_usages(
1152        &self,
1153        semantic_type: &crate::SemanticType,
1154    ) -> Vec<&crate::SemanticTypeAssignment> {
1155        self.semantic_types
1156            .assignments
1157            .values()
1158            .filter(|assignment| assignment.semantic_type == *semantic_type)
1159            .collect()
1160    }
1161
1162    #[must_use]
1163    pub fn serialization_compatibility_of(
1164        &self,
1165        id: &SemanticId,
1166    ) -> Option<crate::SerializationCompatibility> {
1167        self.semantic_type_of(id)
1168            .map(crate::serialization_compatibility)
1169    }
1170
1171    #[must_use]
1172    pub fn is_type_assignable(&self, source: &SemanticId, target: &SemanticId) -> Option<bool> {
1173        Some(crate::is_assignable(
1174            self.semantic_type_of(source)?,
1175            self.semantic_type_of(target)?,
1176        ))
1177    }
1178
1179    #[must_use]
1180    pub fn expressions_in_file(&self, path: &Path) -> Vec<&ExpressionNode> {
1181        self.expression_graph.nodes_in_file(path)
1182    }
1183
1184    #[must_use]
1185    pub fn expressions_at(&self, path: &Path, offset: usize) -> Vec<&ExpressionNode> {
1186        self.expression_graph.nodes_at(path, offset)
1187    }
1188
1189    #[must_use]
1190    pub fn application_roots(&self) -> Vec<&SemanticId> {
1191        self.ownership
1192            .iter()
1193            .filter_map(|(id, owner)| matches!(owner, SemanticOwner::Application).then_some(id))
1194            .collect()
1195    }
1196
1197    #[must_use]
1198    pub fn children_of(&self, owner: &SemanticId) -> Vec<&SemanticId> {
1199        self.ownership
1200            .iter()
1201            .filter_map(|(id, entity_owner)| {
1202                (entity_owner.entity_id() == Some(owner)).then_some(id)
1203            })
1204            .collect()
1205    }
1206
1207    #[must_use]
1208    pub fn descendants_of(&self, owner: &SemanticId) -> Vec<&SemanticId> {
1209        let mut descendants = Vec::new();
1210        self.collect_descendants(owner, &mut descendants);
1211        descendants
1212    }
1213
1214    #[must_use]
1215    pub fn entities_of_kind(&self, kind: SemanticEntityKind) -> Vec<&SemanticId> {
1216        self.ownership
1217            .keys()
1218            .filter(|id| self.entity(id).is_some_and(|entity| entity.kind() == kind))
1219            .collect()
1220    }
1221
1222    #[must_use]
1223    pub fn entities_in_file(&self, path: &Path) -> Vec<&SemanticId> {
1224        self.provenance
1225            .iter()
1226            .filter_map(|(id, provenance)| (provenance.path == path).then_some(id))
1227            .collect()
1228    }
1229
1230    #[must_use]
1231    pub fn entities_at(&self, path: &Path, offset: usize) -> Vec<&SemanticId> {
1232        self.provenance
1233            .iter()
1234            .filter_map(|(id, provenance)| {
1235                (provenance.path == path
1236                    && provenance.span.start <= offset
1237                    && offset < provenance.span.end)
1238                    .then_some(id)
1239            })
1240            .collect()
1241    }
1242
1243    #[must_use]
1244    pub fn references_of_kind(&self, kind: SemanticReferenceKind) -> Vec<&SemanticReference> {
1245        let mut references = self
1246            .references
1247            .iter()
1248            .filter(|reference| reference.kind == kind)
1249            .collect::<Vec<_>>();
1250        references.sort_by(|left, right| {
1251            (left.source.as_str(), left.target.as_str())
1252                .cmp(&(right.source.as_str(), right.target.as_str()))
1253        });
1254        references
1255    }
1256
1257    #[must_use]
1258    pub fn references_in_file(&self, path: &Path) -> Vec<&SemanticReference> {
1259        let mut references = self
1260            .references
1261            .iter()
1262            .filter(|reference| reference.provenance.path == path)
1263            .collect::<Vec<_>>();
1264        references.sort_by(|left, right| {
1265            (left.source.as_str(), left.target.as_str())
1266                .cmp(&(right.source.as_str(), right.target.as_str()))
1267        });
1268        references
1269    }
1270
1271    #[must_use]
1272    pub fn references_at(&self, path: &Path, offset: usize) -> Vec<&SemanticReference> {
1273        let mut references = self
1274            .references
1275            .iter()
1276            .filter(|reference| {
1277                reference.provenance.path == path
1278                    && reference.provenance.span.start <= offset
1279                    && offset < reference.provenance.span.end
1280            })
1281            .collect::<Vec<_>>();
1282        references.sort_by(|left, right| {
1283            (left.source.as_str(), left.target.as_str())
1284                .cmp(&(right.source.as_str(), right.target.as_str()))
1285        });
1286        references
1287    }
1288
1289    fn collect_descendants<'a>(
1290        &'a self,
1291        owner: &SemanticId,
1292        descendants: &mut Vec<&'a SemanticId>,
1293    ) {
1294        for child in self.children_of(owner) {
1295            descendants.push(child);
1296            self.collect_descendants(child, descendants);
1297        }
1298    }
1299
1300    #[must_use]
1301    pub fn references_from(&self, id: &SemanticId) -> Vec<&SemanticReference> {
1302        self.references
1303            .iter()
1304            .filter(move |reference| reference.source == *id)
1305            .collect()
1306    }
1307
1308    #[must_use]
1309    pub fn references_to(&self, id: &SemanticId) -> Vec<&SemanticReference> {
1310        self.references
1311            .iter()
1312            .filter(move |reference| reference.target == *id)
1313            .collect()
1314    }
1315}
1316
1317#[must_use]
1318pub fn build_application_semantic_model(parsed: &ParsedFile) -> ApplicationSemanticModel {
1319    build_application_semantic_model_from_files(std::slice::from_ref(parsed))
1320}
1321
1322/// Assemble canonical ASM from an existing component graph while preserving its identity mode.
1323#[allow(clippy::too_many_lines)]
1324#[must_use]
1325pub fn build_application_semantic_model_from_component_graph(
1326    component_graph: &crate::component_graph::ComponentGraph,
1327) -> ApplicationSemanticModel {
1328    let templates = build_template_graph(component_graph).templates;
1329    let template_entities = build_template_semantic_entities(&templates);
1330    let component_invocations = collect_component_invocations(
1331        &component_graph.components,
1332        &templates,
1333        &template_entities,
1334        &[],
1335        None,
1336        &component_graph.provenance,
1337    );
1338    let component_instance_plan = plan_component_instances(
1339        &component_graph.components,
1340        &component_invocations,
1341        &template_entities,
1342        &component_graph.provenance,
1343    );
1344    let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
1345    let component_composition = analyze_component_composition(
1346        &component_graph
1347            .components
1348            .iter()
1349            .map(|component| component.id.clone())
1350            .collect(),
1351        &component_invocations,
1352        &component_instance_plan,
1353    );
1354    let (computed_values, computed_diagnostics) = classify_computed_values(
1355        &component_graph.components,
1356        collect_computed_values(&component_graph.components, &component_graph.provenance),
1357        &component_graph.provenance,
1358    );
1359    let effects = collect_effects(&component_graph.components, &component_graph.provenance);
1360    let expression_graph =
1361        ExpressionGraph::from_components(&component_graph.components, &component_graph.provenance);
1362    let contexts = collect_context_entities(&component_graph.components, &expression_graph);
1363    let forms = collect_form_entities(&component_graph.components);
1364    let resource_endpoint_resolutions =
1365        collect_resource_endpoint_resolutions(&component_graph.components, None);
1366    let opaque_action_resolutions =
1367        collect_opaque_action_resolutions(&component_graph.components, None);
1368    let base_semantic_types = SemanticTypeModel::from_components(
1369        &component_graph.components,
1370        &component_graph.provenance,
1371    );
1372    let resource_declarations = collect_resource_declarations(
1373        &component_graph.components,
1374        &resource_endpoint_resolutions,
1375        &base_semantic_types,
1376        None,
1377    );
1378    let resource_activations =
1379        collect_resource_activations(&resource_declarations, &component_instance_plan);
1380    let form_field_products = collect_form_field_products(
1381        &component_graph.components,
1382        &forms,
1383        &base_semantic_types,
1384        None,
1385    );
1386    let form_field_declaration_candidates = form_field_products.candidates;
1387    let form_fields = form_field_products.fields;
1388    let form_field_binding_products = collect_form_field_binding_products(
1389        &component_graph.components,
1390        &templates,
1391        &forms,
1392        &form_fields,
1393        &form_field_declaration_candidates,
1394    );
1395    let form_field_binding_candidates = form_field_binding_products.candidates;
1396    let form_field_bindings = form_field_binding_products.bindings;
1397    let slots = collect_slot_entities(&component_graph.components);
1398    let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
1399    let slot_bindings = collect_slot_bindings(
1400        &component_instance_plan,
1401        &component_invocations,
1402        &slots,
1403        &slot_composition.fragments,
1404        &slot_composition.outlets,
1405    );
1406    let (providers, duplicate_provider_declarations) = collect_provider_entities(
1407        &component_graph.components,
1408        &contexts,
1409        &expression_graph,
1410        None,
1411    );
1412    let consumers = collect_consumer_entities(&component_graph.components, &contexts, None);
1413    let instance_context = collect_instance_context_registry(
1414        &component_instance_scope,
1415        &contexts,
1416        &providers,
1417        &consumers,
1418    );
1419    let context_declaration_candidates = collect_context_declaration_candidates(
1420        &component_graph.components,
1421        &contexts,
1422        &providers,
1423        &consumers,
1424    );
1425    let component_scope = ComponentScopeGraph::reflexive(&component_graph.components);
1426    let context_resolutions = collect_context_resolutions(
1427        &consumers,
1428        &contexts,
1429        &providers,
1430        &expression_graph,
1431        &component_scope,
1432    );
1433    let mut provenance = component_graph.provenance.clone();
1434    extend_template_entity_provenance(&mut provenance, &template_entities);
1435    extend_derived_entity_provenance(
1436        &mut provenance,
1437        &contexts,
1438        &forms,
1439        &form_fields,
1440        &form_field_bindings,
1441        &providers,
1442        &consumers,
1443        &slots,
1444        &component_invocations,
1445        &slot_composition.fragments,
1446        &slot_composition.outlets,
1447        &component_instance_plan,
1448        &computed_values,
1449        &effects,
1450    );
1451    let mut ownership = collect_ownership(
1452        &component_graph.components,
1453        &contexts,
1454        &forms,
1455        &form_fields,
1456        &form_field_bindings,
1457        &providers,
1458        &consumers,
1459        &slots,
1460        &component_invocations,
1461        &slot_composition.fragments,
1462        &slot_composition.outlets,
1463        &component_instance_plan,
1464        &computed_values,
1465        &effects,
1466        &templates,
1467        &template_entities,
1468    );
1469    let context_ownership = collect_context_ownership_graph(
1470        &component_graph.components,
1471        &contexts,
1472        &providers,
1473        &consumers,
1474        &expression_graph,
1475        &provenance,
1476    );
1477    let (effect_bodies, effect_statements) =
1478        lower_effect_bodies(&component_graph.components, &effects, &expression_graph);
1479    let mut references = component_graph.references.clone();
1480    references.extend(build_computed_references(
1481        &component_graph.components,
1482        &computed_values,
1483        &resource_declarations,
1484        &expression_graph,
1485    ));
1486    references.extend(build_effect_references(
1487        &component_graph.components,
1488        &effects,
1489        &computed_values,
1490        &expression_graph,
1491    ));
1492    references.extend(build_provider_references(&providers));
1493    references.extend(build_consumer_references(&consumers));
1494    references.extend(build_context_resolution_references(&context_resolutions));
1495    extend_template_references(
1496        &mut references,
1497        &component_graph.components,
1498        &computed_values,
1499        &template_entities,
1500        &ownership,
1501    );
1502    references.extend(build_form_field_binding_references(&form_field_bindings));
1503    let form_ownership = collect_form_ownership_graph(
1504        &component_instance_plan.roots,
1505        &component_graph.components,
1506        &forms,
1507        &form_fields,
1508        &form_field_bindings,
1509        &template_entities,
1510        &ownership,
1511        &references,
1512        &provenance,
1513    );
1514    let validation_products =
1515        collect_validation_products(&component_graph.components, &forms, &form_fields);
1516    let validation_rule_candidates = validation_products.candidates;
1517    let validation_rules = validation_products.rules;
1518    extend_validation_products(
1519        &mut provenance,
1520        &mut ownership,
1521        &mut references,
1522        &validation_rules,
1523    );
1524    let validation_graph = collect_validation_graph(
1525        &component_instance_plan.roots,
1526        &form_ownership,
1527        &forms,
1528        &form_fields,
1529        &validation_rules,
1530        &validation_rule_candidates,
1531        &validation_products.cycles,
1532        &ownership,
1533        &references,
1534    );
1535    let validation_dependency_plans = collect_validation_dependency_plans(
1536        &forms,
1537        &form_fields,
1538        &validation_rules,
1539        &form_ownership,
1540        &validation_graph,
1541    );
1542    let form_tracking =
1543        collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
1544    let semantic_types = finalize_semantic_types(
1545        base_semantic_types.with_resource_types(&resource_declarations),
1546        &component_graph.components,
1547        &contexts,
1548        &forms,
1549        &form_fields,
1550        &providers,
1551        &consumers,
1552        &slots,
1553        &computed_values,
1554        &effects,
1555        &effect_statements,
1556        &expression_graph,
1557        &references,
1558        &template_entities,
1559    );
1560    let context_type_products = collect_context_type_products(
1561        &contexts,
1562        &providers,
1563        &consumers,
1564        &context_resolutions,
1565        &expression_graph,
1566        &semantic_types,
1567    );
1568    let context_dependency = collect_context_dependency_graph(
1569        &component_graph.components,
1570        &contexts,
1571        &providers,
1572        &consumers,
1573        &context_resolutions,
1574        &context_type_products.contexts,
1575        &context_type_products.providers,
1576        &context_type_products.bindings,
1577        &computed_values,
1578        &expression_graph,
1579    );
1580    let context_lifetime = collect_context_lifetime_analysis(
1581        &component_graph.components,
1582        &contexts,
1583        &providers,
1584        &consumers,
1585        &computed_values,
1586        &context_ownership,
1587        &component_scope,
1588        &context_resolutions,
1589        &context_dependency,
1590        &provenance,
1591    );
1592    let effects = validate_effects(
1593        &component_graph.components,
1594        effects,
1595        &effect_statements,
1596        &semantic_types,
1597    );
1598    let (
1599        reactive_graph,
1600        reactive_transitive_analysis,
1601        reactive_cycle_analysis,
1602        computed_evaluation_plan,
1603    ) = build_computed_reactive_products(
1604        &component_graph.components,
1605        &computed_values,
1606        &effects,
1607        &resource_declarations,
1608        &references,
1609        &provenance,
1610    );
1611    let context_evaluation = collect_context_evaluation_plan(
1612        &contexts,
1613        &providers,
1614        &context_resolutions,
1615        &context_type_products.contexts,
1616        &context_type_products.providers,
1617        &context_type_products.bindings,
1618        &context_lifetime,
1619        &context_dependency,
1620        &computed_evaluation_plan,
1621        &component_scope,
1622    );
1623    let effect_reactive_analysis = analyze_effect_reactivity(
1624        &component_graph.components,
1625        &computed_values,
1626        &effects,
1627        &reactive_transitive_analysis,
1628    );
1629    let effect_trigger_plan = derive_effect_trigger_plan(
1630        &component_graph.components,
1631        &effects,
1632        &effect_reactive_analysis,
1633        &provenance,
1634    );
1635    let submissions = collect_submission_products(
1636        &component_graph.components,
1637        &forms,
1638        &form_fields,
1639        &validation_rules,
1640        &effect_trigger_plan,
1641    );
1642    let serialization = collect_serialization_products(
1643        &component_graph.components,
1644        &forms,
1645        &form_fields,
1646        &submissions.plans,
1647    );
1648    let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
1649    let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
1650    let optimized_form_ir = optimize_form_ir(&form_ir);
1651    let submission_host_products = collect_submission_host_products(
1652        &templates,
1653        &component_graph.components,
1654        &forms,
1655        &form_field_bindings,
1656        &submissions.plans,
1657        &serialization.plans,
1658        &optimized_form_ir.optimized,
1659    );
1660    let runtime_forms = build_runtime_form_registry(
1661        &forms,
1662        &form_fields,
1663        &form_field_bindings,
1664        &validation_rules,
1665        &optimized_form_ir.optimized,
1666        &submissions,
1667        &serialization,
1668        &reset,
1669        &submission_host_products.hosts,
1670    );
1671    let effect_execution_plan = plan_effect_execution(
1672        &computed_values,
1673        &effects,
1674        &effect_reactive_analysis,
1675        &effect_trigger_plan,
1676        &reactive_transitive_analysis,
1677        &computed_evaluation_plan,
1678    );
1679    let mut diagnostics = component_graph.diagnostics.clone();
1680    diagnostics.extend(computed_diagnostics);
1681    extend_computed_diagnostics(
1682        &mut diagnostics,
1683        &component_graph.components,
1684        &computed_values,
1685        &resource_declarations,
1686        &expression_graph,
1687        &semantic_types,
1688        &reactive_cycle_analysis,
1689        &provenance,
1690    );
1691    let component_ids = component_graph
1692        .components
1693        .iter()
1694        .map(|component| component.id.clone())
1695        .collect::<BTreeSet<_>>();
1696    let composition_types = collect_composition_type_products(
1697        &component_ids,
1698        &component_invocations,
1699        &slot_bindings,
1700        &slots,
1701        &slot_composition.fragments,
1702        &slot_composition.outlets,
1703        &instance_context,
1704        &context_type_products.bindings,
1705        &context_type_products.contexts,
1706        &context_type_products.providers,
1707        &context_lifetime,
1708        &ownership,
1709        &references,
1710    );
1711    let component_initialization = plan_component_initialization(
1712        &component_instance_plan,
1713        &slot_bindings,
1714        &composition_types,
1715        &instance_context,
1716    );
1717    let mut model = ApplicationSemanticModel {
1718        expression_graph,
1719        semantic_types,
1720        components: components_with_resolved_form_field_candidates(
1721            &component_graph.components,
1722            &form_field_declaration_candidates,
1723        ),
1724        contexts,
1725        providers,
1726        consumers,
1727        forms,
1728        resource_endpoint_resolutions,
1729        opaque_action_resolutions,
1730        resource_declarations,
1731        resource_activations,
1732        form_field_declaration_candidates,
1733        form_fields,
1734        form_field_binding_candidates,
1735        form_field_bindings,
1736        form_ownership,
1737        validation_rule_candidates,
1738        validation_rules,
1739        validation_graph,
1740        validation_dependency_plans,
1741        form_tracking,
1742        submissions,
1743        submission_host_candidates: submission_host_products.candidates,
1744        submission_hosts: submission_host_products.hosts,
1745        serialization,
1746        reset,
1747        form_ir,
1748        optimized_form_ir,
1749        runtime_forms,
1750        slots,
1751        component_invocations,
1752        component_instance_plan,
1753        component_instance_scope,
1754        component_composition,
1755        component_initialization,
1756        component_ir: ComponentIrReport::default(),
1757        component_ir_optimization: OptimizedComponentIrReport::default(),
1758        instance_context,
1759        slot_content_fragments: slot_composition.fragments,
1760        slot_outlets: slot_composition.outlets,
1761        slot_bindings,
1762        composition_types,
1763        context_declaration_candidates,
1764        context_ownership,
1765        context_dependency,
1766        context_lifetime,
1767        context_evaluation,
1768        component_scope,
1769        context_resolutions,
1770        context_types: context_type_products.contexts,
1771        provider_types: context_type_products.providers,
1772        consumer_types: context_type_products.consumers,
1773        context_binding_types: context_type_products.bindings,
1774        duplicate_provider_declarations,
1775        computed_values,
1776        effects,
1777        effect_reactive_analysis,
1778        effect_trigger_plan,
1779        effect_execution_plan,
1780        effect_bodies,
1781        effect_statements,
1782        reactive_graph,
1783        reactive_transitive_analysis,
1784        reactive_cycle_analysis,
1785        computed_evaluation_plan,
1786        templates,
1787        template_entities,
1788        diagnostics,
1789        ownership,
1790        references,
1791        provenance,
1792    };
1793    model
1794        .diagnostics
1795        .extend(crate::collect_effect_diagnostics(&model));
1796    model
1797        .diagnostics
1798        .extend(crate::collect_context_diagnostics(&model));
1799    model
1800        .diagnostics
1801        .extend(crate::collect_form_diagnostics(&model));
1802    model.component_ir = lower_component_ir(&model);
1803    model.component_ir_optimization = optimize_component_ir(&model.component_ir);
1804    model
1805        .diagnostics
1806        .extend(crate::collect_component_diagnostics(&model));
1807    model
1808}
1809
1810#[must_use]
1811pub fn build_application_semantic_model_for_unit(
1812    unit: &CompilationUnit,
1813) -> ApplicationSemanticModel {
1814    build_application_semantic_model_for_unit_with_packages(
1815        unit,
1816        &crate::semantic_package::SemanticPackageResolutionTable::default(),
1817    )
1818}
1819
1820/// Build the canonical application model with caller-supplied package semantics.
1821#[must_use]
1822pub fn build_application_semantic_model_for_unit_with_packages(
1823    unit: &CompilationUnit,
1824    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1825) -> ApplicationSemanticModel {
1826    let symbols = crate::build_symbol_table(unit);
1827    let modules = crate::build_module_graph(unit);
1828    let bindings =
1829        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1830    build_application_semantic_model_from_files_with_bindings(unit.files(), Some(&bindings))
1831}
1832
1833/// Builds the canonical application model for a discovered file-route source
1834/// set. Conventional layouts are compiler-issued default-Slot composition
1835/// edges, so every downstream product observes the same instance topology.
1836///
1837/// # Errors
1838///
1839/// Returns stable route or layout composition diagnostics before publication
1840/// can materialize an incomplete page.
1841pub fn build_file_route_application_semantic_model_for_unit_with_packages(
1842    unit: &CompilationUnit,
1843    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1844) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1845    let symbols = crate::build_symbol_table(unit);
1846    let modules = crate::build_module_graph(unit);
1847    let bindings =
1848        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1849    build_application_semantic_model_from_files_with_bindings_mode(
1850        unit.files(),
1851        Some(&bindings),
1852        ApplicationAssemblyMode::FileRoutes,
1853    )
1854}
1855
1856/// Builds the canonical composed model for one already-resolved file-route
1857/// page. Publication uses this route-scoped entry so shared layouts never
1858/// project sibling pages into the same default Slot.
1859///
1860/// # Errors
1861///
1862/// Returns stable route/layout diagnostics, including an unknown selected
1863/// route component.
1864pub fn build_file_route_application_semantic_model_for_route_with_packages(
1865    unit: &CompilationUnit,
1866    packages: &crate::semantic_package::SemanticPackageResolutionTable,
1867    route_component: &SemanticId,
1868) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1869    let symbols = crate::build_symbol_table(unit);
1870    let modules = crate::build_module_graph(unit);
1871    let bindings =
1872        crate::binding_table::build_binding_table_with_packages(unit, &symbols, &modules, packages);
1873    build_application_semantic_model_from_files_with_bindings_mode(
1874        unit.files(),
1875        Some(&bindings),
1876        ApplicationAssemblyMode::FileRoute(route_component.clone()),
1877    )
1878}
1879
1880fn build_application_semantic_model_from_files(files: &[ParsedFile]) -> ApplicationSemanticModel {
1881    build_application_semantic_model_from_files_with_bindings(files, None)
1882}
1883
1884#[allow(clippy::too_many_lines)]
1885fn build_application_semantic_model_from_files_with_bindings(
1886    files: &[ParsedFile],
1887    bindings: Option<&crate::BindingTable>,
1888) -> ApplicationSemanticModel {
1889    build_application_semantic_model_from_files_with_bindings_mode(
1890        files,
1891        bindings,
1892        ApplicationAssemblyMode::Authored,
1893    )
1894    .expect("authored application assembly cannot derive file-route diagnostics")
1895}
1896
1897#[derive(Debug, Clone, PartialEq, Eq)]
1898enum ApplicationAssemblyMode {
1899    Authored,
1900    FileRoutes,
1901    FileRoute(SemanticId),
1902}
1903
1904#[allow(clippy::too_many_lines)]
1905fn build_application_semantic_model_from_files_with_bindings_mode(
1906    files: &[ParsedFile],
1907    bindings: Option<&crate::BindingTable>,
1908    mode: ApplicationAssemblyMode,
1909) -> Result<ApplicationSemanticModel, FileRouteApplicationModelErrorV1> {
1910    let (mut components, mut templates, mut diagnostics, mut references) =
1911        (Vec::new(), Vec::new(), Vec::new(), Vec::new());
1912    let (mut provenance, mut template_entities, mut type_aliases) =
1913        (BTreeMap::new(), Vec::new(), Vec::new());
1914    for parsed in files {
1915        let component_graph = build_component_graph_for_module(parsed);
1916        let template_graph = build_template_graph(&component_graph);
1917        let file_template_entities = build_template_semantic_entities(&template_graph.templates);
1918
1919        components.extend(component_graph.components);
1920        templates.extend(template_graph.templates);
1921        diagnostics.extend(component_graph.diagnostics);
1922        references.extend(component_graph.references);
1923        provenance.extend(component_graph.provenance);
1924        extend_template_entity_provenance(&mut provenance, &file_template_entities);
1925        template_entities.extend(file_template_entities);
1926        type_aliases.extend(
1927            parsed
1928                .type_aliases
1929                .iter()
1930                .cloned()
1931                .map(|alias| (parsed.path.clone(), alias)),
1932        );
1933    }
1934
1935    resolve_builtin_pure_calls(&mut components);
1936
1937    if let Some(bindings) = bindings {
1938        diagnostics.extend(bindings.diagnostics.iter().map(|diagnostic| {
1939            ComponentDiagnostic::error(
1940                diagnostic.code.clone(),
1941                format!("{}: {}", diagnostic.module.display(), diagnostic.message),
1942            )
1943        }));
1944        resolve_semantic_package_pure_calls(&mut components, bindings);
1945    }
1946
1947    let resource_endpoint_resolutions =
1948        collect_resource_endpoint_resolutions(&components, bindings);
1949    diagnostics.extend(collect_resource_lowering_diagnostics(
1950        &resource_endpoint_resolutions,
1951    ));
1952    let opaque_action_resolutions = collect_opaque_action_resolutions(&components, bindings);
1953    diagnostics.extend(collect_opaque_action_lowering_diagnostics(
1954        &opaque_action_resolutions,
1955    ));
1956
1957    let mut component_invocations = collect_component_invocations(
1958        &components,
1959        &templates,
1960        &template_entities,
1961        files,
1962        bindings,
1963        &provenance,
1964    );
1965    let virtual_invocations = match mode {
1966        ApplicationAssemblyMode::Authored => BTreeMap::new(),
1967        ApplicationAssemblyMode::FileRoutes | ApplicationAssemblyMode::FileRoute(_) => {
1968            let mut graph = crate::build_validated_file_route_graph_from_components_v1(&components)
1969                .map_err(|error| FileRouteApplicationModelErrorV1 {
1970                    code: error.code,
1971                    message: error.message,
1972                })?;
1973            if let ApplicationAssemblyMode::FileRoute(selected) = &mode {
1974                graph.routes.retain(|route| route.component == *selected);
1975                if graph.routes.is_empty() {
1976                    return Err(FileRouteApplicationModelErrorV1 {
1977                        code: "PSROUTE1014_FILE_ROUTE_COMPONENT_UNRESOLVED",
1978                        message: format!("component `{selected}` is not a conventional file route"),
1979                    });
1980                }
1981            }
1982            let plan = crate::build_layout_composition_plan_from_components_v1(&components, &graph)
1983                .map_err(|error| FileRouteApplicationModelErrorV1 {
1984                    code: error.code,
1985                    message: error.message,
1986                })?;
1987            crate::layout_composition_virtual_invocations_from_provenance_v1(&plan, &provenance)
1988        }
1989    };
1990    let component_instance_plan = plan_component_instances_with_virtual_invocations(
1991        &components,
1992        &component_invocations,
1993        &virtual_invocations,
1994        &template_entities,
1995        &provenance,
1996    );
1997    component_invocations.extend(virtual_invocations.clone());
1998    let component_instance_scope = build_component_instance_scope_graph(&component_instance_plan);
1999    let component_composition = analyze_component_composition(
2000        &components
2001            .iter()
2002            .map(|component| component.id.clone())
2003            .collect(),
2004        &component_invocations,
2005        &component_instance_plan,
2006    );
2007
2008    let (computed_values, computed_diagnostics) = classify_computed_values(
2009        &components,
2010        collect_computed_values(&components, &provenance),
2011        &provenance,
2012    );
2013    let effects = collect_effects(&components, &provenance);
2014    let expression_graph = ExpressionGraph::from_components(&components, &provenance);
2015    let contexts = collect_context_entities(&components, &expression_graph);
2016    let forms = collect_form_entities(&components);
2017    let base_semantic_types = SemanticTypeModel::from_components_with_aliases_and_bindings(
2018        &components,
2019        &provenance,
2020        &type_aliases,
2021        bindings,
2022    );
2023    let resource_declarations = collect_resource_declarations(
2024        &components,
2025        &resource_endpoint_resolutions,
2026        &base_semantic_types,
2027        bindings,
2028    );
2029    let resource_activations =
2030        collect_resource_activations(&resource_declarations, &component_instance_plan);
2031    let form_field_products =
2032        collect_form_field_products(&components, &forms, &base_semantic_types, bindings);
2033    let form_field_declaration_candidates = form_field_products.candidates;
2034    let form_fields = form_field_products.fields;
2035    let form_field_binding_products = collect_form_field_binding_products(
2036        &components,
2037        &templates,
2038        &forms,
2039        &form_fields,
2040        &form_field_declaration_candidates,
2041    );
2042    let form_field_binding_candidates = form_field_binding_products.candidates;
2043    let form_field_bindings = form_field_binding_products.bindings;
2044    let slots = collect_slot_entities(&components);
2045    let slot_composition = collect_slot_composition(&templates, &component_invocations, &slots);
2046    let slot_bindings = collect_slot_bindings_with_virtual_invocations(
2047        &component_instance_plan,
2048        &component_invocations,
2049        &virtual_invocations,
2050        &slots,
2051        &slot_composition.fragments,
2052        &slot_composition.outlets,
2053    );
2054    let (providers, duplicate_provider_declarations) =
2055        collect_provider_entities(&components, &contexts, &expression_graph, bindings);
2056    let consumers = collect_consumer_entities(&components, &contexts, bindings);
2057    let instance_context = collect_instance_context_registry(
2058        &component_instance_scope,
2059        &contexts,
2060        &providers,
2061        &consumers,
2062    );
2063    let context_declaration_candidates =
2064        collect_context_declaration_candidates(&components, &contexts, &providers, &consumers);
2065    let component_scope = ComponentScopeGraph::reflexive(&components);
2066    let context_resolutions = collect_context_resolutions(
2067        &consumers,
2068        &contexts,
2069        &providers,
2070        &expression_graph,
2071        &component_scope,
2072    );
2073    diagnostics.extend(computed_diagnostics);
2074    extend_derived_entity_provenance(
2075        &mut provenance,
2076        &contexts,
2077        &forms,
2078        &form_fields,
2079        &form_field_bindings,
2080        &providers,
2081        &consumers,
2082        &slots,
2083        &component_invocations,
2084        &slot_composition.fragments,
2085        &slot_composition.outlets,
2086        &component_instance_plan,
2087        &computed_values,
2088        &effects,
2089    );
2090    let mut ownership = collect_ownership(
2091        &components,
2092        &contexts,
2093        &forms,
2094        &form_fields,
2095        &form_field_bindings,
2096        &providers,
2097        &consumers,
2098        &slots,
2099        &component_invocations,
2100        &slot_composition.fragments,
2101        &slot_composition.outlets,
2102        &component_instance_plan,
2103        &computed_values,
2104        &effects,
2105        &templates,
2106        &template_entities,
2107    );
2108    let context_ownership = collect_context_ownership_graph(
2109        &components,
2110        &contexts,
2111        &providers,
2112        &consumers,
2113        &expression_graph,
2114        &provenance,
2115    );
2116    let (effect_bodies, effect_statements) =
2117        lower_effect_bodies(&components, &effects, &expression_graph);
2118    references.extend(build_computed_references(
2119        &components,
2120        &computed_values,
2121        &resource_declarations,
2122        &expression_graph,
2123    ));
2124    references.extend(build_effect_references(
2125        &components,
2126        &effects,
2127        &computed_values,
2128        &expression_graph,
2129    ));
2130    references.extend(build_provider_references(&providers));
2131    references.extend(build_consumer_references(&consumers));
2132    references.extend(build_context_resolution_references(&context_resolutions));
2133    extend_template_references(
2134        &mut references,
2135        &components,
2136        &computed_values,
2137        &template_entities,
2138        &ownership,
2139    );
2140    references.extend(build_form_field_binding_references(&form_field_bindings));
2141    let form_ownership = collect_form_ownership_graph(
2142        &component_instance_plan.roots,
2143        &components,
2144        &forms,
2145        &form_fields,
2146        &form_field_bindings,
2147        &template_entities,
2148        &ownership,
2149        &references,
2150        &provenance,
2151    );
2152    let validation_products = collect_validation_products(&components, &forms, &form_fields);
2153    let validation_rule_candidates = validation_products.candidates;
2154    let validation_rules = validation_products.rules;
2155    extend_validation_products(
2156        &mut provenance,
2157        &mut ownership,
2158        &mut references,
2159        &validation_rules,
2160    );
2161    let validation_graph = collect_validation_graph(
2162        &component_instance_plan.roots,
2163        &form_ownership,
2164        &forms,
2165        &form_fields,
2166        &validation_rules,
2167        &validation_rule_candidates,
2168        &validation_products.cycles,
2169        &ownership,
2170        &references,
2171    );
2172    let validation_dependency_plans = collect_validation_dependency_plans(
2173        &forms,
2174        &form_fields,
2175        &validation_rules,
2176        &form_ownership,
2177        &validation_graph,
2178    );
2179    let form_tracking =
2180        collect_form_tracking_products(&forms, &form_fields, &form_field_bindings, &form_ownership);
2181    let semantic_types = finalize_semantic_types(
2182        base_semantic_types.with_resource_types(&resource_declarations),
2183        &components,
2184        &contexts,
2185        &forms,
2186        &form_fields,
2187        &providers,
2188        &consumers,
2189        &slots,
2190        &computed_values,
2191        &effects,
2192        &effect_statements,
2193        &expression_graph,
2194        &references,
2195        &template_entities,
2196    );
2197    let context_type_products = collect_context_type_products(
2198        &contexts,
2199        &providers,
2200        &consumers,
2201        &context_resolutions,
2202        &expression_graph,
2203        &semantic_types,
2204    );
2205    let context_dependency = collect_context_dependency_graph(
2206        &components,
2207        &contexts,
2208        &providers,
2209        &consumers,
2210        &context_resolutions,
2211        &context_type_products.contexts,
2212        &context_type_products.providers,
2213        &context_type_products.bindings,
2214        &computed_values,
2215        &expression_graph,
2216    );
2217    let context_lifetime = collect_context_lifetime_analysis(
2218        &components,
2219        &contexts,
2220        &providers,
2221        &consumers,
2222        &computed_values,
2223        &context_ownership,
2224        &component_scope,
2225        &context_resolutions,
2226        &context_dependency,
2227        &provenance,
2228    );
2229    let effects = validate_effects(&components, effects, &effect_statements, &semantic_types);
2230    let (
2231        reactive_graph,
2232        reactive_transitive_analysis,
2233        reactive_cycle_analysis,
2234        computed_evaluation_plan,
2235    ) = build_computed_reactive_products(
2236        &components,
2237        &computed_values,
2238        &effects,
2239        &resource_declarations,
2240        &references,
2241        &provenance,
2242    );
2243    let context_evaluation = collect_context_evaluation_plan(
2244        &contexts,
2245        &providers,
2246        &context_resolutions,
2247        &context_type_products.contexts,
2248        &context_type_products.providers,
2249        &context_type_products.bindings,
2250        &context_lifetime,
2251        &context_dependency,
2252        &computed_evaluation_plan,
2253        &component_scope,
2254    );
2255    let effect_reactive_analysis = analyze_effect_reactivity(
2256        &components,
2257        &computed_values,
2258        &effects,
2259        &reactive_transitive_analysis,
2260    );
2261    let effect_trigger_plan = derive_effect_trigger_plan(
2262        &components,
2263        &effects,
2264        &effect_reactive_analysis,
2265        &provenance,
2266    );
2267    let submissions = collect_submission_products(
2268        &components,
2269        &forms,
2270        &form_fields,
2271        &validation_rules,
2272        &effect_trigger_plan,
2273    );
2274    let serialization =
2275        collect_serialization_products(&components, &forms, &form_fields, &submissions.plans);
2276    let reset = collect_reset_products(&forms, &form_fields, &form_field_bindings, &form_tracking);
2277    let form_ir = lower_form_ir(&component_instance_plan, &forms, &form_fields);
2278    let optimized_form_ir = optimize_form_ir(&form_ir);
2279    let submission_host_products = collect_submission_host_products(
2280        &templates,
2281        &components,
2282        &forms,
2283        &form_field_bindings,
2284        &submissions.plans,
2285        &serialization.plans,
2286        &optimized_form_ir.optimized,
2287    );
2288    let runtime_forms = build_runtime_form_registry(
2289        &forms,
2290        &form_fields,
2291        &form_field_bindings,
2292        &validation_rules,
2293        &optimized_form_ir.optimized,
2294        &submissions,
2295        &serialization,
2296        &reset,
2297        &submission_host_products.hosts,
2298    );
2299    let effect_execution_plan = plan_effect_execution(
2300        &computed_values,
2301        &effects,
2302        &effect_reactive_analysis,
2303        &effect_trigger_plan,
2304        &reactive_transitive_analysis,
2305        &computed_evaluation_plan,
2306    );
2307    extend_computed_diagnostics(
2308        &mut diagnostics,
2309        &components,
2310        &computed_values,
2311        &resource_declarations,
2312        &expression_graph,
2313        &semantic_types,
2314        &reactive_cycle_analysis,
2315        &provenance,
2316    );
2317    let component_ids = components
2318        .iter()
2319        .map(|component| component.id.clone())
2320        .collect::<BTreeSet<_>>();
2321    let composition_types = collect_composition_type_products(
2322        &component_ids,
2323        &component_invocations,
2324        &slot_bindings,
2325        &slots,
2326        &slot_composition.fragments,
2327        &slot_composition.outlets,
2328        &instance_context,
2329        &context_type_products.bindings,
2330        &context_type_products.contexts,
2331        &context_type_products.providers,
2332        &context_lifetime,
2333        &ownership,
2334        &references,
2335    );
2336    let component_initialization = plan_component_initialization(
2337        &component_instance_plan,
2338        &slot_bindings,
2339        &composition_types,
2340        &instance_context,
2341    );
2342    let components = components_with_resolved_form_field_candidates(
2343        &components,
2344        &form_field_declaration_candidates,
2345    );
2346    let mut model = ApplicationSemanticModel {
2347        expression_graph,
2348        semantic_types,
2349        components,
2350        contexts,
2351        providers,
2352        consumers,
2353        forms,
2354        resource_endpoint_resolutions,
2355        opaque_action_resolutions,
2356        resource_declarations,
2357        resource_activations,
2358        form_field_declaration_candidates,
2359        form_fields,
2360        form_field_binding_candidates,
2361        form_field_bindings,
2362        form_ownership,
2363        validation_rule_candidates,
2364        validation_rules,
2365        validation_graph,
2366        validation_dependency_plans,
2367        form_tracking,
2368        submissions,
2369        submission_host_candidates: submission_host_products.candidates,
2370        submission_hosts: submission_host_products.hosts,
2371        serialization,
2372        reset,
2373        form_ir,
2374        optimized_form_ir,
2375        runtime_forms,
2376        slots,
2377        component_invocations,
2378        component_instance_plan,
2379        component_instance_scope,
2380        component_composition,
2381        component_initialization,
2382        component_ir: ComponentIrReport::default(),
2383        component_ir_optimization: OptimizedComponentIrReport::default(),
2384        instance_context,
2385        slot_content_fragments: slot_composition.fragments,
2386        slot_outlets: slot_composition.outlets,
2387        slot_bindings,
2388        composition_types,
2389        context_declaration_candidates,
2390        context_ownership,
2391        context_dependency,
2392        context_lifetime,
2393        context_evaluation,
2394        component_scope,
2395        context_resolutions,
2396        context_types: context_type_products.contexts,
2397        provider_types: context_type_products.providers,
2398        consumer_types: context_type_products.consumers,
2399        context_binding_types: context_type_products.bindings,
2400        duplicate_provider_declarations,
2401        computed_values,
2402        effects,
2403        effect_reactive_analysis,
2404        effect_trigger_plan,
2405        effect_execution_plan,
2406        effect_bodies,
2407        effect_statements,
2408        reactive_graph,
2409        reactive_transitive_analysis,
2410        reactive_cycle_analysis,
2411        computed_evaluation_plan,
2412        templates,
2413        template_entities,
2414        diagnostics,
2415        ownership,
2416        references,
2417        provenance,
2418    };
2419    model
2420        .diagnostics
2421        .extend(crate::collect_effect_diagnostics(&model));
2422    model
2423        .diagnostics
2424        .extend(crate::collect_context_diagnostics(&model));
2425    model
2426        .diagnostics
2427        .extend(crate::collect_form_diagnostics(&model));
2428    model.component_ir = lower_component_ir(&model);
2429    model.component_ir_optimization = optimize_component_ir(&model.component_ir);
2430    model
2431        .diagnostics
2432        .extend(crate::collect_component_diagnostics(&model));
2433    Ok(model)
2434}
2435
2436fn resolve_builtin_pure_calls(components: &mut [ComponentNode]) {
2437    for component in components {
2438        for method in component
2439            .methods
2440            .iter_mut()
2441            .filter(|method| method.is_computed())
2442        {
2443            if let Some(expression) = method.computed_expression.as_mut() {
2444                resolve_builtin_pure_call(expression);
2445            }
2446        }
2447    }
2448}
2449
2450fn resolve_builtin_pure_call(expression: &mut ComputedExpression) {
2451    match &mut expression.kind {
2452        ComputedExpressionKind::Call { callee, arguments } => {
2453            for argument in arguments.iter_mut() {
2454                resolve_builtin_pure_call(argument);
2455            }
2456            let operation = match (callee.as_str(), arguments.len()) {
2457                ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
2458                ("Math.floor", 1) => Some(crate::component_graph::BuiltinPureOperation::MathFloor),
2459                ("Math.ceil", 1) => Some(crate::component_graph::BuiltinPureOperation::MathCeil),
2460                ("Math.round", 1) => Some(crate::component_graph::BuiltinPureOperation::MathRound),
2461                ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
2462                ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
2463                _ => None,
2464            };
2465            if let Some(operation) = operation {
2466                expression.kind = ComputedExpressionKind::BuiltinPureCall {
2467                    operation,
2468                    arguments: std::mem::take(arguments),
2469                };
2470            }
2471        }
2472        ComputedExpressionKind::BuiltinPureCall { arguments, .. }
2473        | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
2474            for argument in arguments {
2475                resolve_builtin_pure_call(argument);
2476            }
2477        }
2478        ComputedExpressionKind::Template { expressions, .. } => {
2479            for expression in expressions {
2480                resolve_builtin_pure_call(expression);
2481            }
2482        }
2483        ComputedExpressionKind::MemberAccess { object, .. }
2484        | ComputedExpressionKind::Unary {
2485            operand: object, ..
2486        } => {
2487            resolve_builtin_pure_call(object);
2488        }
2489        ComputedExpressionKind::IndexAccess { object, index } => {
2490            resolve_builtin_pure_call(object);
2491            resolve_builtin_pure_call(index);
2492        }
2493        ComputedExpressionKind::Conditional {
2494            condition,
2495            when_true,
2496            when_false,
2497        } => {
2498            resolve_builtin_pure_call(condition);
2499            resolve_builtin_pure_call(when_true);
2500            resolve_builtin_pure_call(when_false);
2501        }
2502        ComputedExpressionKind::Arithmetic { left, right, .. }
2503        | ComputedExpressionKind::Comparison { left, right, .. }
2504        | ComputedExpressionKind::Logical { left, right, .. }
2505        | ComputedExpressionKind::NullishCoalescing { left, right } => {
2506            resolve_builtin_pure_call(left);
2507            resolve_builtin_pure_call(right);
2508        }
2509        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
2510    }
2511}
2512
2513fn components_with_resolved_form_field_candidates(
2514    components: &[ComponentNode],
2515    candidates: &[crate::FormFieldDeclarationCandidate],
2516) -> Vec<ComponentNode> {
2517    let candidates = candidates
2518        .iter()
2519        .map(|candidate| (candidate.id.clone(), candidate))
2520        .collect::<BTreeMap<_, _>>();
2521    let mut components = components.to_vec();
2522    for component in &mut components {
2523        for candidate in &mut component.form_field_declaration_candidates {
2524            if let Some(resolved) = candidates.get(&candidate.id) {
2525                *candidate = (*resolved).clone();
2526            }
2527        }
2528    }
2529    components
2530}
2531
2532fn extend_template_entity_provenance(
2533    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2534    template_entities: &[TemplateSemanticEntity],
2535) {
2536    provenance.extend(
2537        template_entities
2538            .iter()
2539            .map(|entity| (entity.id.clone(), entity.provenance.clone())),
2540    );
2541}
2542
2543fn extend_validation_products(
2544    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2545    ownership: &mut BTreeMap<SemanticId, SemanticOwner>,
2546    references: &mut Vec<SemanticReference>,
2547    rules: &BTreeMap<ValidationRuleId, ValidationRule>,
2548) {
2549    for rule in rules.values() {
2550        provenance.insert(rule.id.as_semantic_id().clone(), rule.provenance.clone());
2551        ownership.insert(
2552            rule.id.as_semantic_id().clone(),
2553            SemanticOwner::entity(rule.target_field.as_semantic_id().clone()),
2554        );
2555        if let Some(dependency) = &rule.dependency {
2556            references.push(SemanticReference {
2557                kind: SemanticReferenceKind::ValidationRuleField,
2558                source: rule.id.as_semantic_id().clone(),
2559                target: dependency.as_semantic_id().clone(),
2560                provenance: rule
2561                    .argument_provenance
2562                    .clone()
2563                    .unwrap_or_else(|| rule.decorator_provenance.clone()),
2564            });
2565        }
2566    }
2567    references.sort_by(|left, right| {
2568        (left.source.as_str(), left.kind, left.target.as_str()).cmp(&(
2569            right.source.as_str(),
2570            right.kind,
2571            right.target.as_str(),
2572        ))
2573    });
2574    references.dedup();
2575}
2576
2577#[allow(clippy::too_many_arguments)]
2578fn extend_derived_entity_provenance(
2579    provenance: &mut BTreeMap<SemanticId, SourceProvenance>,
2580    contexts: &BTreeMap<ContextId, ContextEntity>,
2581    forms: &BTreeMap<FormId, FormEntity>,
2582    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2583    form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
2584    providers: &BTreeMap<ProviderId, ProviderEntity>,
2585    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2586    slots: &BTreeMap<SlotId, SlotEntity>,
2587    component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
2588    slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
2589    slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
2590    component_instance_plan: &ComponentInstancePlan,
2591    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2592    effects: &BTreeMap<SemanticId, Effect>,
2593) {
2594    provenance.extend(contexts.values().map(|context| {
2595        (
2596            context.id.as_semantic_id().clone(),
2597            context.provenance.clone(),
2598        )
2599    }));
2600    provenance.extend(
2601        forms
2602            .values()
2603            .map(|form| (form.id.as_semantic_id().clone(), form.provenance.clone())),
2604    );
2605    provenance.extend(
2606        form_fields
2607            .values()
2608            .map(|field| (field.id.as_semantic_id().clone(), field.provenance.clone())),
2609    );
2610    provenance.extend(form_field_bindings.values().map(|binding| {
2611        (
2612            binding.id.as_semantic_id().clone(),
2613            binding.provenance.clone(),
2614        )
2615    }));
2616    provenance.extend(providers.values().map(|provider| {
2617        (
2618            provider.id.as_semantic_id().clone(),
2619            provider.provenance.clone(),
2620        )
2621    }));
2622    provenance.extend(consumers.values().map(|consumer| {
2623        (
2624            consumer.id.as_semantic_id().clone(),
2625            consumer.provenance.clone(),
2626        )
2627    }));
2628    provenance.extend(
2629        slots
2630            .values()
2631            .map(|slot| (slot.id.as_semantic_id().clone(), slot.provenance.clone())),
2632    );
2633    provenance.extend(component_invocations.values().map(|invocation| {
2634        (
2635            invocation.id.as_semantic_id().clone(),
2636            invocation.provenance.clone(),
2637        )
2638    }));
2639    provenance.extend(slot_content_fragments.values().map(|fragment| {
2640        (
2641            fragment.id.as_semantic_id().clone(),
2642            fragment.provenance.clone(),
2643        )
2644    }));
2645    provenance.extend(slot_outlets.values().map(|outlet| {
2646        (
2647            outlet.id.as_semantic_id().clone(),
2648            outlet.provenance.clone(),
2649        )
2650    }));
2651    provenance.extend(component_instance_plan.instances.values().map(|instance| {
2652        (
2653            instance.id.as_semantic_id().clone(),
2654            instance.provenance.clone(),
2655        )
2656    }));
2657    provenance.extend(component_instance_plan.blocked.values().map(|blocked| {
2658        (
2659            blocked.id.as_semantic_id().clone(),
2660            blocked.provenance.clone(),
2661        )
2662    }));
2663    provenance.extend(
2664        computed_values
2665            .iter()
2666            .map(|(id, computed)| (id.clone(), computed.provenance.clone())),
2667    );
2668    provenance.extend(
2669        effects
2670            .iter()
2671            .map(|(id, effect)| (id.clone(), effect.provenance.clone())),
2672    );
2673}
2674
2675#[allow(clippy::too_many_arguments)]
2676fn finalize_semantic_types(
2677    semantic_types: SemanticTypeModel,
2678    components: &[ComponentNode],
2679    contexts: &BTreeMap<ContextId, ContextEntity>,
2680    forms: &BTreeMap<FormId, FormEntity>,
2681    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
2682    providers: &BTreeMap<ProviderId, ProviderEntity>,
2683    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
2684    slots: &BTreeMap<SlotId, SlotEntity>,
2685    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2686    effects: &BTreeMap<SemanticId, Effect>,
2687    effect_statements: &BTreeMap<SemanticId, EffectStatement>,
2688    expression_graph: &ExpressionGraph,
2689    references: &[SemanticReference],
2690    template_entities: &[TemplateSemanticEntity],
2691) -> SemanticTypeModel {
2692    semantic_types
2693        .with_slot_types(slots)
2694        .with_form_types(forms)
2695        .with_form_field_types(form_fields)
2696        .with_context_types(contexts)
2697        .with_provider_types(providers)
2698        .with_consumer_types(consumers)
2699        .with_expression_types(expression_graph, components, contexts, providers)
2700        .with_computed_value_types(components, computed_values, expression_graph, references)
2701        .with_context_source_expression_types(components, contexts, providers, expression_graph)
2702        .with_effect_statement_types(components, effects, effect_statements, expression_graph)
2703        .with_template_binding_types(template_entities, references)
2704        .normalized()
2705}
2706
2707#[allow(clippy::too_many_arguments)]
2708fn extend_computed_diagnostics(
2709    diagnostics: &mut Vec<ComponentDiagnostic>,
2710    components: &[ComponentNode],
2711    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2712    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
2713    expression_graph: &ExpressionGraph,
2714    semantic_types: &SemanticTypeModel,
2715    reactive_cycle_analysis: &IrReactiveCycleAnalysis,
2716    provenance: &BTreeMap<SemanticId, SourceProvenance>,
2717) {
2718    diagnostics.extend(collect_computed_cycle_diagnostics(
2719        reactive_cycle_analysis,
2720        computed_values,
2721    ));
2722    diagnostics.extend(collect_computed_semantic_diagnostics(
2723        components,
2724        computed_values,
2725        resource_declarations,
2726        expression_graph,
2727        semantic_types,
2728        provenance,
2729    ));
2730}
2731
2732#[allow(clippy::too_many_lines)]
2733fn classify_computed_values(
2734    components: &[ComponentNode],
2735    mut computed_values: BTreeMap<SemanticId, ComputedValue>,
2736    provenance: &BTreeMap<SemanticId, SourceProvenance>,
2737) -> (
2738    BTreeMap<SemanticId, ComputedValue>,
2739    Vec<ComponentDiagnostic>,
2740) {
2741    let mut diagnostics = Vec::new();
2742
2743    for computed in computed_values.values_mut() {
2744        let Some(component_id) = computed.owner.entity_id() else {
2745            continue;
2746        };
2747        let Some(component) = components
2748            .iter()
2749            .find(|component| component.id == *component_id)
2750        else {
2751            continue;
2752        };
2753        let Some(method) = component
2754            .methods
2755            .iter()
2756            .find(|method| method.id == computed.method)
2757        else {
2758            continue;
2759        };
2760        let mut violations = Vec::new();
2761
2762        if method.is_async {
2763            violations.push(crate::ComputedPurityViolation {
2764                kind: crate::ComputedPurityViolationKind::Async,
2765                provenance: computed.provenance.clone(),
2766            });
2767        }
2768        if method
2769            .decorators
2770            .iter()
2771            .any(|decorator| decorator == "action")
2772        {
2773            violations.push(crate::ComputedPurityViolation {
2774                kind: crate::ComputedPurityViolationKind::Action,
2775                provenance: computed.provenance.clone(),
2776            });
2777        }
2778        if method
2779            .decorators
2780            .iter()
2781            .any(|decorator| decorator == "effect")
2782        {
2783            violations.push(crate::ComputedPurityViolation {
2784                kind: crate::ComputedPurityViolationKind::Effect,
2785                provenance: computed.provenance.clone(),
2786            });
2787        }
2788        for action in component
2789            .actions
2790            .iter()
2791            .filter(|action| action.method == method.name)
2792        {
2793            violations.push(crate::ComputedPurityViolation {
2794                kind: crate::ComputedPurityViolationKind::StateMutation,
2795                provenance: provenance
2796                    .get(&action.id)
2797                    .expect("computed state update should have canonical provenance")
2798                    .clone(),
2799            });
2800        }
2801        for call in &method.calls {
2802            if method
2803                .computed_expression
2804                .as_ref()
2805                .is_some_and(|expression| {
2806                    contains_semantic_package_pure_call(expression, &call.callee)
2807                        || contains_builtin_pure_call(expression, &call.callee)
2808                })
2809            {
2810                continue;
2811            }
2812            let kind = computed_call_purity_kind(component, &call.callee);
2813            violations.push(crate::ComputedPurityViolation {
2814                kind,
2815                provenance: SourceProvenance::new(&computed.provenance.path, call.span),
2816            });
2817        }
2818
2819        violations.sort_by(|left, right| {
2820            (
2821                left.kind,
2822                left.provenance.path.as_path(),
2823                left.provenance.span.start,
2824                left.provenance.span.end,
2825            )
2826                .cmp(&(
2827                    right.kind,
2828                    right.provenance.path.as_path(),
2829                    right.provenance.span.start,
2830                    right.provenance.span.end,
2831                ))
2832        });
2833        violations
2834            .dedup_by(|left, right| left.kind == right.kind && left.provenance == right.provenance);
2835        computed.purity = if violations.is_empty() {
2836            crate::ComputedPurity::Pure
2837        } else {
2838            crate::ComputedPurity::Impure
2839        };
2840        computed.purity_violations = violations;
2841        diagnostics.extend(computed.purity_violations.iter().map(|violation| {
2842            ComponentDiagnostic {
2843                severity: ComponentDiagnosticSeverity::Error,
2844                effect_id: None,
2845                statement_id: None,
2846                context_declaration_candidate_id: None,
2847                context_id: None,
2848                provider_id: None,
2849                consumer_id: None,
2850                slot_id: None,
2851                invocation_id: None,
2852                component_instance_id: None,
2853                slot_binding_id: None,
2854                structural_region_id: None,
2855                component_id: None,
2856                provider_instance_id: None,
2857                consumer_instance_id: None,
2858                secondary_labels: Vec::new(),
2859                code: ComputedDiagnosticCode::PurityViolation.as_str().to_string(),
2860                message: format!(
2861                    "computed getter `{}` is impure: {}",
2862                    computed.name,
2863                    violation.kind.description()
2864                ),
2865                provenance: Some(violation.provenance.clone()),
2866            }
2867        }));
2868    }
2869
2870    (computed_values, diagnostics)
2871}
2872
2873fn resolve_semantic_package_pure_calls(
2874    components: &mut [ComponentNode],
2875    bindings: &crate::BindingTable,
2876) {
2877    for component in components {
2878        for method in component
2879            .methods
2880            .iter_mut()
2881            .filter(|method| method.is_computed())
2882        {
2883            let Some(expression) = method.computed_expression.as_mut() else {
2884                continue;
2885            };
2886            resolve_semantic_package_pure_call(expression, &component.module_path, bindings);
2887        }
2888    }
2889}
2890
2891fn collect_resource_endpoint_resolutions(
2892    components: &[ComponentNode],
2893    bindings: Option<&crate::BindingTable>,
2894) -> Vec<crate::ResourceEndpointResolution> {
2895    let mut resolutions = Vec::new();
2896    for component in components {
2897        for resource in &component.resource_declaration_candidates {
2898            let outcome = match resource.endpoint_designator.as_deref() {
2899                None => crate::ResourceEndpointResolutionOutcome::MissingDesignator,
2900                Some(designator) => {
2901                    let Some(bindings) = bindings else {
2902                        resolutions.push(crate::ResourceEndpointResolution {
2903                            owner_component: resource.owner_component.clone(),
2904                            field: resource.field.clone(),
2905                            endpoint_designator: resource.endpoint_designator.clone(),
2906                            outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
2907                                designator: designator.to_string(),
2908                            },
2909                            provenance: resource.provenance.clone(),
2910                        });
2911                        continue;
2912                    };
2913                    let Some(binding) = bindings.resolve_import(&component.module_path, designator)
2914                    else {
2915                        resolutions.push(crate::ResourceEndpointResolution {
2916                            owner_component: resource.owner_component.clone(),
2917                            field: resource.field.clone(),
2918                            endpoint_designator: resource.endpoint_designator.clone(),
2919                            outcome: crate::ResourceEndpointResolutionOutcome::UnboundDesignator {
2920                                designator: designator.to_string(),
2921                            },
2922                            provenance: resource.provenance.clone(),
2923                        });
2924                        continue;
2925                    };
2926                    match &binding.target {
2927                        crate::ImportBindingTarget::SemanticPackage {
2928                            package,
2929                            version,
2930                            integrity,
2931                            export,
2932                            kind: crate::SemanticPackageKind::Resource,
2933                            type_signature,
2934                            runtime_module,
2935                            resume_policy,
2936                            resource_endpoint: Some(endpoint),
2937                            ..
2938                        } => crate::ResourceEndpointResolutionOutcome::Resolved(
2939                            crate::ResourceEndpointBinding {
2940                                local_name: binding.local_name.clone(),
2941                                package: package.clone(),
2942                                version: version.clone(),
2943                                integrity: integrity.clone(),
2944                                export: export.clone(),
2945                                type_signature: type_signature.clone(),
2946                                runtime_module: runtime_module.clone(),
2947                                resume_policy: resume_policy.clone(),
2948                                endpoint: endpoint.clone(),
2949                            },
2950                        ),
2951                        crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
2952                            crate::ResourceEndpointResolutionOutcome::NonResourceBinding {
2953                                designator: designator.to_string(),
2954                                kind: kind.clone(),
2955                            }
2956                        }
2957                        _ => crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding {
2958                            designator: designator.to_string(),
2959                        },
2960                    }
2961                }
2962            };
2963            resolutions.push(crate::ResourceEndpointResolution {
2964                owner_component: resource.owner_component.clone(),
2965                field: resource.field.clone(),
2966                endpoint_designator: resource.endpoint_designator.clone(),
2967                outcome,
2968                provenance: resource.provenance.clone(),
2969            });
2970        }
2971    }
2972    resolutions
2973}
2974
2975fn collect_opaque_action_resolutions(
2976    components: &[ComponentNode],
2977    bindings: Option<&crate::BindingTable>,
2978) -> Vec<crate::OpaqueActionResolution> {
2979    let mut resolutions = Vec::new();
2980    for component in components {
2981        for fact in component
2982            .opaque_action_facts
2983            .iter()
2984            .filter(|fact| crate::component_graph::is_valid_opaque_action_fact(fact))
2985        {
2986            let (Some(owner_component), Some(method), Some(package_specifier), Some(export_name)) = (
2987                fact.owner_component.as_ref(),
2988                fact.method.as_ref(),
2989                fact.package.as_ref(),
2990                fact.export.as_ref(),
2991            ) else {
2992                continue;
2993            };
2994            let outcome = bindings
2995                .and_then(|bindings| bindings.module(&component.module_path))
2996                .and_then(|module| {
2997                    module.imports.values().find(|binding| {
2998                        binding.source_module == *package_specifier
2999                            && binding.imported_name == *export_name
3000                    })
3001                })
3002                .map_or_else(
3003                    || crate::OpaqueActionResolutionOutcome::UnboundImport {
3004                        package: package_specifier.clone(),
3005                        export: export_name.clone(),
3006                    },
3007                    |binding| match &binding.target {
3008                        crate::ImportBindingTarget::SemanticPackage {
3009                            package,
3010                            version,
3011                            integrity,
3012                            export,
3013                            kind: crate::SemanticPackageKind::Opaque,
3014                            type_signature,
3015                            runtime_module,
3016                            resume_policy,
3017                            opaque_terminal: Some(terminal),
3018                            ..
3019                        } => crate::OpaqueActionResolutionOutcome::Resolved(
3020                            crate::OpaqueTerminalBinding {
3021                                local_name: binding.local_name.clone(),
3022                                package: package.clone(),
3023                                version: version.clone(),
3024                                integrity: integrity.clone(),
3025                                export: export.clone(),
3026                                type_signature: type_signature.clone(),
3027                                runtime_module: runtime_module.clone(),
3028                                resume_policy: resume_policy.clone(),
3029                                terminal: terminal.clone(),
3030                            },
3031                        ),
3032                        crate::ImportBindingTarget::SemanticPackage { kind, .. } => {
3033                            crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3034                                package: package_specifier.clone(),
3035                                export: export_name.clone(),
3036                                kind: kind.clone(),
3037                            }
3038                        }
3039                        _ => crate::OpaqueActionResolutionOutcome::UnboundImport {
3040                            package: package_specifier.clone(),
3041                            export: export_name.clone(),
3042                        },
3043                    },
3044                );
3045            resolutions.push(crate::OpaqueActionResolution {
3046                activation: fact.id.clone(),
3047                owner_component: owner_component.clone(),
3048                method: method.clone(),
3049                method_name: fact.method_name.clone(),
3050                package_specifier: package_specifier.clone(),
3051                export_name: export_name.clone(),
3052                outcome,
3053                provenance: fact.provenance.clone(),
3054            });
3055        }
3056    }
3057    resolutions.sort_by(|left, right| left.activation.cmp(&right.activation));
3058    resolutions
3059}
3060
3061fn collect_opaque_action_lowering_diagnostics(
3062    resolutions: &[crate::OpaqueActionResolution],
3063) -> Vec<ComponentDiagnostic> {
3064    resolutions
3065        .iter()
3066        .filter_map(|resolution| {
3067            let message = match &resolution.outcome {
3068                crate::OpaqueActionResolutionOutcome::Resolved(_) => return None,
3069                crate::OpaqueActionResolutionOutcome::UnboundImport { package, export } => format!(
3070                    "opaque Action `{}` requires an imported opaque semantic-package export `{package}` / `{export}`",
3071                    resolution.method_name
3072                ),
3073                crate::OpaqueActionResolutionOutcome::NonOpaqueBinding {
3074                    package,
3075                    export,
3076                    kind,
3077                } => format!(
3078                    "opaque Action `{}` selects `{package}` / `{export}` with package kind {kind:?}, not opaque",
3079                    resolution.method_name
3080                ),
3081            };
3082            let mut diagnostic = ComponentDiagnostic::error("PSC1131", message);
3083            diagnostic.provenance = Some(resolution.provenance.clone());
3084            Some(diagnostic)
3085        })
3086        .collect()
3087}
3088
3089fn collect_resource_declarations(
3090    components: &[ComponentNode],
3091    resolutions: &[crate::ResourceEndpointResolution],
3092    semantic_types: &SemanticTypeModel,
3093    bindings: Option<&crate::BindingTable>,
3094) -> BTreeMap<ResourceId, crate::ResourceDeclaration> {
3095    let mut declarations = BTreeMap::new();
3096    for component in components {
3097        for resource in &component.resource_declaration_candidates {
3098            let Some(resolution) = resolutions.iter().find(|resolution| {
3099                resolution.owner_component == resource.owner_component
3100                    && resolution.field == resource.field
3101            }) else {
3102                continue;
3103            };
3104            let crate::ResourceEndpointResolutionOutcome::Resolved(endpoint) = &resolution.outcome
3105            else {
3106                continue;
3107            };
3108            let Some(declared_type) = resource.declared_type.as_ref() else {
3109                continue;
3110            };
3111            let Some(resolved_type) = semantic_types.resolve_declared_type(declared_type, bindings)
3112            else {
3113                continue;
3114            };
3115            let crate::SemanticType::Resource(resource_type) = resolved_type.semantic_type else {
3116                continue;
3117            };
3118            let execution_boundary = match endpoint.endpoint.execution_boundary {
3119                crate::SemanticPackageResourceExecutionBoundary::Client => {
3120                    crate::ResourceExecutionBoundary::Client
3121                }
3122                crate::SemanticPackageResourceExecutionBoundary::Server => {
3123                    crate::ResourceExecutionBoundary::Server
3124                }
3125                crate::SemanticPackageResourceExecutionBoundary::Shared => {
3126                    crate::ResourceExecutionBoundary::Shared
3127                }
3128            };
3129            let key = format!("{}:{}", component.id.as_str(), resource.field);
3130            let declaration = crate::ResourceDeclaration::new(
3131                resource.owner_component.clone(),
3132                resource.field.clone(),
3133                key,
3134                *resource_type.data,
3135                *resource_type.error,
3136                execution_boundary,
3137                BTreeSet::new(),
3138                crate::ResourceRetryPolicy::ExplicitOnly,
3139                crate::ResourceInvalidationPolicy::ExplicitOnly,
3140                resource.provenance.clone(),
3141            );
3142            let Ok(declaration) = declaration else {
3143                continue;
3144            };
3145            declarations.insert(declaration.id.clone(), declaration);
3146        }
3147    }
3148    declarations
3149}
3150
3151fn collect_resource_lowering_diagnostics(
3152    resolutions: &[crate::ResourceEndpointResolution],
3153) -> Vec<ComponentDiagnostic> {
3154    resolutions
3155        .iter()
3156        .filter(|resolution| {
3157            !matches!(
3158                resolution.outcome,
3159                crate::ResourceEndpointResolutionOutcome::Resolved(_)
3160            )
3161        })
3162        .map(|resolution| {
3163            let message = match &resolution.outcome {
3164                crate::ResourceEndpointResolutionOutcome::MissingDesignator => format!(
3165                    "resource declaration `{}` requires one imported semantic-package resource endpoint designator",
3166                    resolution.field
3167                ),
3168                crate::ResourceEndpointResolutionOutcome::UnboundDesignator { designator } => format!(
3169                    "resource declaration `{}` designator `{designator}` does not resolve to an imported semantic-package endpoint",
3170                    resolution.field
3171                ),
3172                crate::ResourceEndpointResolutionOutcome::NonSemanticPackageBinding { designator } => format!(
3173                    "resource declaration `{}` designator `{designator}` must bind a semantic-package resource export",
3174                    resolution.field
3175                ),
3176                crate::ResourceEndpointResolutionOutcome::NonResourceBinding { designator, kind } => format!(
3177                    "resource declaration `{}` designator `{designator}` resolves to package kind {kind:?}, not resource",
3178                    resolution.field
3179                ),
3180                crate::ResourceEndpointResolutionOutcome::Resolved(_) => unreachable!(
3181                    "resolved resource endpoints are valid N6-C13 lowering inputs"
3182                ),
3183            };
3184            let mut diagnostic = ComponentDiagnostic::error("PSC1128", message);
3185            diagnostic.provenance = Some(resolution.provenance.clone());
3186            diagnostic
3187        })
3188        .collect()
3189}
3190
3191fn collect_resource_activations(
3192    declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3193    instances: &ComponentInstancePlan,
3194) -> BTreeMap<ResourceActivationId, crate::ResourceActivation> {
3195    let mut activations = BTreeMap::new();
3196    for instance in instances.instances.values() {
3197        for declaration in declarations
3198            .values()
3199            .filter(|declaration| declaration.owner_component == instance.component)
3200        {
3201            let activation = declaration.activation_for(instance.id.clone());
3202            activations.insert(activation.id.clone(), activation);
3203        }
3204    }
3205    activations
3206}
3207
3208fn resolve_semantic_package_pure_call(
3209    expression: &mut ComputedExpression,
3210    module_path: &Path,
3211    bindings: &crate::BindingTable,
3212) {
3213    match &mut expression.kind {
3214        ComputedExpressionKind::Call { callee, arguments } => {
3215            for argument in arguments.iter_mut() {
3216                resolve_semantic_package_pure_call(argument, module_path, bindings);
3217            }
3218            let Some(binding) = bindings.resolve_import(module_path, callee) else {
3219                let operation = match (callee.as_str(), arguments.len()) {
3220                    ("Math.abs", 1) => Some(crate::component_graph::BuiltinPureOperation::MathAbs),
3221                    ("Math.floor", 1) => {
3222                        Some(crate::component_graph::BuiltinPureOperation::MathFloor)
3223                    }
3224                    ("Math.ceil", 1) => {
3225                        Some(crate::component_graph::BuiltinPureOperation::MathCeil)
3226                    }
3227                    ("Math.round", 1) => {
3228                        Some(crate::component_graph::BuiltinPureOperation::MathRound)
3229                    }
3230                    ("Math.min", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMin),
3231                    ("Math.max", 2) => Some(crate::component_graph::BuiltinPureOperation::MathMax),
3232                    _ => None,
3233                };
3234                if let Some(operation) = operation {
3235                    expression.kind = ComputedExpressionKind::BuiltinPureCall {
3236                        operation,
3237                        arguments: std::mem::take(arguments),
3238                    };
3239                }
3240                return;
3241            };
3242            let crate::ImportBindingTarget::SemanticPackage {
3243                package,
3244                version,
3245                integrity,
3246                export,
3247                kind: crate::semantic_package::SemanticPackageKind::Pure,
3248                runtime_module,
3249                resume_policy,
3250                pure_operation: Some(operation),
3251                ..
3252            } = &binding.target
3253            else {
3254                return;
3255            };
3256            expression.kind = ComputedExpressionKind::SemanticPackagePureCall {
3257                local_name: callee.clone(),
3258                package: package.clone(),
3259                version: version.clone(),
3260                integrity: integrity.clone(),
3261                export: export.clone(),
3262                runtime_module: runtime_module.clone(),
3263                resume_policy: resume_policy.clone(),
3264                operation: *operation,
3265                arguments: std::mem::take(arguments),
3266            };
3267        }
3268        ComputedExpressionKind::BuiltinPureCall { arguments, .. } => {
3269            for argument in arguments {
3270                resolve_semantic_package_pure_call(argument, module_path, bindings);
3271            }
3272        }
3273        ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => {
3274            for argument in arguments {
3275                resolve_semantic_package_pure_call(argument, module_path, bindings);
3276            }
3277        }
3278        ComputedExpressionKind::Template { expressions, .. } => {
3279            for expression in expressions {
3280                resolve_semantic_package_pure_call(expression, module_path, bindings);
3281            }
3282        }
3283        ComputedExpressionKind::MemberAccess { object, .. }
3284        | ComputedExpressionKind::Unary {
3285            operand: object, ..
3286        } => {
3287            resolve_semantic_package_pure_call(object, module_path, bindings);
3288        }
3289        ComputedExpressionKind::IndexAccess { object, index } => {
3290            resolve_semantic_package_pure_call(object, module_path, bindings);
3291            resolve_semantic_package_pure_call(index, module_path, bindings);
3292        }
3293        ComputedExpressionKind::Conditional {
3294            condition,
3295            when_true,
3296            when_false,
3297        } => {
3298            resolve_semantic_package_pure_call(condition, module_path, bindings);
3299            resolve_semantic_package_pure_call(when_true, module_path, bindings);
3300            resolve_semantic_package_pure_call(when_false, module_path, bindings);
3301        }
3302        ComputedExpressionKind::Arithmetic { left, right, .. }
3303        | ComputedExpressionKind::Comparison { left, right, .. }
3304        | ComputedExpressionKind::Logical { left, right, .. }
3305        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3306            resolve_semantic_package_pure_call(left, module_path, bindings);
3307            resolve_semantic_package_pure_call(right, module_path, bindings);
3308        }
3309        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => {}
3310    }
3311}
3312
3313fn contains_semantic_package_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3314    match &expression.kind {
3315        ComputedExpressionKind::SemanticPackagePureCall {
3316            local_name,
3317            arguments,
3318            ..
3319        } => {
3320            local_name == callee
3321                || arguments
3322                    .iter()
3323                    .any(|argument| contains_semantic_package_pure_call(argument, callee))
3324        }
3325        ComputedExpressionKind::Call { arguments, .. } => arguments
3326            .iter()
3327            .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3328        ComputedExpressionKind::BuiltinPureCall { arguments, .. } => arguments
3329            .iter()
3330            .any(|argument| contains_semantic_package_pure_call(argument, callee)),
3331        ComputedExpressionKind::Template { expressions, .. } => expressions
3332            .iter()
3333            .any(|expression| contains_semantic_package_pure_call(expression, callee)),
3334        ComputedExpressionKind::MemberAccess { object, .. }
3335        | ComputedExpressionKind::Unary {
3336            operand: object, ..
3337        } => contains_semantic_package_pure_call(object, callee),
3338        ComputedExpressionKind::IndexAccess { object, index } => {
3339            contains_semantic_package_pure_call(object, callee)
3340                || contains_semantic_package_pure_call(index, callee)
3341        }
3342        ComputedExpressionKind::Conditional {
3343            condition,
3344            when_true,
3345            when_false,
3346        } => {
3347            contains_semantic_package_pure_call(condition, callee)
3348                || contains_semantic_package_pure_call(when_true, callee)
3349                || contains_semantic_package_pure_call(when_false, callee)
3350        }
3351        ComputedExpressionKind::Arithmetic { left, right, .. }
3352        | ComputedExpressionKind::Comparison { left, right, .. }
3353        | ComputedExpressionKind::Logical { left, right, .. }
3354        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3355            contains_semantic_package_pure_call(left, callee)
3356                || contains_semantic_package_pure_call(right, callee)
3357        }
3358        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3359    }
3360}
3361
3362fn contains_builtin_pure_call(expression: &ComputedExpression, callee: &str) -> bool {
3363    match &expression.kind {
3364        ComputedExpressionKind::BuiltinPureCall {
3365            operation,
3366            arguments,
3367        } => {
3368            let operation_callee = match operation {
3369                crate::component_graph::BuiltinPureOperation::MathAbs => "Math.abs",
3370                crate::component_graph::BuiltinPureOperation::MathFloor => "Math.floor",
3371                crate::component_graph::BuiltinPureOperation::MathCeil => "Math.ceil",
3372                crate::component_graph::BuiltinPureOperation::MathRound => "Math.round",
3373                crate::component_graph::BuiltinPureOperation::MathMin => "Math.min",
3374                crate::component_graph::BuiltinPureOperation::MathMax => "Math.max",
3375            };
3376            operation_callee == callee
3377                || arguments
3378                    .iter()
3379                    .any(|argument| contains_builtin_pure_call(argument, callee))
3380        }
3381        ComputedExpressionKind::Call { arguments, .. }
3382        | ComputedExpressionKind::SemanticPackagePureCall { arguments, .. } => arguments
3383            .iter()
3384            .any(|argument| contains_builtin_pure_call(argument, callee)),
3385        ComputedExpressionKind::Template { expressions, .. } => expressions
3386            .iter()
3387            .any(|expression| contains_builtin_pure_call(expression, callee)),
3388        ComputedExpressionKind::MemberAccess { object, .. }
3389        | ComputedExpressionKind::Unary {
3390            operand: object, ..
3391        } => contains_builtin_pure_call(object, callee),
3392        ComputedExpressionKind::IndexAccess { object, index } => {
3393            contains_builtin_pure_call(object, callee) || contains_builtin_pure_call(index, callee)
3394        }
3395        ComputedExpressionKind::Conditional {
3396            condition,
3397            when_true,
3398            when_false,
3399        } => {
3400            contains_builtin_pure_call(condition, callee)
3401                || contains_builtin_pure_call(when_true, callee)
3402                || contains_builtin_pure_call(when_false, callee)
3403        }
3404        ComputedExpressionKind::Arithmetic { left, right, .. }
3405        | ComputedExpressionKind::Comparison { left, right, .. }
3406        | ComputedExpressionKind::Logical { left, right, .. }
3407        | ComputedExpressionKind::NullishCoalescing { left, right } => {
3408            contains_builtin_pure_call(left, callee) || contains_builtin_pure_call(right, callee)
3409        }
3410        ComputedExpressionKind::Literal(_) | ComputedExpressionKind::ThisMember(_) => false,
3411    }
3412}
3413
3414fn collect_computed_cycle_diagnostics(
3415    analysis: &IrReactiveCycleAnalysis,
3416    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3417) -> Vec<ComponentDiagnostic> {
3418    analysis
3419        .cycles
3420        .iter()
3421        .filter_map(|cycle| {
3422            let first = cycle.nodes.first()?;
3423            let computed = computed_values
3424                .values()
3425                .find(|computed| computed.id.as_str() == first)?;
3426            Some(ComponentDiagnostic {
3427                severity: ComponentDiagnosticSeverity::Error,
3428                effect_id: None,
3429                statement_id: None,
3430                context_declaration_candidate_id: None,
3431                context_id: None,
3432                provider_id: None,
3433                consumer_id: None,
3434                slot_id: None,
3435                invocation_id: None,
3436                component_instance_id: None,
3437                slot_binding_id: None,
3438                structural_region_id: None,
3439                component_id: None,
3440                provider_instance_id: None,
3441                consumer_instance_id: None,
3442                secondary_labels: Vec::new(),
3443                code: ComputedDiagnosticCode::DependencyCycle.as_str().to_string(),
3444                message: format!(
3445                    "computed dependency cycle detected among: {}",
3446                    cycle.nodes.join(", ")
3447                ),
3448                provenance: Some(computed.provenance.clone()),
3449            })
3450        })
3451        .collect()
3452}
3453
3454fn collect_computed_semantic_diagnostics(
3455    components: &[ComponentNode],
3456    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3457    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3458    expression_graph: &ExpressionGraph,
3459    semantic_types: &SemanticTypeModel,
3460    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3461) -> Vec<ComponentDiagnostic> {
3462    let mut diagnostics = collect_invalid_computed_declaration_diagnostics(components, provenance);
3463    diagnostics.extend(collect_computed_body_and_read_diagnostics(
3464        components,
3465        computed_values,
3466        resource_declarations,
3467        expression_graph,
3468    ));
3469    diagnostics.extend(collect_computed_type_diagnostics(
3470        computed_values,
3471        semantic_types,
3472    ));
3473    diagnostics.extend(collect_computed_conditional_diagnostics(
3474        components,
3475        expression_graph,
3476        semantic_types,
3477    ));
3478    sort_computed_diagnostics(&mut diagnostics);
3479    diagnostics
3480}
3481
3482fn collect_computed_conditional_diagnostics(
3483    components: &[ComponentNode],
3484    expression_graph: &ExpressionGraph,
3485    semantic_types: &SemanticTypeModel,
3486) -> Vec<ComponentDiagnostic> {
3487    expression_graph
3488        .nodes
3489        .values()
3490        .filter_map(|node| {
3491            let crate::ExpressionNodeKind::Conditional { condition, .. } = &node.kind else {
3492                return None;
3493            };
3494            let condition_type = semantic_types
3495                .assignments
3496                .get(condition)
3497                .map(|assignment| assignment.semantic_type.clone())
3498                .or_else(|| match &expression_graph.node(condition)?.kind {
3499                    crate::ExpressionNodeKind::ThisMember { name } => components
3500                        .iter()
3501                        .find(|component| {
3502                            component
3503                                .methods
3504                                .iter()
3505                                .any(|method| component.id.computed(&method.name) == node.owner)
3506                        })
3507                        .and_then(|component| {
3508                            component
3509                                .state_fields
3510                                .iter()
3511                                .find(|field| field.name == *name)
3512                        })
3513                        .and_then(|field| semantic_types.assignments.get(&field.id))
3514                        .map(|assignment| assignment.semantic_type.clone()),
3515                    _ => None,
3516                })
3517                .unwrap_or(crate::SemanticType::Unknown);
3518            if is_boolean_computed_condition(&condition_type) {
3519                None
3520            } else {
3521                Some(ComponentDiagnostic {
3522                    severity: ComponentDiagnosticSeverity::Error,
3523                    effect_id: None,
3524                    statement_id: None,
3525                    context_declaration_candidate_id: None,
3526                    context_id: None,
3527                    provider_id: None,
3528                    consumer_id: None,
3529                    slot_id: None,
3530                    invocation_id: None,
3531                    component_instance_id: None,
3532                    slot_binding_id: None,
3533                    structural_region_id: None,
3534                    component_id: None,
3535                    provider_instance_id: None,
3536                    consumer_instance_id: None,
3537                    secondary_labels: Vec::new(),
3538                    code: crate::TypeDiagnosticCode::InvalidCondition
3539                        .as_str()
3540                        .to_string(),
3541                    message: format!(
3542                        "computed conditional requires boolean, but has {}",
3543                        crate::semantic_type_text(&condition_type)
3544                    ),
3545                    provenance: Some(node.provenance.clone()),
3546                })
3547            }
3548        })
3549        .collect()
3550}
3551
3552fn is_boolean_computed_condition(semantic_type: &crate::SemanticType) -> bool {
3553    match semantic_type {
3554        crate::SemanticType::Boolean | crate::SemanticType::BooleanLiteral(_) => true,
3555        crate::SemanticType::Union(members) => members.iter().all(is_boolean_computed_condition),
3556        _ => false,
3557    }
3558}
3559
3560fn collect_invalid_computed_declaration_diagnostics(
3561    components: &[ComponentNode],
3562    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3563) -> Vec<ComponentDiagnostic> {
3564    components
3565        .iter()
3566        .flat_map(|component| &component.methods)
3567        .filter(|method| {
3568            method
3569                .decorators
3570                .iter()
3571                .any(|decorator| decorator == "computed")
3572                && !method.is_getter
3573        })
3574        .map(|method| ComponentDiagnostic {
3575            severity: ComponentDiagnosticSeverity::Error,
3576            effect_id: None,
3577            statement_id: None,
3578            context_declaration_candidate_id: None,
3579            context_id: None,
3580            provider_id: None,
3581            consumer_id: None,
3582            slot_id: None,
3583            invocation_id: None,
3584            component_instance_id: None,
3585            slot_binding_id: None,
3586            structural_region_id: None,
3587            component_id: None,
3588            provider_instance_id: None,
3589            consumer_instance_id: None,
3590            secondary_labels: Vec::new(),
3591            code: ComputedDiagnosticCode::InvalidDeclaration
3592                .as_str()
3593                .to_string(),
3594            message: format!(
3595                "@computed() declaration `{}` must decorate a getter",
3596                method.name
3597            ),
3598            provenance: Some(
3599                provenance
3600                    .get(&method.id)
3601                    .expect("component methods should have canonical provenance")
3602                    .clone(),
3603            ),
3604        })
3605        .collect()
3606}
3607
3608fn collect_computed_body_and_read_diagnostics(
3609    components: &[ComponentNode],
3610    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3611    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3612    expression_graph: &ExpressionGraph,
3613) -> Vec<ComponentDiagnostic> {
3614    let mut diagnostics = Vec::new();
3615
3616    for computed in computed_values.values() {
3617        let component_id = computed
3618            .owner
3619            .entity_id()
3620            .expect("computed values should have component owners");
3621        let component = components
3622            .iter()
3623            .find(|component| component.id == *component_id)
3624            .expect("computed values should have owning components");
3625        let method = component
3626            .methods
3627            .iter()
3628            .find(|method| method.id == computed.method)
3629            .expect("computed values should have authored methods");
3630
3631        if method.computed_expression.is_none() {
3632            diagnostics.push(ComponentDiagnostic {
3633                severity: ComponentDiagnosticSeverity::Error,
3634                effect_id: None,
3635                statement_id: None,
3636                context_declaration_candidate_id: None,
3637                context_id: None,
3638                provider_id: None,
3639                consumer_id: None,
3640                slot_id: None,
3641                invocation_id: None,
3642                component_instance_id: None,
3643                slot_binding_id: None,
3644                structural_region_id: None,
3645                component_id: None,
3646                provider_instance_id: None,
3647                consumer_instance_id: None,
3648                secondary_labels: Vec::new(),
3649                code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3650                message: format!(
3651                    "computed getter `{}` has an unsupported body",
3652                    computed.name
3653                ),
3654                provenance: Some(computed.provenance.clone()),
3655            });
3656        }
3657
3658        diagnostics.extend(
3659            expression_graph
3660                .nodes_for(&computed.id)
3661                .into_iter()
3662                .filter_map(|node| {
3663                    unresolved_computed_read_diagnostic(
3664                        component,
3665                        computed_values,
3666                        resource_declarations,
3667                        computed,
3668                        node,
3669                    )
3670                }),
3671        );
3672        diagnostics.extend(computed_resource_projection_diagnostics(
3673            component,
3674            computed,
3675            resource_declarations,
3676            expression_graph,
3677        ));
3678    }
3679
3680    diagnostics
3681}
3682
3683fn unresolved_computed_read_diagnostic(
3684    component: &ComponentNode,
3685    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3686    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3687    computed: &ComputedValue,
3688    node: &ExpressionNode,
3689) -> Option<ComponentDiagnostic> {
3690    let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3691        return None;
3692    };
3693    let resolved = component
3694        .state_fields
3695        .iter()
3696        .any(|field| field.name == *name)
3697        || computed_values.contains_key(&component.id.computed(name))
3698        || resource_declarations.contains_key(&ResourceId::for_owner(&component.id, name));
3699    (!resolved).then(|| ComponentDiagnostic {
3700        severity: ComponentDiagnosticSeverity::Error,
3701        effect_id: None,
3702        statement_id: None,
3703        context_declaration_candidate_id: None,
3704        context_id: None,
3705        provider_id: None,
3706        consumer_id: None,
3707        slot_id: None,
3708        invocation_id: None,
3709        component_instance_id: None,
3710        slot_binding_id: None,
3711        structural_region_id: None,
3712        component_id: None,
3713        provider_instance_id: None,
3714        consumer_instance_id: None,
3715        secondary_labels: Vec::new(),
3716        code: ComputedDiagnosticCode::UnresolvedRead.as_str().to_string(),
3717        message: format!(
3718            "computed getter `{}` reads unresolved member `this.{name}`",
3719            computed.name
3720        ),
3721        provenance: Some(node.provenance.clone()),
3722    })
3723}
3724
3725fn computed_resource_projection_diagnostics(
3726    component: &ComponentNode,
3727    computed: &ComputedValue,
3728    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3729    expression_graph: &ExpressionGraph,
3730) -> Vec<ComponentDiagnostic> {
3731    let nodes = expression_graph.nodes_for(&computed.id);
3732    nodes
3733        .iter()
3734        .filter_map(|node| {
3735            let ExpressionNodeKind::ThisMember { name } = &node.kind else {
3736                return None;
3737            };
3738            resource_declarations
3739                .contains_key(&ResourceId::for_owner(&component.id, name))
3740                .then_some((node, name))
3741        })
3742        .filter(|(node, _)| {
3743            !nodes.iter().any(|candidate| {
3744                is_direct_resource_projection(candidate, &nodes)
3745                    && matches!(
3746                        &candidate.kind,
3747                        ExpressionNodeKind::MemberAccess { object, .. } if object == &node.id
3748                    )
3749            })
3750        })
3751        .map(|(node, name)| ComponentDiagnostic {
3752            severity: ComponentDiagnosticSeverity::Error,
3753            effect_id: None,
3754            statement_id: None,
3755            context_declaration_candidate_id: None,
3756            context_id: None,
3757            provider_id: None,
3758            consumer_id: None,
3759            slot_id: None,
3760            invocation_id: None,
3761            component_instance_id: None,
3762            slot_binding_id: None,
3763            structural_region_id: None,
3764            component_id: None,
3765            provider_instance_id: None,
3766            consumer_instance_id: None,
3767            secondary_labels: Vec::new(),
3768            code: ComputedDiagnosticCode::UnsupportedBody.as_str().to_string(),
3769            message: format!(
3770                "computed getter `{}` may only directly project `this.{name}.data`, `this.{name}.error`, or `this.{name}.state`",
3771                computed.name
3772            ),
3773            provenance: Some(node.provenance.clone()),
3774        })
3775        .collect()
3776}
3777
3778fn is_direct_resource_projection(node: &ExpressionNode, nodes: &[&ExpressionNode]) -> bool {
3779    let ExpressionNodeKind::MemberAccess {
3780        property,
3781        optional: false,
3782        ..
3783    } = &node.kind
3784    else {
3785        return false;
3786    };
3787    matches!(property.as_str(), "data" | "error" | "state")
3788        && !nodes.iter().any(|candidate| {
3789            matches!(
3790                &candidate.kind,
3791                ExpressionNodeKind::MemberAccess { object, .. }
3792                    | ExpressionNodeKind::IndexAccess { object, .. }
3793                    if object == &node.id
3794            )
3795        })
3796}
3797
3798fn collect_computed_type_diagnostics(
3799    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3800    semantic_types: &SemanticTypeModel,
3801) -> Vec<ComponentDiagnostic> {
3802    let mut diagnostics = Vec::new();
3803
3804    for computed_type in semantic_types.computed_values.values() {
3805        let computed = computed_values
3806            .get(&computed_type.computed)
3807            .expect("computed type records should have computed entities");
3808        if computed_type.declared_return_compatible == Some(false) {
3809            let declared = computed_type
3810                .declared_return_type
3811                .as_ref()
3812                .expect("incompatible computed return types should be declared");
3813            diagnostics.push(ComponentDiagnostic {
3814                severity: ComponentDiagnosticSeverity::Error,
3815                effect_id: None,
3816                statement_id: None,
3817                context_declaration_candidate_id: None,
3818                context_id: None,
3819                provider_id: None,
3820                consumer_id: None,
3821                slot_id: None,
3822                invocation_id: None,
3823                component_instance_id: None,
3824                slot_binding_id: None,
3825                structural_region_id: None,
3826                component_id: None,
3827                provider_instance_id: None,
3828                consumer_instance_id: None,
3829                secondary_labels: Vec::new(),
3830                code: ComputedDiagnosticCode::TypeMismatch.as_str().to_string(),
3831                message: format!(
3832                    "computed getter `{}` returns `{}` but declares `{}`",
3833                    computed.name,
3834                    crate::semantic_type_text(&computed_type.semantic_type),
3835                    crate::semantic_type_text(declared)
3836                ),
3837                provenance: Some(computed_type.provenance.clone()),
3838            });
3839        }
3840        if computed_type.serialization == crate::SerializationCompatibility::NotSerializable {
3841            diagnostics.push(ComponentDiagnostic {
3842                severity: ComponentDiagnosticSeverity::Error,
3843                effect_id: None,
3844                statement_id: None,
3845                context_declaration_candidate_id: None,
3846                context_id: None,
3847                provider_id: None,
3848                consumer_id: None,
3849                slot_id: None,
3850                invocation_id: None,
3851                component_instance_id: None,
3852                slot_binding_id: None,
3853                structural_region_id: None,
3854                component_id: None,
3855                provider_instance_id: None,
3856                consumer_instance_id: None,
3857                secondary_labels: Vec::new(),
3858                code: ComputedDiagnosticCode::SerializationViolation
3859                    .as_str()
3860                    .to_string(),
3861                message: format!(
3862                    "computed getter `{}` has a non-serializable result",
3863                    computed.name
3864                ),
3865                provenance: Some(computed_type.provenance.clone()),
3866            });
3867        }
3868    }
3869
3870    diagnostics
3871}
3872
3873fn sort_computed_diagnostics(diagnostics: &mut [ComponentDiagnostic]) {
3874    diagnostics.sort_by(|left, right| {
3875        (
3876            left.code.as_str(),
3877            left.provenance.as_ref().map(|provenance| &provenance.path),
3878            left.provenance
3879                .as_ref()
3880                .map(|provenance| provenance.span.start),
3881            left.provenance
3882                .as_ref()
3883                .map(|provenance| provenance.span.end),
3884            left.message.as_str(),
3885        )
3886            .cmp(&(
3887                right.code.as_str(),
3888                right.provenance.as_ref().map(|provenance| &provenance.path),
3889                right
3890                    .provenance
3891                    .as_ref()
3892                    .map(|provenance| provenance.span.start),
3893                right
3894                    .provenance
3895                    .as_ref()
3896                    .map(|provenance| provenance.span.end),
3897                right.message.as_str(),
3898            ))
3899    });
3900}
3901
3902fn build_computed_reactive_products(
3903    components: &[ComponentNode],
3904    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3905    effects: &BTreeMap<SemanticId, Effect>,
3906    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
3907    references: &[SemanticReference],
3908    provenance: &BTreeMap<SemanticId, SourceProvenance>,
3909) -> (
3910    IrReactiveGraph,
3911    IrReactiveTransitiveAnalysis,
3912    IrReactiveCycleAnalysis,
3913    IrComputedEvaluationPlan,
3914) {
3915    let graph = crate::build_reactive_graph(
3916        components,
3917        computed_values,
3918        effects,
3919        resource_declarations,
3920        references,
3921        provenance,
3922    );
3923    let transitive_analysis = crate::analyze_reactive_transitive_graph(&graph);
3924    let cycle_analysis = crate::analyze_reactive_cycles(&graph);
3925    let evaluation_plan = crate::plan_computed_evaluation(&graph);
3926    (graph, transitive_analysis, cycle_analysis, evaluation_plan)
3927}
3928
3929fn extend_template_references(
3930    references: &mut Vec<SemanticReference>,
3931    components: &[ComponentNode],
3932    computed_values: &BTreeMap<SemanticId, ComputedValue>,
3933    template_entities: &[TemplateSemanticEntity],
3934    ownership: &BTreeMap<SemanticId, SemanticOwner>,
3935) {
3936    references.extend(build_template_state_references(
3937        components,
3938        template_entities,
3939        ownership,
3940    ));
3941    references.extend(build_template_computed_references(
3942        components,
3943        computed_values,
3944        template_entities,
3945        ownership,
3946    ));
3947    references.extend(build_template_event_references(
3948        components,
3949        template_entities,
3950        ownership,
3951    ));
3952    references.extend(build_template_local_references(
3953        components,
3954        template_entities,
3955        ownership,
3956    ));
3957}
3958
3959fn build_form_field_binding_references(
3960    bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
3961) -> Vec<SemanticReference> {
3962    bindings
3963        .values()
3964        .flat_map(|binding| {
3965            [
3966                SemanticReference {
3967                    kind: SemanticReferenceKind::FieldBindingField,
3968                    source: binding.id.as_semantic_id().clone(),
3969                    target: binding.field.as_semantic_id().clone(),
3970                    provenance: binding.expression_provenance.clone(),
3971                },
3972                SemanticReference {
3973                    kind: SemanticReferenceKind::FieldBindingForm,
3974                    source: binding.id.as_semantic_id().clone(),
3975                    target: binding.form.as_semantic_id().clone(),
3976                    provenance: binding.expression_provenance.clone(),
3977                },
3978            ]
3979        })
3980        .collect()
3981}
3982
3983fn computed_call_purity_kind(
3984    component: &ComponentNode,
3985    callee: &str,
3986) -> crate::ComputedPurityViolationKind {
3987    if matches!(callee, "resource" | "this.resource") {
3988        return crate::ComputedPurityViolationKind::Resource;
3989    }
3990    if matches!(
3991        callee,
3992        "Math.random"
3993            | "Date.now"
3994            | "performance.now"
3995            | "crypto.randomUUID"
3996            | "crypto.getRandomValues"
3997    ) {
3998        return crate::ComputedPurityViolationKind::NondeterministicOperation;
3999    }
4000    if callee.starts_with("console.")
4001        || matches!(
4002            callee,
4003            "fetch" | "setTimeout" | "setInterval" | "queueMicrotask"
4004        )
4005    {
4006        return crate::ComputedPurityViolationKind::Effect;
4007    }
4008    if let Some(name) = callee.strip_prefix("this.") {
4009        if let Some(method) = component.methods.iter().find(|method| method.name == name) {
4010            if method.is_action()
4011                || method
4012                    .decorators
4013                    .iter()
4014                    .any(|decorator| decorator == "action")
4015            {
4016                return crate::ComputedPurityViolationKind::Action;
4017            }
4018            if method
4019                .decorators
4020                .iter()
4021                .any(|decorator| decorator == "effect")
4022            {
4023                return crate::ComputedPurityViolationKind::Effect;
4024            }
4025            if method
4026                .decorators
4027                .iter()
4028                .any(|decorator| decorator == "resource")
4029            {
4030                return crate::ComputedPurityViolationKind::Resource;
4031            }
4032        }
4033    }
4034    crate::ComputedPurityViolationKind::ArbitraryMethodCall
4035}
4036
4037fn build_computed_references(
4038    components: &[ComponentNode],
4039    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4040    resource_declarations: &BTreeMap<ResourceId, crate::ResourceDeclaration>,
4041    expression_graph: &ExpressionGraph,
4042) -> Vec<SemanticReference> {
4043    let mut references = BTreeMap::new();
4044
4045    for computed in computed_values.values() {
4046        let Some(component_id) = computed.owner.entity_id() else {
4047            continue;
4048        };
4049        let Some(component) = components
4050            .iter()
4051            .find(|component| component.id == *component_id)
4052        else {
4053            continue;
4054        };
4055
4056        let nodes = expression_graph.nodes_for(&computed.id);
4057        let direct_resource_projection_objects = nodes
4058            .iter()
4059            .filter(|node| is_direct_resource_projection(node, &nodes))
4060            .filter_map(|node| match &node.kind {
4061                crate::ExpressionNodeKind::MemberAccess { object, .. } => Some(object.clone()),
4062                _ => None,
4063            })
4064            .collect::<BTreeSet<_>>();
4065
4066        for node in nodes {
4067            let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4068                continue;
4069            };
4070            let reference = if let Some(field) = component
4071                .state_fields
4072                .iter()
4073                .find(|field| field.name == *name)
4074            {
4075                SemanticReference {
4076                    kind: SemanticReferenceKind::ComputedState,
4077                    source: computed.id.clone(),
4078                    target: field.id.clone(),
4079                    provenance: computed.provenance.clone(),
4080                }
4081            } else if let Some(target) = computed_values.get(&component.id.computed(name)) {
4082                SemanticReference {
4083                    kind: SemanticReferenceKind::ComputedComputed,
4084                    source: computed.id.clone(),
4085                    target: target.id.clone(),
4086                    provenance: computed.provenance.clone(),
4087                }
4088            } else if direct_resource_projection_objects.contains(&node.id) {
4089                let target = ResourceId::for_owner(&component.id, name);
4090                let Some(declaration) = resource_declarations.get(&target) else {
4091                    continue;
4092                };
4093                SemanticReference {
4094                    kind: SemanticReferenceKind::ComputedResource,
4095                    source: computed.id.clone(),
4096                    target: declaration.id.as_semantic_id().clone(),
4097                    provenance: node.provenance.clone(),
4098                }
4099            } else {
4100                continue;
4101            };
4102            references.insert(
4103                (reference.source.clone(), reference.target.clone()),
4104                reference,
4105            );
4106        }
4107    }
4108
4109    references.into_values().collect()
4110}
4111
4112fn build_effect_references(
4113    components: &[ComponentNode],
4114    effects: &BTreeMap<SemanticId, Effect>,
4115    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4116    expression_graph: &ExpressionGraph,
4117) -> Vec<SemanticReference> {
4118    let mut references = Vec::new();
4119    for effect in effects.values() {
4120        let Some(component_id) = effect.owner.entity_id() else {
4121            continue;
4122        };
4123        let Some(component) = components
4124            .iter()
4125            .find(|component| component.id == *component_id)
4126        else {
4127            continue;
4128        };
4129        for node in expression_graph.nodes_for(&effect.id) {
4130            let crate::ExpressionNodeKind::ThisMember { name } = &node.kind else {
4131                continue;
4132            };
4133            let reference = if let Some(field) = component
4134                .state_fields
4135                .iter()
4136                .find(|field| field.name == *name)
4137            {
4138                SemanticReference {
4139                    kind: SemanticReferenceKind::EffectState,
4140                    source: effect.id.clone(),
4141                    target: field.id.clone(),
4142                    provenance: node.provenance.clone(),
4143                }
4144            } else if let Some(computed) = computed_values.get(&component.id.computed(name)) {
4145                SemanticReference {
4146                    kind: SemanticReferenceKind::EffectComputed,
4147                    source: effect.id.clone(),
4148                    target: computed.id.clone(),
4149                    provenance: node.provenance.clone(),
4150                }
4151            } else {
4152                continue;
4153            };
4154            references.push(reference);
4155        }
4156    }
4157    references.sort_by(|left, right| {
4158        (
4159            left.source.as_str(),
4160            left.target.as_str(),
4161            left.provenance.span.start,
4162        )
4163            .cmp(&(
4164                right.source.as_str(),
4165                right.target.as_str(),
4166                right.provenance.span.start,
4167            ))
4168    });
4169    references.dedup_by(|left, right| {
4170        left.kind == right.kind && left.source == right.source && left.target == right.target
4171    });
4172    references
4173}
4174
4175fn build_provider_references(
4176    providers: &BTreeMap<ProviderId, ProviderEntity>,
4177) -> Vec<SemanticReference> {
4178    providers
4179        .values()
4180        .map(|provider| SemanticReference {
4181            kind: SemanticReferenceKind::ProvidesContext,
4182            source: provider.id.as_semantic_id().clone(),
4183            target: provider.context.as_semantic_id().clone(),
4184            provenance: provider.provenance.clone(),
4185        })
4186        .collect()
4187}
4188
4189fn build_consumer_references(
4190    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4191) -> Vec<SemanticReference> {
4192    consumers
4193        .values()
4194        .filter_map(|consumer| {
4195            let ContextResolutionState::Resolved(context) = &consumer.context_resolution else {
4196                return None;
4197            };
4198            Some(SemanticReference {
4199                kind: SemanticReferenceKind::ConsumesContext,
4200                source: consumer.id.as_semantic_id().clone(),
4201                target: context.as_semantic_id().clone(),
4202                provenance: consumer.provenance.clone(),
4203            })
4204        })
4205        .collect()
4206}
4207
4208fn build_context_resolution_references(
4209    resolutions: &BTreeMap<ConsumerId, ContextResolution>,
4210) -> Vec<SemanticReference> {
4211    resolutions
4212        .values()
4213        .filter_map(|resolution| {
4214            let ContextResolutionResult::Provider { provider, .. } = &resolution.result else {
4215                return None;
4216            };
4217            Some(SemanticReference {
4218                kind: SemanticReferenceKind::ResolvesToProvider,
4219                source: resolution.consumer.as_semantic_id().clone(),
4220                target: provider.as_semantic_id().clone(),
4221                provenance: resolution.provenance.clone(),
4222            })
4223        })
4224        .collect()
4225}
4226
4227fn build_template_state_references(
4228    components: &[ComponentNode],
4229    template_entities: &[TemplateSemanticEntity],
4230    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4231) -> Vec<SemanticReference> {
4232    template_entities
4233        .iter()
4234        .filter(|entity| {
4235            matches!(
4236                entity.kind,
4237                TemplateSemanticKind::Binding
4238                    | TemplateSemanticKind::AttributeBinding
4239                    | TemplateSemanticKind::Conditional
4240                    | TemplateSemanticKind::List
4241            )
4242        })
4243        .filter_map(|entity| {
4244            let field_name = entity.expression.as_deref().and_then(this_member_name)?;
4245            let component = template_entity_component(components, ownership, entity)?;
4246            let field = component
4247                .state_fields
4248                .iter()
4249                .find(|field| field.name == field_name)?;
4250
4251            Some(SemanticReference {
4252                kind: SemanticReferenceKind::TemplateState,
4253                source: entity.id.clone(),
4254                target: field.id.clone(),
4255                provenance: entity.provenance.clone(),
4256            })
4257        })
4258        .collect()
4259}
4260
4261fn build_template_computed_references(
4262    components: &[ComponentNode],
4263    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4264    template_entities: &[TemplateSemanticEntity],
4265    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4266) -> Vec<SemanticReference> {
4267    template_entities
4268        .iter()
4269        .filter(|entity| {
4270            matches!(
4271                entity.kind,
4272                TemplateSemanticKind::Binding
4273                    | TemplateSemanticKind::AttributeBinding
4274                    | TemplateSemanticKind::Conditional
4275                    | TemplateSemanticKind::List
4276            )
4277        })
4278        .filter_map(|entity| {
4279            let computed_name = entity.expression.as_deref().and_then(this_member_name)?;
4280            let component = template_entity_component(components, ownership, entity)?;
4281            if component
4282                .state_fields
4283                .iter()
4284                .any(|field| field.name == computed_name)
4285            {
4286                return None;
4287            }
4288            let computed = computed_values.get(&component.id.computed(computed_name))?;
4289
4290            Some(SemanticReference {
4291                kind: SemanticReferenceKind::TemplateComputed,
4292                source: entity.id.clone(),
4293                target: computed.id.clone(),
4294                provenance: entity.provenance.clone(),
4295            })
4296        })
4297        .collect()
4298}
4299
4300fn build_template_event_references(
4301    components: &[ComponentNode],
4302    template_entities: &[TemplateSemanticEntity],
4303    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4304) -> Vec<SemanticReference> {
4305    template_entities
4306        .iter()
4307        .filter(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
4308        .filter_map(|entity| {
4309            let method_name = entity.expression.as_deref().and_then(this_member_name)?;
4310            let component = template_entity_component(components, ownership, entity)?;
4311            let method = component
4312                .methods
4313                .iter()
4314                .find(|method| method.name == method_name)?;
4315
4316            Some(SemanticReference {
4317                kind: SemanticReferenceKind::EventMethod,
4318                source: entity.id.clone(),
4319                target: method.id.clone(),
4320                provenance: entity.provenance.clone(),
4321            })
4322        })
4323        .collect()
4324}
4325
4326fn build_template_local_references(
4327    components: &[ComponentNode],
4328    template_entities: &[TemplateSemanticEntity],
4329    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4330) -> Vec<SemanticReference> {
4331    let mut references = template_entities
4332        .iter()
4333        .filter(|entity| {
4334            matches!(
4335                entity.kind,
4336                TemplateSemanticKind::Binding | TemplateSemanticKind::AttributeBinding
4337            ) && entity.scope == TemplateSemanticScope::Render
4338        })
4339        .filter_map(|entity| {
4340            let name = entity.expression.as_deref()?;
4341            let component = template_entity_component(components, ownership, entity)?;
4342            let render = component
4343                .methods
4344                .iter()
4345                .find(|method| method.name == "render")?;
4346            let local = unique_local_variable(render, name)?;
4347
4348            Some(SemanticReference {
4349                kind: SemanticReferenceKind::TemplateLocal,
4350                source: entity.id.clone(),
4351                target: local.id.clone(),
4352                provenance: entity.provenance.clone(),
4353            })
4354        })
4355        .collect::<Vec<_>>();
4356    references.sort_by(|left, right| {
4357        (left.source.as_str(), left.target.as_str())
4358            .cmp(&(right.source.as_str(), right.target.as_str()))
4359    });
4360    references
4361}
4362
4363fn unique_local_variable<'a>(
4364    method: &'a ComponentMethod,
4365    name: &str,
4366) -> Option<&'a MethodLocalVariable> {
4367    let mut locals = method
4368        .local_variables
4369        .iter()
4370        .filter(|local| local.name == name);
4371    let local = locals.next()?;
4372    locals.next().is_none().then_some(local)
4373}
4374
4375fn template_entity_component<'a>(
4376    components: &'a [ComponentNode],
4377    ownership: &BTreeMap<SemanticId, SemanticOwner>,
4378    entity: &TemplateSemanticEntity,
4379) -> Option<&'a ComponentNode> {
4380    let template_id = ownership.get(&entity.id)?.entity_id()?;
4381    let component_id = ownership.get(template_id)?.entity_id()?;
4382
4383    components
4384        .iter()
4385        .find(|component| component.id == *component_id)
4386}
4387
4388fn this_member_name(expression: &str) -> Option<&str> {
4389    expression.strip_prefix("this.").filter(|name| {
4390        !name.is_empty()
4391            && name
4392                .bytes()
4393                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
4394    })
4395}
4396
4397#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
4398fn collect_ownership(
4399    components: &[ComponentNode],
4400    contexts: &BTreeMap<ContextId, ContextEntity>,
4401    forms: &BTreeMap<FormId, FormEntity>,
4402    form_fields: &BTreeMap<FieldId, FormFieldEntity>,
4403    form_field_bindings: &BTreeMap<FieldBindingId, FormFieldBinding>,
4404    providers: &BTreeMap<ProviderId, ProviderEntity>,
4405    consumers: &BTreeMap<ConsumerId, ConsumerEntity>,
4406    slots: &BTreeMap<SlotId, SlotEntity>,
4407    component_invocations: &BTreeMap<ComponentInvocationId, ComponentInvocationEntity>,
4408    slot_content_fragments: &BTreeMap<SlotContentFragmentId, SlotContentFragment>,
4409    slot_outlets: &BTreeMap<SlotOutletId, SlotOutlet>,
4410    component_instance_plan: &ComponentInstancePlan,
4411    computed_values: &BTreeMap<SemanticId, ComputedValue>,
4412    effects: &BTreeMap<SemanticId, Effect>,
4413    templates: &[TemplateNode],
4414    template_entities: &[TemplateSemanticEntity],
4415) -> BTreeMap<SemanticId, SemanticOwner> {
4416    let mut ownership = BTreeMap::new();
4417
4418    for component in components {
4419        ownership.insert(component.id.clone(), SemanticOwner::Application);
4420
4421        for field in &component.state_fields {
4422            ownership.insert(
4423                field.id.clone(),
4424                SemanticOwner::entity(component.id.clone()),
4425            );
4426        }
4427        for method in &component.methods {
4428            ownership.insert(
4429                method.id.clone(),
4430                SemanticOwner::entity(component.id.clone()),
4431            );
4432            for parameter in &method.parameters {
4433                ownership.insert(
4434                    parameter.id.clone(),
4435                    SemanticOwner::entity(method.id.clone()),
4436                );
4437            }
4438            for local in &method.local_variables {
4439                ownership.insert(local.id.clone(), SemanticOwner::entity(method.id.clone()));
4440            }
4441        }
4442        for computed in computed_values
4443            .values()
4444            .filter(|computed| computed.owner.entity_id() == Some(&component.id))
4445        {
4446            ownership.insert(computed.id.clone(), computed.owner.clone());
4447        }
4448        for context in contexts
4449            .values()
4450            .filter(|context| context.owner.entity_id() == Some(&component.id))
4451        {
4452            ownership.insert(context.id.as_semantic_id().clone(), context.owner.clone());
4453        }
4454        for form in forms
4455            .values()
4456            .filter(|form| form.owner.entity_id() == Some(&component.id))
4457        {
4458            ownership.insert(form.id.as_semantic_id().clone(), form.owner.clone());
4459        }
4460        for field in form_fields
4461            .values()
4462            .filter(|field| field.owner_component == component.id)
4463        {
4464            ownership.insert(
4465                field.id.as_semantic_id().clone(),
4466                SemanticOwner::entity(field.owner_form.as_semantic_id().clone()),
4467            );
4468        }
4469        for provider in providers
4470            .values()
4471            .filter(|provider| provider.owner.entity_id() == Some(&component.id))
4472        {
4473            ownership.insert(provider.id.as_semantic_id().clone(), provider.owner.clone());
4474        }
4475        for consumer in consumers
4476            .values()
4477            .filter(|consumer| consumer.owner.entity_id() == Some(&component.id))
4478        {
4479            ownership.insert(consumer.id.as_semantic_id().clone(), consumer.owner.clone());
4480        }
4481        for slot in slots.values().filter(|slot| slot.owner == component.id) {
4482            ownership.insert(slot.id.as_semantic_id().clone(), slot.semantic_owner());
4483        }
4484        for invocation in component_invocations
4485            .values()
4486            .filter(|invocation| invocation.owner_component == component.id)
4487        {
4488            ownership.insert(
4489                invocation.id.as_semantic_id().clone(),
4490                SemanticOwner::entity(component.id.clone()),
4491            );
4492        }
4493        for fragment in slot_content_fragments
4494            .values()
4495            .filter(|fragment| fragment.owner_component == component.id)
4496        {
4497            ownership.insert(
4498                fragment.id.as_semantic_id().clone(),
4499                SemanticOwner::entity(component.id.clone()),
4500            );
4501        }
4502        for outlet in slot_outlets
4503            .values()
4504            .filter(|outlet| outlet.owner_component == component.id)
4505        {
4506            ownership.insert(
4507                outlet.id.as_semantic_id().clone(),
4508                SemanticOwner::entity(component.id.clone()),
4509            );
4510        }
4511        for effect in effects
4512            .values()
4513            .filter(|effect| effect.owner.entity_id() == Some(&component.id))
4514        {
4515            ownership.insert(effect.id.clone(), effect.owner.clone());
4516        }
4517        for action in &component.actions {
4518            ownership.insert(
4519                action.id.clone(),
4520                SemanticOwner::entity(component.id.method(&action.method)),
4521            );
4522        }
4523        if let Some(render) = &component.render {
4524            for handler in render_event_handlers(render) {
4525                ownership.insert(
4526                    handler.id.clone(),
4527                    SemanticOwner::entity(component.id.template()),
4528                );
4529            }
4530        }
4531    }
4532
4533    for instance in component_instance_plan.instances.values() {
4534        ownership.insert(
4535            instance.id.as_semantic_id().clone(),
4536            instance
4537                .parent_instance
4538                .as_ref()
4539                .map_or(SemanticOwner::Application, |parent| {
4540                    SemanticOwner::entity(parent.as_semantic_id().clone())
4541                }),
4542        );
4543    }
4544    for blocked in component_instance_plan.blocked.values() {
4545        ownership.insert(
4546            blocked.id.as_semantic_id().clone(),
4547            SemanticOwner::entity(blocked.parent_instance.as_semantic_id().clone()),
4548        );
4549    }
4550
4551    for template in templates {
4552        let component = components
4553            .iter()
4554            .find(|component| component.id.template() == template.id)
4555            .expect("template graph should only contain component templates");
4556        ownership.insert(
4557            template.id.clone(),
4558            SemanticOwner::entity(component.id.clone()),
4559        );
4560    }
4561
4562    for binding in form_field_bindings.values() {
4563        ownership.insert(
4564            binding.id.as_semantic_id().clone(),
4565            SemanticOwner::entity(binding.control_entity.clone()),
4566        );
4567    }
4568
4569    for entity in template_entities {
4570        ownership.insert(entity.id.clone(), entity.owner.clone());
4571    }
4572
4573    ownership
4574}
4575
4576#[cfg(test)]
4577mod tests {
4578    use super::{
4579        build_application_semantic_model, build_application_semantic_model_for_unit,
4580        build_file_route_application_semantic_model_for_unit_with_packages, collect_ownership,
4581    };
4582    use crate::{
4583        build_component_graph_for_module, build_template_graph, build_template_semantic_entities,
4584        CompilationUnit, ComponentInstancePlan, SemanticEntity, SemanticEntityKind, SemanticOwner,
4585        SemanticReferenceKind, TemplateSemanticKind,
4586    };
4587
4588    #[test]
4589    fn lowers_supported_primitive_state_annotations_into_canonical_types() {
4590        let parsed = presolve_parser::parse_file(
4591            "src/TypedState.tsx",
4592            r#"
4593@component("x-typed-state")
4594class TypedState extends Component {
4595  count: number = state(0);
4596}
4597"#,
4598        );
4599
4600        let asm = build_application_semantic_model(&parsed);
4601
4602        let field = &asm.components[0].state_fields[0];
4603        let assignment = asm
4604            .semantic_types
4605            .assignments
4606            .get(&field.id)
4607            .expect("canonical declared type");
4608
4609        assert_eq!(assignment.semantic_type, crate::SemanticType::Number);
4610        assert_eq!(assignment.status, crate::SemanticTypeStatus::Declared);
4611        assert_eq!(
4612            assignment.provenance,
4613            field.declared_type.as_ref().unwrap().provenance
4614        );
4615    }
4616
4617    #[test]
4618    fn queries_canonical_type_information_from_the_asm() {
4619        let parsed = presolve_parser::parse_file(
4620            "src/TypeQueries.tsx",
4621            r#"
4622@component("x-type-queries")
4623class TypeQueries extends Component {
4624  count: number = state(0);
4625  label = state("Presolve");
4626}
4627"#,
4628        );
4629        let asm = build_application_semantic_model(&parsed);
4630        let count = &asm.components[0].state_fields[0];
4631        let label = &asm.components[0].state_fields[1];
4632
4633        assert_eq!(
4634            asm.semantic_type_of(&count.id),
4635            Some(&crate::SemanticType::Number)
4636        );
4637        assert_eq!(asm.type_declarations(&crate::SemanticType::Number).len(), 1);
4638        assert_eq!(asm.type_usages(&crate::SemanticType::String).len(), 1);
4639        assert_eq!(
4640            asm.serialization_compatibility_of(&count.id),
4641            Some(crate::SerializationCompatibility::Serializable)
4642        );
4643        assert_eq!(asm.is_type_assignable(&label.id, &count.id), Some(false));
4644    }
4645
4646    #[test]
4647    fn lowers_typed_method_parameters_into_canonical_entities() {
4648        let parsed = presolve_parser::parse_file(
4649            "src/Parameters.tsx",
4650            r#"
4651@component("x-parameters")
4652class Parameters extends Component {
4653  save(title: string, retries?: number) {}
4654}
4655"#,
4656        );
4657
4658        let asm = build_application_semantic_model(&parsed);
4659        let method = &asm.components[0].methods[0];
4660        let parameters = &method.parameters;
4661
4662        assert_eq!(parameters.len(), 2);
4663        assert_eq!(
4664            asm.semantic_types.assignments[&parameters[0].id].semantic_type,
4665            crate::SemanticType::String
4666        );
4667        assert_eq!(
4668            asm.semantic_types.assignments[&parameters[1].id].semantic_type,
4669            crate::SemanticType::Number
4670        );
4671        assert!(parameters.iter().all(|parameter| {
4672            asm.owner(&parameter.id) == Some(&SemanticOwner::entity(method.id.clone()))
4673                && asm
4674                    .entity(&parameter.id)
4675                    .is_some_and(|entity| entity.kind() == SemanticEntityKind::Parameter)
4676        }));
4677    }
4678
4679    #[test]
4680    fn lowers_declared_and_inferred_method_return_types() {
4681        let parsed = presolve_parser::parse_file(
4682            "src/Returns.tsx",
4683            r#"
4684@component("x-returns")
4685class Returns extends Component {
4686  declared(): string { return "Presolve"; }
4687  inferred() { return 1; }
4688}
4689"#,
4690        );
4691
4692        let asm = build_application_semantic_model(&parsed);
4693        let methods = &asm.components[0].methods;
4694        let declared = &methods[0];
4695        let inferred = &methods[1];
4696
4697        assert_eq!(
4698            asm.semantic_types.assignments[&declared.id].semantic_type,
4699            crate::SemanticType::String
4700        );
4701        assert_eq!(
4702            asm.semantic_types.assignments[&declared.id].status,
4703            crate::SemanticTypeStatus::Declared
4704        );
4705        assert_eq!(
4706            asm.semantic_types.assignments[&inferred.id].semantic_type,
4707            crate::SemanticType::Number
4708        );
4709        assert_eq!(
4710            asm.semantic_types.assignments[&inferred.id].status,
4711            crate::SemanticTypeStatus::Inferred
4712        );
4713    }
4714
4715    #[test]
4716    fn establishes_typed_computed_getter_contracts() {
4717        let parsed = presolve_parser::parse_file(
4718            "src/Computed.tsx",
4719            r#"
4720@component("x-computed")
4721class Computed extends Component {
4722  @computed()
4723  get remainingCount(): number { return 1; }
4724
4725  render() { return <p />; }
4726}
4727"#,
4728        );
4729
4730        let asm = build_application_semantic_model(&parsed);
4731        let method = &asm.components[0].methods[0];
4732        let computed_id = asm.components[0].id.computed("remainingCount");
4733        let computed = asm
4734            .semantic_types
4735            .computed_values
4736            .get(&computed_id)
4737            .expect("computed type contract");
4738        let computed_entity = asm
4739            .computed_value(&computed_id)
4740            .expect("first-class computed entity");
4741
4742        assert!(method.is_getter);
4743        assert!(method.is_computed());
4744        assert_eq!(
4745            computed.semantic_type,
4746            crate::SemanticType::NumberLiteral("1".to_string())
4747        );
4748        assert_eq!(
4749            computed.declared_return_type,
4750            Some(crate::SemanticType::Number)
4751        );
4752        assert_eq!(computed.declared_return_compatible, Some(true));
4753        assert_eq!(computed_entity.method, method.id);
4754        assert_eq!(
4755            computed_entity.owner,
4756            crate::SemanticOwner::entity(asm.components[0].id.clone())
4757        );
4758        assert_eq!(
4759            computed_entity.cache_policy,
4760            crate::ComputedCachePolicy::Memoized
4761        );
4762        assert_eq!(computed_entity.purity, crate::ComputedPurity::Pure);
4763        assert!(computed_entity.purity_violations.is_empty());
4764        assert_eq!(
4765            computed_entity.execution_boundary,
4766            crate::ExecutionBoundary::Client
4767        );
4768        assert_eq!(
4769            asm.entity(&computed_id),
4770            Some(SemanticEntity::Computed(computed_entity))
4771        );
4772        assert_eq!(
4773            asm.owner(&computed_id),
4774            Some(&crate::SemanticOwner::entity(asm.components[0].id.clone()))
4775        );
4776        assert_eq!(asm.provenance(&computed_id), asm.provenance(&method.id));
4777        assert_eq!(
4778            asm.entities_of_kind(SemanticEntityKind::Computed),
4779            vec![&computed_id]
4780        );
4781    }
4782
4783    #[test]
4784    fn resolves_computed_reads_to_canonical_state_and_computed_references() {
4785        let parsed = presolve_parser::parse_file(
4786            "src/ComputedReads.tsx",
4787            r#"
4788@component("x-computed-reads")
4789class ComputedReads extends Component {
4790  count = state(1);
4791  profile = state({ hidden: false });
4792
4793  @computed()
4794  get doubled() { return this.count * 2; }
4795
4796  @computed()
4797  get visible() { return this.doubled + this.count + this.count; }
4798
4799  @computed()
4800  get profileHidden() { return this.profile.hidden; }
4801
4802  @computed()
4803  get unresolved() { return this.missing; }
4804}
4805"#,
4806        );
4807        let asm = build_application_semantic_model(&parsed);
4808        let component = &asm.components[0];
4809        let count = component.id.state_field("count");
4810        let profile = component.id.state_field("profile");
4811        let doubled = component.id.computed("doubled");
4812        let visible = component.id.computed("visible");
4813        let profile_hidden = component.id.computed("profileHidden");
4814        let unresolved = component.id.computed("unresolved");
4815
4816        let state_references = asm.references_of_kind(SemanticReferenceKind::ComputedState);
4817        assert_eq!(state_references.len(), 3);
4818        assert!(state_references
4819            .iter()
4820            .any(|reference| reference.source == doubled && reference.target == count));
4821        assert!(state_references
4822            .iter()
4823            .any(|reference| reference.source == visible && reference.target == count));
4824        assert!(state_references.iter().any(|reference| {
4825            reference.source == profile_hidden && reference.target == profile
4826        }));
4827
4828        let computed_references = asm.references_of_kind(SemanticReferenceKind::ComputedComputed);
4829        assert_eq!(computed_references.len(), 1);
4830        assert_eq!(computed_references[0].source, visible);
4831        assert_eq!(computed_references[0].target, doubled);
4832        assert!(!asm
4833            .references
4834            .iter()
4835            .any(|reference| reference.source == unresolved));
4836        assert!(asm
4837            .references
4838            .iter()
4839            .all(|reference| { asm.provenance(&reference.source) == Some(&reference.provenance) }));
4840    }
4841
4842    #[test]
4843    fn populates_reactive_graph_from_direct_computed_reads() {
4844        let parsed = presolve_parser::parse_file(
4845            "src/ComputedReactiveGraph.tsx",
4846            r#"
4847@component("x-computed-reactive-graph")
4848class ComputedReactiveGraph extends Component {
4849  count = state(1);
4850
4851  @computed()
4852  get doubled() { return this.count * 2; }
4853
4854  @computed()
4855  get label() { return this.doubled + 1; }
4856}
4857"#,
4858        );
4859        let asm = build_application_semantic_model(&parsed);
4860        let component = &asm.components[0];
4861        let count = component.id.state_field("count");
4862        let doubled = component.id.computed("doubled");
4863        let label = component.id.computed("label");
4864        let graph = &asm.reactive_graph;
4865
4866        assert_eq!(graph.nodes.len(), 3);
4867        assert_eq!(
4868            graph.nodes[count.as_str()].kind,
4869            crate::IrReactiveNodeKind::State
4870        );
4871        assert_eq!(
4872            graph.nodes[doubled.as_str()].kind,
4873            crate::IrReactiveNodeKind::Computed
4874        );
4875        assert_eq!(
4876            graph.nodes[label.as_str()].kind,
4877            crate::IrReactiveNodeKind::Computed
4878        );
4879
4880        let doubled_dependencies = graph.computed_dependencies(doubled.as_str());
4881        assert_eq!(doubled_dependencies.len(), 1);
4882        assert_eq!(doubled_dependencies[0].target, count.as_str());
4883        let label_dependencies = graph.computed_dependencies(label.as_str());
4884        assert_eq!(label_dependencies.len(), 1);
4885        assert_eq!(label_dependencies[0].target, doubled.as_str());
4886
4887        let count_invalidations = graph.invalidations_from(count.as_str());
4888        assert_eq!(count_invalidations.len(), 1);
4889        assert_eq!(count_invalidations[0].target, doubled.as_str());
4890        let doubled_invalidations = graph.invalidations_from(doubled.as_str());
4891        assert_eq!(doubled_invalidations.len(), 1);
4892        assert_eq!(doubled_invalidations[0].target, label.as_str());
4893        assert!(graph
4894            .invalidations_from(count.as_str())
4895            .iter()
4896            .all(|edge| { edge.target != label.as_str() }));
4897    }
4898
4899    #[test]
4900    fn computes_deterministic_transitive_reactive_dependencies_and_dependents() {
4901        let parsed = presolve_parser::parse_file(
4902            "src/ComputedTransitiveReactiveGraph.tsx",
4903            r#"
4904@component("x-computed-transitive-reactive-graph")
4905class ComputedTransitiveReactiveGraph extends Component {
4906  count = state(1);
4907
4908  @computed()
4909  get doubled() { return this.count * 2; }
4910
4911  @computed()
4912  get label() { return this.doubled + 1; }
4913}
4914"#,
4915        );
4916        let asm = build_application_semantic_model(&parsed);
4917        let component = &asm.components[0];
4918        let count = component.id.state_field("count");
4919        let doubled = component.id.computed("doubled");
4920        let label = component.id.computed("label");
4921        let analysis = &asm.reactive_transitive_analysis;
4922
4923        assert_eq!(analysis.dependencies.len(), 3);
4924        assert_eq!(analysis.dependents.len(), 3);
4925        assert_eq!(
4926            analysis.dependencies_of(count.as_str()),
4927            Vec::<String>::new()
4928        );
4929        assert_eq!(
4930            analysis.dependencies_of(doubled.as_str()),
4931            vec![count.as_str().to_string()]
4932        );
4933        assert_eq!(
4934            analysis.dependencies_of(label.as_str()),
4935            vec![doubled.as_str().to_string(), count.as_str().to_string()]
4936        );
4937        assert_eq!(
4938            analysis.dependents_of(count.as_str()),
4939            vec![doubled.as_str().to_string(), label.as_str().to_string()]
4940        );
4941        assert_eq!(
4942            analysis.dependents_of(doubled.as_str()),
4943            vec![label.as_str().to_string()]
4944        );
4945        assert_eq!(analysis.dependents_of(label.as_str()), Vec::<String>::new());
4946    }
4947
4948    #[test]
4949    fn detects_computed_dependency_cycles_with_stable_diagnostics() {
4950        let parsed = presolve_parser::parse_file(
4951            "src/ComputedDependencyCycles.tsx",
4952            r#"
4953@component("x-computed-dependency-cycles")
4954class ComputedDependencyCycles extends Component {
4955  @computed()
4956  get alpha() { return this.beta; }
4957
4958  @computed()
4959  get beta() { return this.alpha; }
4960
4961  @computed()
4962  get selfLoop() { return this.selfLoop; }
4963}
4964"#,
4965        );
4966        let asm = build_application_semantic_model(&parsed);
4967        let component = &asm.components[0];
4968        let alpha = component.id.computed("alpha");
4969        let beta = component.id.computed("beta");
4970        let self_loop = component.id.computed("selfLoop");
4971
4972        assert_eq!(
4973            asm.reactive_cycle_analysis.cycles,
4974            vec![
4975                crate::IrReactiveCycle {
4976                    nodes: vec![alpha.as_str().to_string(), beta.as_str().to_string()],
4977                },
4978                crate::IrReactiveCycle {
4979                    nodes: vec![self_loop.as_str().to_string()],
4980                },
4981            ]
4982        );
4983        let diagnostics = asm
4984            .diagnostics
4985            .iter()
4986            .filter(|diagnostic| diagnostic.code == "PSC1035")
4987            .collect::<Vec<_>>();
4988        assert_eq!(diagnostics.len(), 2);
4989        assert_eq!(
4990            diagnostics[0].message,
4991            format!("computed dependency cycle detected among: {alpha}, {beta}")
4992        );
4993        assert_eq!(
4994            diagnostics[0].provenance,
4995            Some(
4996                asm.computed_value(&alpha)
4997                    .expect("alpha computed value")
4998                    .provenance
4999                    .clone()
5000            )
5001        );
5002        assert_eq!(
5003            diagnostics[1].message,
5004            format!("computed dependency cycle detected among: {self_loop}")
5005        );
5006        assert_eq!(
5007            diagnostics[1].provenance,
5008            Some(
5009                asm.computed_value(&self_loop)
5010                    .expect("self-loop computed value")
5011                    .provenance
5012                    .clone()
5013            )
5014        );
5015    }
5016
5017    #[test]
5018    fn plans_deterministic_computed_evaluation_order_and_batches() {
5019        let parsed = presolve_parser::parse_file(
5020            "src/ComputedEvaluationPlan.tsx",
5021            r#"
5022@component("x-computed-evaluation-plan")
5023class ComputedEvaluationPlan extends Component {
5024  count = state(1);
5025  other = state(2);
5026
5027  @computed()
5028  get alpha() { return this.count; }
5029
5030  @computed()
5031  get beta() { return this.other; }
5032
5033  @computed()
5034  get gamma() { return this.alpha; }
5035}
5036"#,
5037        );
5038        let asm = build_application_semantic_model(&parsed);
5039        let component = &asm.components[0];
5040        let alpha = component.id.computed("alpha");
5041        let beta = component.id.computed("beta");
5042        let gamma = component.id.computed("gamma");
5043
5044        assert_eq!(
5045            asm.computed_evaluation_plan.evaluation_order,
5046            vec![
5047                alpha.as_str().to_string(),
5048                beta.as_str().to_string(),
5049                gamma.as_str().to_string(),
5050            ]
5051        );
5052        assert_eq!(
5053            asm.computed_evaluation_plan.update_batches,
5054            vec![
5055                vec![alpha.as_str().to_string(), beta.as_str().to_string()],
5056                vec![gamma.as_str().to_string()],
5057            ]
5058        );
5059        assert!(asm.computed_evaluation_plan.unplanned.is_empty());
5060    }
5061
5062    #[test]
5063    fn leaves_cyclic_computed_values_unplanned() {
5064        let parsed = presolve_parser::parse_file(
5065            "src/CyclicComputedEvaluationPlan.tsx",
5066            r#"
5067@component("x-cyclic-computed-evaluation-plan")
5068class CyclicComputedEvaluationPlan extends Component {
5069  @computed()
5070  get alpha() { return this.beta; }
5071
5072  @computed()
5073  get beta() { return this.alpha; }
5074}
5075"#,
5076        );
5077        let asm = build_application_semantic_model(&parsed);
5078        let component = &asm.components[0];
5079        let alpha = component.id.computed("alpha");
5080        let beta = component.id.computed("beta");
5081
5082        assert!(asm.computed_evaluation_plan.evaluation_order.is_empty());
5083        assert!(asm.computed_evaluation_plan.update_batches.is_empty());
5084        assert_eq!(
5085            asm.computed_evaluation_plan.unplanned,
5086            vec![alpha.as_str().to_string(), beta.as_str().to_string()]
5087        );
5088    }
5089
5090    #[test]
5091    fn assigns_inferred_computed_types_and_validates_declared_returns() {
5092        let parsed = presolve_parser::parse_file(
5093            "src/ComputedTypes.tsx",
5094            r#"
5095@component("x-computed-types")
5096class ComputedTypes extends Component {
5097  count: number = state(1);
5098  profile = state({ label: "Presolve" });
5099
5100  @computed()
5101  get doubled(): number { return this.count * 2; }
5102
5103  @computed()
5104  get chained() { return this.doubled + 1; }
5105
5106  @computed()
5107  get label(): string { return this.profile.label; }
5108
5109  @computed()
5110  get invalid(): number { return "wrong"; }
5111
5112  @computed()
5113  get unresolved() { return this.missing; }
5114}
5115"#,
5116        );
5117        let asm = build_application_semantic_model(&parsed);
5118        let component = &asm.components[0];
5119        let doubled = component.id.computed("doubled");
5120        let chained = component.id.computed("chained");
5121        let label = component.id.computed("label");
5122        let invalid = component.id.computed("invalid");
5123        let unresolved = component.id.computed("unresolved");
5124
5125        let types = &asm.semantic_types.computed_values;
5126        assert_eq!(types[&doubled].semantic_type, crate::SemanticType::Number);
5127        assert_eq!(types[&chained].semantic_type, crate::SemanticType::Number);
5128        assert_eq!(types[&label].semantic_type, crate::SemanticType::String);
5129        assert_eq!(
5130            types[&invalid].semantic_type,
5131            crate::SemanticType::StringLiteral("wrong".to_string())
5132        );
5133        assert_eq!(types[&invalid].declared_return_compatible, Some(false));
5134        assert_eq!(types[&doubled].declared_return_compatible, Some(true));
5135        assert_eq!(
5136            types[&unresolved].semantic_type,
5137            crate::SemanticType::Unknown
5138        );
5139        assert_eq!(
5140            types[&unresolved].serialization,
5141            crate::SerializationCompatibility::NotSerializable
5142        );
5143        assert_eq!(
5144            types[&doubled].boundary_compatibility,
5145            crate::BoundaryCompatibility::Compatible
5146        );
5147        assert_eq!(
5148            types[&doubled].execution_boundary,
5149            crate::ExecutionBoundary::Client
5150        );
5151        assert_eq!(
5152            asm.semantic_type_of(&doubled),
5153            Some(&crate::SemanticType::Number)
5154        );
5155        assert_eq!(
5156            asm.serialization_compatibility_of(&chained),
5157            Some(crate::SerializationCompatibility::Serializable)
5158        );
5159    }
5160
5161    #[test]
5162    fn reports_stable_diagnostics_for_computed_semantics() {
5163        let parsed = presolve_parser::parse_file(
5164            "src/ComputedDiagnostics.tsx",
5165            r#"
5166@component("x-computed-diagnostics")
5167class ComputedDiagnostics extends Component {
5168  count = state(1);
5169
5170  @computed()
5171  invalidDeclaration() { return 1; }
5172
5173  @computed()
5174  get unsupportedBody() {
5175    const value = this.count;
5176    return value;
5177  }
5178
5179  @computed()
5180  get unresolvedRead() { return this.missing; }
5181
5182  @computed()
5183  get mismatch(): number { return "wrong"; }
5184
5185  @computed()
5186  get impure() { return helper(); }
5187
5188  @computed()
5189  get cycleA() { return this.cycleB; }
5190
5191  @computed()
5192  get cycleB() { return this.cycleA; }
5193
5194  render() { return <div />; }
5195}
5196"#,
5197        );
5198
5199        let asm = build_application_semantic_model(&parsed);
5200        let repeated = build_application_semantic_model(&parsed);
5201        assert_eq!(asm.diagnostics, repeated.diagnostics);
5202
5203        let component_graph = crate::build_component_graph(&parsed);
5204        let asm_from_component_graph =
5205            super::build_application_semantic_model_from_component_graph(&component_graph);
5206
5207        let codes = asm
5208            .diagnostics
5209            .iter()
5210            .map(|diagnostic| diagnostic.code.as_str())
5211            .collect::<std::collections::BTreeSet<_>>();
5212        assert_eq!(
5213            codes,
5214            std::collections::BTreeSet::from([
5215                "PSC1034", "PSC1035", "PSC1036", "PSC1037", "PSC1038", "PSC1039", "PSC1040",
5216            ])
5217        );
5218        assert_eq!(
5219            asm_from_component_graph
5220                .diagnostics
5221                .iter()
5222                .map(|diagnostic| diagnostic.code.as_str())
5223                .collect::<std::collections::BTreeSet<_>>(),
5224            codes
5225        );
5226        assert!(asm
5227            .diagnostics
5228            .iter()
5229            .all(|diagnostic| diagnostic.provenance.is_some()));
5230        assert!(asm.diagnostics.iter().any(|diagnostic| {
5231            diagnostic.code == crate::ComputedDiagnosticCode::InvalidDeclaration.as_str()
5232                && diagnostic.message
5233                    == "@computed() declaration `invalidDeclaration` must decorate a getter"
5234        }));
5235        assert!(asm.diagnostics.iter().any(|diagnostic| {
5236            diagnostic.code == crate::ComputedDiagnosticCode::UnsupportedBody.as_str()
5237                && diagnostic.message == "computed getter `unsupportedBody` has an unsupported body"
5238        }));
5239        assert!(asm.diagnostics.iter().any(|diagnostic| {
5240            diagnostic.code == crate::ComputedDiagnosticCode::UnresolvedRead.as_str()
5241                && diagnostic.message
5242                    == "computed getter `unresolvedRead` reads unresolved member `this.missing`"
5243        }));
5244        assert!(asm.diagnostics.iter().any(|diagnostic| {
5245            diagnostic.code == crate::ComputedDiagnosticCode::TypeMismatch.as_str()
5246                && diagnostic.message
5247                    == "computed getter `mismatch` returns `\"wrong\"` but declares `number`"
5248        }));
5249        assert!(asm.diagnostics.iter().any(|diagnostic| {
5250            diagnostic.code == crate::ComputedDiagnosticCode::SerializationViolation.as_str()
5251                && diagnostic.message
5252                    == "computed getter `unresolvedRead` has a non-serializable result"
5253        }));
5254    }
5255
5256    #[test]
5257    fn phase_e_audit_preserves_canonical_computed_products() {
5258        let path = "fixtures/0047-computed-diamond/input/ComputedDiamond.tsx";
5259        let parsed = presolve_parser::parse_file(
5260            path,
5261            include_str!("../../../fixtures/0047-computed-diamond/input/ComputedDiamond.tsx"),
5262        );
5263        let asm = build_application_semantic_model(&parsed);
5264        let component_graph = crate::build_component_graph(&parsed);
5265        let asm_from_component_graph =
5266            super::build_application_semantic_model_from_component_graph(&component_graph);
5267        let component = &asm.components[0];
5268        let count = component.id.state_field("count");
5269        let doubled = component.id.computed("doubled");
5270        let tripled = component.id.computed("tripled");
5271        let total = component.id.computed("total");
5272        let expected_owner = crate::SemanticOwner::entity(component.id.clone());
5273
5274        assert!(crate::validate_application_semantic_model(&asm).is_empty());
5275        assert!(crate::validate_application_semantic_model(&asm_from_component_graph).is_empty());
5276        for computed in [&doubled, &tripled, &total] {
5277            assert!(matches!(
5278                asm.entity(computed),
5279                Some(crate::SemanticEntity::Computed(_))
5280            ));
5281            assert_eq!(asm.owner(computed), Some(&expected_owner));
5282            assert_eq!(
5283                asm.semantic_types
5284                    .computed_values
5285                    .get(computed)
5286                    .map(|computed_type| &computed_type.computed),
5287                Some(computed)
5288            );
5289        }
5290
5291        let reactive_references = asm
5292            .references
5293            .iter()
5294            .filter(|reference| {
5295                matches!(
5296                    reference.kind,
5297                    crate::SemanticReferenceKind::ComputedState
5298                        | crate::SemanticReferenceKind::ComputedComputed
5299                )
5300            })
5301            .collect::<Vec<_>>();
5302        assert_eq!(reactive_references.len(), 4);
5303        for reference in reactive_references {
5304            assert!(asm.reactive_graph.edges.iter().any(|edge| {
5305                edge.kind == crate::IrReactiveEdgeKind::Reads
5306                    && edge.source == reference.source.as_str()
5307                    && edge.target == reference.target.as_str()
5308                    && edge.provenance == reference.provenance
5309            }));
5310            assert!(asm.reactive_graph.edges.iter().any(|edge| {
5311                edge.kind == crate::IrReactiveEdgeKind::Invalidates
5312                    && edge.source == reference.target.as_str()
5313                    && edge.target == reference.source.as_str()
5314                    && edge.provenance == reference.provenance
5315            }));
5316        }
5317        assert!(asm.reactive_graph.nodes.contains_key(count.as_str()));
5318        assert_eq!(
5319            asm.computed_evaluation_plan,
5320            crate::plan_computed_evaluation(&asm.reactive_graph)
5321        );
5322        assert_eq!(
5323            asm.computed_evaluation_plan.evaluation_order,
5324            vec![
5325                doubled.as_str().to_string(),
5326                tripled.as_str().to_string(),
5327                total.as_str().to_string(),
5328            ]
5329        );
5330
5331        let ir = crate::lower_components_to_ir(&asm);
5332        assert!(crate::validate_intermediate_representation(&ir).is_empty());
5333        let evaluations = ir
5334            .modules
5335            .iter()
5336            .flat_map(|module| &module.computed_evaluations)
5337            .map(|evaluation| evaluation.computed.clone())
5338            .collect::<Vec<_>>();
5339        assert_eq!(evaluations, vec![doubled, tripled, total]);
5340    }
5341
5342    #[test]
5343    fn classifies_computed_purity_and_reports_unsupported_behavior() {
5344        let mut parsed = presolve_parser::parse_file(
5345            "src/ComputedPurity.tsx",
5346            r#"
5347@component("x-computed-purity")
5348class ComputedPurity extends Component {
5349  count = state(1);
5350
5351  @action()
5352  save() {}
5353
5354  @effect()
5355  sync() {}
5356
5357  @computed()
5358  get pure() { return this.count; }
5359
5360  @computed()
5361  get mutates() { this.count++; return 1; }
5362
5363  @computed()
5364  get actionCall() { return this.save(); }
5365
5366  @computed()
5367  get effectCall() { console.log("effect"); return 1; }
5368
5369  @computed()
5370  get resourceCall() { return resource(); }
5371
5372  @computed()
5373  get arbitraryCall() { return helper(); }
5374
5375  @computed()
5376  get random() { return Math.random(); }
5377
5378  @computed()
5379  get asyncValue() { return 1; }
5380}
5381"#,
5382        );
5383        parsed.classes[0]
5384            .methods
5385            .iter_mut()
5386            .find(|method| method.name == "asyncValue")
5387            .expect("async test getter")
5388            .is_async = true;
5389
5390        let asm = build_application_semantic_model(&parsed);
5391        let component = &asm.components[0];
5392        let pure = component.id.computed("pure");
5393        let mutates = component.id.computed("mutates");
5394        let action_call = component.id.computed("actionCall");
5395        let effect_call = component.id.computed("effectCall");
5396        let resource_call = component.id.computed("resourceCall");
5397        let arbitrary_call = component.id.computed("arbitraryCall");
5398        let random = component.id.computed("random");
5399        let async_value = component.id.computed("asyncValue");
5400
5401        assert_eq!(
5402            asm.computed_value(&pure).expect("pure value").purity,
5403            crate::ComputedPurity::Pure
5404        );
5405        for (computed, kind) in [
5406            (mutates, crate::ComputedPurityViolationKind::StateMutation),
5407            (action_call, crate::ComputedPurityViolationKind::Action),
5408            (effect_call, crate::ComputedPurityViolationKind::Effect),
5409            (resource_call, crate::ComputedPurityViolationKind::Resource),
5410            (
5411                arbitrary_call,
5412                crate::ComputedPurityViolationKind::ArbitraryMethodCall,
5413            ),
5414            (
5415                random,
5416                crate::ComputedPurityViolationKind::NondeterministicOperation,
5417            ),
5418            (async_value, crate::ComputedPurityViolationKind::Async),
5419        ] {
5420            let value = asm.computed_value(&computed).expect("computed value");
5421            assert_eq!(value.purity, crate::ComputedPurity::Impure);
5422            assert!(value
5423                .purity_violations
5424                .iter()
5425                .any(|violation| violation.kind == kind));
5426        }
5427        let diagnostics = asm
5428            .diagnostics
5429            .iter()
5430            .filter(|diagnostic| diagnostic.code == "PSC1034")
5431            .collect::<Vec<_>>();
5432        assert_eq!(diagnostics.len(), 7);
5433        assert!(diagnostics
5434            .iter()
5435            .all(|diagnostic| diagnostic.provenance.is_some()));
5436    }
5437
5438    #[test]
5439    fn establishes_typed_action_input_and_output_contracts() {
5440        let parsed = presolve_parser::parse_file(
5441            "src/Action.tsx",
5442            r#"
5443type ActionResult = number;
5444
5445@component("x-action")
5446class Action extends Component {
5447  @action()
5448  async addTodo(input: string): ActionResult { return 1; }
5449}
5450"#,
5451        );
5452
5453        let asm = build_application_semantic_model(&parsed);
5454        let method = &asm.components[0].methods[0];
5455        let signature = asm
5456            .semantic_types
5457            .action_signatures
5458            .get(&method.id)
5459            .expect("action signature");
5460
5461        assert!(method.is_action());
5462        assert!(signature.is_async);
5463        assert_eq!(signature.input.len(), 1);
5464        assert_eq!(signature.input[0].1, crate::SemanticType::String);
5465        assert_eq!(signature.output, Some(crate::SemanticType::Number));
5466    }
5467
5468    #[test]
5469    fn lowers_supported_literal_state_annotations_into_canonical_types() {
5470        let parsed = presolve_parser::parse_file(
5471            "src/LiteralTypes.tsx",
5472            r#"
5473@component("x-literal-types")
5474class LiteralTypes extends Component {
5475  filter: "all" = state("all");
5476  step: 42 = state(42);
5477  enabled: true = state(true);
5478}
5479"#,
5480        );
5481
5482        let asm = build_application_semantic_model(&parsed);
5483        let types = asm.components[0]
5484            .state_fields
5485            .iter()
5486            .map(|field| {
5487                (
5488                    field.name.as_str(),
5489                    &asm.semantic_types.assignments[&field.id].semantic_type,
5490                )
5491            })
5492            .collect::<Vec<_>>();
5493
5494        assert_eq!(
5495            types,
5496            vec![
5497                (
5498                    "filter",
5499                    &crate::SemanticType::StringLiteral("all".to_string())
5500                ),
5501                (
5502                    "step",
5503                    &crate::SemanticType::NumberLiteral("42".to_string())
5504                ),
5505                ("enabled", &crate::SemanticType::BooleanLiteral(true)),
5506            ]
5507        );
5508    }
5509
5510    #[test]
5511    fn lowers_array_and_tuple_state_annotations_into_canonical_types() {
5512        let parsed = presolve_parser::parse_file(
5513            "src/CollectionTypes.tsx",
5514            r#"
5515@component("x-collection-types")
5516class CollectionTypes extends Component {
5517  names: string[] = state([]);
5518  todos: Todo[] = state([]);
5519  pair: [string, number] = state(["Presolve", 1]);
5520}
5521"#,
5522        );
5523
5524        let asm = build_application_semantic_model(&parsed);
5525        let types = asm.components[0]
5526            .state_fields
5527            .iter()
5528            .map(|field| {
5529                (
5530                    field.name.as_str(),
5531                    &asm.semantic_types.assignments[&field.id].semantic_type,
5532                )
5533            })
5534            .collect::<Vec<_>>();
5535
5536        assert_eq!(
5537            types,
5538            vec![
5539                (
5540                    "names",
5541                    &crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
5542                ),
5543                (
5544                    "todos",
5545                    &crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
5546                ),
5547                (
5548                    "pair",
5549                    &crate::SemanticType::Tuple(vec![
5550                        crate::SemanticType::String,
5551                        crate::SemanticType::Number,
5552                    ]),
5553                ),
5554            ]
5555        );
5556    }
5557
5558    #[test]
5559    fn lowers_structural_object_state_annotations_into_canonical_types() {
5560        let parsed = presolve_parser::parse_file(
5561            "src/ObjectTypes.tsx",
5562            r#"
5563@component("x-object-types")
5564class ObjectTypes extends Component {
5565  todo: { id: string; title: string; completed: boolean } = state({ id: "1", title: "Presolve", completed: false });
5566}
5567"#,
5568        );
5569
5570        let asm = build_application_semantic_model(&parsed);
5571        let field = &asm.components[0].state_fields[0];
5572        let assignment = &asm.semantic_types.assignments[&field.id];
5573        let crate::SemanticType::Object(object) = &assignment.semantic_type else {
5574            panic!("expected structural object type");
5575        };
5576
5577        assert_eq!(
5578            object.properties,
5579            std::collections::BTreeMap::from([
5580                ("completed".to_string(), crate::SemanticType::Boolean),
5581                ("id".to_string(), crate::SemanticType::String),
5582                ("title".to_string(), crate::SemanticType::String),
5583            ])
5584        );
5585    }
5586
5587    #[test]
5588    fn lowers_union_and_nullable_state_annotations_into_canonical_types() {
5589        let parsed = presolve_parser::parse_file(
5590            "src/UnionTypes.tsx",
5591            r#"
5592@component("x-union-types")
5593class UnionTypes extends Component {
5594  filter: "all" | "active" | "completed" = state("all");
5595  user: { id: string } | null = state(null);
5596}
5597"#,
5598        );
5599
5600        let asm = build_application_semantic_model(&parsed);
5601        let fields = &asm.components[0].state_fields;
5602        assert_eq!(
5603            asm.semantic_types.assignments[&fields[0].id].semantic_type,
5604            crate::SemanticType::Union(vec![
5605                crate::SemanticType::StringLiteral("active".to_string()),
5606                crate::SemanticType::StringLiteral("all".to_string()),
5607                crate::SemanticType::StringLiteral("completed".to_string()),
5608            ])
5609        );
5610        assert_eq!(
5611            asm.semantic_types.assignments[&fields[1].id].semantic_type,
5612            crate::SemanticType::Union(vec![
5613                crate::SemanticType::Null,
5614                crate::SemanticType::Object(crate::ObjectType {
5615                    properties: std::collections::BTreeMap::from([(
5616                        "id".to_string(),
5617                        crate::SemanticType::String,
5618                    )]),
5619                }),
5620            ])
5621        );
5622    }
5623
5624    #[test]
5625    fn resolves_local_type_aliases_with_canonical_alias_identity() {
5626        let parsed = presolve_parser::parse_file(
5627            "src/Aliases.tsx",
5628            r#"
5629type TodoId = string;
5630type Filter = "all" | "active" | "completed";
5631
5632@component("x-aliases")
5633class Aliases extends Component {
5634  id: TodoId = state("todo-1");
5635  filter: Filter = state("all");
5636}
5637"#,
5638        );
5639
5640        let asm = build_application_semantic_model(&parsed);
5641        let fields = &asm.components[0].state_fields;
5642        let todo_id = asm
5643            .semantic_types
5644            .aliases
5645            .values()
5646            .find(|alias| alias.name == "TodoId")
5647            .expect("TodoId alias");
5648        let filter = asm
5649            .semantic_types
5650            .aliases
5651            .values()
5652            .find(|alias| alias.name == "Filter")
5653            .expect("Filter alias");
5654
5655        assert_eq!(todo_id.semantic_type, crate::SemanticType::String);
5656        assert_eq!(
5657            filter.semantic_type,
5658            crate::SemanticType::Union(vec![
5659                crate::SemanticType::StringLiteral("active".to_string()),
5660                crate::SemanticType::StringLiteral("all".to_string()),
5661                crate::SemanticType::StringLiteral("completed".to_string()),
5662            ])
5663        );
5664        assert_eq!(
5665            asm.semantic_types.assignments[&fields[0].id].origin,
5666            todo_id.id
5667        );
5668        assert_eq!(
5669            asm.semantic_types.assignments[&fields[1].id].origin,
5670            filter.id
5671        );
5672    }
5673
5674    #[test]
5675    fn resolves_imported_type_aliases_through_named_reexports() {
5676        let unit = CompilationUnit::parse_sources([
5677            (
5678                "src/types.ts",
5679                r#"export type Filter = "all" | "active" | "completed";"#,
5680            ),
5681            ("src/index.ts", r#"export { Filter } from "./types";"#),
5682            (
5683                "src/App.tsx",
5684                r#"
5685import { Filter } from "./index";
5686
5687@component("x-app")
5688class App extends Component {
5689  filter: Filter = state("all");
5690}
5691"#,
5692            ),
5693        ]);
5694
5695        let asm = build_application_semantic_model_for_unit(&unit);
5696        let field = &asm
5697            .components
5698            .iter()
5699            .find(|component| component.class_name == "App")
5700            .expect("App component")
5701            .state_fields[0];
5702        let assignment = &asm.semantic_types.assignments[&field.id];
5703
5704        assert_eq!(
5705            assignment.semantic_type,
5706            crate::SemanticType::Union(vec![
5707                crate::SemanticType::StringLiteral("active".to_string()),
5708                crate::SemanticType::StringLiteral("all".to_string()),
5709                crate::SemanticType::StringLiteral("completed".to_string()),
5710            ])
5711        );
5712        assert_eq!(
5713            assignment.origin,
5714            crate::SemanticId::type_alias_in_module("src/types.ts", "Filter")
5715        );
5716    }
5717
5718    #[test]
5719    fn infers_state_types_from_direct_serializable_initializers() {
5720        let parsed = presolve_parser::parse_file(
5721            "src/InferredState.tsx",
5722            r#"
5723@component("x-inferred-state")
5724class InferredState extends Component {
5725  count = state(0);
5726  todos = state([]);
5727  tags = state(["Presolve"]);
5728  todo = state({ id: "1", completed: false });
5729}
5730"#,
5731        );
5732
5733        let asm = build_application_semantic_model(&parsed);
5734        let fields = &asm.components[0].state_fields;
5735        let types = fields
5736            .iter()
5737            .map(|field| {
5738                let assignment = &asm.semantic_types.assignments[&field.id];
5739                assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
5740                (field.name.as_str(), assignment.semantic_type.clone())
5741            })
5742            .collect::<Vec<_>>();
5743
5744        assert_eq!(
5745            types,
5746            vec![
5747                ("count", crate::SemanticType::Number),
5748                (
5749                    "todos",
5750                    crate::SemanticType::Array(Box::new(crate::SemanticType::Unknown)),
5751                ),
5752                (
5753                    "tags",
5754                    crate::SemanticType::Array(Box::new(crate::SemanticType::String)),
5755                ),
5756                (
5757                    "todo",
5758                    crate::SemanticType::Object(crate::ObjectType {
5759                        properties: std::collections::BTreeMap::from([
5760                            ("completed".to_string(), crate::SemanticType::Boolean),
5761                            ("id".to_string(), crate::SemanticType::String),
5762                        ]),
5763                    }),
5764                ),
5765            ]
5766        );
5767    }
5768
5769    #[test]
5770    fn propagates_canonical_types_to_expression_graph_nodes() {
5771        let parsed = presolve_parser::parse_file(
5772            "src/ExpressionTypes.tsx",
5773            r#"
5774@component("x-expression-types")
5775class ExpressionTypes extends Component {
5776  total = state((1 + 2) * 3);
5777  ready = state(1 < 2);
5778}
5779"#,
5780        );
5781
5782        let asm = build_application_semantic_model(&parsed);
5783        let total = &asm.components[0].state_fields[0];
5784        let ready = &asm.components[0].state_fields[1];
5785        let total_root = asm
5786            .expression_root(&total.id)
5787            .expect("total expression root");
5788        let ready_root = asm
5789            .expression_root(&ready.id)
5790            .expect("ready expression root");
5791
5792        assert_eq!(
5793            asm.semantic_types.assignments[total_root].semantic_type,
5794            crate::SemanticType::Number
5795        );
5796        assert_eq!(
5797            asm.semantic_types.assignments[ready_root].semantic_type,
5798            crate::SemanticType::Boolean
5799        );
5800        assert!(asm
5801            .expression_dependencies(total_root)
5802            .iter()
5803            .all(|id| asm.semantic_types.assignments.contains_key(*id)));
5804    }
5805
5806    #[test]
5807    fn derives_component_ownership_without_legacy_owner_fields() {
5808        let parsed = presolve_parser::parse_file(
5809            "src/Counter.tsx",
5810            r#"
5811@component("x-counter")
5812class Counter extends Component {
5813  count = state(0);
5814
5815  increment() {
5816    this.count++;
5817  }
5818
5819  render() {
5820    return <button onClick={() => this.increment()}>{this.count}</button>;
5821  }
5822}
5823"#,
5824        );
5825        let mut component_graph = build_component_graph_for_module(&parsed);
5826        let mut templates = build_template_graph(&component_graph).templates;
5827        let template_entities = build_template_semantic_entities(&templates);
5828        let component = component_graph
5829            .components
5830            .first_mut()
5831            .expect("component graph should contain Counter");
5832        let component_id = component.id.clone();
5833        let method_id = component.methods[0].id.clone();
5834        let action_id = component.actions[0].id.clone();
5835        let state_id = component.state_fields[0].id.clone();
5836        let event_id = component
5837            .render
5838            .as_ref()
5839            .expect("Counter should render")
5840            .event_handlers[0]
5841            .id
5842            .clone();
5843
5844        component.owner = SemanticOwner::entity(component_id.clone());
5845        component.state_fields[0].owner = SemanticOwner::Application;
5846        component.methods[0].owner = SemanticOwner::Application;
5847        component.actions[0].owner = SemanticOwner::Application;
5848        component
5849            .render
5850            .as_mut()
5851            .expect("Counter should render")
5852            .event_handlers[0]
5853            .owner = SemanticOwner::Application;
5854        templates[0].owner = SemanticOwner::Application;
5855
5856        let ownership = collect_ownership(
5857            &component_graph.components,
5858            &std::collections::BTreeMap::new(),
5859            &std::collections::BTreeMap::new(),
5860            &std::collections::BTreeMap::new(),
5861            &std::collections::BTreeMap::new(),
5862            &std::collections::BTreeMap::new(),
5863            &std::collections::BTreeMap::new(),
5864            &std::collections::BTreeMap::new(),
5865            &std::collections::BTreeMap::new(),
5866            &std::collections::BTreeMap::new(),
5867            &std::collections::BTreeMap::new(),
5868            &ComponentInstancePlan::default(),
5869            &std::collections::BTreeMap::new(),
5870            &std::collections::BTreeMap::new(),
5871            &templates,
5872            &template_entities,
5873        );
5874        assert_eq!(ownership[&component_id], SemanticOwner::Application);
5875        assert_eq!(
5876            ownership[&state_id],
5877            SemanticOwner::entity(component_id.clone())
5878        );
5879        assert_eq!(
5880            ownership[&method_id],
5881            SemanticOwner::entity(component_id.clone())
5882        );
5883        assert_eq!(ownership[&action_id], SemanticOwner::entity(method_id));
5884        assert_eq!(
5885            ownership[&event_id],
5886            SemanticOwner::entity(component_id.clone().template())
5887        );
5888        assert_eq!(
5889            ownership[&templates[0].id],
5890            SemanticOwner::entity(component_id)
5891        );
5892    }
5893
5894    #[test]
5895    #[allow(clippy::too_many_lines)]
5896    fn traverses_application_ownership_in_semantic_id_order() {
5897        let parsed = presolve_parser::parse_file(
5898            "src/Counter.tsx",
5899            r#"
5900@component("x-counter")
5901class Counter extends Component {
5902  count = state(0);
5903
5904  increment() {
5905    this.count++;
5906  }
5907
5908  render() {
5909    return <button onClick={this.increment}>{this.count}</button>;
5910  }
5911}
5912"#,
5913        );
5914
5915        let asm = build_application_semantic_model(&parsed);
5916        let component = &asm.components[0];
5917        let roots = asm.application_roots();
5918
5919        assert_eq!(
5920            roots,
5921            vec![
5922                &component.id,
5923                asm.component_instance_plan
5924                    .instances
5925                    .values()
5926                    .find(|instance| instance.depth == 0)
5927                    .expect("build root instance")
5928                    .id
5929                    .as_semantic_id(),
5930            ]
5931        );
5932        assert_eq!(
5933            asm.children_of(&component.id),
5934            vec![
5935                &component.methods[0].id,
5936                &component.methods[1].id,
5937                &component.state_fields[0].id,
5938                &asm.templates[0].id,
5939            ]
5940        );
5941        assert_eq!(
5942            asm.children_of(&component.methods[0].id),
5943            vec![&component.actions[0].id]
5944        );
5945        assert_eq!(asm.parent_of(&component.id), None);
5946        assert_eq!(
5947            asm.parent_of(&component.actions[0].id),
5948            Some(&component.methods[0].id)
5949        );
5950        assert_eq!(
5951            asm.ancestors_of(&component.actions[0].id),
5952            vec![&component.methods[0].id, &component.id]
5953        );
5954        let descendants = asm.descendants_of(&component.id);
5955        assert_eq!(descendants[0], &component.methods[0].id);
5956        assert_eq!(descendants[1], &component.actions[0].id);
5957        assert_eq!(descendants[2], &component.methods[1].id);
5958        assert_eq!(
5959            descendants.len(),
5960            asm.ownership.len() - asm.application_roots().len()
5961        );
5962        assert_eq!(
5963            asm.entities_of_kind(SemanticEntityKind::Method),
5964            vec![&component.methods[0].id, &component.methods[1].id]
5965        );
5966        assert_eq!(
5967            asm.entities_of_kind(SemanticEntityKind::Action),
5968            vec![&component.actions[0].id]
5969        );
5970        assert_eq!(
5971            asm.entities_of_kind(SemanticEntityKind::StateField),
5972            vec![&component.state_fields[0].id]
5973        );
5974        let state_id = &component.state_fields[0].id;
5975        let state_provenance = asm.provenance(state_id).expect("state provenance");
5976        assert_eq!(
5977            asm.entities_in_file(state_provenance.path.as_path()).len(),
5978            asm.ownership.len()
5979        );
5980        let at_state =
5981            asm.entities_at(state_provenance.path.as_path(), state_provenance.span.start);
5982        assert!(at_state.contains(&state_id));
5983        assert!(at_state.iter().all(|id| {
5984            let provenance = asm.provenance(id).expect("entity provenance");
5985            provenance.span.start <= state_provenance.span.start
5986                && state_provenance.span.start < provenance.span.end
5987        }));
5988        let action_references = asm.references_of_kind(SemanticReferenceKind::ActionState);
5989        assert_eq!(action_references.len(), 1);
5990        assert_eq!(action_references[0].source, component.actions[0].id);
5991        assert_eq!(action_references[0].target, component.state_fields[0].id);
5992        let action_provenance = &action_references[0].provenance;
5993        assert_eq!(
5994            asm.references_in_file(action_provenance.path.as_path())
5995                .len(),
5996            asm.references.len()
5997        );
5998        let at_action = asm.references_at(
5999            action_provenance.path.as_path(),
6000            action_provenance.span.start,
6001        );
6002        assert!(at_action.iter().any(|reference| {
6003            reference.source == component.actions[0].id
6004                && reference.target == component.state_fields[0].id
6005        }));
6006    }
6007
6008    #[test]
6009    fn resolves_template_state_dependencies() {
6010        let parsed = presolve_parser::parse_file(
6011            "src/Panel.tsx",
6012            r#"
6013@component("x-panel")
6014class Panel extends Component {
6015  enabled = state(true);
6016
6017  render() {
6018    return <section hidden={this.enabled}>{this.enabled}{this.enabled ? <span>On</span> : <span>Off</span>}</section>;
6019  }
6020}
6021"#,
6022        );
6023
6024        let asm = build_application_semantic_model(&parsed);
6025        let component = &asm.components[0];
6026        let state = &component.state_fields[0];
6027        let state_references = asm.references_to(&state.id);
6028
6029        assert_eq!(state_references.len(), 3);
6030        assert!(state_references.iter().all(|reference| {
6031            reference.kind == SemanticReferenceKind::TemplateState
6032                && reference.target == state.id
6033                && reference.provenance.path.as_path() == std::path::Path::new("src/Panel.tsx")
6034        }));
6035        assert!(state_references.iter().any(|reference| {
6036            asm.template_entity(&reference.source)
6037                .is_some_and(|entity| entity.kind == TemplateSemanticKind::Binding)
6038        }));
6039        assert!(state_references.iter().any(|reference| {
6040            asm.template_entity(&reference.source)
6041                .is_some_and(|entity| entity.kind == TemplateSemanticKind::AttributeBinding)
6042        }));
6043        assert!(state_references.iter().any(|reference| {
6044            asm.template_entity(&reference.source)
6045                .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6046        }));
6047    }
6048
6049    #[test]
6050    fn resolves_template_bindings_to_canonical_computed_entities() {
6051        let parsed = presolve_parser::parse_file(
6052            "src/ComputedTemplate.tsx",
6053            r#"
6054@component("x-computed-template")
6055class ComputedTemplate extends Component {
6056  @computed()
6057  get label(): string { return "Ready"; }
6058
6059  @computed()
6060  get visible(): boolean { return true; }
6061
6062  render() {
6063    return <section title={this.label}>{this.label}{this.visible ? <span>Visible</span> : <span>Hidden</span>}</section>;
6064  }
6065}
6066"#,
6067        );
6068
6069        let asm = build_application_semantic_model(&parsed);
6070        let component = &asm.components[0];
6071        let label = component.id.computed("label");
6072        let visible = component.id.computed("visible");
6073        let references = asm.references_of_kind(SemanticReferenceKind::TemplateComputed);
6074
6075        assert_eq!(references.len(), 3);
6076        assert_eq!(
6077            references
6078                .iter()
6079                .filter(|reference| reference.target == label)
6080                .count(),
6081            2
6082        );
6083        assert!(references.iter().any(|reference| {
6084            reference.target == visible
6085                && asm
6086                    .template_entity(&reference.source)
6087                    .is_some_and(|entity| entity.kind == TemplateSemanticKind::Conditional)
6088        }));
6089        assert!(references.iter().all(|reference| {
6090            asm.provenance(&reference.source) == Some(&reference.provenance)
6091                && asm.semantic_type_of(&reference.source)
6092                    == asm.semantic_type_of(&reference.target)
6093        }));
6094        assert_eq!(
6095            asm.references_of_kind(SemanticReferenceKind::TemplateState)
6096                .into_iter()
6097                .filter(|reference| reference.target == label || reference.target == visible)
6098                .count(),
6099            0
6100        );
6101
6102        let graph = crate::build_semantic_graph(&asm);
6103        assert_eq!(
6104            graph
6105                .edges
6106                .iter()
6107                .filter(|edge| edge.kind == crate::SemanticGraphEdgeKind::TemplateComputed)
6108                .count(),
6109            3
6110        );
6111    }
6112
6113    #[test]
6114    fn navigates_template_entities_from_canonical_ownership() {
6115        let parsed = presolve_parser::parse_file(
6116            "src/Panel.tsx",
6117            r#"
6118@component("x-panel")
6119class Panel extends Component {
6120  count = state(0);
6121
6122  render() {
6123    return <section>{this.count}</section>;
6124  }
6125}
6126"#,
6127        );
6128        let mut asm = build_application_semantic_model(&parsed);
6129        let template_id = asm.templates[0].id.clone();
6130        let expected = asm.template_entities_for(&template_id).len();
6131
6132        for entity in &mut asm.template_entities {
6133            entity.owner = SemanticOwner::Application;
6134        }
6135
6136        assert_eq!(asm.template_entities_for(&template_id).len(), expected);
6137    }
6138
6139    #[test]
6140    fn leaves_member_expressions_unresolved_without_expression_evaluation() {
6141        let parsed = presolve_parser::parse_file(
6142            "src/Panel.tsx",
6143            r#"
6144@component("x-panel")
6145class Panel extends Component {
6146  enabled = state(true);
6147
6148  render() {
6149    return <section data-value={this.enabled.value}>Panel</section>;
6150  }
6151}
6152"#,
6153        );
6154
6155        let asm = build_application_semantic_model(&parsed);
6156        assert_eq!(
6157            asm.references
6158                .iter()
6159                .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6160                .count(),
6161            0
6162        );
6163    }
6164
6165    #[test]
6166    fn resolves_keyed_list_iterable_to_component_state() {
6167        let parsed = presolve_parser::parse_file(
6168            "src/KeyedList.tsx",
6169            r#"
6170@component("x-keyed-list")
6171class KeyedList extends Component {
6172  items = state([]);
6173
6174  render() {
6175    return <ul>{this.items.map((item) => <li key={item}>{item}</li>)}</ul>;
6176  }
6177}
6178"#,
6179        );
6180
6181        let asm = build_application_semantic_model(&parsed);
6182        let component = &asm.components[0];
6183        let state = &component.state_fields[0];
6184        let reference = asm
6185            .references_to(&state.id)
6186            .into_iter()
6187            .find(|reference| reference.kind == SemanticReferenceKind::TemplateState)
6188            .expect("list iterable state reference");
6189        let list = asm
6190            .template_entity(&reference.source)
6191            .expect("list semantic entity");
6192
6193        assert_eq!(list.kind, TemplateSemanticKind::List);
6194        assert_eq!(list.expression.as_deref(), Some("this.items"));
6195        assert_eq!(reference.provenance, list.provenance);
6196    }
6197
6198    #[test]
6199    fn resolves_template_event_attribute_to_component_method() {
6200        let parsed = presolve_parser::parse_file(
6201            "src/Counter.tsx",
6202            r#"
6203@component("x-counter")
6204class Counter extends Component {
6205  count = state(0);
6206
6207  increment() {
6208    this.count += 1;
6209  }
6210
6211  render() {
6212    return <button onClick={() => this.increment()}>Count</button>;
6213  }
6214}
6215"#,
6216        );
6217
6218        let asm = build_application_semantic_model(&parsed);
6219        let component = &asm.components[0];
6220        let method = component
6221            .methods
6222            .iter()
6223            .find(|method| method.name == "increment")
6224            .expect("increment method");
6225        let reference = asm
6226            .references_to(&method.id)
6227            .into_iter()
6228            .find(|reference| {
6229                reference.kind == SemanticReferenceKind::EventMethod
6230                    && asm
6231                        .template_entity(&reference.source)
6232                        .is_some_and(|entity| entity.kind == TemplateSemanticKind::EventAttribute)
6233            })
6234            .expect("template event method reference");
6235
6236        assert_eq!(
6237            reference.provenance.path,
6238            std::path::Path::new("src/Counter.tsx")
6239        );
6240        assert_eq!(reference.provenance.span.line, 11);
6241    }
6242
6243    #[test]
6244    fn resolves_render_template_bindings_to_unique_method_locals() {
6245        let parsed = presolve_parser::parse_file(
6246            "src/LocalResolution.tsx",
6247            r#"
6248@component("x-local-resolution")
6249class LocalResolution extends Component {
6250  render() {
6251    const title = "Presolve";
6252    return <output title={title}>{title}</output>;
6253  }
6254}
6255"#,
6256        );
6257
6258        let asm = build_application_semantic_model(&parsed);
6259        let component = &asm.components[0];
6260        let render = component
6261            .methods
6262            .iter()
6263            .find(|method| method.name == "render")
6264            .expect("render method");
6265        let local = &render.local_variables[0];
6266        let references = asm.references_of_kind(SemanticReferenceKind::TemplateLocal);
6267        let assignment = asm
6268            .semantic_types
6269            .assignments
6270            .get(&local.id)
6271            .expect("canonical inferred local type");
6272
6273        assert_eq!(
6274            asm.owner(&local.id),
6275            Some(&SemanticOwner::entity(render.id.clone()))
6276        );
6277        assert_eq!(
6278            asm.entities_of_kind(SemanticEntityKind::LocalVariable),
6279            vec![&local.id]
6280        );
6281        assert_eq!(references.len(), 2);
6282        assert!(references.iter().all(|reference| {
6283            reference.target == local.id
6284                && reference.provenance.path == std::path::Path::new("src/LocalResolution.tsx")
6285        }));
6286        assert_eq!(assignment.semantic_type, crate::SemanticType::String);
6287        assert_eq!(assignment.status, crate::SemanticTypeStatus::Inferred);
6288        assert_eq!(assignment.provenance.span, local.span);
6289
6290        let semantic_graph = crate::build_semantic_graph(&asm);
6291        assert!(semantic_graph.nodes.iter().any(|node| {
6292            node.kind == crate::SemanticGraphNodeKind::LocalVariable && node.id == local.id
6293        }));
6294        assert_eq!(
6295            semantic_graph
6296                .edges
6297                .iter()
6298                .filter(|edge| {
6299                    edge.kind == crate::SemanticGraphEdgeKind::TemplateLocal
6300                        && edge.target == local.id
6301                })
6302                .count(),
6303            2
6304        );
6305
6306        let template_graph = build_template_graph(&build_component_graph_for_module(&parsed));
6307        assert_eq!(
6308            crate::generate_static_html(&template_graph),
6309            "<output data-presolve-node=\"n0\" title=\"Presolve\" data-presolve-bindings=\"title\"><!-- presolve-binding:n2:title -->Presolve</output>\n"
6310        );
6311    }
6312
6313    #[test]
6314    fn queries_canonical_expression_graphs_by_dependency_provenance_and_owner() {
6315        let parsed = presolve_parser::parse_file(
6316            "src/ExpressionQueries.tsx",
6317            r#"
6318@component("x-expression-queries")
6319class ExpressionQueries extends Component {
6320  total = state((1 + 2) * 3);
6321}
6322"#,
6323        );
6324
6325        let asm = build_application_semantic_model(&parsed);
6326        let field = &asm.components[0].state_fields[0];
6327        let root = asm.expression_root(&field.id).expect("expression root");
6328        let left = field.id.expression("root.0");
6329        let right = field.id.expression("root.1");
6330
6331        assert_eq!(asm.expression(root).map(|node| &node.id), Some(root));
6332        assert_eq!(asm.expression_owner(root), Some(&field.id));
6333        assert_eq!(asm.expression_dependencies(root), vec![&left, &right]);
6334        assert_eq!(
6335            asm.expression_dependents(&left)
6336                .into_iter()
6337                .map(|node| &node.id)
6338                .collect::<Vec<_>>(),
6339            vec![root]
6340        );
6341        assert_eq!(asm.expressions_for(&field.id).len(), 5);
6342
6343        let provenance = asm
6344            .expression_provenance(root)
6345            .expect("expression provenance");
6346        assert_eq!(
6347            provenance.path,
6348            std::path::Path::new("src/ExpressionQueries.tsx")
6349        );
6350        assert_eq!(asm.expressions_in_file(&provenance.path).len(), 5);
6351        assert_eq!(
6352            asm.expressions_at(&provenance.path, provenance.span.start)
6353                .into_iter()
6354                .map(|node| &node.id)
6355                .collect::<Vec<_>>(),
6356            vec![root]
6357        );
6358    }
6359
6360    #[test]
6361    fn file_route_model_composes_layout_default_slots_into_canonical_instances() {
6362        let unit = CompilationUnit::parse_sources([
6363            (
6364                "app/layout.tsx",
6365                r#"
6366@component()
6367class AppLayout extends Component {
6368  @slot() children!: SlotContent;
6369  render() { return <main><slot /></main>; }
6370}
6371"#,
6372            ),
6373            (
6374                "app/routes/index.tsx",
6375                r#"
6376@component()
6377class Home extends Component {
6378  render() { return <article>Home</article>; }
6379}
6380"#,
6381            ),
6382        ]);
6383        let asm = build_file_route_application_semantic_model_for_unit_with_packages(
6384            &unit,
6385            &crate::SemanticPackageResolutionTable::default(),
6386        )
6387        .expect("valid conventional layout");
6388
6389        assert_eq!(asm.component_instance_plan.roots.len(), 1);
6390        let binding = asm
6391            .slot_bindings
6392            .bindings
6393            .values()
6394            .find(|binding| binding.direct_child_instance.is_some())
6395            .expect("compiler-issued layout Slot binding");
6396        assert_eq!(binding.status, crate::SlotBindingStatus::Bound);
6397        assert!(binding.content_fragment.is_none());
6398        assert!(crate::validate_application_semantic_model(&asm).is_empty());
6399
6400        let html = crate::generate_ordinary_instance_html(&asm);
6401        assert!(html.contains("<main"));
6402        assert!(html.contains("<article"));
6403        assert!(html.contains("Home"));
6404    }
6405
6406    #[test]
6407    fn retains_route_loader_source_facts_before_server_capability_resolution() {
6408        let asm = build_application_semantic_model(&presolve_parser::parse_file(
6409            "app/routes/posts/[slug].tsx",
6410            r#"
6411@component()
6412class Post {
6413  @loader("loadPost") post!: Resource<Post, NotFound>;
6414  render() { return <article />; }
6415}
6416"#,
6417        ));
6418
6419        let loader = &asm.components[0].route_loader_declaration_candidates[0];
6420        assert_eq!(loader.field, "post");
6421        assert!(loader.decorator_invoked);
6422        assert_eq!(loader.decorator_argument_count, 1);
6423        assert_eq!(loader.endpoint_designator.as_deref(), Some("loadPost"));
6424        assert_eq!(
6425            loader
6426                .declared_type
6427                .as_ref()
6428                .map(|type_| type_.text.as_str()),
6429            Some("Resource<Post, NotFound>")
6430        );
6431        assert!(asm
6432            .diagnostics
6433            .iter()
6434            .any(|diagnostic| diagnostic.code == "PSC1132"));
6435    }
6436
6437    #[test]
6438    fn retains_server_action_source_facts_before_route_capability_resolution() {
6439        let asm = build_application_semantic_model(&presolve_parser::parse_file(
6440            "app/routes/posts/[slug].tsx",
6441            r#"
6442@component()
6443class Post {
6444  @serverAction("savePost") save(): void {}
6445  render() { return <form />; }
6446}
6447"#,
6448        ));
6449
6450        let action = &asm.components[0].server_action_facts[0];
6451        assert_eq!(action.method_name, "save");
6452        assert!(action.invoked);
6453        assert_eq!(action.argument_count, 1);
6454        assert_eq!(action.endpoint_designator.as_deref(), Some("savePost"));
6455        assert!(!action.is_action);
6456        assert!(!action.action_invoked);
6457        assert!(!action.has_body_effects);
6458        assert!(asm
6459            .diagnostics
6460            .iter()
6461            .any(|diagnostic| diagnostic.code == "PSC1133"));
6462    }
6463}