1use std::path::PathBuf;
9
10use presolve_parser::ParsedFile;
11use serde::{Deserialize, Serialize};
12
13pub const CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION: u32 = 3;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
17pub struct AuthoredSourceRangeV1 {
18 pub start: usize,
19 pub end: usize,
20 pub line: usize,
21 pub column: usize,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
26pub struct ResolvedIntrinsicIdentityV1 {
27 pub name: String,
28 pub flags: u32,
29 pub declaration_modules: Vec<String>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum CanonicalIntrinsicKindV1 {
39 Component,
40 State,
41 Action,
42 Computed,
43 Effect,
44 Slot,
45 Context,
46 Provide,
47 Consume,
48 Form,
49 Serialize,
50 Field,
51 Validate,
52 Submit,
53 Resource,
54 Loader,
55 ServerAction,
56 Opaque,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum AuthoredSemanticCandidateKindV1 {
67 ResolvedIntrinsic {
68 intrinsic_kind: CanonicalIntrinsicKindV1,
69 intrinsic_identity: ResolvedIntrinsicIdentityV1,
70 },
71 DerivedComputedGetter {
74 state_dependencies: Vec<String>,
75 computed_dependencies: Vec<String>,
76 },
77 TsxBinding,
78 TsxEventReference,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum DerivedAuthoredEvidenceV2 {
86 ComputedGetter {
87 state_dependencies: Vec<String>,
88 computed_dependencies: Vec<String>,
89 },
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
95pub struct ResolvedAuthoredSemanticCandidateV1 {
96 pub subject: String,
99 pub source: AuthoredSourceRangeV1,
100 pub kind: AuthoredSemanticCandidateKindV1,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
105pub struct CanonicalAuthoredDeclarationV1 {
106 pub kind: CanonicalAuthoredDeclarationKindV1,
107 pub subject: String,
108 pub source: AuthoredSourceRangeV1,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub intrinsic_identity: Option<ResolvedIntrinsicIdentityV1>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub derived_evidence: Option<DerivedAuthoredEvidenceV2>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum CanonicalAuthoredDeclarationKindV1 {
120 Component,
121 State,
122 Action,
123 Computed,
124 Effect,
125 Slot,
126 ContextToken,
127 ContextProvider,
128 ContextConsumer,
129 Form,
130 Serialization,
131 FormField,
132 Validation,
133 Submission,
134 Resource,
135 RouteLoader,
136 ServerAction,
137 Capability,
138 TsxBinding,
139 TsxEventReference,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct CanonicalAuthoredSemanticModelV1 {
145 pub schema_version: u32,
146 pub source_path: PathBuf,
147 pub declarations: Vec<CanonicalAuthoredDeclarationV1>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum AuthoredSemanticNormalizationErrorV1 {
153 InvalidSourceRange {
154 subject: String,
155 start: usize,
156 end: usize,
157 source_length: usize,
158 },
159}
160
161impl std::fmt::Display for AuthoredSemanticNormalizationErrorV1 {
162 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 Self::InvalidSourceRange {
165 subject,
166 start,
167 end,
168 source_length,
169 } => write!(
170 formatter,
171 "authored semantic candidate `{subject}` has invalid source range {start}..{end} for source length {source_length}"
172 ),
173 }
174 }
175}
176
177impl std::error::Error for AuthoredSemanticNormalizationErrorV1 {}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum AuthoredSemanticCompositionErrorV1 {
182 Empty,
183 SchemaVersion { actual: u32 },
184 SourcePathMismatch { expected: PathBuf, actual: PathBuf },
185}
186
187impl std::fmt::Display for AuthoredSemanticCompositionErrorV1 {
188 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 match self {
190 Self::Empty => write!(formatter, "cannot compose zero authored semantic models"),
191 Self::SchemaVersion { actual } => write!(
192 formatter,
193 "cannot compose authored semantic schema version {actual}; expected {CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION}"
194 ),
195 Self::SourcePathMismatch { expected, actual } => write!(
196 formatter,
197 "cannot compose authored semantic models from {} and {}",
198 expected.display(),
199 actual.display()
200 ),
201 }
202 }
203}
204
205impl std::error::Error for AuthoredSemanticCompositionErrorV1 {}
206
207pub fn normalize_authored_semantics_v1(
214 parsed: &ParsedFile,
215 candidates: impl IntoIterator<Item = ResolvedAuthoredSemanticCandidateV1>,
216) -> Result<CanonicalAuthoredSemanticModelV1, AuthoredSemanticNormalizationErrorV1> {
217 let source_length = parsed.syntax.source.len();
218 let mut declarations = candidates
219 .into_iter()
220 .map(|candidate| {
221 if candidate.source.start > candidate.source.end || candidate.source.end > source_length
222 {
223 return Err(AuthoredSemanticNormalizationErrorV1::InvalidSourceRange {
224 subject: candidate.subject,
225 start: candidate.source.start,
226 end: candidate.source.end,
227 source_length,
228 });
229 }
230 let (kind, mut intrinsic_identity, mut derived_evidence) =
231 declaration_kind(candidate.kind);
232 if let Some(identity) = &mut intrinsic_identity {
233 identity.declaration_modules.sort();
234 identity.declaration_modules.dedup();
235 }
236 if let Some(DerivedAuthoredEvidenceV2::ComputedGetter {
237 state_dependencies,
238 computed_dependencies,
239 }) = &mut derived_evidence
240 {
241 state_dependencies.sort();
242 state_dependencies.dedup();
243 computed_dependencies.sort();
244 computed_dependencies.dedup();
245 }
246 Ok(CanonicalAuthoredDeclarationV1 {
247 kind,
248 subject: candidate.subject,
249 source: candidate.source,
250 intrinsic_identity,
251 derived_evidence,
252 })
253 })
254 .collect::<Result<Vec<_>, _>>()?;
255
256 declarations.sort();
257 declarations.dedup();
258 Ok(CanonicalAuthoredSemanticModelV1 {
259 schema_version: CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
260 source_path: parsed.path.clone(),
261 declarations,
262 })
263}
264
265pub fn compose_authored_semantics_v1(
272 models: impl IntoIterator<Item = CanonicalAuthoredSemanticModelV1>,
273) -> Result<CanonicalAuthoredSemanticModelV1, AuthoredSemanticCompositionErrorV1> {
274 let mut models = models.into_iter();
275 let first = models
276 .next()
277 .ok_or(AuthoredSemanticCompositionErrorV1::Empty)?;
278 if first.schema_version != CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION {
279 return Err(AuthoredSemanticCompositionErrorV1::SchemaVersion {
280 actual: first.schema_version,
281 });
282 }
283 let source_path = first.source_path.clone();
284 let mut declarations = first.declarations;
285 for model in models {
286 if model.schema_version != CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION {
287 return Err(AuthoredSemanticCompositionErrorV1::SchemaVersion {
288 actual: model.schema_version,
289 });
290 }
291 if model.source_path != source_path {
292 return Err(AuthoredSemanticCompositionErrorV1::SourcePathMismatch {
293 expected: source_path,
294 actual: model.source_path,
295 });
296 }
297 declarations.extend(model.declarations);
298 }
299 declarations.sort();
300 declarations.dedup();
301 Ok(CanonicalAuthoredSemanticModelV1 {
302 schema_version: CANONICAL_AUTHORED_SEMANTICS_SCHEMA_VERSION,
303 source_path,
304 declarations,
305 })
306}
307
308fn declaration_kind(
309 kind: AuthoredSemanticCandidateKindV1,
310) -> (
311 CanonicalAuthoredDeclarationKindV1,
312 Option<ResolvedIntrinsicIdentityV1>,
313 Option<DerivedAuthoredEvidenceV2>,
314) {
315 let AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
316 intrinsic_kind,
317 intrinsic_identity,
318 } = kind
319 else {
320 return match kind {
321 AuthoredSemanticCandidateKindV1::DerivedComputedGetter {
322 state_dependencies,
323 computed_dependencies,
324 } => (
325 CanonicalAuthoredDeclarationKindV1::Computed,
326 None,
327 Some(DerivedAuthoredEvidenceV2::ComputedGetter {
328 state_dependencies,
329 computed_dependencies,
330 }),
331 ),
332 AuthoredSemanticCandidateKindV1::TsxBinding => {
333 (CanonicalAuthoredDeclarationKindV1::TsxBinding, None, None)
334 }
335 AuthoredSemanticCandidateKindV1::TsxEventReference => (
336 CanonicalAuthoredDeclarationKindV1::TsxEventReference,
337 None,
338 None,
339 ),
340 AuthoredSemanticCandidateKindV1::ResolvedIntrinsic { .. } => unreachable!(),
341 };
342 };
343
344 let declaration_kind = match intrinsic_kind {
345 CanonicalIntrinsicKindV1::Component => CanonicalAuthoredDeclarationKindV1::Component,
346 CanonicalIntrinsicKindV1::State => CanonicalAuthoredDeclarationKindV1::State,
347 CanonicalIntrinsicKindV1::Action => CanonicalAuthoredDeclarationKindV1::Action,
348 CanonicalIntrinsicKindV1::Computed => CanonicalAuthoredDeclarationKindV1::Computed,
349 CanonicalIntrinsicKindV1::Effect => CanonicalAuthoredDeclarationKindV1::Effect,
350 CanonicalIntrinsicKindV1::Slot => CanonicalAuthoredDeclarationKindV1::Slot,
351 CanonicalIntrinsicKindV1::Context => CanonicalAuthoredDeclarationKindV1::ContextToken,
352 CanonicalIntrinsicKindV1::Provide => CanonicalAuthoredDeclarationKindV1::ContextProvider,
353 CanonicalIntrinsicKindV1::Consume => CanonicalAuthoredDeclarationKindV1::ContextConsumer,
354 CanonicalIntrinsicKindV1::Form => CanonicalAuthoredDeclarationKindV1::Form,
355 CanonicalIntrinsicKindV1::Serialize => CanonicalAuthoredDeclarationKindV1::Serialization,
356 CanonicalIntrinsicKindV1::Field => CanonicalAuthoredDeclarationKindV1::FormField,
357 CanonicalIntrinsicKindV1::Validate => CanonicalAuthoredDeclarationKindV1::Validation,
358 CanonicalIntrinsicKindV1::Submit => CanonicalAuthoredDeclarationKindV1::Submission,
359 CanonicalIntrinsicKindV1::Resource => CanonicalAuthoredDeclarationKindV1::Resource,
360 CanonicalIntrinsicKindV1::Loader => CanonicalAuthoredDeclarationKindV1::RouteLoader,
361 CanonicalIntrinsicKindV1::ServerAction => CanonicalAuthoredDeclarationKindV1::ServerAction,
362 CanonicalIntrinsicKindV1::Opaque => CanonicalAuthoredDeclarationKindV1::Capability,
363 };
364 (declaration_kind, Some(intrinsic_identity), None)
365}
366
367#[cfg(test)]
368mod tests {
369 use presolve_parser::parse_file;
370
371 use super::{
372 compose_authored_semantics_v1, normalize_authored_semantics_v1,
373 AuthoredSemanticCandidateKindV1, AuthoredSemanticCompositionErrorV1,
374 AuthoredSemanticNormalizationErrorV1, AuthoredSourceRangeV1,
375 CanonicalAuthoredDeclarationKindV1, CanonicalIntrinsicKindV1,
376 ResolvedAuthoredSemanticCandidateV1, ResolvedIntrinsicIdentityV1,
377 };
378
379 fn candidate(
380 subject: &str,
381 start: usize,
382 kind: CanonicalIntrinsicKindV1,
383 ) -> ResolvedAuthoredSemanticCandidateV1 {
384 ResolvedAuthoredSemanticCandidateV1 {
385 subject: subject.to_owned(),
386 source: AuthoredSourceRangeV1 {
387 start,
388 end: start + 5,
389 line: 1,
390 column: start + 1,
391 },
392 kind: AuthoredSemanticCandidateKindV1::ResolvedIntrinsic {
393 intrinsic_kind: kind,
394 intrinsic_identity: ResolvedIntrinsicIdentityV1 {
395 name: "renamedFrameworkExport".to_owned(),
396 flags: 2_097_152,
397 declaration_modules: vec![
398 "node_modules/@presolve/framework/index.d.ts".to_owned()
399 ],
400 },
401 },
402 }
403 }
404
405 #[test]
406 fn normalizes_resolved_candidates_without_using_source_spelling() {
407 let parsed = parse_file("src/Card.tsx", "const Card = frameworkUse();");
408 let state = candidate("Card.count", 20, CanonicalIntrinsicKindV1::State);
409 let component = candidate("Card", 6, CanonicalIntrinsicKindV1::Component);
410
411 let model =
412 normalize_authored_semantics_v1(&parsed, [state.clone(), component.clone(), state])
413 .expect("valid resolved candidates");
414
415 assert_eq!(model.schema_version, 3);
416 assert_eq!(model.declarations.len(), 2);
417 assert_eq!(model.declarations[0].subject, "Card");
418 assert_eq!(
419 model.declarations[0].kind,
420 CanonicalAuthoredDeclarationKindV1::Component
421 );
422 assert_eq!(model.declarations[1].subject, "Card.count");
423 assert_eq!(
424 model.declarations[1].kind,
425 CanonicalAuthoredDeclarationKindV1::State
426 );
427 assert_eq!(
428 model.declarations[1]
429 .intrinsic_identity
430 .as_ref()
431 .unwrap()
432 .name,
433 "renamedFrameworkExport"
434 );
435 assert_eq!(
436 serde_json::to_value(&model).expect("serializable model"),
437 serde_json::json!({
438 "schema_version": 3,
439 "source_path": "src/Card.tsx",
440 "declarations": [
441 {
442 "kind": "component",
443 "subject": "Card",
444 "source": { "start": 6, "end": 11, "line": 1, "column": 7 },
445 "intrinsic_identity": {
446 "name": "renamedFrameworkExport",
447 "flags": 2_097_152,
448 "declaration_modules": ["node_modules/@presolve/framework/index.d.ts"]
449 }
450 },
451 {
452 "kind": "state",
453 "subject": "Card.count",
454 "source": { "start": 20, "end": 25, "line": 1, "column": 21 },
455 "intrinsic_identity": {
456 "name": "renamedFrameworkExport",
457 "flags": 2_097_152,
458 "declaration_modules": ["node_modules/@presolve/framework/index.d.ts"]
459 }
460 }
461 ]
462 })
463 );
464 }
465
466 #[test]
467 fn composes_same_source_models_and_rejects_cross_source_mixing() {
468 let parsed = parse_file("src/Card.tsx", "const Card = frameworkUse();");
469 let component = normalize_authored_semantics_v1(
470 &parsed,
471 [candidate("Card", 6, CanonicalIntrinsicKindV1::Component)],
472 )
473 .unwrap();
474 let state = normalize_authored_semantics_v1(
475 &parsed,
476 [candidate("Card.count", 20, CanonicalIntrinsicKindV1::State)],
477 )
478 .unwrap();
479 let composed = compose_authored_semantics_v1([component.clone(), state]).unwrap();
480 assert_eq!(composed.declarations.len(), 2);
481
482 let other = normalize_authored_semantics_v1(
483 &parse_file("src/Other.tsx", "const Other = frameworkUse();"),
484 [candidate("Other", 6, CanonicalIntrinsicKindV1::Component)],
485 )
486 .unwrap();
487 assert!(matches!(
488 compose_authored_semantics_v1([component, other]),
489 Err(AuthoredSemanticCompositionErrorV1::SourcePathMismatch { .. })
490 ));
491 }
492
493 #[test]
494 fn retains_tsx_binding_and_event_facts_without_an_intrinsic_identity() {
495 let parsed = parse_file(
496 "src/Card.tsx",
497 "const Card = <button onClick={save}>{count}</button>;",
498 );
499 let binding = ResolvedAuthoredSemanticCandidateV1 {
500 subject: "count".to_owned(),
501 source: AuthoredSourceRangeV1 {
502 start: 44,
503 end: 49,
504 line: 1,
505 column: 45,
506 },
507 kind: AuthoredSemanticCandidateKindV1::TsxBinding,
508 };
509 let event = ResolvedAuthoredSemanticCandidateV1 {
510 subject: "save".to_owned(),
511 source: AuthoredSourceRangeV1 {
512 start: 37,
513 end: 41,
514 line: 1,
515 column: 38,
516 },
517 kind: AuthoredSemanticCandidateKindV1::TsxEventReference,
518 };
519
520 let model = normalize_authored_semantics_v1(&parsed, [binding, event])
521 .expect("TSX syntax candidates fit the source AST");
522
523 assert_eq!(model.declarations.len(), 2);
524 assert!(model.declarations.iter().any(|declaration| {
525 declaration.kind == CanonicalAuthoredDeclarationKindV1::TsxBinding
526 && declaration.intrinsic_identity.is_none()
527 }));
528 assert!(model.declarations.iter().any(|declaration| {
529 declaration.kind == CanonicalAuthoredDeclarationKindV1::TsxEventReference
530 && declaration.intrinsic_identity.is_none()
531 }));
532 }
533
534 #[test]
535 fn rejects_candidates_outside_the_general_source_ast_extent() {
536 let parsed = parse_file("src/Card.tsx", "const Card = 1;");
537 let error = normalize_authored_semantics_v1(
538 &parsed,
539 [candidate("Card", 99, CanonicalIntrinsicKindV1::Component)],
540 )
541 .expect_err("invalid range must not enter the canonical model");
542
543 assert!(matches!(
544 error,
545 AuthoredSemanticNormalizationErrorV1::InvalidSourceRange { subject, .. }
546 if subject == "Card"
547 ));
548 }
549}