Skip to main content

presolve_compiler/
consumer.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    context_designator::resolve_context_designator, BindingTable, ComponentNode, ConsumerId,
5    ContextDesignator, ContextEntity, ContextId, ExecutionBoundary, SemanticId, SemanticOwner,
6    SemanticTypeId, SourceProvenance,
7};
8
9/// Canonical resolution of a G3 Consumer's compile-time-only Context designator.
10///
11/// This deliberately records Context identity only. Provider visibility and
12/// selection remain separate compiler products for G4.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ContextResolutionState {
15    Resolved(ContextId),
16    Unresolved,
17}
18
19/// First-class compiler-owned semantic entity for one G3 `@consume()` field.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ConsumerEntity {
22    pub id: ConsumerId,
23    pub owner: SemanticOwner,
24    pub authored_field: SemanticId,
25    pub name: String,
26    pub context_designator: ContextDesignator,
27    pub context_resolution: ContextResolutionState,
28    pub requested_type: crate::DeclaredStateType,
29    pub requested_type_id: SemanticTypeId,
30    pub execution_boundary: ExecutionBoundary,
31    pub provenance: SourceProvenance,
32}
33
34impl ConsumerEntity {
35    #[must_use]
36    pub fn context(&self) -> Option<&ContextId> {
37        match &self.context_resolution {
38            ContextResolutionState::Resolved(context) => Some(context),
39            ContextResolutionState::Unresolved => None,
40        }
41    }
42}
43
44/// Lower valid G3 Consumer declarations without resolving any Provider.
45#[must_use]
46pub fn collect_consumer_entities(
47    components: &[ComponentNode],
48    contexts: &BTreeMap<ContextId, ContextEntity>,
49    bindings: Option<&BindingTable>,
50) -> BTreeMap<ConsumerId, ConsumerEntity> {
51    let mut consumers = BTreeMap::new();
52
53    for component in components {
54        for declaration in &component.consumer_declarations {
55            let Some(context) = resolve_context_designator(
56                &declaration.context_designator,
57                components,
58                contexts,
59                bindings,
60            ) else {
61                // The retained declaration candidate carries the unresolved
62                // designator fact.  An invalid Consumer must not enter G4-G17.
63                continue;
64            };
65            let id = ConsumerId::for_component(&component.id, &declaration.name);
66            let semantic_id = id.as_semantic_id().clone();
67            consumers.insert(
68                id.clone(),
69                ConsumerEntity {
70                    requested_type_id: SemanticTypeId::for_subject(&semantic_id),
71                    id,
72                    owner: SemanticOwner::entity(component.id.clone()),
73                    authored_field: declaration.authored_field.clone(),
74                    name: declaration.name.clone(),
75                    context_designator: declaration.context_designator.clone(),
76                    context_resolution: ContextResolutionState::Resolved(context),
77                    requested_type: declaration.requested_type.clone(),
78                    execution_boundary: ExecutionBoundary::Client,
79                    provenance: declaration.provenance.clone(),
80                },
81            );
82        }
83    }
84
85    consumers
86}
87
88#[cfg(test)]
89mod tests {
90    use crate::{
91        build_application_semantic_model, build_application_semantic_model_for_unit,
92        validate_application_semantic_model, CompilationUnit, ConsumerId, ContextResolutionState,
93        SemanticReferenceKind,
94    };
95
96    #[test]
97    fn lowers_same_module_consumers_without_provider_resolution() {
98        let parsed = presolve_parser::parse_file(
99            "src/toolbar.tsx",
100            r#"
101@component("x-app-shell")
102class AppShell extends Component {
103  @context()
104  theme!: Theme;
105  render() { return <main />; }
106}
107@component("x-toolbar")
108class Toolbar extends Component {
109  @consume(AppShell.theme)
110  theme!: Theme;
111  @consume(AppShell.theme)
112  menuTheme!: Theme;
113  render() { return <main />; }
114}
115"#,
116        );
117        let asm = build_application_semantic_model(&parsed);
118        let toolbar = &asm.components[1];
119        let consumer = asm
120            .consumer(&ConsumerId::for_component(&toolbar.id, "theme"))
121            .expect("consumer");
122
123        assert_eq!(asm.consumers().len(), 2);
124        assert_eq!(consumer.owner.entity_id(), Some(&toolbar.id));
125        assert_eq!(consumer.authored_field, toolbar.id.consumer_field("theme"));
126        assert!(matches!(
127            consumer.context_resolution,
128            ContextResolutionState::Resolved(_)
129        ));
130        assert_eq!(
131            asm.consumers_for_context(consumer.context().unwrap()).len(),
132            2
133        );
134        assert!(asm.references.iter().any(|reference| {
135            reference.kind == SemanticReferenceKind::ConsumesContext
136                && reference.source == *consumer.id.as_semantic_id()
137                && reference.target == *consumer.context().unwrap().as_semantic_id()
138        }));
139        assert!(asm.providers().is_empty());
140        assert!(validate_application_semantic_model(&asm).is_empty());
141    }
142
143    #[test]
144    fn resolves_imported_context_owners_without_runtime_lookup() {
145        let unit = CompilationUnit::parse_sources([
146            (
147                "src/app-shell.tsx",
148                r#"
149@component("x-app-shell")
150class AppShell extends Component {
151  @context()
152  theme!: Theme;
153  render() { return <main />; }
154}
155export { AppShell };
156"#,
157            ),
158            (
159                "src/toolbar.tsx",
160                r#"
161import { AppShell } from "./app-shell";
162@component("x-toolbar")
163class Toolbar extends Component {
164  @consume(AppShell.theme)
165  theme!: Theme;
166  render() { return <main />; }
167}
168"#,
169            ),
170        ]);
171        let asm = build_application_semantic_model_for_unit(&unit);
172
173        assert_eq!(asm.consumers().len(), 1);
174        assert_eq!(
175            asm.consumers()[0].context().unwrap().as_str(),
176            "module:src/app-shell.tsx/component:x-app-shell/context:theme"
177        );
178    }
179
180    #[test]
181    fn retains_unresolved_context_designators_as_invalid_candidates() {
182        let parsed = presolve_parser::parse_file(
183            "src/toolbar.tsx",
184            r#"
185@component("x-toolbar")
186class Toolbar extends Component {
187  @consume(AppShell.notAContext)
188  theme!: Theme;
189  render() { return <main />; }
190}
191"#,
192        );
193        let asm = build_application_semantic_model(&parsed);
194
195        assert!(asm.consumers().is_empty());
196        assert_eq!(
197            asm.context_declaration_candidates()
198                .invalid_candidates()
199                .len(),
200            1
201        );
202        assert!(asm.references.is_empty());
203    }
204
205    #[test]
206    fn excludes_invalid_consumer_forms_and_conflicting_state() {
207        let parsed = presolve_parser::parse_file(
208            "src/toolbar.tsx",
209            r#"
210@component("x-app-shell")
211class AppShell extends Component {
212  @context()
213  theme!: Theme;
214  render() { return <main />; }
215}
216@component("x-toolbar")
217class Toolbar extends Component {
218  @consume("theme")
219  stringTarget!: Theme;
220  @consume(AppShell.theme)
221  missingType!;
222  @consume(AppShell.theme)
223  initialized: Theme = fallbackTheme;
224  @consume(AppShell.theme)
225  static staticConsumer!: Theme;
226  @consume(AppShell.theme)
227  declarationOnly: Theme;
228  @consume(AppShell.theme)
229  stateful: Theme = state("theme");
230  render() { return <main />; }
231}
232"#,
233        );
234        let asm = build_application_semantic_model(&parsed);
235
236        assert!(asm.consumers().is_empty());
237        assert!(asm.components[1].state_fields.is_empty());
238        assert!(asm.references.is_empty());
239    }
240}