1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::Serialize;
4
5use crate::{
6 build_effect_resume_plan, build_runtime_effect_registry, lower_components_to_ir,
7 optimize_effect_ir, ApplicationSemanticModel, CapabilityOperationKind, Effect,
8 EffectActivationStatus, EffectExecutionPolicy, EffectRenderBoundary,
9 EffectSemanticViolationKind, EffectValidation, ExecutionBoundary, IrReactiveEdgeKind,
10 RuntimeEffectRecord, SemanticId, SourceProvenance, EFFECT_CAPABILITY_REGISTRY,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
15pub struct EffectInspectionRegistry {
16 pub records: BTreeMap<SemanticId, EffectInspection>,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct EffectInspectionValidationDiagnostic {
21 pub code: String,
22 pub message: String,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26pub struct EffectInspection {
27 pub validation: EffectInspectionValidation,
28 pub direct_dependencies: EffectInspectionDependencies,
29 pub transitive_dependencies: EffectInspectionDependencies,
30 pub dependents: Vec<String>,
31 pub initial_trigger: Option<EffectInspectionInitialTrigger>,
32 pub action_triggers: Vec<EffectInspectionActionTrigger>,
33 pub schedule: EffectInspectionSchedule,
34 pub capabilities: Vec<EffectInspectionCapability>,
35 pub ir: Option<EffectInspectionIr>,
36 pub runtime: EffectInspectionRuntime,
37 pub resumability: Option<EffectInspectionResumability>,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
41pub struct EffectInspectionValidation {
42 pub status: &'static str,
43 pub violations: Vec<EffectInspectionViolation>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47pub struct EffectInspectionViolation {
48 pub category: &'static str,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub statement_id: Option<String>,
51 pub provenance: EffectInspectionProvenance,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55pub struct EffectInspectionProvenance {
56 pub path: String,
57 pub line: usize,
58 pub column: usize,
59 pub start: usize,
60 pub end: usize,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
64pub struct EffectInspectionDependencies {
65 pub state: Vec<String>,
66 pub computed: Vec<String>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
70pub struct EffectInspectionInitialTrigger {
71 pub policy: &'static str,
72 pub batch_index: u32,
73 pub render_boundary: &'static str,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77pub struct EffectInspectionActionTrigger {
78 pub action_batch_id: String,
79 pub matched_states: Vec<String>,
80 pub required_computed: Vec<String>,
81 pub prerequisite_batches: Vec<EffectInspectionPrerequisiteBatch>,
82 pub effect_batch_index: u32,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
86pub struct EffectInspectionPrerequisiteBatch {
87 pub source_batch_index: u32,
88 pub computed: Vec<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92pub struct EffectInspectionSchedule {
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub initial_effect_batch_index: Option<u32>,
95 pub action_batches: Vec<EffectInspectionScheduledAction>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub unplanned: Option<EffectInspectionUnplanned>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
101pub struct EffectInspectionScheduledAction {
102 pub action_batch_id: String,
103 pub effect_batch_index: u32,
104 pub prerequisite_computed_batch_refs: Vec<u32>,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
108pub struct EffectInspectionUnplanned {
109 pub reason: &'static str,
110 pub unavailable_computed_dependencies: Vec<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114pub struct EffectInspectionCapability {
115 pub operation_id: String,
116 pub runtime_lowering_id: String,
117 pub kind: &'static str,
118 pub boundary: &'static str,
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122pub struct EffectInspectionIr {
123 pub function_id: String,
124 pub instruction_count: usize,
125 pub capability_operation_count: usize,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
129pub struct EffectInspectionRuntime {
130 pub registered: bool,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub function_id: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub execution_policy: Option<&'static str>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub boundary: Option<&'static str>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub initial_membership: Option<EffectInspectionInitialTrigger>,
139 pub action_batch_ids: Vec<String>,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
143pub struct EffectInspectionResumability {
144 pub activation_slot_id: Option<String>,
145 pub initial_status: Option<EffectActivationStatus>,
146 pub render_boundary: Option<&'static str>,
147 pub initial_batch_index: Option<u32>,
148 pub action_batch_ids: Vec<String>,
149 pub manifest_schema_version: u32,
150}
151
152#[must_use]
154pub fn build_effect_inspection_registry(
155 model: &ApplicationSemanticModel,
156) -> EffectInspectionRegistry {
157 let ir = lower_components_to_ir(model);
158 let optimized_ir = optimize_effect_ir(&ir).output;
159 let runtime = build_runtime_effect_registry(model, &optimized_ir);
160 let resumability = build_effect_resume_plan(model, &runtime);
161 let executions = optimized_ir
162 .modules
163 .iter()
164 .flat_map(|module| &module.effect_executions)
165 .map(|execution| (execution.effect.clone(), execution))
166 .collect::<BTreeMap<_, _>>();
167 let functions = optimized_ir
168 .modules
169 .iter()
170 .flat_map(|module| &module.functions)
171 .map(|function| (function.id.clone(), function))
172 .collect::<BTreeMap<_, _>>();
173
174 EffectInspectionRegistry {
175 records: model
176 .effects
177 .iter()
178 .map(|(id, effect)| {
179 (
180 id.clone(),
181 inspect_effect(
182 model,
183 effect,
184 runtime.record(id),
185 resumability.record(id),
186 executions.get(id),
187 &functions,
188 ),
189 )
190 })
191 .collect(),
192 }
193}
194
195#[must_use]
198pub fn validate_effect_inspection_registry(
199 model: &ApplicationSemanticModel,
200 registry: &EffectInspectionRegistry,
201) -> Vec<EffectInspectionValidationDiagnostic> {
202 let expected = build_effect_inspection_registry(model);
203 if expected == *registry {
204 return Vec::new();
205 }
206
207 let mut diagnostics = Vec::new();
208 for effect in model.effects.keys() {
209 match (registry.records.get(effect), expected.records.get(effect)) {
210 (None, Some(_)) => diagnostics.push(validation_diagnostic(
211 "PSINS1701",
212 "canonical effect is missing an inspection record",
213 )),
214 (Some(actual), Some(expected)) if actual != expected => {
215 diagnostics.push(validation_diagnostic(
216 "PSINS1702",
217 "effect inspection record diverges from canonical compiler products",
218 ));
219 }
220 _ => {}
221 }
222 }
223 if registry
224 .records
225 .keys()
226 .any(|effect| !model.effects.contains_key(effect))
227 {
228 diagnostics.push(validation_diagnostic(
229 "PSINS1703",
230 "effect inspection record does not resolve to a canonical effect",
231 ));
232 }
233 diagnostics
234}
235
236#[allow(clippy::too_many_arguments)]
237fn inspect_effect(
238 model: &ApplicationSemanticModel,
239 effect: &Effect,
240 runtime: Option<&RuntimeEffectRecord>,
241 resume: Option<&crate::EffectResumeRecord>,
242 execution: Option<&&crate::IrEffectExecution>,
243 functions: &BTreeMap<SemanticId, &crate::IrFunction>,
244) -> EffectInspection {
245 let initial_trigger = runtime
246 .and_then(|record| record.initial_trigger.as_ref())
247 .map(initial_trigger);
248 let action_triggers = runtime.map_or_else(Vec::new, |record| {
249 record
250 .action_batch_triggers
251 .iter()
252 .map(action_trigger)
253 .collect()
254 });
255 EffectInspection {
256 validation: validation(effect),
257 direct_dependencies: direct_dependencies(model, &effect.id),
258 transitive_dependencies: transitive_dependencies(model, &effect.id),
259 dependents: model
260 .effect_reactive_analysis(&effect.id)
261 .map_or_else(Vec::new, |analysis| semantic_ids(&analysis.dependents)),
262 initial_trigger: initial_trigger.clone(),
263 action_triggers: action_triggers.clone(),
264 schedule: schedule(
265 model,
266 &effect.id,
267 initial_trigger.as_ref(),
268 &action_triggers,
269 ),
270 capabilities: execution.map_or_else(Vec::new, |execution| capabilities(execution)),
271 ir: execution.and_then(|execution| ir_inspection(execution, functions)),
272 runtime: runtime_inspection(runtime, initial_trigger, &action_triggers),
273 resumability: resumability_inspection(resume),
274 }
275}
276
277fn validation(effect: &Effect) -> EffectInspectionValidation {
278 EffectInspectionValidation {
279 status: match effect.validation {
280 EffectValidation::Valid => "valid",
281 EffectValidation::Invalid => "invalid",
282 EffectValidation::Unvalidated => "unvalidated",
283 },
284 violations: effect
285 .semantic_violations
286 .iter()
287 .map(|violation| EffectInspectionViolation {
288 category: violation_category(violation.kind),
289 statement_id: violation.statement.as_ref().map(ToString::to_string),
290 provenance: provenance(&violation.provenance),
291 })
292 .collect(),
293 }
294}
295
296fn direct_dependencies(
297 model: &ApplicationSemanticModel,
298 effect: &SemanticId,
299) -> EffectInspectionDependencies {
300 let dependencies = model
301 .reactive_graph
302 .edges
303 .iter()
304 .filter(|edge| edge.source == effect.as_str() && edge.kind == IrReactiveEdgeKind::Reads)
305 .map(|edge| edge.target.clone())
306 .collect::<BTreeSet<_>>();
307 split_dependencies(model, &dependencies)
308}
309
310fn transitive_dependencies(
311 model: &ApplicationSemanticModel,
312 effect: &SemanticId,
313) -> EffectInspectionDependencies {
314 let dependencies =
315 model
316 .effect_reactive_analysis(effect)
317 .map_or_else(BTreeSet::new, |analysis| {
318 analysis
319 .dependencies
320 .iter()
321 .map(ToString::to_string)
322 .collect()
323 });
324 split_dependencies(model, &dependencies)
325}
326
327fn split_dependencies(
328 model: &ApplicationSemanticModel,
329 dependencies: &BTreeSet<String>,
330) -> EffectInspectionDependencies {
331 EffectInspectionDependencies {
332 state: dependencies
333 .iter()
334 .filter(|id| {
335 model.components.iter().any(|component| {
336 component
337 .state_fields
338 .iter()
339 .any(|field| field.id.as_str() == *id)
340 })
341 })
342 .cloned()
343 .collect(),
344 computed: dependencies
345 .iter()
346 .filter(|id| {
347 model
348 .computed_values
349 .keys()
350 .any(|computed| computed.as_str() == *id)
351 })
352 .cloned()
353 .collect(),
354 }
355}
356
357fn initial_trigger(trigger: &crate::RuntimeInitialEffectTrigger) -> EffectInspectionInitialTrigger {
358 EffectInspectionInitialTrigger {
359 policy: "after_initial_render",
360 batch_index: trigger.effect_batch_index,
361 render_boundary: render_boundary(trigger.render_boundary),
362 }
363}
364
365fn action_trigger(
366 trigger: &crate::RuntimeActionBatchEffectTrigger,
367) -> EffectInspectionActionTrigger {
368 EffectInspectionActionTrigger {
369 action_batch_id: trigger.action_batch.to_string(),
370 matched_states: semantic_ids(&trigger.matched_states),
371 required_computed: semantic_ids(&trigger.required_computed),
372 prerequisite_batches: trigger
373 .prerequisite_batches
374 .iter()
375 .map(|batch| EffectInspectionPrerequisiteBatch {
376 source_batch_index: batch.source_batch_index,
377 computed: semantic_ids(&batch.computed),
378 })
379 .collect(),
380 effect_batch_index: trigger.effect_batch_index,
381 }
382}
383
384fn schedule(
385 model: &ApplicationSemanticModel,
386 effect: &SemanticId,
387 initial: Option<&EffectInspectionInitialTrigger>,
388 actions: &[EffectInspectionActionTrigger],
389) -> EffectInspectionSchedule {
390 let unplanned = model
391 .effect_execution_plan
392 .initial
393 .unplanned_effects
394 .iter()
395 .chain(
396 model
397 .effect_execution_plan
398 .actions
399 .iter()
400 .flat_map(|action| &action.unplanned_effects),
401 )
402 .find(|unplanned| unplanned.effect == *effect)
403 .map(|unplanned| EffectInspectionUnplanned {
404 reason: "unavailable_computed_prerequisite",
405 unavailable_computed_dependencies: semantic_ids(&unplanned.computed_dependencies),
406 });
407 EffectInspectionSchedule {
408 initial_effect_batch_index: initial.map(|trigger| trigger.batch_index),
409 action_batches: actions
410 .iter()
411 .map(|trigger| EffectInspectionScheduledAction {
412 action_batch_id: trigger.action_batch_id.clone(),
413 effect_batch_index: trigger.effect_batch_index,
414 prerequisite_computed_batch_refs: trigger
415 .prerequisite_batches
416 .iter()
417 .map(|batch| batch.source_batch_index)
418 .collect(),
419 })
420 .collect(),
421 unplanned,
422 }
423}
424
425fn capabilities(execution: &crate::IrEffectExecution) -> Vec<EffectInspectionCapability> {
426 execution
427 .capability_operations
428 .iter()
429 .filter_map(|id| EFFECT_CAPABILITY_REGISTRY.operation(*id))
430 .map(|operation| EffectInspectionCapability {
431 operation_id: operation.id.0.to_string(),
432 runtime_lowering_id: operation.runtime_lowering.0.to_string(),
433 kind: match operation.kind {
434 CapabilityOperationKind::MemberAssignment => "member_assignment",
435 CapabilityOperationKind::MethodCall => "method_call",
436 },
437 boundary: execution_boundary(operation.boundary),
438 })
439 .collect()
440}
441
442fn ir_inspection(
443 execution: &crate::IrEffectExecution,
444 functions: &BTreeMap<SemanticId, &crate::IrFunction>,
445) -> Option<EffectInspectionIr> {
446 let function = functions.get(&execution.function)?;
447 Some(EffectInspectionIr {
448 function_id: execution.function.to_string(),
449 instruction_count: function
450 .blocks
451 .iter()
452 .map(|block| block.instructions.len())
453 .sum(),
454 capability_operation_count: execution.capability_operations.len(),
455 })
456}
457
458fn runtime_inspection(
459 runtime: Option<&RuntimeEffectRecord>,
460 initial: Option<EffectInspectionInitialTrigger>,
461 actions: &[EffectInspectionActionTrigger],
462) -> EffectInspectionRuntime {
463 let Some(runtime) = runtime else {
464 return EffectInspectionRuntime {
465 registered: false,
466 function_id: None,
467 execution_policy: None,
468 boundary: None,
469 initial_membership: None,
470 action_batch_ids: Vec::new(),
471 };
472 };
473 EffectInspectionRuntime {
474 registered: true,
475 function_id: Some(runtime.execution_function.to_string()),
476 execution_policy: Some(execution_policy(runtime.initial_trigger_policy)),
477 boundary: Some(execution_boundary(runtime.execution_boundary)),
478 initial_membership: initial,
479 action_batch_ids: actions
480 .iter()
481 .map(|trigger| trigger.action_batch_id.clone())
482 .collect(),
483 }
484}
485
486fn resumability_inspection(
487 resume: Option<&crate::EffectResumeRecord>,
488) -> Option<EffectInspectionResumability> {
489 let record = resume?;
490 Some(EffectInspectionResumability {
491 activation_slot_id: record
492 .activation_slot
493 .as_ref()
494 .map(|slot| slot.as_str().to_string()),
495 initial_status: record.initial_status,
496 render_boundary: record
497 .initial_plan_membership
498 .as_ref()
499 .map(|membership| render_boundary(membership.render_boundary)),
500 initial_batch_index: record
501 .initial_plan_membership
502 .as_ref()
503 .map(|membership| membership.batch_index),
504 action_batch_ids: semantic_ids(&record.action_batches),
505 manifest_schema_version: crate::RESUME_MANIFEST_SCHEMA_VERSION,
506 })
507}
508
509fn semantic_ids(ids: &[SemanticId]) -> Vec<String> {
510 ids.iter().map(ToString::to_string).collect()
511}
512
513fn provenance(provenance: &SourceProvenance) -> EffectInspectionProvenance {
514 EffectInspectionProvenance {
515 path: provenance.path.display().to_string(),
516 line: provenance.span.line,
517 column: provenance.span.column,
518 start: provenance.span.start,
519 end: provenance.span.end,
520 }
521}
522
523fn violation_category(kind: EffectSemanticViolationKind) -> &'static str {
524 match kind {
525 EffectSemanticViolationKind::Async => "async",
526 EffectSemanticViolationKind::ReactiveStateMutation => "reactive_state_mutation",
527 EffectSemanticViolationKind::ActionInvocation => "action_invocation",
528 EffectSemanticViolationKind::EffectInvocation => "effect_invocation",
529 EffectSemanticViolationKind::ComponentMethodInvocation => "component_method_invocation",
530 EffectSemanticViolationKind::UnresolvedComponentCall => "unresolved_component_call",
531 EffectSemanticViolationKind::UnresolvedComponentAssignment => {
532 "unresolved_component_assignment"
533 }
534 EffectSemanticViolationKind::UnknownExternalCapability => "unknown_external_capability",
535 EffectSemanticViolationKind::CapabilitySignature => "capability_signature",
536 EffectSemanticViolationKind::CapabilityBoundary => "capability_boundary",
537 EffectSemanticViolationKind::CapabilitySerialization => "capability_serialization",
538 EffectSemanticViolationKind::ValueReturn => "value_return",
539 EffectSemanticViolationKind::UnsupportedStatement => "unsupported_statement",
540 }
541}
542
543fn execution_policy(policy: EffectExecutionPolicy) -> &'static str {
544 match policy {
545 EffectExecutionPolicy::AfterInitialRenderAndCompletedActionBatch => {
546 "after_initial_render_and_completed_action_batch"
547 }
548 }
549}
550
551fn render_boundary(boundary: EffectRenderBoundary) -> &'static str {
552 match boundary {
553 EffectRenderBoundary::AfterInitialRender => "after_initial_render",
554 }
555}
556
557fn execution_boundary(boundary: ExecutionBoundary) -> &'static str {
558 match boundary {
559 ExecutionBoundary::Client => "client",
560 ExecutionBoundary::Server => "server",
561 }
562}
563
564fn validation_diagnostic(code: &str, message: &str) -> EffectInspectionValidationDiagnostic {
565 EffectInspectionValidationDiagnostic {
566 code: code.to_string(),
567 message: message.to_string(),
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use crate::{
574 build_application_semantic_model, build_effect_inspection_registry,
575 validate_effect_inspection_registry,
576 };
577
578 #[test]
579 fn projects_valid_and_invalid_effects_without_creating_runtime_facts() {
580 let parsed = presolve_parser::parse_file(
581 "src/EffectInspection.tsx",
582 r#"
583@component("x-effect-inspection")
584class EffectInspection extends Component {
585 count = state(1);
586
587 @computed()
588 get doubled() { return this.count * 2; }
589
590 @action()
591 increment() { this.count += 1; }
592
593 @effect()
594 report() { console.log(this.doubled); console.info(this.count); }
595
596 @effect()
597 invalid() { this.count = 0; }
598}
599"#,
600 );
601 let model = build_application_semantic_model(&parsed);
602 let registry = build_effect_inspection_registry(&model);
603 let component = &model.components[0].id;
604 let report = registry
605 .records
606 .get(&component.effect("report"))
607 .expect("valid effect inspection");
608 let invalid = registry
609 .records
610 .get(&component.effect("invalid"))
611 .expect("invalid effect inspection");
612
613 assert_eq!(report.validation.status, "valid");
614 assert_eq!(report.direct_dependencies.computed.len(), 1);
615 assert_eq!(report.direct_dependencies.state.len(), 1);
616 assert_eq!(report.dependents, Vec::<String>::new());
617 assert_eq!(report.action_triggers.len(), 1);
618 assert_eq!(report.capabilities.len(), 2);
619 assert!(report.ir.is_some());
620 assert!(report.runtime.registered);
621 assert_eq!(
622 report
623 .resumability
624 .as_ref()
625 .and_then(|record| record.initial_status),
626 Some(crate::EffectActivationStatus::Pending)
627 );
628
629 assert_eq!(invalid.validation.status, "invalid");
630 assert!(!invalid.validation.violations.is_empty());
631 assert!(invalid.ir.is_none());
632 assert!(!invalid.runtime.registered);
633 assert!(invalid.resumability.is_none());
634 assert!(validate_effect_inspection_registry(&model, ®istry).is_empty());
635 }
636}