Skip to main content

presolve_compiler/
semantic_type.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::path::PathBuf;
4
5use crate::{
6    BindingTable, CapabilityOperationId, CapabilityOperationKind, CapabilityParameters,
7    CapabilityValueContract, ComponentNode, ComputedValue, ConsumerEntity, ContextEntity,
8    DeclaredStateType, Effect, EffectStatement, EffectStatementKind, ExpressionGraph,
9    ExpressionNodeKind, ImportBindingTarget, ProviderEntity, SemanticId, SerializableValue,
10    SlotEntity, SourceProvenance, SymbolKind, EFFECT_CAPABILITY_REGISTRY,
11};
12use crate::{
13    SemanticReference, SemanticReferenceKind, TemplateSemanticEntity, TemplateSemanticKind,
14};
15use presolve_parser::{ParsedFile, ParsedTypeAlias};
16
17/// A compiler-owned operator category used by semantic expression typing.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SemanticOperator {
20    Arithmetic(crate::ArithmeticOperator),
21    Comparison(crate::ComparisonOperator),
22    Logical(crate::LogicalOperator),
23    NullishCoalescing,
24    Unary(crate::component_graph::UnaryOperator),
25}
26
27/// Whether a supported DOM binding writes an HTML attribute or a DOM property.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DomBindingKind {
30    Attribute,
31    Property,
32}
33
34/// Compiler-owned type contract for one supported DOM binding name.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct DomBindingContract {
37    pub name: &'static str,
38    pub kind: DomBindingKind,
39    pub semantic_type: SemanticType,
40}
41
42/// Returns the contract for the currently supported dynamic DOM bindings.
43#[must_use]
44pub fn dom_binding_contract(name: &str) -> Option<DomBindingContract> {
45    match name {
46        "disabled" => Some(DomBindingContract {
47            name: "disabled",
48            kind: DomBindingKind::Property,
49            semantic_type: SemanticType::Boolean,
50        }),
51        "href" => Some(DomBindingContract {
52            name: "href",
53            kind: DomBindingKind::Attribute,
54            semantic_type: SemanticType::String,
55        }),
56        "class" | "for" => Some(DomBindingContract {
57            name: if name == "class" { "class" } else { "for" },
58            kind: DomBindingKind::Attribute,
59            semantic_type: SemanticType::String,
60        }),
61        "role" | "aria-label" | "aria-describedby" | "aria-errormessage" | "aria-controls"
62        | "aria-current" | "aria-live" => Some(DomBindingContract {
63            name: match name {
64                "role" => "role",
65                "aria-label" => "aria-label",
66                "aria-describedby" => "aria-describedby",
67                "aria-errormessage" => "aria-errormessage",
68                "aria-controls" => "aria-controls",
69                "aria-current" => "aria-current",
70                _ => "aria-live",
71            },
72            kind: DomBindingKind::Attribute,
73            semantic_type: SemanticType::String,
74        }),
75        "aria-invalid" | "aria-busy" | "aria-expanded" | "aria-pressed" | "aria-hidden" => {
76            Some(DomBindingContract {
77                name: match name {
78                    "aria-invalid" => "aria-invalid",
79                    "aria-busy" => "aria-busy",
80                    "aria-expanded" => "aria-expanded",
81                    "aria-hidden" => "aria-hidden",
82                    _ => "aria-pressed",
83                },
84                kind: DomBindingKind::Attribute,
85                semantic_type: SemanticType::Boolean,
86            })
87        }
88        "value" => Some(DomBindingContract {
89            name: "value",
90            kind: DomBindingKind::Property,
91            semantic_type: SemanticType::Union(vec![SemanticType::String, SemanticType::Number]),
92        }),
93        _ => None,
94    }
95}
96
97/// Compiler-owned semantic type algebra independent of TypeScript spelling.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum SemanticType {
100    Unknown,
101    Never,
102    Null,
103    Boolean,
104    Number,
105    String,
106    /// Nominal compile-time marker accepted only by canonical Form declarations.
107    Form,
108    /// Nominal built-in type accepted only by canonical Slot declarations.
109    SlotContent,
110    BooleanLiteral(bool),
111    NumberLiteral(String),
112    StringLiteral(String),
113    Array(Box<SemanticType>),
114    Tuple(Vec<SemanticType>),
115    Object(ObjectType),
116    Union(Vec<SemanticType>),
117    Resource(ResourceType),
118}
119
120/// Renders a canonical semantic type using the stable inspection spelling.
121#[must_use]
122pub fn semantic_type_text(semantic_type: &SemanticType) -> String {
123    match semantic_type {
124        SemanticType::Unknown => "unknown".to_string(),
125        SemanticType::Never => "never".to_string(),
126        SemanticType::Null => "null".to_string(),
127        SemanticType::Boolean => "boolean".to_string(),
128        SemanticType::Number => "number".to_string(),
129        SemanticType::String => "string".to_string(),
130        SemanticType::Form => "Form".to_string(),
131        SemanticType::SlotContent => "SlotContent".to_string(),
132        SemanticType::BooleanLiteral(value) => value.to_string(),
133        SemanticType::NumberLiteral(value) => value.clone(),
134        SemanticType::StringLiteral(value) => format!("{value:?}"),
135        SemanticType::Array(element) => format!("{}[]", semantic_type_text(element)),
136        SemanticType::Tuple(items) => format!(
137            "[{}]",
138            items
139                .iter()
140                .map(semantic_type_text)
141                .collect::<Vec<_>>()
142                .join(", ")
143        ),
144        SemanticType::Object(object) => format!(
145            "{{ {} }}",
146            object
147                .properties
148                .iter()
149                .map(|(name, semantic_type)| format!(
150                    "{name}: {}",
151                    semantic_type_text(semantic_type)
152                ))
153                .collect::<Vec<_>>()
154                .join("; ")
155        ),
156        SemanticType::Union(members) => members
157            .iter()
158            .map(semantic_type_text)
159            .collect::<Vec<_>>()
160            .join(" | "),
161        SemanticType::Resource(resource) => format!(
162            "Resource<{}, {}>",
163            semantic_type_text(&resource.data),
164            semantic_type_text(&resource.error)
165        ),
166    }
167}
168
169/// Module-scoped authority for compiler-owned built-in marker types.
170///
171/// The parser supplies binding facts; this authority decides whether an exact
172/// authored marker spelling resolves to the compiler built-in rather than a
173/// module-local declaration or import. Downstream stages consume the resulting
174/// canonical entity and never repeat source-text recognition.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct BuiltinTypeAuthority {
177    form_marker_is_unshadowed: bool,
178}
179
180impl BuiltinTypeAuthority {
181    #[must_use]
182    pub fn for_file(parsed: &ParsedFile) -> Self {
183        let has_local_binding = parsed
184            .local_type_bindings
185            .iter()
186            .any(|binding| binding == "Form");
187        let has_non_presolve_import_binding = parsed.imports.iter().any(|import| {
188            import.specifiers.iter().any(|specifier| {
189                specifier.local == "Form"
190                    && !(import.source == "@presolve/core" && specifier.imported == "Form")
191            })
192        });
193        Self {
194            form_marker_is_unshadowed: !has_local_binding && !has_non_presolve_import_binding,
195        }
196    }
197
198    #[must_use]
199    pub fn recognizes_form_marker(self, type_text: &str) -> bool {
200        self.form_marker_is_unshadowed && type_text == "Form"
201    }
202}
203
204/// Compiler-owned resource contract with explicit data, error, and boundary semantics.
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct ResourceType {
207    pub data: Box<SemanticType>,
208    pub error: Box<SemanticType>,
209    pub pending: bool,
210    pub serializable: bool,
211    pub execution_boundary: ResourceExecutionBoundary,
212}
213
214/// Execution boundary declared by a canonical resource contract.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum ResourceExecutionBoundary {
217    Client,
218    Server,
219    Shared,
220}
221
222/// Whether a canonical semantic type can safely cross a compiler/runtime boundary.
223#[derive(Debug, Clone, Copy, PartialEq, Eq)]
224pub enum SerializationCompatibility {
225    Serializable,
226    NotSerializable,
227}
228
229/// Execution side participating in a compiler/runtime boundary crossing.
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum ExecutionBoundary {
232    Client,
233    Server,
234}
235
236/// Whether a semantic type may cross one execution boundary to another.
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
238pub enum BoundaryCompatibility {
239    Compatible,
240    Incompatible,
241}
242
243/// Determines whether a type can cross from one execution boundary to another.
244#[must_use]
245pub fn boundary_compatibility(
246    semantic_type: &SemanticType,
247    source: ExecutionBoundary,
248    target: ExecutionBoundary,
249) -> BoundaryCompatibility {
250    if source == target {
251        return BoundaryCompatibility::Compatible;
252    }
253    let compatible = match semantic_type {
254        SemanticType::Resource(resource) => {
255            resource.execution_boundary == ResourceExecutionBoundary::Shared
256                && serialization_compatibility(semantic_type)
257                    == SerializationCompatibility::Serializable
258        }
259        _ => serialization_compatibility(semantic_type) == SerializationCompatibility::Serializable,
260    };
261    if compatible {
262        BoundaryCompatibility::Compatible
263    } else {
264        BoundaryCompatibility::Incompatible
265    }
266}
267
268/// Canonicalizes equivalent semantic type forms for stable equality and output.
269#[must_use]
270pub fn normalize_semantic_type(semantic_type: SemanticType) -> SemanticType {
271    match semantic_type {
272        SemanticType::Array(element) => {
273            SemanticType::Array(Box::new(normalize_semantic_type(*element)))
274        }
275        SemanticType::Tuple(items) => {
276            SemanticType::Tuple(items.into_iter().map(normalize_semantic_type).collect())
277        }
278        SemanticType::Object(object) => SemanticType::Object(ObjectType {
279            properties: object
280                .properties
281                .into_iter()
282                .map(|(name, semantic_type)| (name, normalize_semantic_type(semantic_type)))
283                .collect(),
284        }),
285        SemanticType::Union(members) => normalize_union(members),
286        SemanticType::Resource(resource) => SemanticType::Resource(ResourceType {
287            data: Box::new(normalize_semantic_type(*resource.data)),
288            error: Box::new(normalize_semantic_type(*resource.error)),
289            pending: resource.pending,
290            serializable: resource.serializable,
291            execution_boundary: resource.execution_boundary,
292        }),
293        semantic_type => semantic_type,
294    }
295}
296
297fn normalize_union(members: Vec<SemanticType>) -> SemanticType {
298    let mut flattened = Vec::new();
299    for member in members {
300        match normalize_semantic_type(member) {
301            SemanticType::Union(items) => flattened.extend(items),
302            SemanticType::Never => {}
303            member => flattened.push(member),
304        }
305    }
306    flattened.sort_by_key(semantic_type_sort_key);
307    flattened.dedup();
308    match flattened.as_slice() {
309        [] => SemanticType::Never,
310        [member] => member.clone(),
311        _ => SemanticType::Union(flattened),
312    }
313}
314
315fn semantic_type_sort_key(semantic_type: &SemanticType) -> String {
316    format!("{semantic_type:?}")
317}
318
319/// Determines structural serialization compatibility for a canonical type.
320#[must_use]
321pub fn serialization_compatibility(semantic_type: &SemanticType) -> SerializationCompatibility {
322    let serializable = match semantic_type {
323        SemanticType::Unknown
324        | SemanticType::Never
325        | SemanticType::Form
326        | SemanticType::SlotContent => false,
327        SemanticType::Null
328        | SemanticType::Boolean
329        | SemanticType::Number
330        | SemanticType::String
331        | SemanticType::BooleanLiteral(_)
332        | SemanticType::NumberLiteral(_)
333        | SemanticType::StringLiteral(_) => true,
334        SemanticType::Array(element) => {
335            serialization_compatibility(element) == SerializationCompatibility::Serializable
336        }
337        SemanticType::Tuple(items) | SemanticType::Union(items) => items.iter().all(|item| {
338            serialization_compatibility(item) == SerializationCompatibility::Serializable
339        }),
340        SemanticType::Object(object) => object.properties.values().all(|property| {
341            serialization_compatibility(property) == SerializationCompatibility::Serializable
342        }),
343        SemanticType::Resource(resource) => {
344            resource.serializable
345                && serialization_compatibility(&resource.data)
346                    == SerializationCompatibility::Serializable
347                && serialization_compatibility(&resource.error)
348                    == SerializationCompatibility::Serializable
349        }
350    };
351    if serializable {
352        SerializationCompatibility::Serializable
353    } else {
354        SerializationCompatibility::NotSerializable
355    }
356}
357
358/// Structural object shape in the canonical semantic type algebra.
359///
360/// C1 establishes only the representation. Object declaration lowering,
361/// property provenance, and member resolution are later Phase C slices.
362#[derive(Debug, Clone, PartialEq, Eq, Default)]
363pub struct ObjectType {
364    pub properties: BTreeMap<String, SemanticType>,
365}
366
367/// Canonical type assignments owned by the application semantic model.
368///
369/// C1 provides the stable container but deliberately populates no assignments.
370/// State inference, expression propagation, and typed declarations are added by
371/// later Phase C slices.
372#[derive(Debug, Clone, PartialEq, Eq, Default)]
373pub struct SemanticTypeModel {
374    pub assignments: BTreeMap<SemanticId, SemanticTypeAssignment>,
375    pub aliases: BTreeMap<SemanticId, SemanticTypeAlias>,
376    pub list_scopes: BTreeMap<SemanticId, ListTemplateScopeType>,
377    pub member_accesses: BTreeMap<SemanticId, MemberAccessType>,
378    pub computed_values: BTreeMap<SemanticId, ComputedValueType>,
379    pub action_signatures: BTreeMap<SemanticId, ActionSignatureType>,
380    pub effect_statements: BTreeMap<SemanticId, EffectStatementTypeRecord>,
381}
382
383/// Compatibility facts recorded by effect typing without issuing F5 diagnostics.
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum EffectCompatibility {
386    Compatible,
387    Incompatible,
388    Unknown,
389}
390
391/// The compiler-owned classification of one authored effect statement.
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393pub enum EffectOperationClassification {
394    RecognizedCapability,
395    UnknownExternalCapability,
396    ReactiveStateAssignment,
397    ComponentActionCall,
398    ComponentEffectCall,
399    ComponentMethodCall,
400    UnresolvedComponentCall,
401    UnresolvedComponentAssignment,
402    BareReturn,
403    ValueReturn,
404    Empty,
405    UnsupportedStatement,
406}
407
408/// Immutable F4 typing and compatibility facts keyed by effect statement identity.
409#[derive(Debug, Clone, PartialEq, Eq)]
410pub struct EffectStatementTypeRecord {
411    pub statement: SemanticId,
412    pub operation_classification: EffectOperationClassification,
413    pub operand_types: Vec<SemanticType>,
414    pub target_type: Option<SemanticType>,
415    pub signature_compatibility: EffectCompatibility,
416    pub boundary_compatibility: EffectCompatibility,
417    pub serialization_compatibility: EffectCompatibility,
418    pub capability_operation: Option<CapabilityOperationId>,
419    pub provenance: SourceProvenance,
420}
421
422/// Canonical item and index type information for one template list scope.
423#[derive(Debug, Clone, PartialEq, Eq)]
424pub struct ListTemplateScopeType {
425    pub list: SemanticId,
426    pub item_name: String,
427    pub item_type: SemanticType,
428    pub index_name: Option<String>,
429    pub index_type: Option<SemanticType>,
430}
431
432/// Canonical result of one supported template list-item member access.
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct MemberAccessType {
435    pub entity: SemanticId,
436    pub expression: String,
437    pub semantic_type: Option<SemanticType>,
438}
439
440/// Canonical semantic type contract for one computed getter.
441#[derive(Debug, Clone, PartialEq, Eq)]
442pub struct ComputedValueType {
443    pub computed: SemanticId,
444    pub method: SemanticId,
445    pub semantic_type: SemanticType,
446    pub status: SemanticTypeStatus,
447    pub declared_return_type: Option<SemanticType>,
448    pub declared_return_compatible: Option<bool>,
449    pub serialization: SerializationCompatibility,
450    pub execution_boundary: ExecutionBoundary,
451    pub boundary_compatibility: BoundaryCompatibility,
452    pub provenance: SourceProvenance,
453}
454
455/// Canonical input/output type contract for one decorator-marked action method.
456#[derive(Debug, Clone, PartialEq, Eq)]
457pub struct ActionSignatureType {
458    pub method: SemanticId,
459    pub input: Vec<(SemanticId, SemanticType)>,
460    pub output: Option<SemanticType>,
461    pub is_async: bool,
462}
463
464/// Stable identity for one compiler-owned type assignment.
465#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
466pub struct SemanticTypeId(SemanticId);
467
468impl SemanticTypeId {
469    #[must_use]
470    pub fn for_subject(subject: &SemanticId) -> Self {
471        Self(subject.semantic_type())
472    }
473
474    #[must_use]
475    pub fn as_semantic_id(&self) -> &SemanticId {
476        &self.0
477    }
478}
479
480impl fmt::Display for SemanticTypeId {
481    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
482        self.0.fmt(formatter)
483    }
484}
485
486/// Whether a semantic type came from an authored declaration or compiler inference.
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum SemanticTypeStatus {
489    Declared,
490    Inferred,
491}
492
493/// Stable families for diagnostics produced from canonical semantic types.
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum TypeDiagnosticFamily {
496    UnknownType,
497    InvalidOperator,
498    IncompatibleAssignment,
499    MissingMember,
500    InvalidCondition,
501    NonIterableList,
502    NonRenderableValue,
503    InvalidBinding,
504    NonSerializableState,
505}
506
507/// Stable codes for diagnostics produced from canonical semantic types.
508#[derive(Debug, Clone, Copy, PartialEq, Eq)]
509pub enum TypeDiagnosticCode {
510    IncompatibleStateInitializer,
511    IncompatibleAssignment,
512    InvalidToggleTarget,
513    InvalidNumericMutationTarget,
514    InvalidCompoundMutationTarget,
515    InvalidCompoundMutationOperand,
516    InvalidArithmeticOperator,
517    InvalidComparisonOperator,
518    InvalidLogicalOperator,
519    InvalidNullishOperator,
520    InvalidUnaryOperator,
521    NonRenderableValue,
522    InvalidBinding,
523    InvalidCondition,
524    NonIterableList,
525    MissingMember,
526    UnknownType,
527    NonSerializableState,
528}
529
530impl TypeDiagnosticCode {
531    #[must_use]
532    pub const fn as_str(self) -> &'static str {
533        match self {
534            Self::IncompatibleStateInitializer => "PSC1016",
535            Self::IncompatibleAssignment => "PSC1017",
536            Self::InvalidToggleTarget => "PSC1018",
537            Self::InvalidNumericMutationTarget => "PSC1019",
538            Self::InvalidCompoundMutationTarget => "PSC1020",
539            Self::InvalidCompoundMutationOperand => "PSC1021",
540            Self::InvalidArithmeticOperator => "PSC1022",
541            Self::InvalidComparisonOperator => "PSC1023",
542            Self::InvalidLogicalOperator => "PSC1024",
543            Self::InvalidNullishOperator => "PSC1025",
544            Self::InvalidUnaryOperator => "PSC1026",
545            Self::NonRenderableValue => "PSC1027",
546            Self::InvalidBinding => "PSC1028",
547            Self::InvalidCondition => "PSC1029",
548            Self::NonIterableList => "PSC1030",
549            Self::MissingMember => "PSC1031",
550            Self::UnknownType => "PSC1032",
551            Self::NonSerializableState => "PSC1033",
552        }
553    }
554
555    #[must_use]
556    pub const fn family(self) -> TypeDiagnosticFamily {
557        match self {
558            Self::UnknownType => TypeDiagnosticFamily::UnknownType,
559            Self::InvalidArithmeticOperator
560            | Self::InvalidComparisonOperator
561            | Self::InvalidLogicalOperator
562            | Self::InvalidNullishOperator
563            | Self::InvalidUnaryOperator => TypeDiagnosticFamily::InvalidOperator,
564            Self::IncompatibleStateInitializer
565            | Self::IncompatibleAssignment
566            | Self::InvalidToggleTarget
567            | Self::InvalidNumericMutationTarget
568            | Self::InvalidCompoundMutationTarget
569            | Self::InvalidCompoundMutationOperand => TypeDiagnosticFamily::IncompatibleAssignment,
570            Self::MissingMember => TypeDiagnosticFamily::MissingMember,
571            Self::InvalidCondition => TypeDiagnosticFamily::InvalidCondition,
572            Self::NonIterableList => TypeDiagnosticFamily::NonIterableList,
573            Self::NonRenderableValue => TypeDiagnosticFamily::NonRenderableValue,
574            Self::InvalidBinding => TypeDiagnosticFamily::InvalidBinding,
575            Self::NonSerializableState => TypeDiagnosticFamily::NonSerializableState,
576        }
577    }
578}
579
580/// A type assignment with canonical identity, semantic origin, and authored location.
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct SemanticTypeAssignment {
583    pub id: SemanticTypeId,
584    pub subject: SemanticId,
585    pub semantic_type: SemanticType,
586    pub origin: SemanticId,
587    pub status: SemanticTypeStatus,
588    pub provenance: SourceProvenance,
589}
590
591/// A named authored type alias resolved to canonical semantic type meaning.
592#[derive(Debug, Clone, PartialEq, Eq)]
593pub struct SemanticTypeAlias {
594    pub id: SemanticId,
595    pub name: String,
596    pub semantic_type: SemanticType,
597    pub provenance: SourceProvenance,
598}
599
600/// Canonical result of resolving one authored type annotation through the
601/// existing type and binding authorities.
602#[derive(Debug, Clone, PartialEq, Eq)]
603pub struct ResolvedDeclaredSemanticType {
604    pub semantic_type: SemanticType,
605    pub alias_origin: Option<SemanticId>,
606}
607
608impl SemanticTypeModel {
609    #[must_use]
610    pub fn resolve_declared_type(
611        &self,
612        declared_type: &DeclaredStateType,
613        bindings: Option<&BindingTable>,
614    ) -> Option<ResolvedDeclaredSemanticType> {
615        let local_alias = self.aliases.values().find(|alias| {
616            alias.provenance.path == declared_type.provenance.path
617                && alias.name == declared_type.text
618        });
619        let imported_alias = bindings
620            .and_then(|bindings| {
621                bindings.resolve_import(&declared_type.provenance.path, &declared_type.text)
622            })
623            .and_then(|binding| match &binding.target {
624                ImportBindingTarget::Symbol(symbol) if symbol.kind == SymbolKind::TypeAlias => {
625                    self.aliases.get(&symbol.id)
626                }
627                _ => None,
628            });
629        let alias = local_alias.or(imported_alias);
630        let semantic_type = alias
631            .map(|alias| alias.semantic_type.clone())
632            .or_else(|| semantic_type_from_annotation(&declared_type.text))?;
633        Some(ResolvedDeclaredSemanticType {
634            semantic_type: normalize_semantic_type(semantic_type),
635            alias_origin: alias.map(|alias| alias.id.clone()),
636        })
637    }
638
639    #[must_use]
640    pub fn from_components(
641        components: &[ComponentNode],
642        provenance: &BTreeMap<SemanticId, SourceProvenance>,
643    ) -> Self {
644        Self::from_components_with_aliases_and_bindings(components, provenance, &[], None)
645    }
646
647    #[must_use]
648    pub fn from_components_with_aliases(
649        components: &[ComponentNode],
650        provenance: &BTreeMap<SemanticId, SourceProvenance>,
651        parsed_aliases: &[(PathBuf, ParsedTypeAlias)],
652    ) -> Self {
653        Self::from_components_with_aliases_and_bindings(
654            components,
655            provenance,
656            parsed_aliases,
657            None,
658        )
659    }
660
661    #[must_use]
662    pub fn from_components_with_aliases_and_bindings(
663        components: &[ComponentNode],
664        provenance: &BTreeMap<SemanticId, SourceProvenance>,
665        parsed_aliases: &[(PathBuf, ParsedTypeAlias)],
666        bindings: Option<&BindingTable>,
667    ) -> Self {
668        let aliases = parsed_aliases
669            .iter()
670            .filter_map(|(path, alias)| {
671                semantic_type_from_annotation(&alias.type_text).map(|semantic_type| {
672                    let id = SemanticId::type_alias_in_module(path, &alias.name);
673                    (
674                        id.clone(),
675                        SemanticTypeAlias {
676                            id,
677                            name: alias.name.clone(),
678                            semantic_type,
679                            provenance: SourceProvenance::new(path, alias.type_span),
680                        },
681                    )
682                })
683            })
684            .collect::<BTreeMap<_, _>>();
685        let aliases_by_path_and_name = aliases
686            .values()
687            .map(|alias| ((alias.provenance.path.clone(), alias.name.clone()), alias))
688            .collect::<BTreeMap<_, _>>();
689        let mut assignments = BTreeMap::new();
690
691        for field in components
692            .iter()
693            .flat_map(|component| &component.state_fields)
694        {
695            let (semantic_type, origin, status, assignment_provenance) =
696                if let Some(declared_type) = &field.declared_type {
697                    let alias = aliases_by_path_and_name
698                        .get(&(
699                            declared_type.provenance.path.clone(),
700                            declared_type.text.clone(),
701                        ))
702                        .copied();
703                    let imported_alias = bindings
704                        .and_then(|bindings| {
705                            bindings
706                                .resolve_import(&declared_type.provenance.path, &declared_type.text)
707                        })
708                        .and_then(|binding| match &binding.target {
709                            ImportBindingTarget::Symbol(symbol)
710                                if symbol.kind == SymbolKind::TypeAlias =>
711                            {
712                                aliases.get(&symbol.id)
713                            }
714                            _ => None,
715                        });
716                    let alias = alias.or(imported_alias);
717                    let semantic_type = alias
718                        .map(|alias| alias.semantic_type.clone())
719                        .or_else(|| semantic_type_from_annotation(&declared_type.text));
720                    let Some(semantic_type) = semantic_type else {
721                        continue;
722                    };
723                    (
724                        semantic_type,
725                        alias.map_or_else(|| field.id.clone(), |alias| alias.id.clone()),
726                        SemanticTypeStatus::Declared,
727                        declared_type.provenance.clone(),
728                    )
729                } else {
730                    let Some(value) = &field.initial_value else {
731                        continue;
732                    };
733                    let Some(field_provenance) = provenance.get(&field.id) else {
734                        continue;
735                    };
736                    (
737                        infer_serializable_value_type(value),
738                        field.id.clone(),
739                        SemanticTypeStatus::Inferred,
740                        field_provenance.clone(),
741                    )
742                };
743            assignments.insert(
744                field.id.clone(),
745                SemanticTypeAssignment {
746                    id: SemanticTypeId::for_subject(&field.id),
747                    subject: field.id.clone(),
748                    semantic_type,
749                    origin,
750                    status,
751                    provenance: assignment_provenance,
752                },
753            );
754        }
755
756        extend_with_local_type_assignments(&mut assignments, components, provenance);
757        extend_with_parameter_type_assignments(&mut assignments, components, &aliases, bindings);
758        extend_with_method_return_type_assignments(
759            &mut assignments,
760            components,
761            provenance,
762            &aliases,
763            bindings,
764        );
765
766        Self::from_assignments_and_aliases(assignments, aliases)
767            .with_action_signature_types(components)
768    }
769
770    /// Attaches explicit G1 Context declaration contracts without performing
771    /// provider/consumer compatibility analysis.
772    #[must_use]
773    pub fn with_context_types(
774        mut self,
775        contexts: &BTreeMap<crate::ContextId, ContextEntity>,
776    ) -> Self {
777        for context in contexts.values() {
778            let semantic_type = self
779                .aliases
780                .values()
781                .find(|alias| {
782                    alias.provenance.path == context.declared_type.provenance.path
783                        && alias.name == context.declared_type.text
784                })
785                .map_or_else(
786                    || {
787                        semantic_type_from_annotation(&context.declared_type.text)
788                            .unwrap_or(SemanticType::Unknown)
789                    },
790                    |alias| alias.semantic_type.clone(),
791                );
792            let subject = context.id.as_semantic_id().clone();
793            self.assignments.insert(
794                subject.clone(),
795                SemanticTypeAssignment {
796                    id: context.declared_type_id.clone(),
797                    subject: subject.clone(),
798                    semantic_type,
799                    origin: subject,
800                    status: SemanticTypeStatus::Declared,
801                    provenance: context.provenance.clone(),
802                },
803            );
804        }
805        self
806    }
807
808    /// Attaches compiler-issued Resource declaration types after endpoint
809    /// resolution has established their exact data, error, and boundary facts.
810    #[must_use]
811    pub fn with_resource_types(
812        mut self,
813        resources: &BTreeMap<crate::ResourceId, crate::ResourceDeclaration>,
814    ) -> Self {
815        for resource in resources.values() {
816            let subject = resource.id.as_semantic_id().clone();
817            self.assignments.insert(
818                subject.clone(),
819                SemanticTypeAssignment {
820                    id: SemanticTypeId::for_subject(&subject),
821                    subject: subject.clone(),
822                    semantic_type: SemanticType::Resource(ResourceType {
823                        data: Box::new(resource.data_type.clone()),
824                        error: Box::new(resource.error_type.clone()),
825                        pending: true,
826                        serializable: true,
827                        execution_boundary: resource.execution_boundary,
828                    }),
829                    origin: subject,
830                    status: SemanticTypeStatus::Declared,
831                    provenance: resource.provenance.clone(),
832                },
833            );
834        }
835        self
836    }
837
838    /// Attaches the exact nominal H1 `SlotContent` type to canonical Slot
839    /// entities. Alias and structural compatibility are intentionally absent.
840    #[must_use]
841    pub fn with_slot_types(mut self, slots: &BTreeMap<crate::SlotId, SlotEntity>) -> Self {
842        for slot in slots.values() {
843            let subject = slot.id.as_semantic_id().clone();
844            self.assignments.insert(
845                subject.clone(),
846                SemanticTypeAssignment {
847                    id: SemanticTypeId::for_subject(&subject),
848                    subject: subject.clone(),
849                    semantic_type: SemanticType::SlotContent,
850                    origin: subject,
851                    status: SemanticTypeStatus::Declared,
852                    provenance: slot.provenance.clone(),
853                },
854            );
855        }
856        self
857    }
858
859    /// Attaches the exact nominal I2 `Form` marker to canonical Form entities.
860    /// Aliases, unions, generics, and structural compatibility never enter
861    /// this authority because invalid declaration candidates do not lower.
862    #[must_use]
863    pub fn with_form_types(mut self, forms: &BTreeMap<crate::FormId, crate::FormEntity>) -> Self {
864        for form in forms.values() {
865            let subject = form.id.as_semantic_id().clone();
866            self.assignments.insert(
867                subject.clone(),
868                SemanticTypeAssignment {
869                    id: SemanticTypeId::for_subject(&subject),
870                    subject: subject.clone(),
871                    semantic_type: SemanticType::Form,
872                    origin: subject,
873                    status: SemanticTypeStatus::Declared,
874                    provenance: form.provenance.clone(),
875                },
876            );
877        }
878        self
879    }
880
881    /// Attaches I3 Form Field assignments produced by the canonical field
882    /// resolver. No annotation parsing, inference, or assignability is repeated.
883    #[must_use]
884    pub fn with_form_field_types(
885        mut self,
886        fields: &BTreeMap<crate::FieldId, crate::FormFieldEntity>,
887    ) -> Self {
888        self.assignments.extend(fields.values().map(|field| {
889            (
890                field.id.as_semantic_id().clone(),
891                field.type_assignment.clone(),
892            )
893        }));
894        self
895    }
896
897    /// Attaches explicit G2 Provider declaration contracts without deciding
898    /// provider-value or Context compatibility.
899    #[must_use]
900    pub fn with_provider_types(
901        mut self,
902        providers: &BTreeMap<crate::ProviderId, ProviderEntity>,
903    ) -> Self {
904        for provider in providers.values() {
905            let semantic_type = self
906                .aliases
907                .values()
908                .find(|alias| {
909                    alias.provenance.path == provider.declared_type.provenance.path
910                        && alias.name == provider.declared_type.text
911                })
912                .map_or_else(
913                    || {
914                        semantic_type_from_annotation(&provider.declared_type.text)
915                            .unwrap_or(SemanticType::Unknown)
916                    },
917                    |alias| alias.semantic_type.clone(),
918                );
919            let subject = provider.id.as_semantic_id().clone();
920            self.assignments.insert(
921                subject.clone(),
922                SemanticTypeAssignment {
923                    id: provider.declared_type_id.clone(),
924                    subject: subject.clone(),
925                    semantic_type,
926                    origin: subject,
927                    status: SemanticTypeStatus::Declared,
928                    provenance: provider.provenance.clone(),
929                },
930            );
931        }
932        self
933    }
934
935    /// Attaches explicit G3 Consumer requested-type contracts without deciding
936    /// Context, Provider, or Consumer compatibility.
937    #[must_use]
938    pub fn with_consumer_types(
939        mut self,
940        consumers: &BTreeMap<crate::ConsumerId, ConsumerEntity>,
941    ) -> Self {
942        for consumer in consumers.values() {
943            let semantic_type = self
944                .aliases
945                .values()
946                .find(|alias| {
947                    alias.provenance.path == consumer.requested_type.provenance.path
948                        && alias.name == consumer.requested_type.text
949                })
950                .map_or_else(
951                    || {
952                        semantic_type_from_annotation(&consumer.requested_type.text)
953                            .unwrap_or(SemanticType::Unknown)
954                    },
955                    |alias| alias.semantic_type.clone(),
956                );
957            let subject = consumer.id.as_semantic_id().clone();
958            self.assignments.insert(
959                subject.clone(),
960                SemanticTypeAssignment {
961                    id: consumer.requested_type_id.clone(),
962                    subject: subject.clone(),
963                    semantic_type,
964                    origin: subject,
965                    status: SemanticTypeStatus::Declared,
966                    provenance: consumer.provenance.clone(),
967                },
968            );
969        }
970        self
971    }
972
973    fn from_assignments_and_aliases(
974        assignments: BTreeMap<SemanticId, SemanticTypeAssignment>,
975        aliases: BTreeMap<SemanticId, SemanticTypeAlias>,
976    ) -> Self {
977        Self {
978            assignments,
979            aliases,
980            list_scopes: BTreeMap::new(),
981            member_accesses: BTreeMap::new(),
982            computed_values: BTreeMap::new(),
983            action_signatures: BTreeMap::new(),
984            effect_statements: BTreeMap::new(),
985        }
986    }
987
988    /// Attaches canonical inferred and declared contracts to computed entities.
989    ///
990    /// # Panics
991    ///
992    /// Panics when a computed expression ID does not resolve in `graph`.
993    #[allow(clippy::too_many_lines)]
994    #[must_use]
995    pub fn with_computed_value_types(
996        mut self,
997        components: &[ComponentNode],
998        computed_values: &BTreeMap<SemanticId, ComputedValue>,
999        graph: &ExpressionGraph,
1000        references: &[SemanticReference],
1001    ) -> Self {
1002        let computed_components = components
1003            .iter()
1004            .flat_map(|component| {
1005                component
1006                    .methods
1007                    .iter()
1008                    .filter(|method| method.is_computed())
1009                    .map(move |method| (component.id.computed(&method.name), component))
1010            })
1011            .collect::<BTreeMap<_, _>>();
1012        let mut computed_types = BTreeMap::new();
1013        let mut expression_types = BTreeMap::new();
1014
1015        for computed in computed_values.values() {
1016            let mut visiting = BTreeSet::new();
1017            infer_computed_type(
1018                &computed.id,
1019                graph,
1020                &self.assignments,
1021                &computed_components,
1022                computed_values,
1023                references,
1024                &mut computed_types,
1025                &mut expression_types,
1026                &mut visiting,
1027            );
1028        }
1029
1030        for (id, semantic_type) in expression_types {
1031            let node = graph.node(&id).expect("computed expression graph node");
1032            self.assignments.insert(
1033                id.clone(),
1034                SemanticTypeAssignment {
1035                    id: SemanticTypeId::for_subject(&id),
1036                    subject: id,
1037                    semantic_type,
1038                    origin: node.owner.clone(),
1039                    status: SemanticTypeStatus::Inferred,
1040                    provenance: node.provenance.clone(),
1041                },
1042            );
1043        }
1044
1045        for computed in computed_values.values() {
1046            let has_expression = graph.root_for(&computed.id).is_some();
1047            let inferred = computed_types
1048                .get(&computed.id)
1049                .cloned()
1050                .unwrap_or(SemanticType::Unknown);
1051            let method_assignment = self.assignments.get(&computed.method);
1052            let declared_return_type = method_assignment
1053                .filter(|assignment| assignment.status == SemanticTypeStatus::Declared)
1054                .map(|assignment| assignment.semantic_type.clone());
1055            let declared_return_compatible = declared_return_type
1056                .as_ref()
1057                .filter(|_| has_expression)
1058                .map(|declared| is_assignable(&inferred, declared));
1059            let (semantic_type, status, origin) = if has_expression {
1060                (
1061                    inferred,
1062                    SemanticTypeStatus::Inferred,
1063                    computed.method.clone(),
1064                )
1065            } else if let Some(assignment) = method_assignment {
1066                (
1067                    assignment.semantic_type.clone(),
1068                    assignment.status,
1069                    assignment.origin.clone(),
1070                )
1071            } else {
1072                (
1073                    SemanticType::Unknown,
1074                    SemanticTypeStatus::Inferred,
1075                    computed.method.clone(),
1076                )
1077            };
1078            let serialization = serialization_compatibility(&semantic_type);
1079            let boundary_compatibility = boundary_compatibility(
1080                &semantic_type,
1081                computed.execution_boundary,
1082                ExecutionBoundary::Client,
1083            );
1084
1085            self.assignments.insert(
1086                computed.id.clone(),
1087                SemanticTypeAssignment {
1088                    id: SemanticTypeId::for_subject(&computed.id),
1089                    subject: computed.id.clone(),
1090                    semantic_type: semantic_type.clone(),
1091                    origin,
1092                    status,
1093                    provenance: computed.provenance.clone(),
1094                },
1095            );
1096            self.computed_values.insert(
1097                computed.id.clone(),
1098                ComputedValueType {
1099                    computed: computed.id.clone(),
1100                    method: computed.method.clone(),
1101                    semantic_type,
1102                    status,
1103                    declared_return_type,
1104                    declared_return_compatible,
1105                    serialization,
1106                    execution_boundary: computed.execution_boundary,
1107                    boundary_compatibility,
1108                    provenance: computed.provenance.clone(),
1109                },
1110            );
1111        }
1112
1113        self
1114    }
1115
1116    /// Attaches inferred types to canonical Provider and Context-default
1117    /// expressions using their already-resolved component ownership and the
1118    /// existing State/Computed type products. This creates no Context
1119    /// compatibility result; G5 consumes these facts separately.
1120    #[must_use]
1121    pub fn with_context_source_expression_types(
1122        mut self,
1123        components: &[ComponentNode],
1124        contexts: &BTreeMap<crate::ContextId, ContextEntity>,
1125        providers: &BTreeMap<crate::ProviderId, ProviderEntity>,
1126        graph: &ExpressionGraph,
1127    ) -> Self {
1128        let components_by_id = components
1129            .iter()
1130            .map(|component| (component.id.clone(), component))
1131            .collect::<BTreeMap<_, _>>();
1132        let owners = contexts
1133            .values()
1134            .filter_map(|context| {
1135                context
1136                    .owner
1137                    .entity_id()
1138                    .map(|owner| (context.id.as_semantic_id().clone(), owner.clone()))
1139            })
1140            .chain(providers.values().filter_map(|provider| {
1141                provider
1142                    .owner
1143                    .entity_id()
1144                    .map(|owner| (provider.id.as_semantic_id().clone(), owner.clone()))
1145            }))
1146            .collect::<BTreeMap<_, _>>();
1147        let expression_ids = owners
1148            .keys()
1149            .flat_map(|owner| graph.nodes_for(owner))
1150            .map(|node| node.id.clone())
1151            .collect::<Vec<_>>();
1152        let mut inferred = BTreeMap::new();
1153        for id in expression_ids {
1154            infer_context_source_expression_type(
1155                &id,
1156                graph,
1157                &self.assignments,
1158                &owners,
1159                &components_by_id,
1160                &mut inferred,
1161            );
1162        }
1163        for (id, semantic_type) in inferred {
1164            let Some(node) = graph.node(&id) else {
1165                continue;
1166            };
1167            self.assignments.insert(
1168                id.clone(),
1169                SemanticTypeAssignment {
1170                    id: SemanticTypeId::for_subject(&id),
1171                    subject: id,
1172                    semantic_type,
1173                    origin: node.owner.clone(),
1174                    status: SemanticTypeStatus::Inferred,
1175                    provenance: node.provenance.clone(),
1176                },
1177            );
1178        }
1179        self
1180    }
1181
1182    fn with_action_signature_types(mut self, components: &[ComponentNode]) -> Self {
1183        for method in components
1184            .iter()
1185            .flat_map(|component| &component.methods)
1186            .filter(|method| method.is_action())
1187        {
1188            let input = method
1189                .parameters
1190                .iter()
1191                .filter_map(|parameter| {
1192                    self.assignments
1193                        .get(&parameter.id)
1194                        .map(|assignment| (parameter.id.clone(), assignment.semantic_type.clone()))
1195                })
1196                .collect();
1197            let output = self
1198                .assignments
1199                .get(&method.id)
1200                .map(|assignment| assignment.semantic_type.clone());
1201            self.action_signatures.insert(
1202                method.id.clone(),
1203                ActionSignatureType {
1204                    method: method.id.clone(),
1205                    input,
1206                    output,
1207                    is_async: method.is_async,
1208                },
1209            );
1210        }
1211        self
1212    }
1213
1214    #[must_use]
1215    pub fn normalized(mut self) -> Self {
1216        for assignment in self.assignments.values_mut() {
1217            assignment.semantic_type = normalize_semantic_type(assignment.semantic_type.clone());
1218        }
1219        for alias in self.aliases.values_mut() {
1220            alias.semantic_type = normalize_semantic_type(alias.semantic_type.clone());
1221        }
1222        for scope in self.list_scopes.values_mut() {
1223            scope.item_type = normalize_semantic_type(scope.item_type.clone());
1224            if let Some(index_type) = &mut scope.index_type {
1225                *index_type = normalize_semantic_type(index_type.clone());
1226            }
1227        }
1228        for access in self.member_accesses.values_mut() {
1229            if let Some(semantic_type) = &mut access.semantic_type {
1230                *semantic_type = normalize_semantic_type(semantic_type.clone());
1231            }
1232        }
1233        for computed in self.computed_values.values_mut() {
1234            computed.semantic_type = normalize_semantic_type(computed.semantic_type.clone());
1235        }
1236        for action in self.action_signatures.values_mut() {
1237            for (_, input) in &mut action.input {
1238                *input = normalize_semantic_type(input.clone());
1239            }
1240            if let Some(output) = &mut action.output {
1241                *output = normalize_semantic_type(output.clone());
1242            }
1243        }
1244        for statement in self.effect_statements.values_mut() {
1245            statement.operand_types = statement
1246                .operand_types
1247                .iter()
1248                .cloned()
1249                .map(normalize_semantic_type)
1250                .collect();
1251            if let Some(target_type) = &mut statement.target_type {
1252                *target_type = normalize_semantic_type(target_type.clone());
1253            }
1254        }
1255        self
1256    }
1257
1258    /// Attaches inferred types to canonical declaration expression nodes.
1259    ///
1260    /// # Panics
1261    ///
1262    /// Panics when the graph's node index contains an ID that does not resolve
1263    /// to an expression node.
1264    #[must_use]
1265    pub fn with_expression_types(
1266        mut self,
1267        graph: &ExpressionGraph,
1268        components: &[ComponentNode],
1269        contexts: &BTreeMap<crate::ContextId, ContextEntity>,
1270        providers: &BTreeMap<crate::ProviderId, ProviderEntity>,
1271    ) -> Self {
1272        let declaration_owners = components
1273            .iter()
1274            .flat_map(|component| component.state_fields.iter().map(|field| field.id.clone()))
1275            .chain(
1276                contexts
1277                    .keys()
1278                    .map(|context| context.as_semantic_id().clone()),
1279            )
1280            .chain(
1281                providers
1282                    .keys()
1283                    .map(|provider| provider.as_semantic_id().clone()),
1284            )
1285            .collect::<BTreeSet<_>>();
1286        let nodes = graph
1287            .nodes
1288            .values()
1289            .filter(|node| declaration_owners.contains(&node.owner))
1290            .map(|node| node.id.clone())
1291            .collect::<Vec<_>>();
1292        for id in nodes {
1293            let semantic_type = expression_semantic_type(&id, graph, &self.assignments);
1294            let node = graph.node(&id).expect("expression graph node");
1295            self.assignments.insert(
1296                id.clone(),
1297                SemanticTypeAssignment {
1298                    id: SemanticTypeId::for_subject(&id),
1299                    subject: id,
1300                    semantic_type,
1301                    origin: node.owner.clone(),
1302                    status: SemanticTypeStatus::Inferred,
1303                    provenance: node.provenance.clone(),
1304                },
1305            );
1306        }
1307        self
1308    }
1309
1310    /// Attaches effect operand types and statement-level capability facts.
1311    ///
1312    /// This consumes only the immutable registry and existing component/ASM
1313    /// products. It intentionally records incompatibilities without emitting
1314    /// semantic diagnostics; F5 owns language-rule rejection.
1315    ///
1316    /// # Panics
1317    ///
1318    /// Panics when the graph's effect expression index contains an ID that
1319    /// does not resolve to an expression node.
1320    #[must_use]
1321    pub fn with_effect_statement_types(
1322        mut self,
1323        components: &[ComponentNode],
1324        effects: &BTreeMap<SemanticId, Effect>,
1325        statements: &BTreeMap<SemanticId, EffectStatement>,
1326        graph: &ExpressionGraph,
1327    ) -> Self {
1328        let components_by_id = components
1329            .iter()
1330            .map(|component| (component.id.clone(), component))
1331            .collect::<BTreeMap<_, _>>();
1332        let effect_nodes = effects
1333            .values()
1334            .flat_map(|effect| graph.nodes_for(&effect.id))
1335            .map(|node| node.id.clone())
1336            .collect::<Vec<_>>();
1337        let mut expression_types = BTreeMap::new();
1338        for id in effect_nodes {
1339            infer_effect_expression_type(
1340                &id,
1341                graph,
1342                &self.assignments,
1343                &components_by_id,
1344                effects,
1345                &mut expression_types,
1346            );
1347        }
1348        for (id, semantic_type) in expression_types {
1349            let node = graph.node(&id).expect("effect expression graph node");
1350            self.assignments.insert(
1351                id.clone(),
1352                SemanticTypeAssignment {
1353                    id: SemanticTypeId::for_subject(&id),
1354                    subject: id,
1355                    semantic_type,
1356                    origin: node.owner.clone(),
1357                    status: SemanticTypeStatus::Inferred,
1358                    provenance: node.provenance.clone(),
1359                },
1360            );
1361        }
1362        for statement in statements.values() {
1363            let Some(effect) = effects.get(&statement.owner) else {
1364                continue;
1365            };
1366            let Some(component_id) = effect.owner.entity_id() else {
1367                continue;
1368            };
1369            let Some(component) = components_by_id.get(component_id) else {
1370                continue;
1371            };
1372            let record = effect_statement_type_record(
1373                statement,
1374                effect,
1375                component,
1376                graph,
1377                &self.assignments,
1378            );
1379            self.effect_statements.insert(statement.id.clone(), record);
1380        }
1381        self
1382    }
1383
1384    /// Attaches inferred types to direct text-binding entities with resolved
1385    /// state or local targets.
1386    #[must_use]
1387    pub fn with_template_binding_types(
1388        mut self,
1389        entities: &[TemplateSemanticEntity],
1390        references: &[SemanticReference],
1391    ) -> Self {
1392        for entity in entities.iter().filter(|entity| {
1393            matches!(
1394                entity.kind,
1395                TemplateSemanticKind::Binding
1396                    | TemplateSemanticKind::AttributeBinding
1397                    | TemplateSemanticKind::Conditional
1398            )
1399        }) {
1400            let Some(reference) = references.iter().find(|reference| {
1401                reference.source == entity.id
1402                    && matches!(
1403                        reference.kind,
1404                        SemanticReferenceKind::TemplateState
1405                            | SemanticReferenceKind::TemplateComputed
1406                            | SemanticReferenceKind::TemplateLocal
1407                    )
1408            }) else {
1409                continue;
1410            };
1411            let Some(target) = self.assignments.get(&reference.target) else {
1412                continue;
1413            };
1414            self.assignments.insert(
1415                entity.id.clone(),
1416                SemanticTypeAssignment {
1417                    id: SemanticTypeId::for_subject(&entity.id),
1418                    subject: entity.id.clone(),
1419                    semantic_type: target.semantic_type.clone(),
1420                    origin: target.origin.clone(),
1421                    status: SemanticTypeStatus::Inferred,
1422                    provenance: entity.provenance.clone(),
1423                },
1424            );
1425        }
1426        self.with_list_scope_types(entities, references)
1427            .with_list_member_types(entities)
1428    }
1429
1430    fn with_list_scope_types(
1431        mut self,
1432        entities: &[TemplateSemanticEntity],
1433        references: &[SemanticReference],
1434    ) -> Self {
1435        for entity in entities
1436            .iter()
1437            .filter(|entity| entity.kind == TemplateSemanticKind::List)
1438        {
1439            let Some(reference) = references.iter().find(|reference| {
1440                reference.source == entity.id
1441                    && matches!(
1442                        reference.kind,
1443                        SemanticReferenceKind::TemplateState
1444                            | SemanticReferenceKind::TemplateComputed
1445                    )
1446            }) else {
1447                continue;
1448            };
1449            let Some(iterable) = self.assignments.get(&reference.target) else {
1450                continue;
1451            };
1452            let iterable_type = iterable.semantic_type.clone();
1453            let iterable_origin = iterable.origin.clone();
1454            self.assignments.insert(
1455                entity.id.clone(),
1456                SemanticTypeAssignment {
1457                    id: SemanticTypeId::for_subject(&entity.id),
1458                    subject: entity.id.clone(),
1459                    semantic_type: iterable_type.clone(),
1460                    origin: iterable_origin,
1461                    status: SemanticTypeStatus::Inferred,
1462                    provenance: entity.provenance.clone(),
1463                },
1464            );
1465            let Some((item_name, item_type)) = entity
1466                .list_item_variable
1467                .as_ref()
1468                .zip(iterable_element_type(&iterable_type))
1469            else {
1470                continue;
1471            };
1472            self.list_scopes.insert(
1473                entity.id.clone(),
1474                ListTemplateScopeType {
1475                    list: entity.id.clone(),
1476                    item_name: item_name.clone(),
1477                    item_type,
1478                    index_name: entity.list_index_variable.clone(),
1479                    index_type: entity
1480                        .list_index_variable
1481                        .as_ref()
1482                        .map(|_| SemanticType::Number),
1483                },
1484            );
1485        }
1486        self
1487    }
1488
1489    fn with_list_member_types(mut self, entities: &[TemplateSemanticEntity]) -> Self {
1490        for entity in entities.iter().filter(|entity| {
1491            matches!(
1492                entity.kind,
1493                TemplateSemanticKind::Binding | TemplateSemanticKind::AttributeBinding
1494            ) && entity.scope == crate::TemplateSemanticScope::ListItem
1495        }) {
1496            let Some(expression) = entity.expression.as_deref() else {
1497                continue;
1498            };
1499            let Some((root, path)) = expression.split_once('.') else {
1500                continue;
1501            };
1502            let scopes = self
1503                .list_scopes
1504                .values()
1505                .filter(|scope| scope.item_name == root)
1506                .collect::<Vec<_>>();
1507            if scopes.len() != 1 {
1508                continue;
1509            }
1510            let semantic_type = member_access_type(&scopes[0].item_type, path);
1511            self.member_accesses.insert(
1512                entity.id.clone(),
1513                MemberAccessType {
1514                    entity: entity.id.clone(),
1515                    expression: expression.to_string(),
1516                    semantic_type: semantic_type.clone(),
1517                },
1518            );
1519            if let Some(semantic_type) = semantic_type {
1520                self.assignments.insert(
1521                    entity.id.clone(),
1522                    SemanticTypeAssignment {
1523                        id: SemanticTypeId::for_subject(&entity.id),
1524                        subject: entity.id.clone(),
1525                        semantic_type,
1526                        origin: scopes[0].list.clone(),
1527                        status: SemanticTypeStatus::Inferred,
1528                        provenance: entity.provenance.clone(),
1529                    },
1530                );
1531            }
1532        }
1533        self
1534    }
1535}
1536
1537fn member_access_type(semantic_type: &SemanticType, path: &str) -> Option<SemanticType> {
1538    let mut current = semantic_type;
1539    for member in path.split('.') {
1540        if matches!(current, SemanticType::Unknown) {
1541            return Some(SemanticType::Unknown);
1542        }
1543        let SemanticType::Object(object) = current else {
1544            return None;
1545        };
1546        current = object.properties.get(member)?;
1547    }
1548    Some(current.clone())
1549}
1550
1551fn iterable_element_type(semantic_type: &SemanticType) -> Option<SemanticType> {
1552    match semantic_type {
1553        SemanticType::Array(element) => Some((**element).clone()),
1554        SemanticType::Tuple(items) if items.is_empty() => Some(SemanticType::Unknown),
1555        SemanticType::Tuple(items) if items.windows(2).all(|pair| pair[0] == pair[1]) => {
1556            items.first().cloned()
1557        }
1558        SemanticType::Tuple(items) => Some(SemanticType::Union(items.clone())),
1559        SemanticType::Unknown => Some(SemanticType::Unknown),
1560        _ => None,
1561    }
1562}
1563
1564fn extend_with_local_type_assignments(
1565    assignments: &mut BTreeMap<SemanticId, SemanticTypeAssignment>,
1566    components: &[ComponentNode],
1567    provenance: &BTreeMap<SemanticId, SourceProvenance>,
1568) {
1569    for local in components
1570        .iter()
1571        .flat_map(|component| &component.methods)
1572        .flat_map(|method| &method.local_variables)
1573    {
1574        let Some(local_provenance) = provenance.get(&local.id) else {
1575            continue;
1576        };
1577        assignments.insert(
1578            local.id.clone(),
1579            SemanticTypeAssignment {
1580                id: SemanticTypeId::for_subject(&local.id),
1581                subject: local.id.clone(),
1582                semantic_type: infer_serializable_value_type(&local.value),
1583                origin: local.id.clone(),
1584                status: SemanticTypeStatus::Inferred,
1585                provenance: local_provenance.clone(),
1586            },
1587        );
1588    }
1589}
1590
1591fn extend_with_parameter_type_assignments(
1592    assignments: &mut BTreeMap<SemanticId, SemanticTypeAssignment>,
1593    components: &[ComponentNode],
1594    aliases: &BTreeMap<SemanticId, SemanticTypeAlias>,
1595    bindings: Option<&BindingTable>,
1596) {
1597    let aliases_by_path_and_name = aliases
1598        .values()
1599        .map(|alias| ((alias.provenance.path.clone(), alias.name.clone()), alias))
1600        .collect::<BTreeMap<_, _>>();
1601
1602    for parameter in components
1603        .iter()
1604        .flat_map(|component| &component.methods)
1605        .flat_map(|method| &method.parameters)
1606    {
1607        let Some(declared_type) = &parameter.declared_type else {
1608            continue;
1609        };
1610        let alias = aliases_by_path_and_name
1611            .get(&(
1612                declared_type.provenance.path.clone(),
1613                declared_type.text.clone(),
1614            ))
1615            .copied();
1616        let imported_alias = bindings
1617            .and_then(|bindings| {
1618                bindings.resolve_import(&declared_type.provenance.path, &declared_type.text)
1619            })
1620            .and_then(|binding| match &binding.target {
1621                ImportBindingTarget::Symbol(symbol) if symbol.kind == SymbolKind::TypeAlias => {
1622                    aliases.get(&symbol.id)
1623                }
1624                _ => None,
1625            });
1626        let alias = alias.or(imported_alias);
1627        let semantic_type = alias
1628            .map(|alias| alias.semantic_type.clone())
1629            .or_else(|| semantic_type_from_annotation(&declared_type.text));
1630        let Some(semantic_type) = semantic_type else {
1631            continue;
1632        };
1633        assignments.insert(
1634            parameter.id.clone(),
1635            SemanticTypeAssignment {
1636                id: SemanticTypeId::for_subject(&parameter.id),
1637                subject: parameter.id.clone(),
1638                semantic_type,
1639                origin: alias.map_or_else(|| parameter.id.clone(), |alias| alias.id.clone()),
1640                status: SemanticTypeStatus::Declared,
1641                provenance: declared_type.provenance.clone(),
1642            },
1643        );
1644    }
1645}
1646
1647fn extend_with_method_return_type_assignments(
1648    assignments: &mut BTreeMap<SemanticId, SemanticTypeAssignment>,
1649    components: &[ComponentNode],
1650    provenance: &BTreeMap<SemanticId, SourceProvenance>,
1651    aliases: &BTreeMap<SemanticId, SemanticTypeAlias>,
1652    bindings: Option<&BindingTable>,
1653) {
1654    let aliases_by_path_and_name = aliases
1655        .values()
1656        .map(|alias| ((alias.provenance.path.clone(), alias.name.clone()), alias))
1657        .collect::<BTreeMap<_, _>>();
1658
1659    for method in components.iter().flat_map(|component| &component.methods) {
1660        let declared = method.declared_return_type.as_ref();
1661        let (semantic_type, origin, status, assignment_provenance) =
1662            if let Some(declared) = declared {
1663                let alias = aliases_by_path_and_name
1664                    .get(&(declared.provenance.path.clone(), declared.text.clone()))
1665                    .copied()
1666                    .or_else(|| {
1667                        bindings
1668                            .and_then(|bindings| {
1669                                bindings.resolve_import(&declared.provenance.path, &declared.text)
1670                            })
1671                            .and_then(|binding| match &binding.target {
1672                                ImportBindingTarget::Symbol(symbol)
1673                                    if symbol.kind == SymbolKind::TypeAlias =>
1674                                {
1675                                    aliases.get(&symbol.id)
1676                                }
1677                                _ => None,
1678                            })
1679                    });
1680                let Some(semantic_type) = alias
1681                    .map(|alias| alias.semantic_type.clone())
1682                    .or_else(|| semantic_type_from_annotation(&declared.text))
1683                else {
1684                    continue;
1685                };
1686                (
1687                    semantic_type,
1688                    alias.map_or_else(|| method.id.clone(), |alias| alias.id.clone()),
1689                    SemanticTypeStatus::Declared,
1690                    declared.provenance.clone(),
1691                )
1692            } else {
1693                let Some(return_type) = inferred_return_type(&method.return_values) else {
1694                    continue;
1695                };
1696                let Some(method_provenance) = provenance.get(&method.id) else {
1697                    continue;
1698                };
1699                (
1700                    return_type,
1701                    method.id.clone(),
1702                    SemanticTypeStatus::Inferred,
1703                    method_provenance.clone(),
1704                )
1705            };
1706        assignments.insert(
1707            method.id.clone(),
1708            SemanticTypeAssignment {
1709                id: SemanticTypeId::for_subject(&method.id),
1710                subject: method.id.clone(),
1711                semantic_type,
1712                origin,
1713                status,
1714                provenance: assignment_provenance,
1715            },
1716        );
1717    }
1718}
1719
1720fn inferred_return_type(values: &[SerializableValue]) -> Option<SemanticType> {
1721    let mut types = values
1722        .iter()
1723        .map(infer_serializable_value_type)
1724        .collect::<Vec<_>>();
1725    match types.as_slice() {
1726        [] => None,
1727        [semantic_type] => Some(semantic_type.clone()),
1728        _ if types.windows(2).all(|window| window[0] == window[1]) => types.pop(),
1729        _ => Some(SemanticType::Union(types)),
1730    }
1731}
1732
1733#[allow(clippy::too_many_arguments)]
1734fn infer_effect_expression_type(
1735    id: &SemanticId,
1736    graph: &ExpressionGraph,
1737    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
1738    components: &BTreeMap<SemanticId, &ComponentNode>,
1739    effects: &BTreeMap<SemanticId, Effect>,
1740    expression_types: &mut BTreeMap<SemanticId, SemanticType>,
1741) -> SemanticType {
1742    if let Some(semantic_type) = expression_types.get(id) {
1743        return semantic_type.clone();
1744    }
1745    let node = graph.node(id).expect("effect expression graph node");
1746    let child_type =
1747        |child: &SemanticId, expression_types: &mut BTreeMap<SemanticId, SemanticType>| {
1748            infer_effect_expression_type(
1749                child,
1750                graph,
1751                assignments,
1752                components,
1753                effects,
1754                expression_types,
1755            )
1756        };
1757    let semantic_type = match &node.kind {
1758        ExpressionNodeKind::Literal(value) => state_initializer_value_type(value),
1759        ExpressionNodeKind::Boolean(value) => SemanticType::BooleanLiteral(*value),
1760        ExpressionNodeKind::Template { .. } => SemanticType::String,
1761        ExpressionNodeKind::Identifier(_) | ExpressionNodeKind::Call { .. } => {
1762            SemanticType::Unknown
1763        }
1764        ExpressionNodeKind::SemanticPackagePureCall {
1765            operation: crate::semantic_package::SemanticPackagePureOperation::Identity,
1766            arguments,
1767            ..
1768        } => arguments.first().map_or(SemanticType::Unknown, |argument| {
1769            child_type(argument, expression_types)
1770        }),
1771        ExpressionNodeKind::BuiltinPureCall {
1772            operation:
1773                crate::component_graph::BuiltinPureOperation::MathAbs
1774                | crate::component_graph::BuiltinPureOperation::MathFloor
1775                | crate::component_graph::BuiltinPureOperation::MathCeil
1776                | crate::component_graph::BuiltinPureOperation::MathRound
1777                | crate::component_graph::BuiltinPureOperation::MathMin
1778                | crate::component_graph::BuiltinPureOperation::MathMax,
1779            ..
1780        } => SemanticType::Number,
1781        ExpressionNodeKind::ThisMember { name } => effects
1782            .get(&node.owner)
1783            .and_then(|effect| effect.owner.entity_id())
1784            .and_then(|component_id| components.get(component_id))
1785            .and_then(|component| {
1786                component
1787                    .state_fields
1788                    .iter()
1789                    .find(|field| field.name == *name)
1790                    .map(|field| field.id.clone())
1791                    .or_else(|| {
1792                        let computed = component.id.computed(name);
1793                        assignments.contains_key(&computed).then_some(computed)
1794                    })
1795            })
1796            .and_then(|target| assignments.get(&target))
1797            .map_or(SemanticType::Unknown, |assignment| {
1798                assignment.semantic_type.clone()
1799            }),
1800        ExpressionNodeKind::MemberAccess {
1801            object, property, ..
1802        } => computed_member_access_type(&child_type(object, expression_types), property),
1803        ExpressionNodeKind::IndexAccess { object, index } => computed_index_access_type(
1804            &child_type(object, expression_types),
1805            &child_type(index, expression_types),
1806        ),
1807        ExpressionNodeKind::Conditional {
1808            when_true,
1809            when_false,
1810            ..
1811        } => conditional_result_type(
1812            child_type(when_true, expression_types),
1813            child_type(when_false, expression_types),
1814        ),
1815        ExpressionNodeKind::Arithmetic {
1816            left,
1817            right,
1818            operator,
1819        } => operator_result_type(
1820            SemanticOperator::Arithmetic(*operator),
1821            &[
1822                child_type(left, expression_types),
1823                child_type(right, expression_types),
1824            ],
1825        )
1826        .unwrap_or(SemanticType::Unknown),
1827        ExpressionNodeKind::Comparison {
1828            left,
1829            right,
1830            operator,
1831        } => operator_result_type(
1832            SemanticOperator::Comparison(*operator),
1833            &[
1834                child_type(left, expression_types),
1835                child_type(right, expression_types),
1836            ],
1837        )
1838        .unwrap_or(SemanticType::Unknown),
1839        ExpressionNodeKind::Logical {
1840            left,
1841            right,
1842            operator,
1843        } => operator_result_type(
1844            SemanticOperator::Logical(*operator),
1845            &[
1846                child_type(left, expression_types),
1847                child_type(right, expression_types),
1848            ],
1849        )
1850        .unwrap_or(SemanticType::Unknown),
1851        ExpressionNodeKind::NullishCoalescing { left, right } => operator_result_type(
1852            SemanticOperator::NullishCoalescing,
1853            &[
1854                child_type(left, expression_types),
1855                child_type(right, expression_types),
1856            ],
1857        )
1858        .unwrap_or(SemanticType::Unknown),
1859        ExpressionNodeKind::Unary { operand, operator } => operator_result_type(
1860            SemanticOperator::Unary(*operator),
1861            &[child_type(operand, expression_types)],
1862        )
1863        .unwrap_or(SemanticType::Unknown),
1864    };
1865    expression_types.insert(id.clone(), semantic_type.clone());
1866    semantic_type
1867}
1868
1869#[allow(clippy::too_many_lines)]
1870fn effect_statement_type_record(
1871    statement: &EffectStatement,
1872    effect: &Effect,
1873    component: &ComponentNode,
1874    graph: &ExpressionGraph,
1875    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
1876) -> EffectStatementTypeRecord {
1877    let expression_type = |id: &SemanticId| {
1878        assignments
1879            .get(id)
1880            .map_or(SemanticType::Unknown, |assignment| {
1881                assignment.semantic_type.clone()
1882            })
1883    };
1884    let record = |operation_classification,
1885                  operand_types,
1886                  target_type,
1887                  signature_compatibility,
1888                  boundary_compatibility,
1889                  serialization_compatibility,
1890                  capability_operation| EffectStatementTypeRecord {
1891        statement: statement.id.clone(),
1892        operation_classification,
1893        operand_types,
1894        target_type,
1895        signature_compatibility,
1896        boundary_compatibility,
1897        serialization_compatibility,
1898        capability_operation,
1899        provenance: statement.provenance.clone(),
1900    };
1901    match &statement.kind {
1902        EffectStatementKind::ExternalMemberAssignment { target, value } => {
1903            let value_type = expression_type(value);
1904            if let Some(name) = this_member_name(target, graph) {
1905                if let Some(field) = component
1906                    .state_fields
1907                    .iter()
1908                    .find(|field| field.name == name)
1909                {
1910                    let target_type = assignments
1911                        .get(&field.id)
1912                        .map(|assignment| assignment.semantic_type.clone());
1913                    let signature = target_type
1914                        .as_ref()
1915                        .map_or(EffectCompatibility::Unknown, |target| {
1916                            effect_assignability(&value_type, target)
1917                        });
1918                    return record(
1919                        EffectOperationClassification::ReactiveStateAssignment,
1920                        vec![value_type],
1921                        target_type,
1922                        signature,
1923                        EffectCompatibility::Compatible,
1924                        EffectCompatibility::Compatible,
1925                        None,
1926                    );
1927                }
1928                return record(
1929                    EffectOperationClassification::UnresolvedComponentAssignment,
1930                    vec![value_type],
1931                    None,
1932                    EffectCompatibility::Unknown,
1933                    EffectCompatibility::Unknown,
1934                    EffectCompatibility::Unknown,
1935                    None,
1936                );
1937            }
1938            let path = static_capability_path(target, graph);
1939            let operation = path.as_deref().and_then(|path| {
1940                EFFECT_CAPABILITY_REGISTRY
1941                    .operation_at(path, CapabilityOperationKind::MemberAssignment)
1942            });
1943            let Some(operation) = operation else {
1944                return record(
1945                    EffectOperationClassification::UnknownExternalCapability,
1946                    vec![value_type],
1947                    None,
1948                    EffectCompatibility::Unknown,
1949                    EffectCompatibility::Unknown,
1950                    EffectCompatibility::Unknown,
1951                    None,
1952                );
1953            };
1954            let target_type = operation_value_type(operation.signature.parameters, 0);
1955            let signature = target_type
1956                .as_ref()
1957                .map_or(EffectCompatibility::Unknown, |target| {
1958                    effect_assignability(&value_type, target)
1959                });
1960            record(
1961                EffectOperationClassification::RecognizedCapability,
1962                vec![value_type],
1963                target_type,
1964                signature,
1965                effect_boundary_compatibility(effect, operation.boundary),
1966                EffectCompatibility::Compatible,
1967                Some(operation.id),
1968            )
1969        }
1970        EffectStatementKind::CapabilityCall { callee, arguments } => {
1971            let argument_types = arguments.iter().map(expression_type).collect::<Vec<_>>();
1972            if let Some(name) = this_member_name(callee, graph) {
1973                let classification = component
1974                    .methods
1975                    .iter()
1976                    .find(|method| method.name == name)
1977                    .map_or(
1978                        EffectOperationClassification::UnresolvedComponentCall,
1979                        |method| {
1980                            if method.is_action() {
1981                                EffectOperationClassification::ComponentActionCall
1982                            } else if method.is_effect() {
1983                                EffectOperationClassification::ComponentEffectCall
1984                            } else {
1985                                EffectOperationClassification::ComponentMethodCall
1986                            }
1987                        },
1988                    );
1989                return record(
1990                    classification,
1991                    argument_types,
1992                    None,
1993                    EffectCompatibility::Unknown,
1994                    EffectCompatibility::Compatible,
1995                    EffectCompatibility::Compatible,
1996                    None,
1997                );
1998            }
1999            let path = static_capability_path(callee, graph);
2000            let operation = path.as_deref().and_then(|path| {
2001                EFFECT_CAPABILITY_REGISTRY.operation_at(path, CapabilityOperationKind::MethodCall)
2002            });
2003            let Some(operation) = operation else {
2004                return record(
2005                    EffectOperationClassification::UnknownExternalCapability,
2006                    argument_types,
2007                    None,
2008                    EffectCompatibility::Incompatible,
2009                    EffectCompatibility::Unknown,
2010                    EffectCompatibility::Unknown,
2011                    None,
2012                );
2013            };
2014            let signature =
2015                operation_signature_compatibility(operation.signature.parameters, &argument_types);
2016            let serialization = match operation.argument_serialization {
2017                crate::ArgumentSerializationPolicy::None => EffectCompatibility::Compatible,
2018                crate::ArgumentSerializationPolicy::Structural => argument_types.iter().fold(
2019                    EffectCompatibility::Compatible,
2020                    |compatibility, semantic_type| {
2021                        combine_effect_compatibility(
2022                            compatibility,
2023                            if serialization_compatibility(semantic_type)
2024                                == SerializationCompatibility::Serializable
2025                            {
2026                                EffectCompatibility::Compatible
2027                            } else {
2028                                EffectCompatibility::Incompatible
2029                            },
2030                        )
2031                    },
2032                ),
2033            };
2034            record(
2035                EffectOperationClassification::RecognizedCapability,
2036                argument_types,
2037                None,
2038                signature,
2039                effect_boundary_compatibility(effect, operation.boundary),
2040                serialization,
2041                Some(operation.id),
2042            )
2043        }
2044        EffectStatementKind::EffectReturn { value: None } => record(
2045            EffectOperationClassification::BareReturn,
2046            Vec::new(),
2047            None,
2048            EffectCompatibility::Compatible,
2049            EffectCompatibility::Compatible,
2050            EffectCompatibility::Compatible,
2051            None,
2052        ),
2053        EffectStatementKind::EffectReturn { value: Some(value) } => record(
2054            EffectOperationClassification::ValueReturn,
2055            vec![expression_type(value)],
2056            None,
2057            EffectCompatibility::Incompatible,
2058            EffectCompatibility::Compatible,
2059            EffectCompatibility::Compatible,
2060            None,
2061        ),
2062        EffectStatementKind::Empty => record(
2063            EffectOperationClassification::Empty,
2064            Vec::new(),
2065            None,
2066            EffectCompatibility::Compatible,
2067            EffectCompatibility::Compatible,
2068            EffectCompatibility::Compatible,
2069            None,
2070        ),
2071        EffectStatementKind::Unsupported(_) => record(
2072            EffectOperationClassification::UnsupportedStatement,
2073            Vec::new(),
2074            None,
2075            EffectCompatibility::Incompatible,
2076            EffectCompatibility::Unknown,
2077            EffectCompatibility::Unknown,
2078            None,
2079        ),
2080    }
2081}
2082
2083fn this_member_name<'a>(id: &SemanticId, graph: &'a ExpressionGraph) -> Option<&'a str> {
2084    match &graph.node(id)?.kind {
2085        ExpressionNodeKind::ThisMember { name } => Some(name),
2086        _ => None,
2087    }
2088}
2089
2090fn static_capability_path(id: &SemanticId, graph: &ExpressionGraph) -> Option<String> {
2091    match &graph.node(id)?.kind {
2092        ExpressionNodeKind::Identifier(name) if name != "this" => Some(name.clone()),
2093        ExpressionNodeKind::MemberAccess {
2094            object, property, ..
2095        } => Some(format!(
2096            "{}.{}",
2097            static_capability_path(object, graph)?,
2098            property
2099        )),
2100        _ => None,
2101    }
2102}
2103
2104fn operation_value_type(parameters: CapabilityParameters, index: usize) -> Option<SemanticType> {
2105    match parameters {
2106        CapabilityParameters::Fixed(parameters) => parameters
2107            .get(index)
2108            .and_then(|parameter| parameter.semantic_type()),
2109        CapabilityParameters::Variadic(parameter) => parameter.semantic_type(),
2110    }
2111}
2112
2113fn operation_signature_compatibility(
2114    parameters: CapabilityParameters,
2115    arguments: &[SemanticType],
2116) -> EffectCompatibility {
2117    match parameters {
2118        CapabilityParameters::Fixed(parameters) if parameters.len() != arguments.len() => {
2119            EffectCompatibility::Incompatible
2120        }
2121        CapabilityParameters::Fixed(parameters) => parameters.iter().zip(arguments).fold(
2122            EffectCompatibility::Compatible,
2123            |compatibility, (parameter, argument)| {
2124                combine_effect_compatibility(
2125                    compatibility,
2126                    capability_value_compatibility(*parameter, argument),
2127                )
2128            },
2129        ),
2130        CapabilityParameters::Variadic(parameter) => arguments.iter().fold(
2131            EffectCompatibility::Compatible,
2132            |compatibility, argument| {
2133                combine_effect_compatibility(
2134                    compatibility,
2135                    capability_value_compatibility(parameter, argument),
2136                )
2137            },
2138        ),
2139    }
2140}
2141
2142fn capability_value_compatibility(
2143    contract: CapabilityValueContract,
2144    value: &SemanticType,
2145) -> EffectCompatibility {
2146    match contract {
2147        CapabilityValueContract::String => effect_assignability(
2148            value,
2149            &contract
2150                .semantic_type()
2151                .expect("string capability contract type"),
2152        ),
2153        CapabilityValueContract::SerializableDiagnosticValue => {
2154            if serialization_compatibility(value) == SerializationCompatibility::Serializable {
2155                EffectCompatibility::Compatible
2156            } else {
2157                EffectCompatibility::Incompatible
2158            }
2159        }
2160    }
2161}
2162
2163fn effect_assignability(source: &SemanticType, target: &SemanticType) -> EffectCompatibility {
2164    if source == &SemanticType::Unknown || source == &SemanticType::Never {
2165        EffectCompatibility::Unknown
2166    } else if is_assignable(source, target) {
2167        EffectCompatibility::Compatible
2168    } else {
2169        EffectCompatibility::Incompatible
2170    }
2171}
2172
2173fn effect_boundary_compatibility(
2174    effect: &Effect,
2175    operation_boundary: ExecutionBoundary,
2176) -> EffectCompatibility {
2177    if effect.execution_boundary == operation_boundary {
2178        EffectCompatibility::Compatible
2179    } else {
2180        EffectCompatibility::Incompatible
2181    }
2182}
2183
2184fn combine_effect_compatibility(
2185    left: EffectCompatibility,
2186    right: EffectCompatibility,
2187) -> EffectCompatibility {
2188    match (left, right) {
2189        (EffectCompatibility::Incompatible, _) | (_, EffectCompatibility::Incompatible) => {
2190            EffectCompatibility::Incompatible
2191        }
2192        (EffectCompatibility::Unknown, _) | (_, EffectCompatibility::Unknown) => {
2193            EffectCompatibility::Unknown
2194        }
2195        _ => EffectCompatibility::Compatible,
2196    }
2197}
2198
2199fn expression_semantic_type(
2200    id: &SemanticId,
2201    graph: &ExpressionGraph,
2202    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
2203) -> SemanticType {
2204    let node = graph.node(id).expect("expression graph node");
2205    let child_type = |id: &SemanticId| {
2206        assignments.get(id).map_or_else(
2207            || expression_semantic_type(id, graph, assignments),
2208            |assignment| assignment.semantic_type.clone(),
2209        )
2210    };
2211
2212    match &node.kind {
2213        ExpressionNodeKind::Literal(value) => state_initializer_value_type(value),
2214        ExpressionNodeKind::Boolean(value) => SemanticType::BooleanLiteral(*value),
2215        ExpressionNodeKind::Template { .. } => SemanticType::String,
2216        ExpressionNodeKind::Identifier(_)
2217        | ExpressionNodeKind::Call { .. }
2218        | ExpressionNodeKind::SemanticPackagePureCall { .. }
2219        | ExpressionNodeKind::BuiltinPureCall { .. }
2220        | ExpressionNodeKind::ThisMember { .. }
2221        | ExpressionNodeKind::MemberAccess { .. }
2222        | ExpressionNodeKind::IndexAccess { .. }
2223        | ExpressionNodeKind::Conditional { .. } => SemanticType::Unknown,
2224        ExpressionNodeKind::Arithmetic {
2225            left,
2226            right,
2227            operator,
2228        } => operator_result_type(
2229            SemanticOperator::Arithmetic(*operator),
2230            &[child_type(left), child_type(right)],
2231        )
2232        .unwrap_or(SemanticType::Unknown),
2233        ExpressionNodeKind::Comparison {
2234            left,
2235            right,
2236            operator,
2237        } => operator_result_type(
2238            SemanticOperator::Comparison(*operator),
2239            &[child_type(left), child_type(right)],
2240        )
2241        .unwrap_or(SemanticType::Unknown),
2242        ExpressionNodeKind::Logical {
2243            left,
2244            right,
2245            operator,
2246        } => operator_result_type(
2247            SemanticOperator::Logical(*operator),
2248            &[child_type(left), child_type(right)],
2249        )
2250        .unwrap_or(SemanticType::Unknown),
2251        ExpressionNodeKind::NullishCoalescing { left, right } => operator_result_type(
2252            SemanticOperator::NullishCoalescing,
2253            &[child_type(left), child_type(right)],
2254        )
2255        .unwrap_or(SemanticType::Unknown),
2256        ExpressionNodeKind::Unary { operand, operator } => {
2257            operator_result_type(SemanticOperator::Unary(*operator), &[child_type(operand)])
2258                .unwrap_or(SemanticType::Unknown)
2259        }
2260    }
2261}
2262
2263#[allow(clippy::too_many_arguments)]
2264fn infer_computed_type(
2265    computed: &SemanticId,
2266    graph: &ExpressionGraph,
2267    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
2268    computed_components: &BTreeMap<SemanticId, &ComponentNode>,
2269    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2270    references: &[SemanticReference],
2271    computed_types: &mut BTreeMap<SemanticId, SemanticType>,
2272    expression_types: &mut BTreeMap<SemanticId, SemanticType>,
2273    visiting: &mut BTreeSet<SemanticId>,
2274) -> SemanticType {
2275    if let Some(semantic_type) = computed_types.get(computed) {
2276        return semantic_type.clone();
2277    }
2278    if !visiting.insert(computed.clone()) {
2279        return SemanticType::Unknown;
2280    }
2281
2282    let semantic_type = graph
2283        .root_for(computed)
2284        .map_or(SemanticType::Unknown, |root| {
2285            infer_computed_expression_type(
2286                root,
2287                graph,
2288                assignments,
2289                computed_components,
2290                computed_values,
2291                references,
2292                computed_types,
2293                expression_types,
2294                visiting,
2295            )
2296        });
2297    visiting.remove(computed);
2298    computed_types.insert(computed.clone(), semantic_type.clone());
2299    semantic_type
2300}
2301
2302fn infer_context_source_expression_type(
2303    id: &SemanticId,
2304    graph: &ExpressionGraph,
2305    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
2306    owners: &BTreeMap<SemanticId, SemanticId>,
2307    components: &BTreeMap<SemanticId, &ComponentNode>,
2308    inferred: &mut BTreeMap<SemanticId, SemanticType>,
2309) -> SemanticType {
2310    if let Some(semantic_type) = inferred.get(id) {
2311        return semantic_type.clone();
2312    }
2313    let Some(node) = graph.node(id) else {
2314        return SemanticType::Unknown;
2315    };
2316    let child = |child: &SemanticId, inferred: &mut BTreeMap<SemanticId, SemanticType>| {
2317        infer_context_source_expression_type(
2318            child,
2319            graph,
2320            assignments,
2321            owners,
2322            components,
2323            inferred,
2324        )
2325    };
2326    let semantic_type = match &node.kind {
2327        ExpressionNodeKind::Literal(value) => state_initializer_value_type(value),
2328        ExpressionNodeKind::Boolean(value) => SemanticType::BooleanLiteral(*value),
2329        ExpressionNodeKind::Template { .. } => SemanticType::String,
2330        ExpressionNodeKind::Identifier(_) | ExpressionNodeKind::Call { .. } => {
2331            SemanticType::Unknown
2332        }
2333        ExpressionNodeKind::SemanticPackagePureCall {
2334            operation: crate::semantic_package::SemanticPackagePureOperation::Identity,
2335            arguments,
2336            ..
2337        } => arguments
2338            .first()
2339            .map_or(SemanticType::Unknown, |argument| child(argument, inferred)),
2340        ExpressionNodeKind::BuiltinPureCall {
2341            operation:
2342                crate::component_graph::BuiltinPureOperation::MathAbs
2343                | crate::component_graph::BuiltinPureOperation::MathFloor
2344                | crate::component_graph::BuiltinPureOperation::MathCeil
2345                | crate::component_graph::BuiltinPureOperation::MathRound
2346                | crate::component_graph::BuiltinPureOperation::MathMin
2347                | crate::component_graph::BuiltinPureOperation::MathMax,
2348            ..
2349        } => SemanticType::Number,
2350        ExpressionNodeKind::ThisMember { name } => owners
2351            .get(&node.owner)
2352            .and_then(|owner| components.get(owner))
2353            .and_then(|component| {
2354                component
2355                    .state_fields
2356                    .iter()
2357                    .find(|field| field.name == *name)
2358                    .map(|field| field.id.clone())
2359                    .or_else(|| {
2360                        let computed = component.id.computed(name);
2361                        assignments.contains_key(&computed).then_some(computed)
2362                    })
2363            })
2364            .and_then(|target| assignments.get(&target))
2365            .map_or(SemanticType::Unknown, |assignment| {
2366                assignment.semantic_type.clone()
2367            }),
2368        ExpressionNodeKind::MemberAccess {
2369            object, property, ..
2370        } => computed_member_access_type(&child(object, inferred), property),
2371        ExpressionNodeKind::IndexAccess { object, index } => {
2372            computed_index_access_type(&child(object, inferred), &child(index, inferred))
2373        }
2374        ExpressionNodeKind::Conditional {
2375            when_true,
2376            when_false,
2377            ..
2378        } => conditional_result_type(child(when_true, inferred), child(when_false, inferred)),
2379        ExpressionNodeKind::Arithmetic {
2380            left,
2381            right,
2382            operator,
2383        } => operator_result_type(
2384            SemanticOperator::Arithmetic(*operator),
2385            &[child(left, inferred), child(right, inferred)],
2386        )
2387        .unwrap_or(SemanticType::Unknown),
2388        ExpressionNodeKind::Comparison {
2389            left,
2390            right,
2391            operator,
2392        } => operator_result_type(
2393            SemanticOperator::Comparison(*operator),
2394            &[child(left, inferred), child(right, inferred)],
2395        )
2396        .unwrap_or(SemanticType::Unknown),
2397        ExpressionNodeKind::Logical {
2398            left,
2399            right,
2400            operator,
2401        } => operator_result_type(
2402            SemanticOperator::Logical(*operator),
2403            &[child(left, inferred), child(right, inferred)],
2404        )
2405        .unwrap_or(SemanticType::Unknown),
2406        ExpressionNodeKind::NullishCoalescing { left, right } => operator_result_type(
2407            SemanticOperator::NullishCoalescing,
2408            &[child(left, inferred), child(right, inferred)],
2409        )
2410        .unwrap_or(SemanticType::Unknown),
2411        ExpressionNodeKind::Unary { operand, operator } => operator_result_type(
2412            SemanticOperator::Unary(*operator),
2413            &[child(operand, inferred)],
2414        )
2415        .unwrap_or(SemanticType::Unknown),
2416    };
2417    inferred.insert(id.clone(), semantic_type.clone());
2418    semantic_type
2419}
2420
2421#[allow(clippy::too_many_arguments)]
2422fn infer_computed_expression_type(
2423    id: &SemanticId,
2424    graph: &ExpressionGraph,
2425    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
2426    computed_components: &BTreeMap<SemanticId, &ComponentNode>,
2427    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2428    references: &[SemanticReference],
2429    computed_types: &mut BTreeMap<SemanticId, SemanticType>,
2430    expression_types: &mut BTreeMap<SemanticId, SemanticType>,
2431    visiting: &mut BTreeSet<SemanticId>,
2432) -> SemanticType {
2433    if let Some(semantic_type) = expression_types.get(id) {
2434        return semantic_type.clone();
2435    }
2436
2437    let node = graph.node(id).expect("computed expression graph node");
2438    let child_type = |child: &SemanticId,
2439                      expression_types: &mut BTreeMap<SemanticId, SemanticType>,
2440                      computed_types: &mut BTreeMap<SemanticId, SemanticType>,
2441                      visiting: &mut BTreeSet<SemanticId>| {
2442        infer_computed_expression_type(
2443            child,
2444            graph,
2445            assignments,
2446            computed_components,
2447            computed_values,
2448            references,
2449            computed_types,
2450            expression_types,
2451            visiting,
2452        )
2453    };
2454    let semantic_type = match &node.kind {
2455        ExpressionNodeKind::Literal(value) => state_initializer_value_type(value),
2456        ExpressionNodeKind::Boolean(value) => SemanticType::BooleanLiteral(*value),
2457        ExpressionNodeKind::Template { .. } => SemanticType::String,
2458        ExpressionNodeKind::Identifier(_) | ExpressionNodeKind::Call { .. } => {
2459            SemanticType::Unknown
2460        }
2461        ExpressionNodeKind::SemanticPackagePureCall {
2462            operation: crate::semantic_package::SemanticPackagePureOperation::Identity,
2463            arguments,
2464            ..
2465        } => arguments.first().map_or(SemanticType::Unknown, |argument| {
2466            child_type(argument, expression_types, computed_types, visiting)
2467        }),
2468        ExpressionNodeKind::BuiltinPureCall {
2469            operation:
2470                crate::component_graph::BuiltinPureOperation::MathAbs
2471                | crate::component_graph::BuiltinPureOperation::MathFloor
2472                | crate::component_graph::BuiltinPureOperation::MathCeil
2473                | crate::component_graph::BuiltinPureOperation::MathRound
2474                | crate::component_graph::BuiltinPureOperation::MathMin
2475                | crate::component_graph::BuiltinPureOperation::MathMax,
2476            ..
2477        } => SemanticType::Number,
2478        ExpressionNodeKind::ThisMember { name } => infer_computed_read_type(
2479            &node.owner,
2480            name,
2481            assignments,
2482            computed_components,
2483            computed_values,
2484            references,
2485            graph,
2486            computed_types,
2487            expression_types,
2488            visiting,
2489        ),
2490        ExpressionNodeKind::MemberAccess {
2491            object, property, ..
2492        } => {
2493            let object = child_type(object, expression_types, computed_types, visiting);
2494            computed_member_access_type(&object, property)
2495        }
2496        ExpressionNodeKind::IndexAccess { object, index } => {
2497            let object = child_type(object, expression_types, computed_types, visiting);
2498            let index = child_type(index, expression_types, computed_types, visiting);
2499            computed_index_access_type(&object, &index)
2500        }
2501        ExpressionNodeKind::Conditional {
2502            when_true,
2503            when_false,
2504            ..
2505        } => conditional_result_type(
2506            child_type(when_true, expression_types, computed_types, visiting),
2507            child_type(when_false, expression_types, computed_types, visiting),
2508        ),
2509        ExpressionNodeKind::Arithmetic {
2510            left,
2511            right,
2512            operator,
2513        } => operator_result_type(
2514            SemanticOperator::Arithmetic(*operator),
2515            &[
2516                child_type(left, expression_types, computed_types, visiting),
2517                child_type(right, expression_types, computed_types, visiting),
2518            ],
2519        )
2520        .unwrap_or(SemanticType::Unknown),
2521        ExpressionNodeKind::Comparison {
2522            left,
2523            right,
2524            operator,
2525        } => operator_result_type(
2526            SemanticOperator::Comparison(*operator),
2527            &[
2528                child_type(left, expression_types, computed_types, visiting),
2529                child_type(right, expression_types, computed_types, visiting),
2530            ],
2531        )
2532        .unwrap_or(SemanticType::Unknown),
2533        ExpressionNodeKind::Logical {
2534            left,
2535            right,
2536            operator,
2537        } => operator_result_type(
2538            SemanticOperator::Logical(*operator),
2539            &[
2540                child_type(left, expression_types, computed_types, visiting),
2541                child_type(right, expression_types, computed_types, visiting),
2542            ],
2543        )
2544        .unwrap_or(SemanticType::Unknown),
2545        ExpressionNodeKind::NullishCoalescing { left, right } => operator_result_type(
2546            SemanticOperator::NullishCoalescing,
2547            &[
2548                child_type(left, expression_types, computed_types, visiting),
2549                child_type(right, expression_types, computed_types, visiting),
2550            ],
2551        )
2552        .unwrap_or(SemanticType::Unknown),
2553        ExpressionNodeKind::Unary { operand, operator } => operator_result_type(
2554            SemanticOperator::Unary(*operator),
2555            &[child_type(
2556                operand,
2557                expression_types,
2558                computed_types,
2559                visiting,
2560            )],
2561        )
2562        .unwrap_or(SemanticType::Unknown),
2563    };
2564    expression_types.insert(id.clone(), semantic_type.clone());
2565    semantic_type
2566}
2567
2568#[allow(clippy::too_many_arguments)]
2569fn infer_computed_read_type(
2570    computed: &SemanticId,
2571    name: &str,
2572    assignments: &BTreeMap<SemanticId, SemanticTypeAssignment>,
2573    computed_components: &BTreeMap<SemanticId, &ComponentNode>,
2574    computed_values: &BTreeMap<SemanticId, ComputedValue>,
2575    references: &[SemanticReference],
2576    graph: &ExpressionGraph,
2577    computed_types: &mut BTreeMap<SemanticId, SemanticType>,
2578    expression_types: &mut BTreeMap<SemanticId, SemanticType>,
2579    visiting: &mut BTreeSet<SemanticId>,
2580) -> SemanticType {
2581    let Some(component) = computed_components.get(computed) else {
2582        return SemanticType::Unknown;
2583    };
2584    let target = component
2585        .state_fields
2586        .iter()
2587        .find(|field| field.name == name)
2588        .map(|field| field.id.clone())
2589        .or_else(|| {
2590            let target = component.id.computed(name);
2591            computed_values.contains_key(&target).then_some(target)
2592        })
2593        .or_else(|| {
2594            let target = component.id.resource(name);
2595            assignments.contains_key(&target).then_some(target)
2596        });
2597    let Some(target) = target else {
2598        return SemanticType::Unknown;
2599    };
2600    if !references
2601        .iter()
2602        .any(|reference| reference.source == *computed && reference.target == target)
2603    {
2604        return SemanticType::Unknown;
2605    }
2606    if computed_values.contains_key(&target) {
2607        return infer_computed_type(
2608            &target,
2609            graph,
2610            assignments,
2611            computed_components,
2612            computed_values,
2613            references,
2614            computed_types,
2615            expression_types,
2616            visiting,
2617        );
2618    }
2619    assignments
2620        .get(&target)
2621        .map_or(SemanticType::Unknown, |assignment| {
2622            assignment.semantic_type.clone()
2623        })
2624}
2625
2626fn computed_member_access_type(semantic_type: &SemanticType, property: &str) -> SemanticType {
2627    match semantic_type {
2628        SemanticType::Object(object) => object
2629            .properties
2630            .get(property)
2631            .cloned()
2632            .unwrap_or(SemanticType::Unknown),
2633        SemanticType::Resource(resource) => match property {
2634            "data" => normalize_semantic_type(SemanticType::Union(vec![
2635                resource.data.as_ref().clone(),
2636                SemanticType::Null,
2637            ])),
2638            "error" => normalize_semantic_type(SemanticType::Union(vec![
2639                resource.error.as_ref().clone(),
2640                SemanticType::Null,
2641            ])),
2642            "state" => SemanticType::String,
2643            _ => SemanticType::Unknown,
2644        },
2645        _ => SemanticType::Unknown,
2646    }
2647}
2648
2649fn computed_index_access_type(object: &SemanticType, index: &SemanticType) -> SemanticType {
2650    match (object, index) {
2651        (SemanticType::Tuple(items), SemanticType::NumberLiteral(index)) => index
2652            .parse::<usize>()
2653            .ok()
2654            .and_then(|index| items.get(index))
2655            .cloned()
2656            .unwrap_or(SemanticType::Unknown),
2657        (SemanticType::Array(element), SemanticType::Number | SemanticType::NumberLiteral(_)) => {
2658            element.as_ref().clone()
2659        }
2660        (SemanticType::Object(object), SemanticType::StringLiteral(property)) => object
2661            .properties
2662            .get(property)
2663            .cloned()
2664            .unwrap_or(SemanticType::Unknown),
2665        _ => SemanticType::Unknown,
2666    }
2667}
2668
2669fn conditional_result_type(when_true: SemanticType, when_false: SemanticType) -> SemanticType {
2670    if when_true == when_false {
2671        when_true
2672    } else {
2673        normalize_semantic_type(SemanticType::Union(vec![when_true, when_false]))
2674    }
2675}
2676
2677/// Returns the result type for one valid semantic operator application.
2678///
2679/// An absent result means the supplied operand types are not valid for that
2680/// operator. Unknown operands deliberately propagate `unknown` rather than
2681/// being treated as a valid concrete operation.
2682#[must_use]
2683pub fn operator_result_type(
2684    operator: SemanticOperator,
2685    operands: &[SemanticType],
2686) -> Option<SemanticType> {
2687    match operator {
2688        SemanticOperator::Arithmetic(_) | SemanticOperator::Comparison(_) => {
2689            let [left, right] = operands else {
2690                return None;
2691            };
2692            if left == &SemanticType::Unknown || right == &SemanticType::Unknown {
2693                Some(SemanticType::Unknown)
2694            } else if is_number_like(left) && is_number_like(right) {
2695                Some(match operator {
2696                    SemanticOperator::Arithmetic(_) => SemanticType::Number,
2697                    SemanticOperator::Comparison(_) => SemanticType::Boolean,
2698                    _ => unreachable!("operator category was matched above"),
2699                })
2700            } else {
2701                None
2702            }
2703        }
2704        SemanticOperator::Logical(_) => {
2705            let [left, right] = operands else {
2706                return None;
2707            };
2708            if left == &SemanticType::Unknown || right == &SemanticType::Unknown {
2709                Some(SemanticType::Unknown)
2710            } else if is_boolean_like(left) && is_boolean_like(right) {
2711                Some(SemanticType::Boolean)
2712            } else {
2713                None
2714            }
2715        }
2716        SemanticOperator::NullishCoalescing => {
2717            let [left, right] = operands else {
2718                return None;
2719            };
2720            if left == &SemanticType::Unknown || right == &SemanticType::Unknown {
2721                return Some(SemanticType::Unknown);
2722            }
2723            let left = remove_null(left);
2724            if left == SemanticType::Never {
2725                Some(right.clone())
2726            } else if left == *right {
2727                Some(left)
2728            } else {
2729                Some(SemanticType::Union(vec![left, right.clone()]))
2730            }
2731        }
2732        SemanticOperator::Unary(crate::component_graph::UnaryOperator::Not) => {
2733            let [operand] = operands else {
2734                return None;
2735            };
2736            if operand == &SemanticType::Unknown {
2737                Some(SemanticType::Unknown)
2738            } else if is_boolean_like(operand) {
2739                Some(SemanticType::Boolean)
2740            } else {
2741                None
2742            }
2743        }
2744        SemanticOperator::Unary(
2745            crate::component_graph::UnaryOperator::Plus
2746            | crate::component_graph::UnaryOperator::Minus,
2747        ) => {
2748            let [operand] = operands else {
2749                return None;
2750            };
2751            if operand == &SemanticType::Unknown {
2752                Some(SemanticType::Unknown)
2753            } else if is_number_like(operand) {
2754                Some(SemanticType::Number)
2755            } else {
2756                None
2757            }
2758        }
2759    }
2760}
2761
2762fn is_number_like(semantic_type: &SemanticType) -> bool {
2763    matches!(
2764        semantic_type,
2765        SemanticType::Number | SemanticType::NumberLiteral(_)
2766    )
2767}
2768
2769fn is_boolean_like(semantic_type: &SemanticType) -> bool {
2770    matches!(
2771        semantic_type,
2772        SemanticType::Boolean | SemanticType::BooleanLiteral(_)
2773    )
2774}
2775
2776fn remove_null(semantic_type: &SemanticType) -> SemanticType {
2777    match semantic_type {
2778        SemanticType::Null => SemanticType::Never,
2779        SemanticType::Union(members) => {
2780            let members = members
2781                .iter()
2782                .map(remove_null)
2783                .filter(|member| member != &SemanticType::Never)
2784                .collect::<Vec<_>>();
2785            match members.as_slice() {
2786                [] => SemanticType::Never,
2787                [member] => member.clone(),
2788                _ => SemanticType::Union(members),
2789            }
2790        }
2791        _ => semantic_type.clone(),
2792    }
2793}
2794
2795#[must_use]
2796pub fn infer_serializable_value_type(value: &SerializableValue) -> SemanticType {
2797    match value {
2798        SerializableValue::Null => SemanticType::Null,
2799        SerializableValue::Number(_) => SemanticType::Number,
2800        SerializableValue::String(_) => SemanticType::String,
2801        SerializableValue::Boolean(_) => SemanticType::Boolean,
2802        SerializableValue::Array(values) if values.is_empty() => {
2803            SemanticType::Array(Box::new(SemanticType::Unknown))
2804        }
2805        SerializableValue::Array(values) => {
2806            let mut types = values
2807                .iter()
2808                .map(infer_serializable_value_type)
2809                .collect::<Vec<_>>();
2810            let element = if types.windows(2).all(|window| window[0] == window[1]) {
2811                types.pop().unwrap_or(SemanticType::Unknown)
2812            } else {
2813                SemanticType::Union(types)
2814            };
2815            SemanticType::Array(Box::new(element))
2816        }
2817        SerializableValue::Object(values) => SemanticType::Object(ObjectType {
2818            properties: values
2819                .iter()
2820                .map(|(name, value)| (name.clone(), infer_serializable_value_type(value)))
2821                .collect(),
2822        }),
2823    }
2824}
2825
2826fn semantic_type_from_annotation(text: &str) -> Option<SemanticType> {
2827    let text = text.trim();
2828    let union_members = split_top_level(text, '|');
2829    if union_members.len() > 1 {
2830        return Some(SemanticType::Union(
2831            union_members
2832                .into_iter()
2833                .map(|member| {
2834                    semantic_type_from_annotation(member).unwrap_or(SemanticType::Unknown)
2835                })
2836                .collect(),
2837        ));
2838    }
2839    if let Some(element) = text.strip_suffix("[]") {
2840        return Some(SemanticType::Array(Box::new(
2841            semantic_type_from_annotation(element).unwrap_or(SemanticType::Unknown),
2842        )));
2843    }
2844    if text.starts_with('[') && text.ends_with(']') {
2845        let items = &text[1..text.len() - 1];
2846        return Some(SemanticType::Tuple(
2847            split_top_level(items, ',')
2848                .into_iter()
2849                .filter(|item| !item.trim().is_empty())
2850                .map(|item| semantic_type_from_annotation(item).unwrap_or(SemanticType::Unknown))
2851                .collect(),
2852        ));
2853    }
2854    if text.starts_with('{') && text.ends_with('}') {
2855        return object_type(text);
2856    }
2857    if let Some(arguments) = text
2858        .strip_prefix("Resource<")
2859        .and_then(|value| value.strip_suffix('>'))
2860    {
2861        let arguments = split_top_level(arguments, ',');
2862        if arguments.len() == 2 {
2863            return Some(SemanticType::Resource(ResourceType {
2864                data: Box::new(semantic_type_from_annotation(arguments[0])?),
2865                error: Box::new(semantic_type_from_annotation(arguments[1])?),
2866                pending: true,
2867                serializable: true,
2868                // The selected semantic-package endpoint is the authority for
2869                // the actual execution boundary during Resource lowering.
2870                execution_boundary: ResourceExecutionBoundary::Shared,
2871            }));
2872        }
2873    }
2874
2875    match text {
2876        "string" => Some(SemanticType::String),
2877        "number" => Some(SemanticType::Number),
2878        "boolean" => Some(SemanticType::Boolean),
2879        "null" => Some(SemanticType::Null),
2880        "SlotContent" => Some(SemanticType::SlotContent),
2881        "true" => Some(SemanticType::BooleanLiteral(true)),
2882        "false" => Some(SemanticType::BooleanLiteral(false)),
2883        _ => string_literal_type(text).or_else(|| numeric_literal_type(text)),
2884    }
2885}
2886
2887fn object_type(text: &str) -> Option<SemanticType> {
2888    let mut properties = BTreeMap::new();
2889    let fields = &text[1..text.len() - 1];
2890
2891    for field in split_top_level(fields, ';')
2892        .into_iter()
2893        .filter(|field| !field.trim().is_empty())
2894    {
2895        let (name, type_text) = field.split_once(':')?;
2896        let name = name.trim();
2897        if name.is_empty() {
2898            return None;
2899        }
2900        properties.insert(
2901            name.to_string(),
2902            semantic_type_from_annotation(type_text).unwrap_or(SemanticType::Unknown),
2903        );
2904    }
2905
2906    Some(SemanticType::Object(ObjectType { properties }))
2907}
2908
2909fn split_top_level(text: &str, delimiter: char) -> Vec<&str> {
2910    let mut depth = 0usize;
2911    let mut start = 0usize;
2912    let mut parts = Vec::new();
2913
2914    for (index, character) in text.char_indices() {
2915        match character {
2916            '{' | '[' | '(' => depth += 1,
2917            '}' | ']' | ')' => depth = depth.saturating_sub(1),
2918            _ => {}
2919        }
2920        if character == delimiter && depth == 0 {
2921            parts.push(&text[start..index]);
2922            start = index + character.len_utf8();
2923        }
2924    }
2925    parts.push(&text[start..]);
2926    parts
2927}
2928
2929fn string_literal_type(text: &str) -> Option<SemanticType> {
2930    let quote = text.chars().next()?;
2931    (matches!(quote, '\'' | '"') && text.ends_with(quote) && text.len() >= 2)
2932        .then(|| SemanticType::StringLiteral(text[1..text.len() - 1].to_string()))
2933}
2934
2935fn numeric_literal_type(text: &str) -> Option<SemanticType> {
2936    text.parse::<f64>()
2937        .ok()
2938        .filter(|value| value.is_finite())
2939        .map(|_| SemanticType::NumberLiteral(text.to_string()))
2940}
2941
2942/// Canonical semantic compatibility relation shared by compiler consumers.
2943#[must_use]
2944pub fn is_assignable(source: &SemanticType, target: &SemanticType) -> bool {
2945    match (source, target) {
2946        (_, SemanticType::Unknown)
2947        | (SemanticType::Unknown | SemanticType::Never, _)
2948        | (SemanticType::Null, SemanticType::Null)
2949        | (SemanticType::Boolean | SemanticType::BooleanLiteral(_), SemanticType::Boolean)
2950        | (SemanticType::Number | SemanticType::NumberLiteral(_), SemanticType::Number)
2951        | (SemanticType::String | SemanticType::StringLiteral(_), SemanticType::String)
2952        | (SemanticType::Form, SemanticType::Form)
2953        | (SemanticType::SlotContent, SemanticType::SlotContent) => true,
2954        (SemanticType::BooleanLiteral(source), SemanticType::BooleanLiteral(target)) => {
2955            source == target
2956        }
2957        (SemanticType::NumberLiteral(source), SemanticType::NumberLiteral(target))
2958        | (SemanticType::StringLiteral(source), SemanticType::StringLiteral(target)) => {
2959            source == target
2960        }
2961        (SemanticType::Tuple(source), SemanticType::Tuple(target)) => {
2962            source.len() == target.len()
2963                && source
2964                    .iter()
2965                    .zip(target)
2966                    .all(|(source, target)| is_assignable(source, target))
2967        }
2968        (SemanticType::Tuple(source), SemanticType::Array(target)) => {
2969            source.iter().all(|source| is_assignable(source, target))
2970        }
2971        (SemanticType::Array(source), SemanticType::Array(target)) => is_assignable(source, target),
2972        (SemanticType::Object(source), SemanticType::Object(target)) => {
2973            target.properties.iter().all(|(name, target)| {
2974                source
2975                    .properties
2976                    .get(name)
2977                    .is_some_and(|source| is_assignable(source, target))
2978            })
2979        }
2980        (SemanticType::Resource(source), SemanticType::Resource(target)) => {
2981            source.pending == target.pending
2982                && source.serializable == target.serializable
2983                && source.execution_boundary == target.execution_boundary
2984                && is_assignable(&source.data, &target.data)
2985                && is_assignable(&source.error, &target.error)
2986        }
2987        (SemanticType::Union(source), target) => {
2988            source.iter().all(|source| is_assignable(source, target))
2989        }
2990        (source, SemanticType::Union(target)) => {
2991            target.iter().any(|target| is_assignable(source, target))
2992        }
2993        _ => false,
2994    }
2995}
2996
2997/// Compatibility alias retained for C10 callers.
2998#[must_use]
2999pub fn is_state_initializer_assignable(source: &SemanticType, target: &SemanticType) -> bool {
3000    is_assignable(source, target)
3001}
3002
3003#[must_use]
3004pub fn state_initializer_value_type(value: &SerializableValue) -> SemanticType {
3005    match value {
3006        SerializableValue::Null => SemanticType::Null,
3007        SerializableValue::Number(value) => SemanticType::NumberLiteral(value.clone()),
3008        SerializableValue::String(value) => SemanticType::StringLiteral(value.clone()),
3009        SerializableValue::Boolean(value) => SemanticType::BooleanLiteral(*value),
3010        SerializableValue::Array(values) if values.is_empty() => {
3011            SemanticType::Array(Box::new(SemanticType::Unknown))
3012        }
3013        SerializableValue::Array(values) => {
3014            SemanticType::Tuple(values.iter().map(state_initializer_value_type).collect())
3015        }
3016        SerializableValue::Object(values) => SemanticType::Object(ObjectType {
3017            properties: values
3018                .iter()
3019                .map(|(name, value)| (name.clone(), state_initializer_value_type(value)))
3020                .collect(),
3021        }),
3022    }
3023}
3024
3025#[cfg(test)]
3026mod tests {
3027    use std::collections::BTreeMap;
3028
3029    use super::{
3030        boundary_compatibility, dom_binding_contract, is_assignable, normalize_semantic_type,
3031        operator_result_type, serialization_compatibility, BoundaryCompatibility,
3032        ExecutionBoundary, ObjectType, ResourceExecutionBoundary, ResourceType, SemanticOperator,
3033        SemanticType, SemanticTypeAssignment, SemanticTypeId, SemanticTypeStatus,
3034        SerializationCompatibility, TypeDiagnosticCode, TypeDiagnosticFamily,
3035    };
3036    use crate::{
3037        component_graph::UnaryOperator, ArithmeticOperator, ComparisonOperator, LogicalOperator,
3038        SemanticId, SourceProvenance,
3039    };
3040
3041    #[test]
3042    fn defines_typed_accessibility_attribute_bindings() {
3043        for name in [
3044            "role",
3045            "aria-label",
3046            "aria-describedby",
3047            "aria-errormessage",
3048            "aria-controls",
3049            "aria-current",
3050            "aria-live",
3051        ] {
3052            assert_eq!(
3053                dom_binding_contract(name).unwrap().semantic_type,
3054                SemanticType::String,
3055                "{name} must retain its compiler-owned string contract"
3056            );
3057        }
3058        for name in [
3059            "aria-invalid",
3060            "aria-busy",
3061            "aria-expanded",
3062            "aria-pressed",
3063            "aria-hidden",
3064        ] {
3065            assert_eq!(
3066                dom_binding_contract(name).unwrap().semantic_type,
3067                SemanticType::Boolean,
3068                "{name} must retain its compiler-owned boolean contract"
3069            );
3070        }
3071        assert!(dom_binding_contract("aria-owns").is_none());
3072    }
3073
3074    #[test]
3075    fn represents_core_compiler_owned_type_forms() {
3076        let todo = SemanticType::Object(ObjectType {
3077            properties: BTreeMap::from([
3078                ("completed".to_string(), SemanticType::Boolean),
3079                ("title".to_string(), SemanticType::String),
3080            ]),
3081        });
3082        let types = vec![
3083            SemanticType::Unknown,
3084            SemanticType::Never,
3085            SemanticType::Null,
3086            SemanticType::Boolean,
3087            SemanticType::Number,
3088            SemanticType::String,
3089            SemanticType::BooleanLiteral(true),
3090            SemanticType::NumberLiteral("42".to_string()),
3091            SemanticType::StringLiteral("all".to_string()),
3092            SemanticType::Array(Box::new(SemanticType::Number)),
3093            SemanticType::Tuple(vec![SemanticType::String, SemanticType::Number]),
3094            todo,
3095            SemanticType::Union(vec![SemanticType::String, SemanticType::Null]),
3096            SemanticType::Resource(ResourceType {
3097                data: Box::new(SemanticType::String),
3098                error: Box::new(SemanticType::Unknown),
3099                pending: true,
3100                serializable: true,
3101                execution_boundary: ResourceExecutionBoundary::Shared,
3102            }),
3103        ];
3104
3105        assert_eq!(types.len(), 14);
3106    }
3107
3108    #[test]
3109    fn assigns_stable_codes_to_canonical_type_diagnostic_families() {
3110        assert_eq!(TypeDiagnosticCode::UnknownType.as_str(), "PSC1032");
3111        assert_eq!(
3112            TypeDiagnosticCode::InvalidArithmeticOperator.family(),
3113            TypeDiagnosticFamily::InvalidOperator
3114        );
3115        assert_eq!(
3116            TypeDiagnosticCode::IncompatibleAssignment.family(),
3117            TypeDiagnosticFamily::IncompatibleAssignment
3118        );
3119        assert_eq!(
3120            TypeDiagnosticCode::NonSerializableState.family(),
3121            TypeDiagnosticFamily::NonSerializableState
3122        );
3123    }
3124
3125    #[test]
3126    fn preserves_type_assignment_identity_status_and_provenance() {
3127        let subject = SemanticId::component(Some("x-counter"), "Counter").state_field("count");
3128        let assignment = SemanticTypeAssignment {
3129            id: SemanticTypeId::for_subject(&subject),
3130            subject: subject.clone(),
3131            semantic_type: SemanticType::Number,
3132            origin: subject.clone(),
3133            status: SemanticTypeStatus::Declared,
3134            provenance: SourceProvenance::new(
3135                "src/Counter.tsx",
3136                presolve_parser::SourceSpan {
3137                    start: 42,
3138                    end: 48,
3139                    line: 4,
3140                    column: 10,
3141                },
3142            ),
3143        };
3144
3145        assert_eq!(
3146            assignment.id.to_string(),
3147            "component:x-counter/state:count/type:semantic"
3148        );
3149        assert_eq!(assignment.origin, subject);
3150        assert_eq!(assignment.status, SemanticTypeStatus::Declared);
3151        assert_eq!(
3152            assignment.provenance.path,
3153            std::path::Path::new("src/Counter.tsx")
3154        );
3155    }
3156
3157    #[test]
3158    fn determines_structural_serialization_compatibility() {
3159        assert_eq!(
3160            serialization_compatibility(&SemanticType::Number),
3161            SerializationCompatibility::Serializable
3162        );
3163        assert_eq!(
3164            serialization_compatibility(&SemanticType::Object(ObjectType {
3165                properties: BTreeMap::from([(
3166                    "todos".to_string(),
3167                    SemanticType::Array(Box::new(SemanticType::String)),
3168                )]),
3169            })),
3170            SerializationCompatibility::Serializable
3171        );
3172        assert_eq!(
3173            serialization_compatibility(&SemanticType::Unknown),
3174            SerializationCompatibility::NotSerializable
3175        );
3176        assert_eq!(
3177            serialization_compatibility(&SemanticType::Resource(ResourceType {
3178                data: Box::new(SemanticType::String),
3179                error: Box::new(SemanticType::Unknown),
3180                pending: true,
3181                serializable: true,
3182                execution_boundary: ResourceExecutionBoundary::Shared,
3183            })),
3184            SerializationCompatibility::NotSerializable
3185        );
3186    }
3187
3188    #[test]
3189    fn determines_server_client_boundary_compatibility() {
3190        assert_eq!(
3191            boundary_compatibility(
3192                &SemanticType::String,
3193                ExecutionBoundary::Server,
3194                ExecutionBoundary::Client,
3195            ),
3196            BoundaryCompatibility::Compatible
3197        );
3198        assert_eq!(
3199            boundary_compatibility(
3200                &SemanticType::Resource(ResourceType {
3201                    data: Box::new(SemanticType::String),
3202                    error: Box::new(SemanticType::String),
3203                    pending: true,
3204                    serializable: true,
3205                    execution_boundary: ResourceExecutionBoundary::Client,
3206                }),
3207                ExecutionBoundary::Client,
3208                ExecutionBoundary::Server,
3209            ),
3210            BoundaryCompatibility::Incompatible
3211        );
3212    }
3213
3214    #[test]
3215    fn normalizes_equivalent_union_forms_deterministically() {
3216        assert_eq!(
3217            normalize_semantic_type(SemanticType::Union(vec![
3218                SemanticType::String,
3219                SemanticType::Never,
3220                SemanticType::Union(vec![SemanticType::String, SemanticType::Null]),
3221                SemanticType::Null,
3222            ])),
3223            SemanticType::Union(vec![SemanticType::Null, SemanticType::String])
3224        );
3225        assert_eq!(
3226            normalize_semantic_type(SemanticType::Union(vec![SemanticType::Never])),
3227            SemanticType::Never
3228        );
3229    }
3230
3231    #[test]
3232    fn centralizes_assignability_across_normalized_type_forms() {
3233        assert!(is_assignable(
3234            &SemanticType::StringLiteral("all".to_string()),
3235            &SemanticType::Union(vec![
3236                SemanticType::StringLiteral("active".to_string()),
3237                SemanticType::StringLiteral("all".to_string()),
3238            ])
3239        ));
3240        assert!(is_assignable(
3241            &SemanticType::Tuple(vec![SemanticType::NumberLiteral("1".to_string())]),
3242            &SemanticType::Array(Box::new(SemanticType::Number))
3243        ));
3244        assert!(!is_assignable(&SemanticType::String, &SemanticType::Number));
3245    }
3246
3247    #[test]
3248    fn defines_explicit_operand_and_result_contracts_for_operators() {
3249        assert_eq!(
3250            operator_result_type(
3251                SemanticOperator::Arithmetic(ArithmeticOperator::Add),
3252                &[
3253                    SemanticType::NumberLiteral("1".to_string()),
3254                    SemanticType::Number,
3255                ],
3256            ),
3257            Some(SemanticType::Number)
3258        );
3259        assert_eq!(
3260            operator_result_type(
3261                SemanticOperator::Comparison(ComparisonOperator::LessThan),
3262                &[
3263                    SemanticType::Number,
3264                    SemanticType::NumberLiteral("2".to_string())
3265                ],
3266            ),
3267            Some(SemanticType::Boolean)
3268        );
3269        assert_eq!(
3270            operator_result_type(
3271                SemanticOperator::Logical(LogicalOperator::And),
3272                &[SemanticType::BooleanLiteral(true), SemanticType::Boolean],
3273            ),
3274            Some(SemanticType::Boolean)
3275        );
3276        assert_eq!(
3277            operator_result_type(
3278                SemanticOperator::Unary(UnaryOperator::Not),
3279                &[SemanticType::BooleanLiteral(false)],
3280            ),
3281            Some(SemanticType::Boolean)
3282        );
3283        assert_eq!(
3284            operator_result_type(
3285                SemanticOperator::NullishCoalescing,
3286                &[
3287                    SemanticType::Union(vec![SemanticType::String, SemanticType::Null]),
3288                    SemanticType::StringLiteral("fallback".to_string()),
3289                ],
3290            ),
3291            Some(SemanticType::Union(vec![
3292                SemanticType::String,
3293                SemanticType::StringLiteral("fallback".to_string()),
3294            ]))
3295        );
3296        assert_eq!(
3297            operator_result_type(
3298                SemanticOperator::Arithmetic(ArithmeticOperator::Add),
3299                &[SemanticType::String, SemanticType::Number],
3300            ),
3301            None
3302        );
3303        assert_eq!(
3304            operator_result_type(
3305                SemanticOperator::Logical(LogicalOperator::Or),
3306                &[SemanticType::Number, SemanticType::Boolean],
3307            ),
3308            None
3309        );
3310    }
3311}