1use std::collections::{BTreeMap, BTreeSet};
8
9use presolve_parser::{ParsedFile, SourceSpan};
10
11use crate::{
12 normalize_authored_semantics_v1, AuthoredSemanticCandidateKindV1,
13 AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
14 CanonicalAuthoredDeclarationKindV1, CanonicalAuthoredSemanticModelV1, CanonicalIntrinsicKindV1,
15 ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct EffectFieldSiteV1 {
21 pub subject: String,
22 pub declaration_source: AuthoredSourceRangeV1,
23 pub callee_source: AuthoredSourceRangeV1,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ResolvedEffectFieldV1 {
29 pub callee_source: AuthoredSourceRangeV1,
30 pub effect_identity: ResolvedIntrinsicIdentityV1,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct EffectFieldLoweringV1 {
36 pub sites: Vec<EffectFieldSiteV1>,
37 pub model: CanonicalAuthoredSemanticModelV1,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum EffectFieldLoweringErrorV1 {
43 ComponentSourcePathMismatch,
44 UnknownComponentDeclaration { start: usize, end: usize },
45 DuplicateResolution { start: usize, end: usize },
46 UnknownEffectResolution { start: usize, end: usize },
47 InvalidAuthoredSemantics(AuthoredSemanticNormalizationErrorV1),
48}
49
50impl std::fmt::Display for EffectFieldLoweringErrorV1 {
51 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::ComponentSourcePathMismatch => write!(
54 formatter,
55 "component and Effect authored-semantic products must describe the same source file"
56 ),
57 Self::UnknownComponentDeclaration { start, end } => write!(
58 formatter,
59 "canonical component declaration has no source class at {start}..{end}"
60 ),
61 Self::DuplicateResolution { start, end } => {
62 write!(
63 formatter,
64 "duplicate Effect field resolution at {start}..{end}"
65 )
66 }
67 Self::UnknownEffectResolution { start, end } => write!(
68 formatter,
69 "Effect field resolution has no source field callee at {start}..{end}"
70 ),
71 Self::InvalidAuthoredSemantics(error) => error.fmt(formatter),
72 }
73 }
74}
75
76impl std::error::Error for EffectFieldLoweringErrorV1 {}
77
78pub fn effect_field_sites_v1(
80 parsed: &ParsedFile,
81 component_model: &CanonicalAuthoredSemanticModelV1,
82) -> Result<Vec<EffectFieldSiteV1>, EffectFieldLoweringErrorV1> {
83 if component_model.source_path != parsed.path {
84 return Err(EffectFieldLoweringErrorV1::ComponentSourcePathMismatch);
85 }
86 let components = component_model
87 .declarations
88 .iter()
89 .filter(|declaration| declaration.kind == CanonicalAuthoredDeclarationKindV1::Component)
90 .map(|declaration| (declaration.subject.clone(), range_key(declaration.source)))
91 .collect::<BTreeSet<_>>();
92 for (subject, declaration_key) in &components {
93 if !parsed
94 .classes
95 .iter()
96 .any(|class| class.name == *subject && range_key(range(class.span)) == *declaration_key)
97 {
98 return Err(EffectFieldLoweringErrorV1::UnknownComponentDeclaration {
99 start: declaration_key.0,
100 end: declaration_key.1,
101 });
102 }
103 }
104 let mut sites = parsed
105 .classes
106 .iter()
107 .filter(|class| components.contains(&(class.name.clone(), range_key(range(class.span)))))
108 .flat_map(|class| {
109 class.properties.iter().filter_map(move |property| {
110 let call = property.initializer_call.as_ref()?;
111 (!property.is_static).then_some(EffectFieldSiteV1 {
112 subject: format!("{}.{}", class.name, property.name),
113 declaration_source: range(property.span),
114 callee_source: range(call.callee_span),
115 })
116 })
117 })
118 .collect::<Vec<_>>();
119 sites.sort_by_key(|site| {
120 (
121 site.callee_source.start,
122 site.callee_source.end,
123 site.subject.clone(),
124 )
125 });
126 Ok(sites)
127}
128
129pub fn lower_effect_fields_v1(
131 parsed: &ParsedFile,
132 component_model: &CanonicalAuthoredSemanticModelV1,
133 resolutions: impl IntoIterator<Item = ResolvedEffectFieldV1>,
134) -> Result<EffectFieldLoweringV1, EffectFieldLoweringErrorV1> {
135 let sites = effect_field_sites_v1(parsed, component_model)?;
136 let known_sites = sites
137 .iter()
138 .map(|site| range_key(site.callee_source))
139 .collect::<BTreeSet<_>>();
140 let mut resolution_by_site = BTreeMap::new();
141 for resolution in resolutions {
142 let key = range_key(resolution.callee_source);
143 if !known_sites.contains(&key) {
144 return Err(EffectFieldLoweringErrorV1::UnknownEffectResolution {
145 start: key.0,
146 end: key.1,
147 });
148 }
149 if resolution_by_site.insert(key, resolution).is_some() {
150 return Err(EffectFieldLoweringErrorV1::DuplicateResolution {
151 start: key.0,
152 end: key.1,
153 });
154 }
155 }
156 let candidates = sites.iter().filter_map(|site| {
157 let resolution = resolution_by_site.get(&range_key(site.callee_source))?;
158 Some(ResolvedAuthoredSemanticCandidateV1 {
159 subject: site.subject.clone(),
160 source: site.declaration_source,
161 kind: AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
162 intrinsic_kind: CanonicalIntrinsicKindV1::Effect,
163 intrinsic_identity: resolution.effect_identity.clone(),
164 },
165 })
166 });
167 let model = normalize_authored_semantics_v1(parsed, candidates)
168 .map_err(EffectFieldLoweringErrorV1::InvalidAuthoredSemantics)?;
169 Ok(EffectFieldLoweringV1 { sites, model })
170}
171
172fn range(span: SourceSpan) -> AuthoredSourceRangeV1 {
173 AuthoredSourceRangeV1 {
174 start: span.start,
175 end: span.end,
176 line: span.line,
177 column: span.column,
178 }
179}
180
181fn range_key(range: AuthoredSourceRangeV1) -> (usize, usize) {
182 (range.start, range.end)
183}
184
185#[cfg(test)]
186mod tests {
187 use presolve_parser::parse_file;
188
189 use crate::{
190 lower_component_inheritance_v1, CanonicalAuthoredDeclarationKindV1,
191 ResolvedComponentInheritanceV1, ResolvedIntrinsicIdentityV1,
192 };
193
194 use super::{effect_field_sites_v1, lower_effect_fields_v1, ResolvedEffectFieldV1};
195
196 fn identity(name: &str) -> ResolvedIntrinsicIdentityV1 {
197 ResolvedIntrinsicIdentityV1 {
198 name: name.to_owned(),
199 flags: 32,
200 declaration_modules: vec!["node_modules/presolve/src/index.d.ts".to_owned()],
201 }
202 }
203
204 #[test]
205 fn lowers_only_authority_proven_effect_fields_of_canonical_components() {
206 let parsed = parse_file(
207 "src/Counter.tsx",
208 r#"
209class Counter extends Base {
210 sync = observe(() => {});
211 helper = ordinaryHelper(() => {});
212 static invalid = observe(() => {});
213}
214@component() class LegacyOnly { sync = observe(() => {}); }
215"#,
216 );
217 let component_site = crate::component_inheritance_sites_v1(&parsed)
218 .into_iter()
219 .next()
220 .unwrap();
221 let components = lower_component_inheritance_v1(
222 &parsed,
223 [ResolvedComponentInheritanceV1 {
224 heritage_source: component_site.heritage_source,
225 component_identity: identity("Component"),
226 }],
227 )
228 .unwrap()
229 .model;
230 let sites = effect_field_sites_v1(&parsed, &components).unwrap();
231 assert_eq!(sites.len(), 2);
232 assert_eq!(sites[0].subject, "Counter.sync");
233 assert_eq!(sites[1].subject, "Counter.helper");
234
235 let model = lower_effect_fields_v1(
236 &parsed,
237 &components,
238 [ResolvedEffectFieldV1 {
239 callee_source: sites[0].callee_source,
240 effect_identity: identity("effect"),
241 }],
242 )
243 .unwrap();
244 assert_eq!(model.model.declarations.len(), 1);
245 assert_eq!(
246 model.model.declarations[0].kind,
247 CanonicalAuthoredDeclarationKindV1::Effect
248 );
249 assert_eq!(model.model.declarations[0].subject, "Counter.sync");
250 }
251}