Skip to main content

presolve_compiler/
application_semantic_model.rs

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