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