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