Skip to main content

presolve_compiler/
state_initializer_lowering.rs

1//! Decorator-free V2 `state(initial)` recognition at the syntax/authority boundary.
2//!
3//! Direct field-initializer calls are selected by the parser without attaching
4//! framework meaning. This module admits only instance fields of already
5//! authority-proven V2 components, then joins an exact resolved callee to the
6//! canonical authored-semantic model.
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use presolve_parser::{ParsedFile, SourceSpan};
11
12use crate::{
13    normalize_authored_semantics_v1, AuthoredSemanticCandidateKindV1,
14    AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
15    CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1,
16    ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
17};
18
19/// A direct instance-field initializer call on an authority-proven component.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct StateInitializerSiteV1 {
22    pub subject: String,
23    pub declaration_source: AuthoredSourceRangeV1,
24    pub callee_source: AuthoredSourceRangeV1,
25}
26
27/// TypeScript proof that a selected direct field-initializer callee is `state`.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ResolvedStateInitializerV1 {
30    pub callee_source: AuthoredSourceRangeV1,
31    pub state_identity: ResolvedIntrinsicIdentityV1,
32}
33
34/// A validated canonical V2 State-recognition product.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct StateInitializerLoweringV1 {
37    pub sites: Vec<StateInitializerSiteV1>,
38    pub model: CanonicalAuthoredSemanticModelV1,
39}
40
41/// A bad component- or authority-to-source join for V2 State recognition.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum StateInitializerLoweringErrorV1 {
44    ComponentSourcePathMismatch,
45    UnknownComponentDeclaration { start: usize, end: usize },
46    DuplicateResolution { start: usize, end: usize },
47    UnknownStateResolution { start: usize, end: usize },
48    InvalidAuthoredSemantics(AuthoredSemanticNormalizationErrorV1),
49}
50
51impl std::fmt::Display for StateInitializerLoweringErrorV1 {
52    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        match self {
54            Self::ComponentSourcePathMismatch => write!(
55                formatter,
56                "component and State authored-semantic products must describe the same source file"
57            ),
58            Self::UnknownComponentDeclaration { start, end } => write!(
59                formatter,
60                "canonical component declaration has no source class at {start}..{end}"
61            ),
62            Self::DuplicateResolution { start, end } => {
63                write!(
64                    formatter,
65                    "duplicate State initializer resolution at {start}..{end}"
66                )
67            }
68            Self::UnknownStateResolution { start, end } => write!(
69                formatter,
70                "State initializer resolution has no source field callee at {start}..{end}"
71            ),
72            Self::InvalidAuthoredSemantics(error) => error.fmt(formatter),
73        }
74    }
75}
76
77impl std::error::Error for StateInitializerLoweringErrorV1 {}
78
79/// Select direct instance-field calls owned by canonical V2 components.
80pub fn state_initializer_sites_v1(
81    parsed: &ParsedFile,
82    component_model: &CanonicalAuthoredSemanticModelV1,
83) -> Result<Vec<StateInitializerSiteV1>, StateInitializerLoweringErrorV1> {
84    if component_model.source_path != parsed.path {
85        return Err(StateInitializerLoweringErrorV1::ComponentSourcePathMismatch);
86    }
87    let components = component_model
88        .declarations
89        .iter()
90        .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::Component)
91        .map(|declaration| (declaration.subject.clone(), range_key(declaration.source)))
92        .collect::<BTreeSet<_>>();
93    for (subject, declaration_key) in &components {
94        if !parsed
95            .classes
96            .iter()
97            .any(|class| class.name == *subject && range_key(range(class.span)) == *declaration_key)
98        {
99            return Err(
100                StateInitializerLoweringErrorV1::UnknownComponentDeclaration {
101                    start: declaration_key.0,
102                    end: declaration_key.1,
103                },
104            );
105        }
106    }
107
108    let mut sites = parsed
109        .classes
110        .iter()
111        .filter(|class| components.contains(&(class.name.clone(), range_key(range(class.span)))))
112        .flat_map(|class| {
113            class.properties.iter().filter_map(move |property| {
114                let call = property.initializer_call.as_ref()?;
115                (!property.is_static).then_some(StateInitializerSiteV1 {
116                    subject: format!("{}.{}", class.name, property.name),
117                    declaration_source: range(property.span),
118                    callee_source: range(call.callee_span),
119                })
120            })
121        })
122        .collect::<Vec<_>>();
123    sites.sort_by_key(|site| {
124        (
125            site.callee_source.start,
126            site.callee_source.end,
127            site.subject.clone(),
128        )
129    });
130    Ok(sites)
131}
132
133/// Lower only authority-proven V2 State initializer calls.
134pub fn lower_state_initializers_v1(
135    parsed: &ParsedFile,
136    component_model: &CanonicalAuthoredSemanticModelV1,
137    resolutions: impl IntoIterator<Item = ResolvedStateInitializerV1>,
138) -> Result<StateInitializerLoweringV1, StateInitializerLoweringErrorV1> {
139    let sites = state_initializer_sites_v1(parsed, component_model)?;
140    let known_sites = sites
141        .iter()
142        .map(|site| range_key(site.callee_source))
143        .collect::<BTreeSet<_>>();
144    let mut resolution_by_site = BTreeMap::new();
145    for resolution in resolutions {
146        let key = range_key(resolution.callee_source);
147        if !known_sites.contains(&key) {
148            return Err(StateInitializerLoweringErrorV1::UnknownStateResolution {
149                start: key.0,
150                end: key.1,
151            });
152        }
153        if resolution_by_site.insert(key, resolution).is_some() {
154            return Err(StateInitializerLoweringErrorV1::DuplicateResolution {
155                start: key.0,
156                end: key.1,
157            });
158        }
159    }
160    let candidates = sites.iter().filter_map(|site| {
161        let resolution = resolution_by_site.get(&range_key(site.callee_source))?;
162        Some(ResolvedAuthoredSemanticCandidateV1 {
163            subject: site.subject.clone(),
164            source: site.declaration_source,
165            kind: AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
166                intrinsic_kind: CanonicalIntrinsicKindV1::State,
167                intrinsic_identity: resolution.state_identity.clone(),
168            },
169        })
170    });
171    let model = normalize_authored_semantics_v1(parsed, candidates)
172        .map_err(StateInitializerLoweringErrorV1::InvalidAuthoredSemantics)?;
173    Ok(StateInitializerLoweringV1 { sites, model })
174}
175
176fn range(span: SourceSpan) -> AuthoredSourceRangeV1 {
177    AuthoredSourceRangeV1 {
178        start: span.start,
179        end: span.end,
180        line: span.line,
181        column: span.column,
182    }
183}
184
185fn range_key(range: AuthoredSourceRangeV1) -> (usize, usize) {
186    (range.start, range.end)
187}
188
189#[cfg(test)]
190mod tests {
191    use presolve_parser::parse_file;
192
193    use crate::{
194        lower_component_inheritance_v1, CanonicalAuthoredDeclarationKindV1,
195        ResolvedComponentInheritanceV1, ResolvedIntrinsicIdentityV1,
196    };
197
198    use super::{
199        lower_state_initializers_v1, state_initializer_sites_v1, ResolvedStateInitializerV1,
200        StateInitializerLoweringErrorV1,
201    };
202
203    fn identity(name: &str) -> ResolvedIntrinsicIdentityV1 {
204        ResolvedIntrinsicIdentityV1 {
205            name: name.to_owned(),
206            flags: 32,
207            declaration_modules: vec!["node_modules/presolve/src/index.d.ts".to_owned()],
208        }
209    }
210
211    #[test]
212    fn lowers_only_authority_proven_instance_state_calls_of_canonical_components() {
213        let source = r#"
214class Counter extends Base {
215  count = reactiveCell(0);
216  label = ordinaryHelper("count");
217  static invalid = reactiveCell(1);
218}
219@component() class LegacyOnly { count = reactiveCell(0); }
220"#;
221        let parsed = parse_file("src/Counter.tsx", source);
222        let component_site = crate::component_inheritance_sites_v1(&parsed)
223            .into_iter()
224            .next()
225            .unwrap();
226        let components = lower_component_inheritance_v1(
227            &parsed,
228            [ResolvedComponentInheritanceV1 {
229                heritage_source: component_site.heritage_source,
230                component_identity: identity("Component"),
231            }],
232        )
233        .unwrap()
234        .model;
235        let sites = state_initializer_sites_v1(&parsed, &components).unwrap();
236        assert_eq!(sites.len(), 2);
237        assert_eq!(sites[0].subject, "Counter.count");
238        assert_eq!(sites[1].subject, "Counter.label");
239
240        let model = lower_state_initializers_v1(
241            &parsed,
242            &components,
243            [ResolvedStateInitializerV1 {
244                callee_source: sites[0].callee_source,
245                state_identity: identity("state"),
246            }],
247        )
248        .unwrap();
249        assert_eq!(model.model.declarations.len(), 1);
250        assert_eq!(
251            model.model.declarations[0].kind,
252            CanonicalAuthoredDeclarationKindV1::State
253        );
254        assert_eq!(model.model.declarations[0].subject, "Counter.count");
255    }
256
257    #[test]
258    fn rejects_dangling_state_authority_results() {
259        let parsed = parse_file(
260            "src/Counter.tsx",
261            "class Counter extends Base { value = cell(0); }",
262        );
263        let component_site = crate::component_inheritance_sites_v1(&parsed)
264            .into_iter()
265            .next()
266            .unwrap();
267        let components = lower_component_inheritance_v1(
268            &parsed,
269            [ResolvedComponentInheritanceV1 {
270                heritage_source: component_site.heritage_source,
271                component_identity: identity("Component"),
272            }],
273        )
274        .unwrap()
275        .model;
276        let error = lower_state_initializers_v1(
277            &parsed,
278            &components,
279            [ResolvedStateInitializerV1 {
280                callee_source: crate::AuthoredSourceRangeV1 {
281                    start: 0,
282                    end: 5,
283                    line: 1,
284                    column: 1,
285                },
286                state_identity: identity("state"),
287            }],
288        )
289        .expect_err("resolved calls must join an instance field initializer");
290        assert!(matches!(
291            error,
292            StateInitializerLoweringErrorV1::UnknownStateResolution { .. }
293        ));
294    }
295}