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