1use crate::application_semantic_model::ApplicationSemanticModel;
2use crate::semantic_id::SemanticOwner;
3use crate::{
4 build_ordinary_template_instance_registry, build_runtime_component_artifact,
5 build_template_manifest_from_asm, validate_ordinary_template_instance_registry,
6 validate_runtime_component_artifact, validate_template_manifest, EffectOperationClassification,
7 EffectRenderBoundary, EffectValidation, ManifestEventKind, OrdinaryTemplateIntegrityCode,
8 SemanticTypeId, EFFECT_CAPABILITY_REGISTRY, TEMPLATE_MANIFEST_SCHEMA_VERSION,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AsmValidationDiagnostic {
13 pub code: String,
14 pub message: String,
15}
16
17#[must_use]
18#[allow(clippy::too_many_lines)]
19pub fn validate_application_semantic_model(
20 model: &ApplicationSemanticModel,
21) -> Vec<AsmValidationDiagnostic> {
22 let mut diagnostics = Vec::new();
23
24 for (id, owner) in &model.ownership {
25 if model.entity(id).is_none() {
26 diagnostics.push(AsmValidationDiagnostic {
27 code: "PSASM1001".to_string(),
28 message: format!("ownership references missing semantic entity `{id}`"),
29 });
30 }
31 if !model.provenance.contains_key(id) {
32 diagnostics.push(AsmValidationDiagnostic {
33 code: "PSASM1002".to_string(),
34 message: format!("semantic entity `{id}` is missing source provenance"),
35 });
36 }
37 if let SemanticOwner::Entity(owner_id) = owner {
38 if model.entity(owner_id).is_none() {
39 diagnostics.push(AsmValidationDiagnostic {
40 code: "PSASM1003".to_string(),
41 message: format!("semantic entity `{id}` has missing owner `{owner_id}`"),
42 });
43 }
44 }
45 }
46
47 for id in model.provenance.keys() {
48 if !model.ownership.contains_key(id) {
49 diagnostics.push(AsmValidationDiagnostic {
50 code: "PSASM1004".to_string(),
51 message: format!("provenance references unowned semantic entity `{id}`"),
52 });
53 }
54 }
55
56 for reference in &model.references {
57 if model.entity(&reference.source).is_none() || model.entity(&reference.target).is_none() {
58 diagnostics.push(AsmValidationDiagnostic {
59 code: "PSASM1005".to_string(),
60 message: format!(
61 "reference from `{}` to `{}` has a missing endpoint",
62 reference.source, reference.target
63 ),
64 });
65 }
66 let source_provenance_matches = model.provenance(&reference.source)
67 == Some(&reference.provenance)
68 || model.form_field_bindings.values().any(|binding| {
69 binding.id.as_semantic_id() == &reference.source
70 && binding.expression_provenance == reference.provenance
71 })
72 || model.validation_rules.values().any(|rule| {
73 rule.id.as_semantic_id() == &reference.source
74 && rule.argument_provenance.as_ref() == Some(&reference.provenance)
75 })
76 || model
77 .consumer_for_semantic_id(&reference.source)
78 .is_some_and(|consumer| {
79 consumer.context_designator.provenance == reference.provenance
80 })
81 || model
82 .expression_graph
83 .nodes_for(&reference.source)
84 .iter()
85 .any(|node| node.provenance == reference.provenance);
86 if !source_provenance_matches {
87 diagnostics.push(AsmValidationDiagnostic {
88 code: "PSASM1006".to_string(),
89 message: format!(
90 "reference source `{}` has mismatched provenance",
91 reference.source
92 ),
93 });
94 }
95 }
96
97 validate_semantic_types(model, &mut diagnostics);
98 validate_form_field_bindings(model, &mut diagnostics);
99 validate_form_ownership(model, &mut diagnostics);
100 validate_form_validation(model, &mut diagnostics);
101 validate_form_tracking(model, &mut diagnostics);
102 validate_form_submissions(model, &mut diagnostics);
103 validate_form_serialization(model, &mut diagnostics);
104 validate_form_reset(model, &mut diagnostics);
105 validate_contexts(model, &mut diagnostics);
106 validate_providers(model, &mut diagnostics);
107 validate_consumers(model, &mut diagnostics);
108 validate_context_resolution(model, &mut diagnostics);
109 validate_context_typing(model, &mut diagnostics);
110 validate_context_ownership(model, &mut diagnostics);
111 validate_context_dependency(model, &mut diagnostics);
112 validate_context_lifetime(model, &mut diagnostics);
113 validate_context_evaluation(model, &mut diagnostics);
114 validate_component_instance_scope(model, &mut diagnostics);
115 validate_component_composition(model, &mut diagnostics);
116 validate_component_initialization(model, &mut diagnostics);
117 validate_component_ir(model, &mut diagnostics);
118 validate_optimized_component_ir(model, &mut diagnostics);
119 validate_instance_context(model, &mut diagnostics);
120 validate_slot_bindings(model, &mut diagnostics);
121 validate_composition_types(model, &mut diagnostics);
122 validate_effect_statement_types(model, &mut diagnostics);
123 validate_effect_execution_plan(model, &mut diagnostics);
124 validate_component_diagnostic_metadata(model, &mut diagnostics);
125 validate_template_action_bindings(model, &mut diagnostics);
126 validate_ordinary_template_instance_projection(model, &mut diagnostics);
127
128 diagnostics
129}
130
131fn validate_ordinary_template_instance_projection(
132 model: &ApplicationSemanticModel,
133 diagnostics: &mut Vec<AsmValidationDiagnostic>,
134) {
135 let registry = build_ordinary_template_instance_registry(model);
136 let artifact = build_runtime_component_artifact(model, &model.component_ir_optimization);
137 let manifest = build_template_manifest_from_asm(model);
138 if validate_ordinary_template_instance_registry(model, ®istry).is_err() {
139 diagnostics.push(AsmValidationDiagnostic {
140 code: OrdinaryTemplateIntegrityCode::StaleRegistry
141 .as_str()
142 .to_string(),
143 message: "ordinary template registry drifted from canonical products".to_string(),
144 });
145 }
146 if validate_runtime_component_artifact(&artifact).is_err()
147 || validate_template_manifest(&manifest).is_err()
148 {
149 diagnostics.push(AsmValidationDiagnostic {
150 code: OrdinaryTemplateIntegrityCode::ArtifactManifestDrift
151 .as_str()
152 .to_string(),
153 message: "ordinary template artifact or manifest projection is invalid".to_string(),
154 });
155 }
156}
157
158fn validate_form_validation(
159 model: &ApplicationSemanticModel,
160 diagnostics: &mut Vec<AsmValidationDiagnostic>,
161) {
162 let expected_products =
163 crate::collect_validation_products(&model.components, &model.forms, &model.form_fields);
164 if model.validation_rule_candidates != expected_products.candidates
165 || model.validation_rules != expected_products.rules
166 {
167 diagnostics.push(AsmValidationDiagnostic {
168 code: "PSASM1241".to_string(),
169 message: "validation products do not match canonical I6 lowering".to_string(),
170 });
171 }
172 let validation = crate::validate_validation_graph(
173 &model.validation_graph,
174 &model.component_instance_plan.roots,
175 &model.form_ownership,
176 &model.forms,
177 &model.form_fields,
178 &model.validation_rules,
179 &model.validation_rule_candidates,
180 );
181 diagnostics.extend(
182 validation
183 .diagnostics
184 .iter()
185 .map(|diagnostic| AsmValidationDiagnostic {
186 code: diagnostic.code.clone(),
187 message: diagnostic.message.clone(),
188 }),
189 );
190 if model.validation_graph.validation != validation {
191 diagnostics.push(AsmValidationDiagnostic {
192 code: "PSASM1240".to_string(),
193 message: "validation graph retained stale validation facts".to_string(),
194 });
195 }
196 let expected_plans = crate::collect_validation_dependency_plans(
197 &model.forms,
198 &model.form_fields,
199 &model.validation_rules,
200 &model.form_ownership,
201 &model.validation_graph,
202 );
203 if model.validation_dependency_plans != expected_plans {
204 diagnostics.push(AsmValidationDiagnostic {
205 code: "PSASM1272".to_string(),
206 message: "validation dependency plans do not match canonical I7 planning".to_string(),
207 });
208 }
209 let planning = crate::validate_validation_dependency_plans(
210 &model.validation_dependency_plans,
211 &model.forms,
212 &model.form_fields,
213 &model.validation_rules,
214 &model.form_ownership,
215 &model.validation_graph,
216 );
217 diagnostics.extend(
218 planning
219 .diagnostics
220 .iter()
221 .map(|diagnostic| AsmValidationDiagnostic {
222 code: diagnostic.code.clone(),
223 message: diagnostic.message.clone(),
224 }),
225 );
226 if model.validation_dependency_plans.validation != planning {
227 diagnostics.push(AsmValidationDiagnostic {
228 code: "PSASM1271".to_string(),
229 message: "validation dependency plans retained stale validation facts".to_string(),
230 });
231 }
232}
233
234fn validate_form_tracking(
235 model: &ApplicationSemanticModel,
236 diagnostics: &mut Vec<AsmValidationDiagnostic>,
237) {
238 let expected = crate::collect_form_tracking_products(
239 &model.forms,
240 &model.form_fields,
241 &model.form_field_bindings,
242 &model.form_ownership,
243 );
244 if model.form_tracking != expected {
245 diagnostics.push(AsmValidationDiagnostic {
246 code: "PSASM1285".to_string(),
247 message: "I8 form tracking products do not match canonical declaration planning"
248 .to_string(),
249 });
250 }
251 let dirty = crate::validate_dirty_tracking_graph(
252 &model.form_tracking.dirty,
253 &model.forms,
254 &model.form_fields,
255 &model.form_field_bindings,
256 &model.form_ownership,
257 );
258 let touched = crate::validate_touched_tracking_graph(
259 &model.form_tracking.touched,
260 &model.forms,
261 &model.form_fields,
262 &model.form_field_bindings,
263 &model.form_ownership,
264 );
265 diagnostics.extend(
266 dirty
267 .diagnostics
268 .iter()
269 .chain(touched.diagnostics.iter())
270 .map(|diagnostic| AsmValidationDiagnostic {
271 code: diagnostic.code.clone(),
272 message: diagnostic.message.clone(),
273 }),
274 );
275}
276
277fn validate_form_submissions(
278 model: &ApplicationSemanticModel,
279 diagnostics: &mut Vec<AsmValidationDiagnostic>,
280) {
281 let expected = crate::collect_submission_products(
282 &model.components,
283 &model.forms,
284 &model.form_fields,
285 &model.validation_rules,
286 &model.effect_trigger_plan,
287 );
288 if model.submissions != expected {
289 diagnostics.push(AsmValidationDiagnostic {
290 code: "PSASM1286".to_string(),
291 message: "I9 submission products do not match canonical declaration planning"
292 .to_string(),
293 });
294 }
295}
296
297fn validate_form_serialization(
298 model: &ApplicationSemanticModel,
299 diagnostics: &mut Vec<AsmValidationDiagnostic>,
300) {
301 let expected = crate::collect_serialization_products(
302 &model.components,
303 &model.forms,
304 &model.form_fields,
305 &model.submissions.plans,
306 );
307 if model.serialization != expected {
308 diagnostics.push(AsmValidationDiagnostic {
309 code: "PSASM1287".to_string(),
310 message: "I10 serialization products do not match canonical declaration planning"
311 .to_string(),
312 });
313 }
314}
315
316fn validate_form_reset(
317 model: &ApplicationSemanticModel,
318 diagnostics: &mut Vec<AsmValidationDiagnostic>,
319) {
320 let expected = crate::collect_reset_products(
321 &model.forms,
322 &model.form_fields,
323 &model.form_field_bindings,
324 &model.form_tracking,
325 );
326 if model.reset != expected {
327 diagnostics.push(AsmValidationDiagnostic {
328 code: "PSASM1288".to_string(),
329 message: "I11 reset products do not match canonical declaration planning".to_string(),
330 });
331 }
332}
333
334fn validate_form_ownership(
335 model: &ApplicationSemanticModel,
336 diagnostics: &mut Vec<AsmValidationDiagnostic>,
337) {
338 let validation = crate::validate_form_ownership_graph(&model.form_ownership, model);
339 diagnostics.extend(
340 validation
341 .diagnostics
342 .iter()
343 .map(|diagnostic| AsmValidationDiagnostic {
344 code: diagnostic.code.clone(),
345 message: diagnostic.message.clone(),
346 }),
347 );
348 if model.form_ownership.validation != validation {
349 diagnostics.push(AsmValidationDiagnostic {
350 code: "PSASM1220".to_string(),
351 message: "Form ownership graph retained stale validation facts".to_string(),
352 });
353 }
354}
355
356fn validate_form_field_bindings(
357 model: &ApplicationSemanticModel,
358 diagnostics: &mut Vec<AsmValidationDiagnostic>,
359) {
360 let expected = crate::collect_form_field_binding_products(
361 &model.components,
362 &model.templates,
363 &model.forms,
364 &model.form_fields,
365 &model.form_field_declaration_candidates,
366 );
367 if model.form_field_binding_candidates != expected.candidates
368 || model.form_field_bindings != expected.bindings
369 {
370 diagnostics.push(AsmValidationDiagnostic {
371 code: "PSASM1202".to_string(),
372 message: "Form Field bindings do not match canonical I3/template lowering".to_string(),
373 });
374 }
375}
376
377fn validate_component_ir(
378 model: &ApplicationSemanticModel,
379 diagnostics: &mut Vec<AsmValidationDiagnostic>,
380) {
381 if !crate::validate_component_ir(model, &model.component_ir).is_empty() {
382 diagnostics.push(AsmValidationDiagnostic {
383 code: "PSASM1199".to_string(),
384 message: "component IR does not match canonical H10 operations".to_string(),
385 });
386 }
387}
388
389fn validate_optimized_component_ir(
390 model: &ApplicationSemanticModel,
391 diagnostics: &mut Vec<AsmValidationDiagnostic>,
392) {
393 if !crate::validate_optimized_component_ir(&model.component_ir_optimization).is_empty() {
394 diagnostics.push(AsmValidationDiagnostic {
395 code: "PSASM1200".to_string(),
396 message: "optimized component IR does not match the canonical H12 projection"
397 .to_string(),
398 });
399 }
400}
401
402fn validate_component_initialization(
403 model: &ApplicationSemanticModel,
404 diagnostics: &mut Vec<AsmValidationDiagnostic>,
405) {
406 let expected = crate::plan_component_initialization(
407 &model.component_instance_plan,
408 &model.slot_bindings,
409 &model.composition_types,
410 &model.instance_context,
411 );
412 if model.component_initialization != expected {
413 diagnostics.push(AsmValidationDiagnostic {
414 code: "PSASM1198".to_string(),
415 message: "component initialization plan does not match canonical H4/H6/H7/H8 products"
416 .to_string(),
417 });
418 }
419}
420
421fn validate_component_composition(
422 model: &ApplicationSemanticModel,
423 diagnostics: &mut Vec<AsmValidationDiagnostic>,
424) {
425 let components = model
426 .components
427 .iter()
428 .map(|component| component.id.clone())
429 .collect();
430 let expected = crate::analyze_component_composition(
431 &components,
432 &model.component_invocations,
433 &model.component_instance_plan,
434 );
435 if model.component_composition != expected {
436 diagnostics.push(AsmValidationDiagnostic {
437 code: "PSASM1197".to_string(),
438 message: "component composition cycles do not match canonical resolved invocations"
439 .to_string(),
440 });
441 }
442}
443
444fn validate_composition_types(
445 model: &ApplicationSemanticModel,
446 diagnostics: &mut Vec<AsmValidationDiagnostic>,
447) {
448 let component_ids = model
449 .components
450 .iter()
451 .map(|component| component.id.clone())
452 .collect();
453 let expected = crate::collect_composition_type_products(
454 &component_ids,
455 &model.component_invocations,
456 &model.slot_bindings,
457 &model.slots,
458 &model.slot_content_fragments,
459 &model.slot_outlets,
460 &model.instance_context,
461 &model.context_binding_types,
462 &model.context_types,
463 &model.provider_types,
464 &model.context_lifetime,
465 &model.ownership,
466 &model.references,
467 );
468 if model.composition_types != expected {
469 diagnostics.push(AsmValidationDiagnostic {
470 code: "PSASM1196".to_string(),
471 message: "composition typing does not match canonical H2/H6/H7 products".to_string(),
472 });
473 }
474}
475
476fn validate_slot_bindings(
477 model: &ApplicationSemanticModel,
478 diagnostics: &mut Vec<AsmValidationDiagnostic>,
479) {
480 let expected = crate::collect_slot_bindings(
481 &model.component_instance_plan,
482 &model.component_invocations,
483 &model.slots,
484 &model.slot_content_fragments,
485 &model.slot_outlets,
486 );
487 if model.slot_bindings != expected {
488 diagnostics.push(AsmValidationDiagnostic {
489 code: "PSASM1195".to_string(),
490 message: "Slot binding registry does not match canonical H3 facts and H4 instances"
491 .to_string(),
492 });
493 }
494}
495
496fn validate_instance_context(
497 model: &ApplicationSemanticModel,
498 diagnostics: &mut Vec<AsmValidationDiagnostic>,
499) {
500 let expected = crate::collect_instance_context_registry(
501 &model.component_instance_scope,
502 &model.contexts,
503 &model.providers,
504 &model.consumers,
505 );
506 if model.instance_context != expected {
507 diagnostics.push(AsmValidationDiagnostic {
508 code: "PSASM1194".to_string(),
509 message:
510 "instance Context registry does not match canonical declarations and H5 ancestry"
511 .to_string(),
512 });
513 }
514}
515
516fn validate_component_instance_scope(
517 model: &ApplicationSemanticModel,
518 diagnostics: &mut Vec<AsmValidationDiagnostic>,
519) {
520 let expected = crate::build_component_instance_scope_graph(&model.component_instance_plan);
521 if model.component_instance_scope != expected {
522 diagnostics.push(AsmValidationDiagnostic {
523 code: "PSASM1192".to_string(),
524 message: "component instance scope graph does not match the canonical H4 plan"
525 .to_string(),
526 });
527 }
528 diagnostics.extend(
529 crate::validate_component_instance_scope_graph(&model.component_instance_scope)
530 .into_iter()
531 .map(|diagnostic| AsmValidationDiagnostic {
532 code: "PSASM1193".to_string(),
533 message: format!(
534 "component instance scope {:?} at `{}`{}",
535 diagnostic.violation,
536 diagnostic.instance,
537 diagnostic.related.map_or_else(String::new, |related| {
538 format!(" related to `{related}`")
539 })
540 ),
541 }),
542 );
543}
544
545fn validate_context_dependency(
546 model: &ApplicationSemanticModel,
547 diagnostics: &mut Vec<AsmValidationDiagnostic>,
548) {
549 let expected = crate::collect_context_dependency_graph(
550 &model.components,
551 &model.contexts,
552 &model.providers,
553 &model.consumers,
554 &model.context_resolutions,
555 &model.context_types,
556 &model.provider_types,
557 &model.context_binding_types,
558 &model.computed_values,
559 &model.expression_graph,
560 );
561 if model.context_dependency != expected {
562 diagnostics.push(AsmValidationDiagnostic {
563 code: "PSASM1189".to_string(),
564 message: "Context dependency graph does not match canonical ASM products".to_string(),
565 });
566 }
567}
568
569fn validate_context_lifetime(
570 model: &ApplicationSemanticModel,
571 diagnostics: &mut Vec<AsmValidationDiagnostic>,
572) {
573 let expected = crate::collect_context_lifetime_analysis(
574 &model.components,
575 &model.contexts,
576 &model.providers,
577 &model.consumers,
578 &model.computed_values,
579 &model.context_ownership,
580 &model.component_scope,
581 &model.context_resolutions,
582 &model.context_dependency,
583 &model.provenance,
584 );
585 if model.context_lifetime != expected {
586 diagnostics.push(AsmValidationDiagnostic {
587 code: "PSASM1190".to_string(),
588 message: "Context lifetime analysis does not match canonical ASM products".to_string(),
589 });
590 }
591}
592
593fn validate_context_evaluation(
594 model: &ApplicationSemanticModel,
595 diagnostics: &mut Vec<AsmValidationDiagnostic>,
596) {
597 let expected = crate::collect_context_evaluation_plan(
598 &model.contexts,
599 &model.providers,
600 &model.context_resolutions,
601 &model.context_types,
602 &model.provider_types,
603 &model.context_binding_types,
604 &model.context_lifetime,
605 &model.context_dependency,
606 &model.computed_evaluation_plan,
607 &model.component_scope,
608 );
609 if model.context_evaluation != expected {
610 diagnostics.push(AsmValidationDiagnostic {
611 code: "PSASM1191".to_string(),
612 message: "Context evaluation plan does not match canonical ASM products".to_string(),
613 });
614 }
615}
616
617#[allow(clippy::too_many_lines)]
618fn validate_context_ownership(
619 model: &ApplicationSemanticModel,
620 diagnostics: &mut Vec<AsmValidationDiagnostic>,
621) {
622 let graph = &model.context_ownership;
623 if graph
624 .nodes
625 .iter()
626 .any(|node| matches!(node.id, crate::ContextOwnershipNodeId::Component(ref id) if model.component(id).is_none()))
627 {
628 diagnostics.push(AsmValidationDiagnostic {
629 code: "PSASM1182".to_string(),
630 message: "Context ownership graph references a missing component".to_string(),
631 });
632 }
633 if graph.nodes.len()
634 != model.contexts.len()
635 + model.providers.len()
636 + model.consumers.len()
637 + graph
638 .nodes
639 .iter()
640 .filter(|node| matches!(node.kind, crate::ContextOwnershipNodeKind::Component))
641 .count()
642 {
643 diagnostics.push(AsmValidationDiagnostic {
644 code: "PSASM1183".to_string(),
645 message: "Context ownership graph has an invalid node domain".to_string(),
646 });
647 }
648 for context in model.contexts.values() {
649 let expected_owner = context.owner.entity_id();
650 let default = context.default_expression.as_ref();
651 if graph.owner_of_context(&context.id) != expected_owner
652 || graph.context_default_expression(&context.id) != default
653 || !expected_owner.is_some_and(|owner| {
654 exactly_one_ownership_edge(
655 graph,
656 &crate::ContextOwnershipOwnerId::Component(owner.clone()),
657 &crate::ContextOwnershipTargetId::Context(context.id.clone()),
658 crate::ContextOwnershipEdgeKind::ComponentOwnsContext,
659 )
660 })
661 {
662 diagnostics.push(AsmValidationDiagnostic {
663 code: "PSASM1184".to_string(),
664 message: format!("Context `{}` has an invalid ownership record", context.id),
665 });
666 }
667 if let Some(default) = default {
668 let default_provenance = model
669 .expression_graph
670 .node(default)
671 .map(|node| &node.provenance);
672 let default_edge = graph.edges.iter().find(|edge| {
673 edge.owner == crate::ContextOwnershipOwnerId::Context(context.id.clone())
674 && edge.owned == crate::ContextOwnershipTargetId::Expression(default.clone())
675 && edge.kind == crate::ContextOwnershipEdgeKind::ContextOwnsDefaultExpression
676 });
677 if default_edge.is_none_or(|edge| Some(&edge.provenance) != default_provenance) {
678 diagnostics.push(AsmValidationDiagnostic {
679 code: "PSASM1188".to_string(),
680 message: format!(
681 "Context `{}` has an invalid default ownership edge",
682 context.id
683 ),
684 });
685 }
686 }
687 }
688 for provider in model.providers.values() {
689 if graph.owner_of_provider(&provider.id) != provider.owner.entity_id()
690 || !provider.owner.entity_id().is_some_and(|owner| {
691 exactly_one_ownership_edge(
692 graph,
693 &crate::ContextOwnershipOwnerId::Component(owner.clone()),
694 &crate::ContextOwnershipTargetId::Provider(provider.id.clone()),
695 crate::ContextOwnershipEdgeKind::ComponentOwnsProvider,
696 )
697 })
698 {
699 diagnostics.push(AsmValidationDiagnostic {
700 code: "PSASM1185".to_string(),
701 message: format!("Provider `{}` has an invalid ownership record", provider.id),
702 });
703 }
704 }
705 for consumer in model.consumers.values() {
706 if graph.owner_of_consumer(&consumer.id) != consumer.owner.entity_id()
707 || !consumer.owner.entity_id().is_some_and(|owner| {
708 exactly_one_ownership_edge(
709 graph,
710 &crate::ContextOwnershipOwnerId::Component(owner.clone()),
711 &crate::ContextOwnershipTargetId::Consumer(consumer.id.clone()),
712 crate::ContextOwnershipEdgeKind::ComponentOwnsConsumer,
713 )
714 })
715 {
716 diagnostics.push(AsmValidationDiagnostic {
717 code: "PSASM1186".to_string(),
718 message: format!("Consumer `{}` has an invalid ownership record", consumer.id),
719 });
720 }
721 }
722 if graph.edges.windows(2).any(|pair| {
723 (&pair[0].owner, pair[0].kind, &pair[0].owned)
724 > (&pair[1].owner, pair[1].kind, &pair[1].owned)
725 }) || graph.edges.iter().any(|edge| {
726 !matches!(
727 (&edge.owner, &edge.owned, edge.kind),
728 (
729 crate::ContextOwnershipOwnerId::Component(_),
730 crate::ContextOwnershipTargetId::Context(_),
731 crate::ContextOwnershipEdgeKind::ComponentOwnsContext
732 ) | (
733 crate::ContextOwnershipOwnerId::Component(_),
734 crate::ContextOwnershipTargetId::Provider(_),
735 crate::ContextOwnershipEdgeKind::ComponentOwnsProvider
736 ) | (
737 crate::ContextOwnershipOwnerId::Component(_),
738 crate::ContextOwnershipTargetId::Consumer(_),
739 crate::ContextOwnershipEdgeKind::ComponentOwnsConsumer
740 ) | (
741 crate::ContextOwnershipOwnerId::Context(_),
742 crate::ContextOwnershipTargetId::Expression(_),
743 crate::ContextOwnershipEdgeKind::ContextOwnsDefaultExpression
744 )
745 )
746 }) {
747 diagnostics.push(AsmValidationDiagnostic {
748 code: "PSASM1187".to_string(),
749 message: "Context ownership graph has an invalid edge domain or order".to_string(),
750 });
751 }
752}
753
754fn exactly_one_ownership_edge(
755 graph: &crate::ContextOwnershipGraph,
756 source: &crate::ContextOwnershipOwnerId,
757 target: &crate::ContextOwnershipTargetId,
758 kind: crate::ContextOwnershipEdgeKind,
759) -> bool {
760 graph
761 .edges
762 .iter()
763 .filter(|edge| &edge.owner == source && &edge.owned == target && edge.kind == kind)
764 .count()
765 == 1
766}
767
768fn validate_context_typing(
769 model: &ApplicationSemanticModel,
770 diagnostics: &mut Vec<AsmValidationDiagnostic>,
771) {
772 if model.context_types.len() != model.contexts.len()
773 || model.provider_types.len() != model.providers.len()
774 || model.consumer_types.len() != model.consumers.len()
775 || model.context_binding_types.len() != model.consumers.len()
776 {
777 diagnostics.push(AsmValidationDiagnostic {
778 code: "PSASM1176".to_string(),
779 message: "Context typing records do not exactly cover canonical entities".to_string(),
780 });
781 }
782 for context in model.contexts.values() {
783 if model.context_type(&context.id).is_none_or(|record| {
784 record.declared_type != context.declared_type_id
785 || record.normalized_type != context.declared_type_id
786 }) {
787 diagnostics.push(AsmValidationDiagnostic {
788 code: "PSASM1177".to_string(),
789 message: format!("Context `{}` has an invalid type record", context.id),
790 });
791 }
792 }
793 for provider in model.providers.values() {
794 if model.provider_type(&provider.id).is_none_or(|record| {
795 record.declared_type != provider.declared_type_id
796 || record.inferred_value_type == provider.declared_type_id
797 || record.context != Some(provider.context.clone())
798 }) {
799 diagnostics.push(AsmValidationDiagnostic {
800 code: "PSASM1178".to_string(),
801 message: format!("Provider `{}` has an invalid type record", provider.id),
802 });
803 }
804 }
805 for consumer in model.consumers.values() {
806 let Some(binding) = model.context_binding_type(&consumer.id) else {
807 continue;
808 };
809 if model.consumer_type(&consumer.id).is_none_or(|record| {
810 record.requested_type != consumer.requested_type_id
811 || record.context != consumer.context().cloned()
812 }) || model
813 .context_resolution(&consumer.id)
814 .is_none_or(|resolution| binding.resolution != resolution.result)
815 {
816 diagnostics.push(AsmValidationDiagnostic {
817 code: "PSASM1179".to_string(),
818 message: format!("Consumer `{}` has an invalid type record", consumer.id),
819 });
820 }
821 if let crate::ContextResolutionResult::Provider { provider, .. } = &binding.resolution {
822 if binding.provider.as_ref() != Some(provider) {
823 diagnostics.push(AsmValidationDiagnostic {
824 code: "PSASM1180".to_string(),
825 message: format!("Consumer `{}` does not retain its G4 Provider", consumer.id),
826 });
827 }
828 }
829 if binding.overall == crate::ContextBindingCompatibility::Compatible
830 && (binding.source_to_context != crate::CompatibilityStatus::Compatible
831 || binding.context_to_consumer != crate::CompatibilityStatus::Compatible
832 || binding.boundary_compatibility != crate::CompatibilityStatus::Compatible
833 || binding.serialization != crate::ContextSerializationCompatibility::Serializable)
834 {
835 diagnostics.push(AsmValidationDiagnostic {
836 code: "PSASM1181".to_string(),
837 message: format!(
838 "Consumer `{}` has a permissively compatible binding",
839 consumer.id
840 ),
841 });
842 }
843 }
844}
845
846#[allow(clippy::too_many_lines)]
847fn validate_context_resolution(
848 model: &ApplicationSemanticModel,
849 diagnostics: &mut Vec<AsmValidationDiagnostic>,
850) {
851 for scope_diagnostic in model.component_scope.diagnostics() {
852 diagnostics.push(AsmValidationDiagnostic {
853 code: "PSASM1165".to_string(),
854 message: scope_diagnostic.message,
855 });
856 }
857 if model.context_resolutions.len() != model.consumers.len()
858 || model
859 .context_resolutions
860 .keys()
861 .any(|consumer| !model.consumers.contains_key(consumer))
862 {
863 diagnostics.push(AsmValidationDiagnostic {
864 code: "PSASM1166".to_string(),
865 message: "Context resolution records do not exactly cover canonical Consumers"
866 .to_string(),
867 });
868 }
869
870 for (consumer_id, consumer) in &model.consumers {
871 let Some(resolution) = model.context_resolution(consumer_id) else {
872 continue;
873 };
874 if resolution.consumer != *consumer_id
875 || resolution.provenance != consumer.context_designator.provenance
876 || resolution.context != consumer.context().cloned()
877 {
878 diagnostics.push(AsmValidationDiagnostic {
879 code: "PSASM1167".to_string(),
880 message: format!("consumer `{consumer_id}` has a non-canonical resolution record"),
881 });
882 }
883 let expected_scopes = consumer.owner.entity_id().map_or_else(Vec::new, |owner| {
884 model.component_scope.ancestor_chain(owner)
885 });
886 if resolution.searched_scopes != expected_scopes {
887 diagnostics.push(AsmValidationDiagnostic {
888 code: "PSASM1168".to_string(),
889 message: format!(
890 "consumer `{consumer_id}` searched non-canonical component scopes"
891 ),
892 });
893 }
894
895 match &resolution.result {
896 crate::ContextResolutionResult::Provider {
897 provider,
898 provider_owner,
899 distance,
900 } => {
901 let provider_entity = model.provider(provider);
902 let context_matches = resolution.context.as_ref().is_some_and(|context| {
903 provider_entity.is_some_and(|provider| &provider.context == context)
904 });
905 let owner_matches = provider_entity.and_then(|provider| provider.owner.entity_id())
906 == Some(provider_owner);
907 let expected_owner = resolution.searched_scopes.get(*distance as usize);
908 if !context_matches || !owner_matches || expected_owner != Some(provider_owner) {
909 diagnostics.push(AsmValidationDiagnostic {
910 code: "PSASM1169".to_string(),
911 message: format!(
912 "consumer `{consumer_id}` has an invalid Provider resolution"
913 ),
914 });
915 }
916 if (*distance as usize) > resolution.searched_scopes.len()
917 || resolution.searched_scopes[..*distance as usize]
918 .iter()
919 .any(|scope| {
920 !resolution_candidates(model, scope, resolution.context.as_ref())
921 .is_empty()
922 })
923 || resolution_candidates(model, provider_owner, resolution.context.as_ref())
924 .len()
925 != 1
926 {
927 diagnostics.push(AsmValidationDiagnostic {
928 code: "PSASM1170".to_string(),
929 message: format!(
930 "consumer `{consumer_id}` does not select the nearest Provider"
931 ),
932 });
933 }
934 }
935 crate::ContextResolutionResult::ContextDefault {
936 context,
937 expression,
938 } => {
939 let default_is_canonical = model.context(context).is_some_and(|context_entity| {
940 context_entity.default_expression.is_some()
941 && model.expression_root(context.as_semantic_id()) == Some(expression)
942 });
943 if resolution.context.as_ref() != Some(context)
944 || !default_is_canonical
945 || resolution
946 .searched_scopes
947 .iter()
948 .any(|scope| !resolution_candidates(model, scope, Some(context)).is_empty())
949 {
950 diagnostics.push(AsmValidationDiagnostic {
951 code: "PSASM1171".to_string(),
952 message: format!(
953 "consumer `{consumer_id}` has an invalid Context default fallback"
954 ),
955 });
956 }
957 }
958 crate::ContextResolutionResult::Unresolved => {
959 let has_visible_provider = resolution.searched_scopes.iter().any(|scope| {
960 !resolution_candidates(model, scope, resolution.context.as_ref()).is_empty()
961 });
962 let has_default = resolution.context.as_ref().is_some_and(|context| {
963 model
964 .context(context)
965 .is_some_and(|entity| entity.default_expression.is_some())
966 });
967 if resolution.context.is_none() || has_visible_provider || has_default {
968 diagnostics.push(AsmValidationDiagnostic {
969 code: "PSASM1172".to_string(),
970 message: format!(
971 "consumer `{consumer_id}` has an invalid unresolved result"
972 ),
973 });
974 }
975 }
976 crate::ContextResolutionResult::Ambiguous {
977 providers,
978 distance,
979 } => {
980 let Some(scope) = resolution.searched_scopes.get(*distance as usize) else {
981 diagnostics.push(AsmValidationDiagnostic {
982 code: "PSASM1173".to_string(),
983 message: format!(
984 "consumer `{consumer_id}` has an invalid ambiguity distance"
985 ),
986 });
987 continue;
988 };
989 let candidates = resolution_candidates(model, scope, resolution.context.as_ref());
990 if providers.len() < 2
991 || providers.windows(2).any(|pair| pair[0] >= pair[1])
992 || *providers != candidates
993 || resolution.searched_scopes[..*distance as usize]
994 .iter()
995 .any(|scope| {
996 !resolution_candidates(model, scope, resolution.context.as_ref())
997 .is_empty()
998 })
999 {
1000 diagnostics.push(AsmValidationDiagnostic {
1001 code: "PSASM1174".to_string(),
1002 message: format!(
1003 "consumer `{consumer_id}` has an invalid ambiguity result"
1004 ),
1005 });
1006 }
1007 }
1008 crate::ContextResolutionResult::InvalidContextReference => {
1009 if resolution.context.is_some() || consumer.context().is_some() {
1010 diagnostics.push(AsmValidationDiagnostic {
1011 code: "PSASM1175".to_string(),
1012 message: format!(
1013 "consumer `{consumer_id}` has an invalid Context-reference result"
1014 ),
1015 });
1016 }
1017 }
1018 }
1019 }
1020}
1021
1022fn resolution_candidates(
1023 model: &ApplicationSemanticModel,
1024 scope: &crate::SemanticId,
1025 context: Option<&crate::ContextId>,
1026) -> Vec<crate::ProviderId> {
1027 let Some(context) = context else {
1028 return Vec::new();
1029 };
1030 model
1031 .providers
1032 .values()
1033 .filter(|provider| {
1034 provider.owner.entity_id() == Some(scope) && provider.context == *context
1035 })
1036 .map(|provider| provider.id.clone())
1037 .collect()
1038}
1039
1040fn validate_contexts(
1041 model: &ApplicationSemanticModel,
1042 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1043) {
1044 for context in model.contexts.values() {
1045 let Some(component_id) = context.owner.entity_id() else {
1046 diagnostics.push(AsmValidationDiagnostic {
1047 code: "PSASM1135".to_string(),
1048 message: format!("context `{}` is not component-owned", context.id),
1049 });
1050 continue;
1051 };
1052 let Some(component) = model.component(component_id) else {
1053 diagnostics.push(AsmValidationDiagnostic {
1054 code: "PSASM1136".to_string(),
1055 message: format!("context `{}` has a missing component owner", context.id),
1056 });
1057 continue;
1058 };
1059 if context.id != crate::ContextId::for_component(component_id, &context.name) {
1060 diagnostics.push(AsmValidationDiagnostic {
1061 code: "PSASM1137".to_string(),
1062 message: format!("context `{}` has a non-canonical identity", context.id),
1063 });
1064 }
1065 if context.authored_field != component.id.context_field(&context.name) {
1066 diagnostics.push(AsmValidationDiagnostic {
1067 code: "PSASM1138".to_string(),
1068 message: format!(
1069 "context `{}` has a non-canonical authored field",
1070 context.id
1071 ),
1072 });
1073 }
1074 if context.declared_type.text.is_empty() {
1075 diagnostics.push(AsmValidationDiagnostic {
1076 code: "PSASM1139".to_string(),
1077 message: format!(
1078 "context `{}` is missing an explicit declared type",
1079 context.id
1080 ),
1081 });
1082 }
1083 if context.execution_boundary != crate::ExecutionBoundary::Client {
1084 diagnostics.push(AsmValidationDiagnostic {
1085 code: "PSASM1140".to_string(),
1086 message: format!(
1087 "context `{}` has a non-client execution boundary",
1088 context.id
1089 ),
1090 });
1091 }
1092 if component
1093 .state_fields
1094 .iter()
1095 .any(|field| field.name == context.name)
1096 {
1097 diagnostics.push(AsmValidationDiagnostic {
1098 code: "PSASM1141".to_string(),
1099 message: format!("context `{}` also lowered as state", context.id),
1100 });
1101 }
1102 let declaration = component
1103 .context_declarations
1104 .iter()
1105 .find(|declaration| declaration.authored_field == context.authored_field);
1106 if declaration.is_none_or(|declaration| declaration.provenance != context.provenance) {
1107 diagnostics.push(AsmValidationDiagnostic {
1108 code: "PSASM1142".to_string(),
1109 message: format!(
1110 "context `{}` has non-canonical field provenance",
1111 context.id
1112 ),
1113 });
1114 }
1115 if context.default_expression != model.expression_root(context.id.as_semantic_id()).cloned()
1116 {
1117 diagnostics.push(AsmValidationDiagnostic {
1118 code: "PSASM1143".to_string(),
1119 message: format!("context `{}` has an invalid default expression", context.id),
1120 });
1121 }
1122 }
1123}
1124
1125#[allow(clippy::too_many_lines)]
1126fn validate_providers(
1127 model: &ApplicationSemanticModel,
1128 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1129) {
1130 for provider in model.providers.values() {
1131 let Some(component_id) = provider.owner.entity_id() else {
1132 diagnostics.push(AsmValidationDiagnostic {
1133 code: "PSASM1144".to_string(),
1134 message: format!("provider `{}` is not component-owned", provider.id),
1135 });
1136 continue;
1137 };
1138 let Some(component) = model.component(component_id) else {
1139 diagnostics.push(AsmValidationDiagnostic {
1140 code: "PSASM1145".to_string(),
1141 message: format!("provider `{}` has a missing component owner", provider.id),
1142 });
1143 continue;
1144 };
1145 if provider.id != crate::ProviderId::for_component(component_id, &provider.name) {
1146 diagnostics.push(AsmValidationDiagnostic {
1147 code: "PSASM1146".to_string(),
1148 message: format!("provider `{}` has a non-canonical identity", provider.id),
1149 });
1150 }
1151 if provider.authored_field != component.id.provider_field(&provider.name) {
1152 diagnostics.push(AsmValidationDiagnostic {
1153 code: "PSASM1147".to_string(),
1154 message: format!(
1155 "provider `{}` has a non-canonical authored field",
1156 provider.id
1157 ),
1158 });
1159 }
1160 let context = model.context(&provider.context);
1161 if context.is_none() {
1162 diagnostics.push(AsmValidationDiagnostic {
1163 code: "PSASM1148".to_string(),
1164 message: format!("provider `{}` targets a missing Context", provider.id),
1165 });
1166 }
1167 if context.is_some_and(|context| {
1168 context.name != provider.context_designator.context_member
1169 || context
1170 .owner
1171 .entity_id()
1172 .and_then(|owner| model.component(owner))
1173 .is_none_or(|owner| {
1174 owner.class_name != provider.context_designator.component_symbol
1175 })
1176 }) {
1177 diagnostics.push(AsmValidationDiagnostic {
1178 code: "PSASM1149".to_string(),
1179 message: format!(
1180 "provider `{}` has a mismatched Context designator",
1181 provider.id
1182 ),
1183 });
1184 }
1185 if provider.declared_type.text.is_empty() {
1186 diagnostics.push(AsmValidationDiagnostic {
1187 code: "PSASM1150".to_string(),
1188 message: format!(
1189 "provider `{}` is missing an explicit declared type",
1190 provider.id
1191 ),
1192 });
1193 }
1194 if model.expression_root(provider.id.as_semantic_id()) != Some(&provider.value_expression) {
1195 diagnostics.push(AsmValidationDiagnostic {
1196 code: "PSASM1151".to_string(),
1197 message: format!("provider `{}` has an invalid value expression", provider.id),
1198 });
1199 }
1200 if provider.execution_boundary != crate::ExecutionBoundary::Client {
1201 diagnostics.push(AsmValidationDiagnostic {
1202 code: "PSASM1152".to_string(),
1203 message: format!(
1204 "provider `{}` has a non-client execution boundary",
1205 provider.id
1206 ),
1207 });
1208 }
1209 if component
1210 .state_fields
1211 .iter()
1212 .any(|field| field.name == provider.name)
1213 || component
1214 .context_declarations
1215 .iter()
1216 .any(|context| context.name == provider.name)
1217 {
1218 diagnostics.push(AsmValidationDiagnostic {
1219 code: "PSASM1153".to_string(),
1220 message: format!(
1221 "provider `{}` has a conflicting semantic primitive",
1222 provider.id
1223 ),
1224 });
1225 }
1226 let declaration = component
1227 .provider_declarations
1228 .iter()
1229 .find(|declaration| declaration.authored_field == provider.authored_field);
1230 if declaration.is_none_or(|declaration| declaration.provenance != provider.provenance) {
1231 diagnostics.push(AsmValidationDiagnostic {
1232 code: "PSASM1154".to_string(),
1233 message: format!(
1234 "provider `{}` has non-canonical field provenance",
1235 provider.id
1236 ),
1237 });
1238 }
1239 }
1240}
1241
1242#[allow(clippy::too_many_lines)]
1243fn validate_consumers(
1244 model: &ApplicationSemanticModel,
1245 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1246) {
1247 for consumer in model.consumers.values() {
1248 let Some(component_id) = consumer.owner.entity_id() else {
1249 diagnostics.push(AsmValidationDiagnostic {
1250 code: "PSASM1155".to_string(),
1251 message: format!("consumer `{}` is not component-owned", consumer.id),
1252 });
1253 continue;
1254 };
1255 let Some(component) = model.component(component_id) else {
1256 diagnostics.push(AsmValidationDiagnostic {
1257 code: "PSASM1156".to_string(),
1258 message: format!("consumer `{}` has a missing component owner", consumer.id),
1259 });
1260 continue;
1261 };
1262 if consumer.id != crate::ConsumerId::for_component(component_id, &consumer.name) {
1263 diagnostics.push(AsmValidationDiagnostic {
1264 code: "PSASM1157".to_string(),
1265 message: format!("consumer `{}` has a non-canonical identity", consumer.id),
1266 });
1267 }
1268 if consumer.authored_field != component.id.consumer_field(&consumer.name) {
1269 diagnostics.push(AsmValidationDiagnostic {
1270 code: "PSASM1158".to_string(),
1271 message: format!(
1272 "consumer `{}` has a non-canonical authored field",
1273 consumer.id
1274 ),
1275 });
1276 }
1277 if let crate::ContextResolutionState::Resolved(context_id) = &consumer.context_resolution {
1278 let context = model.context(context_id);
1279 if context.is_none() {
1280 diagnostics.push(AsmValidationDiagnostic {
1281 code: "PSASM1159".to_string(),
1282 message: format!("consumer `{}` targets a missing Context", consumer.id),
1283 });
1284 }
1285 if context.is_some_and(|context| {
1286 context.name != consumer.context_designator.context_member
1287 || context
1288 .owner
1289 .entity_id()
1290 .and_then(|owner| model.component(owner))
1291 .is_none_or(|owner| {
1292 owner.class_name != consumer.context_designator.component_symbol
1293 })
1294 }) {
1295 diagnostics.push(AsmValidationDiagnostic {
1296 code: "PSASM1160".to_string(),
1297 message: format!(
1298 "consumer `{}` has a mismatched Context designator",
1299 consumer.id
1300 ),
1301 });
1302 }
1303 }
1304 if consumer.requested_type.text.is_empty() {
1305 diagnostics.push(AsmValidationDiagnostic {
1306 code: "PSASM1161".to_string(),
1307 message: format!(
1308 "consumer `{}` is missing an explicit requested type",
1309 consumer.id
1310 ),
1311 });
1312 }
1313 if consumer.execution_boundary != crate::ExecutionBoundary::Client {
1314 diagnostics.push(AsmValidationDiagnostic {
1315 code: "PSASM1162".to_string(),
1316 message: format!(
1317 "consumer `{}` has a non-client execution boundary",
1318 consumer.id
1319 ),
1320 });
1321 }
1322 if component
1323 .state_fields
1324 .iter()
1325 .any(|field| field.name == consumer.name)
1326 || component
1327 .context_declarations
1328 .iter()
1329 .any(|context| context.name == consumer.name)
1330 || component
1331 .provider_declarations
1332 .iter()
1333 .any(|provider| provider.name == consumer.name)
1334 {
1335 diagnostics.push(AsmValidationDiagnostic {
1336 code: "PSASM1163".to_string(),
1337 message: format!(
1338 "consumer `{}` has a conflicting semantic primitive",
1339 consumer.id
1340 ),
1341 });
1342 }
1343 let declaration = component
1344 .consumer_declarations
1345 .iter()
1346 .find(|declaration| declaration.authored_field == consumer.authored_field);
1347 if declaration.is_none_or(|declaration| declaration.provenance != consumer.provenance) {
1348 diagnostics.push(AsmValidationDiagnostic {
1349 code: "PSASM1164".to_string(),
1350 message: format!(
1351 "consumer `{}` has non-canonical field provenance",
1352 consumer.id
1353 ),
1354 });
1355 }
1356 }
1357}
1358
1359#[allow(clippy::too_many_lines)]
1360fn validate_component_diagnostic_metadata(
1361 model: &ApplicationSemanticModel,
1362 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1363) {
1364 let actual_component_diagnostics = model
1365 .diagnostics
1366 .iter()
1367 .filter(|item| ("PSC1068"..="PSC1083").contains(&item.code.as_str()))
1368 .cloned()
1369 .collect::<Vec<_>>();
1370 let expected_component_diagnostics = crate::collect_component_diagnostics(model);
1371 if actual_component_diagnostics != expected_component_diagnostics {
1372 diagnostics.push(AsmValidationDiagnostic {
1373 code: "PSASM1201".to_string(),
1374 message: "component diagnostics do not match the canonical H19 projection".to_string(),
1375 });
1376 }
1377 for diagnostic in &model.diagnostics {
1378 let valid_context = diagnostic
1379 .context_id
1380 .as_ref()
1381 .is_none_or(|id| model.context(id).is_some());
1382 let valid_provider = diagnostic
1383 .provider_id
1384 .as_ref()
1385 .is_none_or(|id| model.provider(id).is_some());
1386 let valid_consumer = diagnostic
1387 .consumer_id
1388 .as_ref()
1389 .is_none_or(|id| model.consumer(id).is_some());
1390 let valid_candidate = diagnostic
1391 .context_declaration_candidate_id
1392 .as_ref()
1393 .is_none_or(|id| {
1394 model
1395 .context_declaration_candidates()
1396 .candidate(id)
1397 .is_some()
1398 });
1399 if !(valid_context && valid_provider && valid_consumer && valid_candidate) {
1400 diagnostics.push(AsmValidationDiagnostic {
1401 code: "PSASM1135".to_string(),
1402 message: format!(
1403 "compiler diagnostic `{}` references an unknown Context diagnostic subject",
1404 diagnostic.code
1405 ),
1406 });
1407 }
1408 if let (Some(provider), Some(context)) = (
1409 diagnostic
1410 .provider_id
1411 .as_ref()
1412 .and_then(|id| model.provider(id)),
1413 diagnostic.context_id.as_ref(),
1414 ) {
1415 if &provider.context != context {
1416 diagnostics.push(AsmValidationDiagnostic {
1417 code: "PSASM1138".to_string(),
1418 message: format!(
1419 "compiler diagnostic `{}` references a Provider for the wrong Context",
1420 diagnostic.code
1421 ),
1422 });
1423 }
1424 }
1425 if let (Some(consumer), Some(context)) = (
1426 diagnostic
1427 .consumer_id
1428 .as_ref()
1429 .and_then(|id| model.consumer(id)),
1430 diagnostic.context_id.as_ref(),
1431 ) {
1432 if consumer.context() != Some(context) {
1433 diagnostics.push(AsmValidationDiagnostic {
1434 code: "PSASM1139".to_string(),
1435 message: format!(
1436 "compiler diagnostic `{}` references a Consumer for the wrong Context",
1437 diagnostic.code
1438 ),
1439 });
1440 }
1441 }
1442 if diagnostic.context_declaration_candidate_id.is_some()
1443 && (diagnostic.context_id.is_some()
1444 || diagnostic.provider_id.is_some()
1445 || diagnostic.consumer_id.is_some())
1446 {
1447 diagnostics.push(AsmValidationDiagnostic { code: "PSASM1136".to_string(), message: format!("invalid Context declaration diagnostic `{}` carries a semantic entity identity", diagnostic.code) });
1448 }
1449 if ("PSC1052"..="PSC1067").contains(&diagnostic.code.as_str()) {
1450 if let Some(primary) = diagnostic.provenance.as_ref() {
1451 if !context_diagnostic_primary_is_canonical(model, diagnostic, primary) {
1452 diagnostics.push(AsmValidationDiagnostic {
1453 code: "PSASM1140".to_string(),
1454 message: format!(
1455 "Context diagnostic `{}` has non-canonical primary provenance",
1456 diagnostic.code
1457 ),
1458 });
1459 }
1460 }
1461 if diagnostic.code == "PSC1058" {
1462 let mut expected = diagnostic
1463 .consumer_id
1464 .as_ref()
1465 .and_then(|consumer| model.context_resolutions.get(consumer))
1466 .and_then(|resolution| match &resolution.result {
1467 crate::ContextResolutionResult::Ambiguous { providers, .. } => {
1468 Some(providers.clone())
1469 }
1470 _ => None,
1471 })
1472 .unwrap_or_default();
1473 expected.sort();
1474 expected.dedup();
1475 let expected = expected
1476 .iter()
1477 .filter_map(|id| model.provider(id))
1478 .map(|provider| crate::DiagnosticSecondaryLabel {
1479 provenance: provider.provenance.clone(),
1480 message: format!("Candidate Provider `{}`.", provider.id),
1481 })
1482 .collect::<Vec<_>>();
1483 if diagnostic.secondary_labels != expected {
1484 diagnostics.push(AsmValidationDiagnostic {
1485 code: "PSASM1134".to_string(),
1486 message: "Context ambiguity diagnostic has non-canonical Provider evidence"
1487 .to_string(),
1488 });
1489 }
1490 }
1491 }
1492 let effect = diagnostic.effect_id.as_ref().and_then(|effect_id| {
1493 model
1494 .effects
1495 .values()
1496 .find(|effect| effect.id.as_str() == effect_id.as_str())
1497 });
1498 if diagnostic.effect_id.is_some() && effect.is_none() {
1499 diagnostics.push(AsmValidationDiagnostic {
1500 code: "PSASM1129".to_string(),
1501 message: format!(
1502 "compiler diagnostic `{}` references a missing effect subject",
1503 diagnostic.code
1504 ),
1505 });
1506 }
1507
1508 let statement = diagnostic.statement_id.as_ref().and_then(|statement_id| {
1509 model
1510 .effect_statements
1511 .values()
1512 .find(|statement| statement.id.as_str() == statement_id.as_str())
1513 });
1514 if diagnostic.statement_id.is_some() && diagnostic.effect_id.is_none() {
1515 diagnostics.push(AsmValidationDiagnostic {
1516 code: "PSASM1130".to_string(),
1517 message: format!(
1518 "compiler diagnostic `{}` has an effect statement without an effect subject",
1519 diagnostic.code
1520 ),
1521 });
1522 }
1523 if diagnostic.statement_id.is_some()
1524 && statement
1525 .is_none_or(|statement| effect.is_none_or(|effect| statement.owner != effect.id))
1526 {
1527 diagnostics.push(AsmValidationDiagnostic {
1528 code: "PSASM1131".to_string(),
1529 message: format!(
1530 "compiler diagnostic `{}` has a statement that does not belong to its effect",
1531 diagnostic.code
1532 ),
1533 });
1534 }
1535
1536 let primary_subject = statement
1537 .map(|statement| &statement.provenance)
1538 .or_else(|| effect.map(|effect| &effect.provenance));
1539 if let (Some(primary), Some(subject)) = (&diagnostic.provenance, primary_subject) {
1540 if !provenance_contains(subject, primary) {
1541 diagnostics.push(AsmValidationDiagnostic {
1542 code: "PSASM1132".to_string(),
1543 message: format!(
1544 "compiler diagnostic `{}` has non-canonical primary provenance",
1545 diagnostic.code
1546 ),
1547 });
1548 }
1549 }
1550
1551 let mut sorted = diagnostic.secondary_labels.clone();
1552 sorted.sort_by(secondary_label_order);
1553 sorted.dedup();
1554 if sorted != diagnostic.secondary_labels {
1555 diagnostics.push(AsmValidationDiagnostic {
1556 code: "PSASM1133".to_string(),
1557 message: format!(
1558 "compiler diagnostic `{}` has unordered or duplicate secondary labels",
1559 diagnostic.code
1560 ),
1561 });
1562 }
1563 for label in &diagnostic.secondary_labels {
1564 if diagnostic.provenance.as_ref() == Some(&label.provenance) {
1565 diagnostics.push(AsmValidationDiagnostic {
1566 code: "PSASM1137".to_string(),
1567 message: format!(
1568 "compiler diagnostic `{}` repeats primary provenance as a secondary label",
1569 diagnostic.code
1570 ),
1571 });
1572 }
1573 let canonical = model
1574 .provenance
1575 .values()
1576 .any(|provenance| provenance == &label.provenance)
1577 || model
1578 .expression_graph
1579 .nodes
1580 .values()
1581 .any(|expression| expression.provenance == label.provenance)
1582 || context_diagnostic_secondary_is_canonical(model, diagnostic, &label.provenance);
1583 if !canonical {
1584 diagnostics.push(AsmValidationDiagnostic {
1585 code: "PSASM1134".to_string(),
1586 message: format!(
1587 "compiler diagnostic `{}` has non-canonical secondary-label provenance",
1588 diagnostic.code
1589 ),
1590 });
1591 }
1592 }
1593 }
1594}
1595
1596fn context_diagnostic_secondary_is_canonical(
1597 model: &ApplicationSemanticModel,
1598 diagnostic: &crate::ComponentDiagnostic,
1599 provenance: &crate::SourceProvenance,
1600) -> bool {
1601 match diagnostic.code.as_str() {
1602 "PSC1059" => diagnostic
1603 .provider_id
1604 .as_ref()
1605 .and_then(|id| model.provider(id))
1606 .is_some_and(|provider| &provider.declared_type.provenance == provenance),
1607 "PSC1060" | "PSC1061" | "PSC1062" | "PSC1063" | "PSC1064" => diagnostic
1608 .context_id
1609 .as_ref()
1610 .and_then(|id| model.context(id))
1611 .is_some_and(|context| &context.declared_type.provenance == provenance),
1612 _ => false,
1613 }
1614}
1615
1616#[allow(clippy::too_many_lines)]
1617fn context_diagnostic_primary_is_canonical(
1618 model: &ApplicationSemanticModel,
1619 diagnostic: &crate::ComponentDiagnostic,
1620 primary: &crate::SourceProvenance,
1621) -> bool {
1622 match diagnostic.code.as_str() {
1623 "PSC1052" | "PSC1053" | "PSC1054" | "PSC1055" | "PSC1056" => diagnostic
1624 .context_declaration_candidate_id
1625 .as_ref()
1626 .and_then(|id| model.context_declaration_candidates().candidate(id))
1627 .is_some_and(|candidate| {
1628 let crate::ContextDeclarationStatus::Invalid(violations) = &candidate.status else {
1629 return false;
1630 };
1631 let expected = match violations.first() {
1632 Some(crate::ContextDeclarationViolation::StaticDeclarationUnsupported) => {
1633 candidate.authored.static_modifier_provenance.as_ref()
1634 }
1635 Some(
1636 crate::ContextDeclarationViolation::UnsupportedInitializer
1637 | crate::ContextDeclarationViolation::ForbiddenInitializer
1638 | crate::ContextDeclarationViolation::MissingInitializer,
1639 ) => candidate.authored.initializer_provenance.as_ref(),
1640 Some(
1641 crate::ContextDeclarationViolation::ContextDesignatorUnsupported
1642 | crate::ContextDeclarationViolation::UnresolvedContextDesignator,
1643 ) => candidate
1644 .authored
1645 .context_designator
1646 .as_ref()
1647 .map(|designator| &designator.provenance),
1648 Some(_) => Some(&candidate.authored.decorator_provenance),
1649 None => None,
1650 }
1651 .unwrap_or(&candidate.authored.provenance);
1652 expected == primary
1653 }),
1654 "PSC1057" | "PSC1058" => diagnostic
1655 .consumer_id
1656 .as_ref()
1657 .and_then(|id| model.context_resolutions.get(id))
1658 .is_some_and(|record| &record.provenance == primary),
1659 "PSC1059" => {
1660 diagnostic
1661 .provider_id
1662 .as_ref()
1663 .and_then(|id| model.provider(id))
1664 .and_then(|provider| {
1665 model
1666 .expression_graph
1667 .provenance_of(&provider.value_expression)
1668 })
1669 == Some(primary)
1670 }
1671 "PSC1060" => diagnostic
1672 .provider_id
1673 .as_ref()
1674 .and_then(|id| model.provider(id))
1675 .is_some_and(|provider| &provider.declared_type.provenance == primary),
1676 "PSC1061" => {
1677 diagnostic
1678 .context_id
1679 .as_ref()
1680 .and_then(|id| model.context(id))
1681 .and_then(|context| context.default_expression.as_ref())
1682 .and_then(|expression| model.expression_graph.provenance_of(expression))
1683 == Some(primary)
1684 }
1685 "PSC1062" => diagnostic
1686 .consumer_id
1687 .as_ref()
1688 .and_then(|id| model.consumer(id))
1689 .is_some_and(|consumer| &consumer.requested_type.provenance == primary),
1690 "PSC1063" | "PSC1064" => {
1691 if let Some(provider) = diagnostic
1692 .provider_id
1693 .as_ref()
1694 .and_then(|id| model.provider(id))
1695 {
1696 return model
1697 .expression_graph
1698 .provenance_of(&provider.value_expression)
1699 == Some(primary);
1700 }
1701 diagnostic
1702 .context_id
1703 .as_ref()
1704 .and_then(|id| model.context(id))
1705 .is_some_and(|context| {
1706 context
1707 .default_expression
1708 .as_ref()
1709 .and_then(|expression| model.expression_graph.provenance_of(expression))
1710 .unwrap_or(&context.declared_type.provenance)
1711 == primary
1712 })
1713 }
1714 "PSC1065" => {
1715 diagnostic
1716 .consumer_id
1717 .as_ref()
1718 .and_then(|id| model.context_lifetime.binding_lifetimes.get(id))
1719 .is_some_and(|record| &record.provenance == primary)
1720 || model
1721 .context_lifetime
1722 .dependency_lifetimes
1723 .iter()
1724 .any(|record| &record.provenance == primary)
1725 }
1726 "PSC1066" | "PSC1067" => model
1727 .context_evaluation
1728 .source_entries
1729 .values()
1730 .filter(|entry| diagnostic.context_id.as_ref() == Some(&entry.context))
1731 .filter(|entry| match &entry.source {
1732 crate::ContextValueSourceId::Provider(provider) => {
1733 diagnostic.provider_id.as_ref() == Some(provider)
1734 }
1735 crate::ContextValueSourceId::ContextDefault(_) => diagnostic.provider_id.is_none(),
1736 })
1737 .any(|entry| {
1738 if diagnostic.code == "PSC1067" {
1739 return model
1740 .expression_graph
1741 .provenance_of(&entry.expression_root)
1742 .unwrap_or(&entry.provenance)
1743 == primary;
1744 }
1745 let dependent = match &entry.source {
1746 crate::ContextValueSourceId::Provider(provider) => {
1747 crate::ContextDependencyNodeId::Provider(provider.clone())
1748 }
1749 crate::ContextValueSourceId::ContextDefault(context) => {
1750 crate::ContextDependencyNodeId::ContextDefault(context.clone())
1751 }
1752 };
1753 entry.reasons.iter().any(|reason| {
1754 let dependency = match reason {
1755 crate::ContextSourceBlockReason::MissingStateDependency(id) => {
1756 crate::ContextDependencyNodeId::State(id.clone())
1757 }
1758 crate::ContextSourceBlockReason::UnavailableComputedDependency(id) => {
1759 crate::ContextDependencyNodeId::Computed(id.clone())
1760 }
1761 _ => return false,
1762 };
1763 model.context_dependency.edges.iter().any(|edge| {
1764 edge.dependent == dependent
1765 && edge.dependency == dependency
1766 && &edge.provenance == primary
1767 })
1768 }) || &entry.provenance == primary
1769 }),
1770 _ => false,
1771 }
1772}
1773
1774fn provenance_contains(
1775 subject: &crate::SourceProvenance,
1776 primary: &crate::SourceProvenance,
1777) -> bool {
1778 subject.path == primary.path
1779 && subject.span.start <= primary.span.start
1780 && primary.span.end <= subject.span.end
1781}
1782
1783fn secondary_label_order(
1784 left: &crate::DiagnosticSecondaryLabel,
1785 right: &crate::DiagnosticSecondaryLabel,
1786) -> std::cmp::Ordering {
1787 (
1788 left.provenance.path.as_path(),
1789 left.provenance.span.start,
1790 left.provenance.span.end,
1791 left.message.as_str(),
1792 )
1793 .cmp(&(
1794 right.provenance.path.as_path(),
1795 right.provenance.span.start,
1796 right.provenance.span.end,
1797 right.message.as_str(),
1798 ))
1799}
1800
1801fn validate_template_action_bindings(
1802 model: &ApplicationSemanticModel,
1803 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1804) {
1805 let manifest = build_template_manifest_from_asm(model);
1806 if manifest.schema_version < 2 || manifest.schema_version > TEMPLATE_MANIFEST_SCHEMA_VERSION {
1807 return;
1808 }
1809 if manifest.schema_version == TEMPLATE_MANIFEST_SCHEMA_VERSION {
1810 for event in &manifest.ordinary_events {
1811 let component = model
1812 .components
1813 .iter()
1814 .find(|component| component.id.as_str() == event.component_id);
1815 let Some(component) = component else {
1819 diagnostics.push(AsmValidationDiagnostic {
1820 code: "PSASM1126".to_string(),
1821 message: "ordinary event references a missing component".to_string(),
1822 });
1823 continue;
1824 };
1825 let method = component
1826 .methods
1827 .iter()
1828 .find(|method| method.id.as_str() == event.handler_method_id);
1829 let batch = method.and_then(|method| {
1830 model
1831 .effect_trigger_plan
1832 .action_batches
1833 .values()
1834 .find(|batch| batch.authored_action_method == method.id)
1835 });
1836 if batch.is_none_or(|batch| Some(batch.id.as_str()) != event.action_batch_id.as_deref())
1837 {
1838 diagnostics.push(AsmValidationDiagnostic {
1839 code: "PSASM1128".to_string(),
1840 message: "ordinary event does not resolve to its canonical F8 action batch"
1841 .to_string(),
1842 });
1843 }
1844 }
1845 return;
1846 }
1847 for component_manifest in &manifest.components {
1848 let Some(component) = model
1849 .components
1850 .iter()
1851 .find(|component| component.class_name == component_manifest.name)
1852 else {
1853 diagnostics.push(AsmValidationDiagnostic {
1854 code: "PSASM1126".to_string(),
1855 message: format!(
1856 "template manifest references missing component `{}`",
1857 component_manifest.name
1858 ),
1859 });
1860 continue;
1861 };
1862 for event in &component_manifest.template.events {
1863 if event.kind != Some(ManifestEventKind::Action) {
1864 diagnostics.push(AsmValidationDiagnostic {
1865 code: "PSASM1127".to_string(),
1866 message: format!(
1867 "template event `{}` is missing canonical action binding metadata",
1868 event.node
1869 ),
1870 });
1871 continue;
1872 }
1873 let method = component
1874 .methods
1875 .iter()
1876 .find(|method| Some(method.id.as_str()) == event.method_id.as_deref());
1877 let batch = method.and_then(|method| {
1878 model
1879 .effect_trigger_plan
1880 .action_batches
1881 .values()
1882 .find(|batch| batch.authored_action_method == method.id)
1883 });
1884 if batch.is_none_or(|batch| Some(batch.id.as_str()) != event.action_batch_id.as_deref())
1885 {
1886 diagnostics.push(AsmValidationDiagnostic {
1887 code: "PSASM1128".to_string(),
1888 message: format!(
1889 "template event `{}` does not resolve to its canonical F8 action batch",
1890 event.node
1891 ),
1892 });
1893 }
1894 }
1895 }
1896}
1897
1898fn validate_effect_execution_plan(
1899 model: &ApplicationSemanticModel,
1900 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1901) {
1902 let plan = model.effect_execution_plan();
1903 if plan.initial.render_boundary != Some(EffectRenderBoundary::AfterInitialRender) {
1904 diagnostics.push(AsmValidationDiagnostic {
1905 code: "PSASM1120".to_string(),
1906 message: "initial effect execution plan is missing the after-initial-render boundary"
1907 .to_string(),
1908 });
1909 }
1910 validate_effect_execution_entry(
1911 model,
1912 &model.effect_trigger_plan.initial_effects,
1913 &plan.initial.required_computed,
1914 &plan.initial.prerequisite_batches,
1915 &plan.initial.effect_batches,
1916 &plan.initial.unplanned_effects,
1917 "initial",
1918 diagnostics,
1919 );
1920 for action in &plan.actions {
1921 let Some(trigger) = model
1922 .effect_trigger_plan
1923 .action_batch_triggers
1924 .iter()
1925 .find(|trigger| trigger.action_batch == action.action_batch)
1926 else {
1927 diagnostics.push(AsmValidationDiagnostic {
1928 code: "PSASM1121".to_string(),
1929 message: format!(
1930 "effect execution plan references untriggered action batch `{}`",
1931 action.action_batch
1932 ),
1933 });
1934 continue;
1935 };
1936 validate_effect_execution_entry(
1937 model,
1938 &trigger.effects,
1939 &action.required_computed,
1940 &action.prerequisite_batches,
1941 &action.effect_batches,
1942 &action.unplanned_effects,
1943 action.action_batch.as_str(),
1944 diagnostics,
1945 );
1946 }
1947}
1948
1949#[allow(clippy::too_many_arguments)]
1950fn validate_effect_execution_entry(
1951 model: &ApplicationSemanticModel,
1952 eligible_effects: &[crate::SemanticId],
1953 required_computed: &[crate::SemanticId],
1954 prerequisite_batches: &[crate::EffectComputedPrerequisiteBatch],
1955 effect_batches: &[crate::EffectExecutionBatch],
1956 unplanned_effects: &[crate::UnplannedEffect],
1957 context: &str,
1958 diagnostics: &mut Vec<AsmValidationDiagnostic>,
1959) {
1960 let required = required_computed
1961 .iter()
1962 .collect::<std::collections::BTreeSet<_>>();
1963 if required.len() != required_computed.len()
1964 || required_computed
1965 .iter()
1966 .any(|computed| !model.computed_values.contains_key(computed))
1967 {
1968 diagnostics.push(AsmValidationDiagnostic {
1969 code: "PSASM1122".to_string(),
1970 message: format!("effect plan `{context}` has invalid required computed membership"),
1971 });
1972 }
1973 let mut batch_membership = std::collections::BTreeSet::new();
1974 let mut prior_source_index = None;
1975 for batch in prerequisite_batches {
1976 let expected = model
1977 .computed_evaluation_plan
1978 .update_batches
1979 .get(batch.source_batch_index as usize)
1980 .map(|source| {
1981 source
1982 .iter()
1983 .filter_map(|id| {
1984 model
1985 .computed_values
1986 .keys()
1987 .find(|computed| computed.as_str() == id)
1988 })
1989 .filter(|computed| required.contains(computed))
1990 .cloned()
1991 .collect::<Vec<_>>()
1992 });
1993 if expected.as_ref() != Some(&batch.computed)
1994 || prior_source_index.is_some_and(|prior| prior >= batch.source_batch_index)
1995 {
1996 diagnostics.push(AsmValidationDiagnostic {
1997 code: "PSASM1123".to_string(),
1998 message: format!(
1999 "effect plan `{context}` does not preserve canonical computed batch membership"
2000 ),
2001 });
2002 }
2003 prior_source_index = Some(batch.source_batch_index);
2004 batch_membership.extend(batch.computed.iter());
2005 }
2006 if batch_membership != required {
2007 diagnostics.push(AsmValidationDiagnostic {
2008 code: "PSASM1124".to_string(),
2009 message: format!(
2010 "effect plan `{context}` required computed values do not match prerequisite batches"
2011 ),
2012 });
2013 }
2014
2015 let scheduled = effect_batches
2016 .iter()
2017 .flat_map(|batch| &batch.effects)
2018 .collect::<std::collections::BTreeSet<_>>();
2019 let unplanned = unplanned_effects
2020 .iter()
2021 .map(|record| &record.effect)
2022 .collect::<std::collections::BTreeSet<_>>();
2023 let eligible = eligible_effects
2024 .iter()
2025 .collect::<std::collections::BTreeSet<_>>();
2026 let covered = scheduled
2027 .union(&unplanned)
2028 .copied()
2029 .collect::<std::collections::BTreeSet<_>>();
2030 if covered != eligible
2031 || scheduled.intersection(&unplanned).next().is_some()
2032 || scheduled.iter().any(|effect| {
2033 model
2034 .effects
2035 .get(*effect)
2036 .is_none_or(|effect| effect.validation != EffectValidation::Valid)
2037 })
2038 {
2039 diagnostics.push(AsmValidationDiagnostic {
2040 code: "PSASM1125".to_string(),
2041 message: format!("effect plan `{context}` has invalid effect eligibility membership"),
2042 });
2043 }
2044 if effect_batches.iter().enumerate().any(|(index, batch)| {
2045 batch.index != u32::try_from(index).expect("effect scheduler batch index should fit u32")
2046 }) {
2047 diagnostics.push(AsmValidationDiagnostic {
2048 code: "PSASM1126".to_string(),
2049 message: format!("effect plan `{context}` has non-contiguous terminal batch indexes"),
2050 });
2051 }
2052}
2053
2054fn validate_effect_statement_types(
2055 model: &ApplicationSemanticModel,
2056 diagnostics: &mut Vec<AsmValidationDiagnostic>,
2057) {
2058 for (statement, record) in &model.semantic_types.effect_statements {
2059 if statement != &record.statement {
2060 diagnostics.push(AsmValidationDiagnostic {
2061 code: "PSASM1110".to_string(),
2062 message: format!(
2063 "effect statement type record key `{statement}` does not match statement `{}`",
2064 record.statement
2065 ),
2066 });
2067 }
2068 let Some(canonical_statement) = model.effect_statement(statement) else {
2069 diagnostics.push(AsmValidationDiagnostic {
2070 code: "PSASM1111".to_string(),
2071 message: format!(
2072 "effect statement type record references missing statement `{statement}`"
2073 ),
2074 });
2075 continue;
2076 };
2077 if canonical_statement.provenance != record.provenance {
2078 diagnostics.push(AsmValidationDiagnostic {
2079 code: "PSASM1112".to_string(),
2080 message: format!(
2081 "effect statement type record for `{statement}` has inconsistent provenance"
2082 ),
2083 });
2084 }
2085 let operation_exists = record.capability_operation.is_some_and(|operation_id| {
2086 EFFECT_CAPABILITY_REGISTRY
2087 .definitions()
2088 .iter()
2089 .flat_map(|definition| definition.operations)
2090 .any(|operation| operation.id == operation_id)
2091 });
2092 if record.operation_classification == EffectOperationClassification::RecognizedCapability
2093 && !operation_exists
2094 {
2095 diagnostics.push(AsmValidationDiagnostic {
2096 code: "PSASM1113".to_string(),
2097 message: format!(
2098 "recognized effect statement `{statement}` has no registry capability operation"
2099 ),
2100 });
2101 }
2102 if record.operation_classification != EffectOperationClassification::RecognizedCapability
2103 && record.capability_operation.is_some()
2104 {
2105 diagnostics.push(AsmValidationDiagnostic {
2106 code: "PSASM1114".to_string(),
2107 message: format!(
2108 "non-capability effect statement `{statement}` unexpectedly names a registry operation"
2109 ),
2110 });
2111 }
2112 }
2113}
2114
2115fn validate_semantic_types(
2116 model: &ApplicationSemanticModel,
2117 diagnostics: &mut Vec<AsmValidationDiagnostic>,
2118) {
2119 for (subject, assignment) in &model.semantic_types.assignments {
2120 if subject != &assignment.subject {
2121 diagnostics.push(AsmValidationDiagnostic {
2122 code: "PSASM1101".to_string(),
2123 message: format!(
2124 "semantic type assignment map key `{subject}` does not match subject `{}`",
2125 assignment.subject
2126 ),
2127 });
2128 }
2129 if assignment.id != SemanticTypeId::for_subject(&assignment.subject) {
2130 diagnostics.push(AsmValidationDiagnostic {
2131 code: "PSASM1102".to_string(),
2132 message: format!(
2133 "semantic type assignment for `{subject}` has an invalid canonical type ID `{}`",
2134 assignment.id
2135 ),
2136 });
2137 }
2138 let subject_provenance = model
2139 .provenance(&assignment.subject)
2140 .or_else(|| model.expression_provenance(&assignment.subject));
2141 if subject_provenance.is_none() {
2142 diagnostics.push(AsmValidationDiagnostic {
2143 code: "PSASM1103".to_string(),
2144 message: format!(
2145 "semantic type assignment references missing subject `{}`",
2146 assignment.subject
2147 ),
2148 });
2149 }
2150 if subject_provenance != Some(&assignment.provenance) {
2151 diagnostics.push(AsmValidationDiagnostic {
2152 code: "PSASM1104".to_string(),
2153 message: format!(
2154 "semantic type assignment for `{subject}` has inconsistent provenance"
2155 ),
2156 });
2157 }
2158 if model.entity(&assignment.origin).is_none()
2159 && !model
2160 .semantic_types
2161 .aliases
2162 .contains_key(&assignment.origin)
2163 {
2164 diagnostics.push(AsmValidationDiagnostic {
2165 code: "PSASM1105".to_string(),
2166 message: format!(
2167 "semantic type assignment for `{subject}` has unresolved origin `{}`",
2168 assignment.origin
2169 ),
2170 });
2171 }
2172 }
2173
2174 for alias in model.semantic_types.aliases.values() {
2175 let expected = crate::SemanticId::type_alias_in_module(&alias.provenance.path, &alias.name);
2176 if alias.id != expected {
2177 diagnostics.push(AsmValidationDiagnostic {
2178 code: "PSASM1106".to_string(),
2179 message: format!(
2180 "type alias `{}` has invalid canonical identity `{}`",
2181 alias.name, alias.id
2182 ),
2183 });
2184 }
2185 }
2186}