1use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6 build_computed_instance_slot_registry, build_effect_resume_plan,
7 build_runtime_component_registry, build_runtime_computed_registry,
8 build_state_instance_storage_registry, lower_components_to_ir, optimize_effect_ir,
9 ApplicationSemanticModel, ComponentInstanceId, ComponentInstanceStatus, ComponentRootId,
10 ComputedInstanceCacheSlotId, ComputedInstanceDirtySlotId, ComputedPurity,
11 ContextSourceInstanceOwner, ContextValueSourceId, EffectActivationSlotId, FormFieldDirtySlotId,
12 FormFieldTouchedSlotId, FormFieldValidationSlotId, FormFieldValueSlotId, FormInstanceId,
13 FormSubmissionStateSlotId, FormValidationAggregateSlotId, InstanceContextValueSlotId,
14 IrStorageId, ResumeBoundaryId, ResumeSlotId, SemanticId, SerializationCompatibility,
15 SourceProvenance, StateInstanceSlotId,
16};
17
18pub const RESUME_LIVENESS_PLAN_VERSION: u32 = 1;
19
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
21pub enum ResumeExistingSlot {
22 State(StateInstanceSlotId),
23 ComputedCache(ComputedInstanceCacheSlotId),
24 ComputedDirty(ComputedInstanceDirtySlotId),
25 Context(InstanceContextValueSlotId),
26 FormFieldValue(FormFieldValueSlotId),
27 FormFieldDirty(FormFieldDirtySlotId),
28 FormFieldTouched(FormFieldTouchedSlotId),
29 FormFieldValidation(FormFieldValidationSlotId),
30 FormValidationAggregate(FormValidationAggregateSlotId),
31 FormSubmission(FormSubmissionStateSlotId),
32 EffectActivation(EffectActivationSlotId),
33}
34
35impl ResumeExistingSlot {
36 #[must_use]
37 pub fn text(&self) -> String {
38 match self {
39 Self::State(slot) => slot.to_string(),
40 Self::ComputedCache(slot) => slot.to_string(),
41 Self::ComputedDirty(slot) => slot.to_string(),
42 Self::Context(slot) => slot.to_string(),
43 Self::FormFieldValue(slot) => slot.as_str().to_string(),
44 Self::FormFieldDirty(slot) => slot.as_str().to_string(),
45 Self::FormFieldTouched(slot) => slot.as_str().to_string(),
46 Self::FormFieldValidation(slot) => slot.as_str().to_string(),
47 Self::FormValidationAggregate(slot) => slot.as_str().to_string(),
48 Self::FormSubmission(slot) => slot.as_str().to_string(),
49 Self::EffectActivation(slot) => slot.as_str().to_string(),
50 }
51 }
52
53 #[must_use]
54 pub fn resume_slot_id(&self) -> ResumeSlotId {
55 ResumeSlotId::for_existing_storage(&self.text())
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
60pub enum ResumeLivenessOwner {
61 ApplicationRoot(ComponentRootId),
62 ComponentInstance(ComponentInstanceId),
63 FormInstance(FormInstanceId),
64 Semantic(SemanticId),
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
68pub enum ResumeRetentionReason {
69 MutableState,
70 NonRecomputableComputedCache,
71 ComputedDirtyState,
72 PureEagerComputedCache,
73 ComputedDirtyAfterRecompute,
74 ContextConsumerDependency,
75 FormFieldValue,
76 FormFieldDirty,
77 FormFieldTouched,
78 FormRuleResult,
79 FormAggregateValidation,
80 StableFormSubmission,
81 EffectSchedulerMetadata,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
85pub enum ResumeLivenessBlockReason {
86 RequiredNonSerializableValue,
87 MissingCanonicalSource,
88 UnknownDependency,
89 RecomputeProofUnavailable,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
93pub enum ResumeLivenessClassificationKind {
94 Retained,
95 Recomputable,
96 Excluded,
97 Blocked,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ResumeLivenessSlot {
102 pub resume_slot_id: ResumeSlotId,
103 pub existing_slot: ResumeExistingSlot,
104 pub owner: ResumeLivenessOwner,
105 pub boundary_candidate: Option<ResumeBoundaryId>,
106 pub direct_dependencies: Vec<ResumeExistingSlot>,
107 pub transitive_dependencies: Vec<ResumeExistingSlot>,
108 pub provenance: SourceProvenance,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct ResumeRetainedSlot {
113 pub slot: ResumeLivenessSlot,
114 pub reason: ResumeRetentionReason,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[allow(clippy::struct_excessive_bools)]
119pub struct ResumeRecomputationProof {
120 pub pure: bool,
121 pub deterministic: bool,
122 pub eager_evaluator: bool,
123 pub fixed_post_restore_order: bool,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct ResumeRecomputableSlot {
128 pub slot: ResumeLivenessSlot,
129 pub reason: ResumeRetentionReason,
130 pub proof: ResumeRecomputationProof,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct ResumeExcludedSlot {
135 pub slot: ResumeLivenessSlot,
136 pub reason: ResumeRetentionReason,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct ResumeLivenessBlock {
141 pub slot: ResumeLivenessSlot,
142 pub reason: ResumeLivenessBlockReason,
143 pub integrity_code: ResumeLivenessIntegrityCode,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ResumeLivenessPlan {
148 pub version: u32,
149 pub retained: Vec<ResumeRetainedSlot>,
150 pub recomputable: Vec<ResumeRecomputableSlot>,
151 pub excluded: Vec<ResumeExcludedSlot>,
152 pub blocked: Vec<ResumeLivenessBlock>,
153 pub slot_index: BTreeMap<ResumeExistingSlot, ResumeLivenessClassificationKind>,
154}
155
156pub enum ResumeLivenessClassificationRef<'a> {
157 Retained(&'a ResumeRetainedSlot),
158 Recomputable(&'a ResumeRecomputableSlot),
159 Excluded(&'a ResumeExcludedSlot),
160 Blocked(&'a ResumeLivenessBlock),
161}
162
163impl ResumeLivenessPlan {
164 #[must_use]
165 pub fn classification(
166 &self,
167 existing_slot: &ResumeExistingSlot,
168 ) -> Option<ResumeLivenessClassificationRef<'_>> {
169 self.retained
170 .iter()
171 .find(|record| &record.slot.existing_slot == existing_slot)
172 .map(ResumeLivenessClassificationRef::Retained)
173 .or_else(|| {
174 self.recomputable
175 .iter()
176 .find(|record| &record.slot.existing_slot == existing_slot)
177 .map(ResumeLivenessClassificationRef::Recomputable)
178 })
179 .or_else(|| {
180 self.excluded
181 .iter()
182 .find(|record| &record.slot.existing_slot == existing_slot)
183 .map(ResumeLivenessClassificationRef::Excluded)
184 })
185 .or_else(|| {
186 self.blocked
187 .iter()
188 .find(|record| &record.slot.existing_slot == existing_slot)
189 .map(ResumeLivenessClassificationRef::Blocked)
190 })
191 }
192
193 #[must_use]
194 pub fn slots_for_owner(&self, owner: &ResumeLivenessOwner) -> Vec<&ResumeLivenessSlot> {
195 self.all_slots()
196 .into_iter()
197 .filter(|slot| &slot.owner == owner)
198 .collect()
199 }
200
201 #[must_use]
202 pub fn slots_for_boundary(&self, boundary: &ResumeBoundaryId) -> Vec<&ResumeLivenessSlot> {
203 self.all_slots()
204 .into_iter()
205 .filter(|slot| slot.boundary_candidate.as_ref() == Some(boundary))
206 .collect()
207 }
208
209 #[must_use]
210 pub fn slots_for_reason(&self, reason: ResumeRetentionReason) -> Vec<&ResumeLivenessSlot> {
211 let mut slots = self
212 .retained
213 .iter()
214 .filter(|record| record.reason == reason)
215 .map(|record| &record.slot)
216 .chain(
217 self.recomputable
218 .iter()
219 .filter(|record| record.reason == reason)
220 .map(|record| &record.slot),
221 )
222 .chain(
223 self.excluded
224 .iter()
225 .filter(|record| record.reason == reason)
226 .map(|record| &record.slot),
227 )
228 .collect::<Vec<_>>();
229 slots.sort_by(|left, right| left.existing_slot.cmp(&right.existing_slot));
230 slots
231 }
232
233 fn all_slots(&self) -> Vec<&ResumeLivenessSlot> {
234 let mut slots = self
235 .retained
236 .iter()
237 .map(|record| &record.slot)
238 .chain(self.recomputable.iter().map(|record| &record.slot))
239 .chain(self.excluded.iter().map(|record| &record.slot))
240 .chain(self.blocked.iter().map(|record| &record.slot))
241 .collect::<Vec<_>>();
242 slots.sort_by(|left, right| left.existing_slot.cmp(&right.existing_slot));
243 slots
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
248pub enum ResumeLivenessIntegrityCode {
249 DuplicateClassification,
250 MissingStorageOwner,
251 UnknownDependency,
252 InvalidRetentionReason,
253 RecomputeWithoutProof,
254 RequiredUnsupportedValue,
255 InvalidCandidatePromotion,
256 ProvenanceOrderIndexDrift,
257}
258
259impl ResumeLivenessIntegrityCode {
260 #[must_use]
261 pub const fn code(self) -> &'static str {
262 match self {
263 Self::DuplicateClassification => "PSASM1320",
264 Self::MissingStorageOwner => "PSASM1321",
265 Self::UnknownDependency => "PSASM1322",
266 Self::InvalidRetentionReason => "PSASM1323",
267 Self::RecomputeWithoutProof => "PSASM1324",
268 Self::RequiredUnsupportedValue => "PSASM1325",
269 Self::InvalidCandidatePromotion => "PSASM1326",
270 Self::ProvenanceOrderIndexDrift => "PSASM1327",
271 }
272 }
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct ResumeLivenessIntegrityDiagnostic {
277 pub code: ResumeLivenessIntegrityCode,
278 pub slot: Option<ResumeExistingSlot>,
279 pub message: String,
280}
281
282#[derive(Debug, Clone)]
283struct ClassificationEvidence {
284 kind: ResumeLivenessClassificationKind,
285 transitive_dependencies: Vec<ResumeExistingSlot>,
286}
287
288#[must_use]
289#[allow(clippy::too_many_lines)]
290pub fn build_resume_liveness_plan(model: &ApplicationSemanticModel) -> ResumeLivenessPlan {
291 let ir = lower_components_to_ir(model);
292 let state = build_state_instance_storage_registry(model, &ir);
293 let computed = build_computed_instance_slot_registry(model, &ir);
294 let computed_runtime = build_runtime_computed_registry(model, &ir);
295 let component_registry =
296 build_runtime_component_registry(model, &model.component_ir_optimization);
297 let effect_registry =
298 crate::build_runtime_effect_registry(model, &optimize_effect_ir(&ir).output);
299 let effect_resume = build_effect_resume_plan(model, &effect_registry);
300
301 let mut retained = Vec::new();
302 let mut recomputable = Vec::new();
303 let mut excluded = Vec::new();
304 let mut blocked = Vec::new();
305 let mut evidence = BTreeMap::<ResumeExistingSlot, ClassificationEvidence>::new();
306
307 for record in &state.records {
308 let existing_slot = ResumeExistingSlot::State(record.slot_id.clone());
309 let slot = liveness_slot(
310 existing_slot.clone(),
311 ResumeLivenessOwner::ComponentInstance(record.component_instance_id.clone()),
312 Some(ResumeBoundaryId::component_instance(
313 &record.component_instance_id,
314 )),
315 Vec::new(),
316 Vec::new(),
317 record.provenance.clone(),
318 );
319 if record.serialization == SerializationCompatibility::Serializable {
320 retained.push(ResumeRetainedSlot {
321 slot,
322 reason: ResumeRetentionReason::MutableState,
323 });
324 insert_evidence(
325 &mut evidence,
326 existing_slot,
327 ResumeLivenessClassificationKind::Retained,
328 Vec::new(),
329 );
330 } else {
331 blocked.push(ResumeLivenessBlock {
332 slot,
333 reason: ResumeLivenessBlockReason::RequiredNonSerializableValue,
334 integrity_code: ResumeLivenessIntegrityCode::RequiredUnsupportedValue,
335 });
336 insert_evidence(
337 &mut evidence,
338 existing_slot,
339 ResumeLivenessClassificationKind::Blocked,
340 Vec::new(),
341 );
342 }
343 }
344
345 let computed_by_pair = computed
346 .records
347 .iter()
348 .map(|record| {
349 (
350 (
351 record.component_instance_id.clone(),
352 record.computed_id.as_str().to_string(),
353 ),
354 record,
355 )
356 })
357 .collect::<BTreeMap<_, _>>();
358 let mut ordered_computed = Vec::new();
359 for instance in model.component_instance_plan.instances.values() {
360 if instance.status != ComponentInstanceStatus::Planned {
361 continue;
362 }
363 for computed_id in &model.computed_evaluation_plan.evaluation_order {
364 if let Some(record) = computed_by_pair.get(&(instance.id.clone(), computed_id.clone()))
365 {
366 ordered_computed.push(*record);
367 }
368 }
369 }
370 for record in &computed.records {
371 if !ordered_computed
372 .iter()
373 .any(|candidate| candidate.cache_slot_id == record.cache_slot_id)
374 {
375 ordered_computed.push(record);
376 }
377 }
378
379 for record in ordered_computed {
380 let Some(declaration) = computed_runtime.record(&record.computed_id) else {
381 let provenance = model.computed_values[&record.computed_id]
382 .provenance
383 .clone();
384 classify_computed_block(
385 record,
386 &provenance,
387 &mut blocked,
388 &mut evidence,
389 ResumeLivenessBlockReason::MissingCanonicalSource,
390 );
391 continue;
392 };
393 let (direct_dependencies, unknown_dependency) = dependency_slots(
394 model,
395 &record.component_instance_id,
396 &declaration.dependencies,
397 &state,
398 &computed,
399 );
400 let transitive_dependencies = transitive_dependencies(&direct_dependencies, &evidence);
401 let dependency_ready = !unknown_dependency
402 && direct_dependencies.iter().all(|dependency| {
403 evidence.get(dependency).is_some_and(|evidence| {
404 matches!(
405 evidence.kind,
406 ResumeLivenessClassificationKind::Retained
407 | ResumeLivenessClassificationKind::Recomputable
408 )
409 })
410 });
411 let pure = model
412 .computed_values
413 .get(&record.computed_id)
414 .is_some_and(|value| value.purity == ComputedPurity::Pure);
415 let eager = model
416 .computed_evaluation_plan
417 .evaluation_order
418 .iter()
419 .any(|computed| computed == record.computed_id.as_str());
420 let cache_existing = ResumeExistingSlot::ComputedCache(record.cache_slot_id.clone());
421 let cache_slot = liveness_slot(
422 cache_existing.clone(),
423 ResumeLivenessOwner::ComponentInstance(record.component_instance_id.clone()),
424 Some(ResumeBoundaryId::component_instance(
425 &record.component_instance_id,
426 )),
427 direct_dependencies,
428 transitive_dependencies.clone(),
429 declaration.provenance.clone(),
430 );
431 let cache_kind = if pure && eager && dependency_ready {
432 recomputable.push(ResumeRecomputableSlot {
433 slot: cache_slot,
434 reason: ResumeRetentionReason::PureEagerComputedCache,
435 proof: complete_recomputation_proof(),
436 });
437 ResumeLivenessClassificationKind::Recomputable
438 } else if declaration.serialization == SerializationCompatibility::Serializable {
439 retained.push(ResumeRetainedSlot {
440 slot: cache_slot,
441 reason: ResumeRetentionReason::NonRecomputableComputedCache,
442 });
443 ResumeLivenessClassificationKind::Retained
444 } else {
445 blocked.push(ResumeLivenessBlock {
446 slot: cache_slot,
447 reason: if unknown_dependency {
448 ResumeLivenessBlockReason::UnknownDependency
449 } else {
450 ResumeLivenessBlockReason::RecomputeProofUnavailable
451 },
452 integrity_code: if unknown_dependency {
453 ResumeLivenessIntegrityCode::UnknownDependency
454 } else {
455 ResumeLivenessIntegrityCode::RequiredUnsupportedValue
456 },
457 });
458 ResumeLivenessClassificationKind::Blocked
459 };
460 insert_evidence(
461 &mut evidence,
462 cache_existing.clone(),
463 cache_kind,
464 transitive_dependencies,
465 );
466
467 let dirty_existing = ResumeExistingSlot::ComputedDirty(record.dirty_slot_id.clone());
468 let dirty_slot = liveness_slot(
469 dirty_existing.clone(),
470 ResumeLivenessOwner::ComponentInstance(record.component_instance_id.clone()),
471 Some(ResumeBoundaryId::component_instance(
472 &record.component_instance_id,
473 )),
474 vec![cache_existing.clone()],
475 vec![cache_existing],
476 declaration.provenance.clone(),
477 );
478 match cache_kind {
479 ResumeLivenessClassificationKind::Recomputable => {
480 recomputable.push(ResumeRecomputableSlot {
481 slot: dirty_slot,
482 reason: ResumeRetentionReason::ComputedDirtyAfterRecompute,
483 proof: complete_recomputation_proof(),
484 });
485 }
486 ResumeLivenessClassificationKind::Retained => {
487 retained.push(ResumeRetainedSlot {
488 slot: dirty_slot,
489 reason: ResumeRetentionReason::ComputedDirtyState,
490 });
491 }
492 ResumeLivenessClassificationKind::Blocked
493 | ResumeLivenessClassificationKind::Excluded => {
494 blocked.push(ResumeLivenessBlock {
495 slot: dirty_slot,
496 reason: ResumeLivenessBlockReason::RecomputeProofUnavailable,
497 integrity_code: ResumeLivenessIntegrityCode::RequiredUnsupportedValue,
498 });
499 }
500 }
501 insert_evidence(&mut evidence, dirty_existing, cache_kind, Vec::new());
502 }
503
504 let context_bindings = component_registry
505 .instance_context_bindings
506 .iter()
507 .map(|binding| (binding.runtime_slot.clone(), binding))
508 .collect::<BTreeMap<_, _>>();
509 for (runtime_slot, binding) in context_bindings {
510 let existing_slot = ResumeExistingSlot::Context(runtime_slot);
511 let (owner, component_instance, boundary) = match &binding.selected_source.owner {
512 ContextSourceInstanceOwner::Provider(provider) => (
513 ResumeLivenessOwner::ComponentInstance(provider.component_instance.clone()),
514 provider.component_instance.clone(),
515 Some(ResumeBoundaryId::component_instance(
516 &provider.component_instance,
517 )),
518 ),
519 ContextSourceInstanceOwner::RootDefault(default) => {
520 let instance = ComponentInstanceId::for_root(&default.owner_root);
521 (
522 ResumeLivenessOwner::ApplicationRoot(default.owner_root.clone()),
523 instance,
524 Some(ResumeBoundaryId::application_root(&default.owner_root)),
525 )
526 }
527 };
528 let declaration_source = match &binding.selected_source.owner {
529 ContextSourceInstanceOwner::Provider(provider) => {
530 ContextValueSourceId::Provider(provider.provider.clone())
531 }
532 ContextSourceInstanceOwner::RootDefault(default) => {
533 ContextValueSourceId::ContextDefault(default.context.clone())
534 }
535 };
536 let Some(source) = model
537 .context_evaluation
538 .context_source_plan(&declaration_source)
539 else {
540 let slot = liveness_slot(
541 existing_slot.clone(),
542 owner,
543 boundary,
544 Vec::new(),
545 Vec::new(),
546 binding.provenance.clone(),
547 );
548 blocked.push(ResumeLivenessBlock {
549 slot,
550 reason: ResumeLivenessBlockReason::MissingCanonicalSource,
551 integrity_code: ResumeLivenessIntegrityCode::MissingStorageOwner,
552 });
553 insert_evidence(
554 &mut evidence,
555 existing_slot,
556 ResumeLivenessClassificationKind::Blocked,
557 Vec::new(),
558 );
559 continue;
560 };
561 let mut semantic_dependencies = source.required_state.clone();
562 semantic_dependencies.extend(source.required_computed.clone());
563 let (direct_dependencies, unknown_dependency) = dependency_slots(
564 model,
565 &component_instance,
566 &semantic_dependencies,
567 &state,
568 &computed,
569 );
570 let transitive_dependencies = transitive_dependencies(&direct_dependencies, &evidence);
571 let slot = liveness_slot(
572 existing_slot.clone(),
573 owner,
574 boundary,
575 direct_dependencies,
576 transitive_dependencies.clone(),
577 binding.provenance.clone(),
578 );
579 if unknown_dependency {
580 blocked.push(ResumeLivenessBlock {
581 slot,
582 reason: ResumeLivenessBlockReason::UnknownDependency,
583 integrity_code: ResumeLivenessIntegrityCode::UnknownDependency,
584 });
585 insert_evidence(
586 &mut evidence,
587 existing_slot,
588 ResumeLivenessClassificationKind::Blocked,
589 transitive_dependencies,
590 );
591 } else {
592 retained.push(ResumeRetainedSlot {
593 slot,
594 reason: ResumeRetentionReason::ContextConsumerDependency,
595 });
596 insert_evidence(
597 &mut evidence,
598 existing_slot,
599 ResumeLivenessClassificationKind::Retained,
600 transitive_dependencies,
601 );
602 }
603 }
604
605 for instance in model.optimized_form_ir.optimized.instances.values() {
606 let owner = ResumeLivenessOwner::FormInstance(instance.id.clone());
607 let boundary = Some(ResumeBoundaryId::form_instance(&instance.id));
608 for (field_id, value_slot) in &instance.storage.value {
609 let Some(field) = model.form_fields.get(field_id) else {
610 continue;
611 };
612 classify_form_value(
613 value_slot,
614 field,
615 &owner,
616 boundary.as_ref(),
617 &mut retained,
618 &mut blocked,
619 &mut evidence,
620 );
621 for (existing_slot, reason) in [
622 (
623 ResumeExistingSlot::FormFieldDirty(instance.storage.dirty[field_id].clone()),
624 ResumeRetentionReason::FormFieldDirty,
625 ),
626 (
627 ResumeExistingSlot::FormFieldTouched(
628 instance.storage.touched[field_id].clone(),
629 ),
630 ResumeRetentionReason::FormFieldTouched,
631 ),
632 (
633 ResumeExistingSlot::FormFieldValidation(
634 instance.storage.validation[field_id].clone(),
635 ),
636 ResumeRetentionReason::FormRuleResult,
637 ),
638 ] {
639 retain_slot(
640 existing_slot,
641 owner.clone(),
642 boundary.clone(),
643 field.provenance.clone(),
644 reason,
645 &mut retained,
646 &mut evidence,
647 );
648 }
649 }
650 let form_provenance = model.forms[&instance.form].provenance.clone();
651 for (existing_slot, reason) in [
652 (
653 ResumeExistingSlot::FormValidationAggregate(instance.storage.aggregate.clone()),
654 ResumeRetentionReason::FormAggregateValidation,
655 ),
656 (
657 ResumeExistingSlot::FormSubmission(instance.storage.submission.clone()),
658 ResumeRetentionReason::StableFormSubmission,
659 ),
660 ] {
661 retain_slot(
662 existing_slot,
663 owner.clone(),
664 boundary.clone(),
665 form_provenance.clone(),
666 reason,
667 &mut retained,
668 &mut evidence,
669 );
670 }
671 }
672
673 for record in effect_resume.records {
674 let Some(activation_slot) = record.activation_slot else {
675 continue;
676 };
677 let existing_slot = ResumeExistingSlot::EffectActivation(activation_slot);
678 excluded.push(ResumeExcludedSlot {
679 slot: liveness_slot(
680 existing_slot.clone(),
681 ResumeLivenessOwner::Semantic(record.effect),
682 None,
683 Vec::new(),
684 Vec::new(),
685 record.provenance,
686 ),
687 reason: ResumeRetentionReason::EffectSchedulerMetadata,
688 });
689 insert_evidence(
690 &mut evidence,
691 existing_slot,
692 ResumeLivenessClassificationKind::Excluded,
693 Vec::new(),
694 );
695 }
696
697 retained.sort_by(|left, right| left.slot.existing_slot.cmp(&right.slot.existing_slot));
698 recomputable.sort_by(|left, right| left.slot.existing_slot.cmp(&right.slot.existing_slot));
699 excluded.sort_by(|left, right| left.slot.existing_slot.cmp(&right.slot.existing_slot));
700 blocked.sort_by(|left, right| left.slot.existing_slot.cmp(&right.slot.existing_slot));
701 let slot_index = retained
702 .iter()
703 .map(|record| {
704 (
705 record.slot.existing_slot.clone(),
706 ResumeLivenessClassificationKind::Retained,
707 )
708 })
709 .chain(recomputable.iter().map(|record| {
710 (
711 record.slot.existing_slot.clone(),
712 ResumeLivenessClassificationKind::Recomputable,
713 )
714 }))
715 .chain(excluded.iter().map(|record| {
716 (
717 record.slot.existing_slot.clone(),
718 ResumeLivenessClassificationKind::Excluded,
719 )
720 }))
721 .chain(blocked.iter().map(|record| {
722 (
723 record.slot.existing_slot.clone(),
724 ResumeLivenessClassificationKind::Blocked,
725 )
726 }))
727 .collect();
728 ResumeLivenessPlan {
729 version: RESUME_LIVENESS_PLAN_VERSION,
730 retained,
731 recomputable,
732 excluded,
733 blocked,
734 slot_index,
735 }
736}
737
738fn complete_recomputation_proof() -> ResumeRecomputationProof {
739 ResumeRecomputationProof {
740 pure: true,
741 deterministic: true,
742 eager_evaluator: true,
743 fixed_post_restore_order: true,
744 }
745}
746
747fn insert_evidence(
748 evidence: &mut BTreeMap<ResumeExistingSlot, ClassificationEvidence>,
749 slot: ResumeExistingSlot,
750 kind: ResumeLivenessClassificationKind,
751 transitive_dependencies: Vec<ResumeExistingSlot>,
752) {
753 evidence.insert(
754 slot,
755 ClassificationEvidence {
756 kind,
757 transitive_dependencies,
758 },
759 );
760}
761
762fn classify_computed_block(
763 record: &crate::ComputedInstanceSlotRecord,
764 provenance: &SourceProvenance,
765 blocked: &mut Vec<ResumeLivenessBlock>,
766 evidence: &mut BTreeMap<ResumeExistingSlot, ClassificationEvidence>,
767 reason: ResumeLivenessBlockReason,
768) {
769 for existing_slot in [
770 ResumeExistingSlot::ComputedCache(record.cache_slot_id.clone()),
771 ResumeExistingSlot::ComputedDirty(record.dirty_slot_id.clone()),
772 ] {
773 blocked.push(ResumeLivenessBlock {
774 slot: liveness_slot(
775 existing_slot.clone(),
776 ResumeLivenessOwner::ComponentInstance(record.component_instance_id.clone()),
777 Some(ResumeBoundaryId::component_instance(
778 &record.component_instance_id,
779 )),
780 Vec::new(),
781 Vec::new(),
782 provenance.clone(),
783 ),
784 reason,
785 integrity_code: ResumeLivenessIntegrityCode::MissingStorageOwner,
786 });
787 insert_evidence(
788 evidence,
789 existing_slot,
790 ResumeLivenessClassificationKind::Blocked,
791 Vec::new(),
792 );
793 }
794}
795
796fn classify_form_value(
797 value_slot: &FormFieldValueSlotId,
798 field: &crate::FormFieldEntity,
799 owner: &ResumeLivenessOwner,
800 boundary: Option<&ResumeBoundaryId>,
801 retained: &mut Vec<ResumeRetainedSlot>,
802 blocked: &mut Vec<ResumeLivenessBlock>,
803 evidence: &mut BTreeMap<ResumeExistingSlot, ClassificationEvidence>,
804) {
805 let existing_slot = ResumeExistingSlot::FormFieldValue(value_slot.clone());
806 let slot = liveness_slot(
807 existing_slot.clone(),
808 owner.clone(),
809 boundary.cloned(),
810 Vec::new(),
811 Vec::new(),
812 field.provenance.clone(),
813 );
814 if crate::serialization_compatibility(&field.semantic_type)
815 == SerializationCompatibility::Serializable
816 {
817 retained.push(ResumeRetainedSlot {
818 slot,
819 reason: ResumeRetentionReason::FormFieldValue,
820 });
821 insert_evidence(
822 evidence,
823 existing_slot,
824 ResumeLivenessClassificationKind::Retained,
825 Vec::new(),
826 );
827 } else {
828 blocked.push(ResumeLivenessBlock {
829 slot,
830 reason: ResumeLivenessBlockReason::RequiredNonSerializableValue,
831 integrity_code: ResumeLivenessIntegrityCode::RequiredUnsupportedValue,
832 });
833 insert_evidence(
834 evidence,
835 existing_slot,
836 ResumeLivenessClassificationKind::Blocked,
837 Vec::new(),
838 );
839 }
840}
841
842fn retain_slot(
843 existing_slot: ResumeExistingSlot,
844 owner: ResumeLivenessOwner,
845 boundary: Option<ResumeBoundaryId>,
846 provenance: SourceProvenance,
847 reason: ResumeRetentionReason,
848 retained: &mut Vec<ResumeRetainedSlot>,
849 evidence: &mut BTreeMap<ResumeExistingSlot, ClassificationEvidence>,
850) {
851 retained.push(ResumeRetainedSlot {
852 slot: liveness_slot(
853 existing_slot.clone(),
854 owner,
855 boundary,
856 Vec::new(),
857 Vec::new(),
858 provenance,
859 ),
860 reason,
861 });
862 insert_evidence(
863 evidence,
864 existing_slot,
865 ResumeLivenessClassificationKind::Retained,
866 Vec::new(),
867 );
868}
869
870fn dependency_slots(
871 model: &ApplicationSemanticModel,
872 component_instance: &ComponentInstanceId,
873 dependencies: &[SemanticId],
874 state: &crate::StateInstanceStorageRegistry,
875 computed: &crate::ComputedInstanceSlotRegistry,
876) -> (Vec<ResumeExistingSlot>, bool) {
877 let mut slots = Vec::new();
878 let mut unknown = false;
879 for dependency in dependencies {
880 if model
881 .components
882 .iter()
883 .flat_map(|component| &component.state_fields)
884 .any(|state| state.id == *dependency)
885 {
886 let storage = IrStorageId::for_semantic_origin(dependency);
887 if let Some(record) = state.record(component_instance, &storage) {
888 slots.push(ResumeExistingSlot::State(record.slot_id.clone()));
889 } else {
890 unknown = true;
891 }
892 } else if let Some(record) = computed.records.iter().find(|record| {
893 record.component_instance_id == *component_instance && record.computed_id == *dependency
894 }) {
895 slots.push(ResumeExistingSlot::ComputedCache(
896 record.cache_slot_id.clone(),
897 ));
898 } else {
899 unknown = true;
900 }
901 }
902 slots.sort();
903 slots.dedup();
904 (slots, unknown)
905}
906
907fn transitive_dependencies(
908 direct: &[ResumeExistingSlot],
909 evidence: &BTreeMap<ResumeExistingSlot, ClassificationEvidence>,
910) -> Vec<ResumeExistingSlot> {
911 let mut transitive = direct.iter().cloned().collect::<BTreeSet<_>>();
912 for dependency in direct {
913 if let Some(dependency_evidence) = evidence.get(dependency) {
914 transitive.extend(dependency_evidence.transitive_dependencies.iter().cloned());
915 }
916 }
917 transitive.into_iter().collect()
918}
919
920fn liveness_slot(
921 existing_slot: ResumeExistingSlot,
922 owner: ResumeLivenessOwner,
923 boundary_candidate: Option<ResumeBoundaryId>,
924 direct_dependencies: Vec<ResumeExistingSlot>,
925 transitive_dependencies: Vec<ResumeExistingSlot>,
926 provenance: SourceProvenance,
927) -> ResumeLivenessSlot {
928 ResumeLivenessSlot {
929 resume_slot_id: existing_slot.resume_slot_id(),
930 existing_slot,
931 owner,
932 boundary_candidate,
933 direct_dependencies,
934 transitive_dependencies,
935 provenance,
936 }
937}
938
939#[must_use]
940pub fn validate_resume_liveness_plan(
941 model: &ApplicationSemanticModel,
942 plan: &ResumeLivenessPlan,
943) -> Vec<ResumeLivenessIntegrityDiagnostic> {
944 let mut diagnostics = Vec::new();
945 let canonical = build_resume_liveness_plan(model);
946 let mut classified = BTreeSet::new();
947 for slot in plan.all_slots() {
948 if !classified.insert(slot.existing_slot.clone()) {
949 diagnostics.push(integrity(
950 ResumeLivenessIntegrityCode::DuplicateClassification,
951 Some(slot.existing_slot.clone()),
952 "runtime slot received more than one liveness classification",
953 ));
954 }
955 for dependency in slot
956 .direct_dependencies
957 .iter()
958 .chain(&slot.transitive_dependencies)
959 {
960 if !plan.slot_index.contains_key(dependency) {
961 diagnostics.push(integrity(
962 ResumeLivenessIntegrityCode::UnknownDependency,
963 Some(slot.existing_slot.clone()),
964 "liveness evidence referenced an unknown runtime slot",
965 ));
966 }
967 }
968 let Some(expected) = canonical.classification(&slot.existing_slot) else {
969 diagnostics.push(integrity(
970 ResumeLivenessIntegrityCode::MissingStorageOwner,
971 Some(slot.existing_slot.clone()),
972 "liveness classification does not correspond to canonical runtime storage",
973 ));
974 continue;
975 };
976 let Some(actual) = plan.classification(&slot.existing_slot) else {
977 continue;
978 };
979 if classification_slot(&actual).owner != classification_slot(&expected).owner {
980 diagnostics.push(integrity(
981 ResumeLivenessIntegrityCode::MissingStorageOwner,
982 Some(slot.existing_slot.clone()),
983 "liveness classification owner disagrees with canonical instance storage",
984 ));
985 }
986 if classification_slot(&actual).boundary_candidate
987 != classification_slot(&expected).boundary_candidate
988 {
989 diagnostics.push(integrity(
990 ResumeLivenessIntegrityCode::InvalidCandidatePromotion,
991 Some(slot.existing_slot.clone()),
992 "runtime slot was promoted to a noncanonical resume boundary candidate",
993 ));
994 }
995 if classification_kind(&expected) == ResumeLivenessClassificationKind::Blocked
996 && classification_kind(&actual) != ResumeLivenessClassificationKind::Blocked
997 {
998 diagnostics.push(integrity(
999 ResumeLivenessIntegrityCode::RequiredUnsupportedValue,
1000 Some(slot.existing_slot.clone()),
1001 "required unsupported value was classified as resumable",
1002 ));
1003 }
1004 if classification_kind(&actual) != classification_kind(&expected)
1005 || classification_reason(&actual) != classification_reason(&expected)
1006 {
1007 diagnostics.push(integrity(
1008 ResumeLivenessIntegrityCode::InvalidRetentionReason,
1009 Some(slot.existing_slot.clone()),
1010 "runtime slot liveness classification or reason disagrees with canonical policy",
1011 ));
1012 }
1013 }
1014 for record in &plan.recomputable {
1015 if !record.proof.pure
1016 || !record.proof.deterministic
1017 || !record.proof.eager_evaluator
1018 || !record.proof.fixed_post_restore_order
1019 {
1020 diagnostics.push(integrity(
1021 ResumeLivenessIntegrityCode::RecomputeWithoutProof,
1022 Some(record.slot.existing_slot.clone()),
1023 "recomputable slot was missing one required canonical proof",
1024 ));
1025 }
1026 }
1027 if plan.version != RESUME_LIVENESS_PLAN_VERSION || plan != &canonical {
1028 diagnostics.push(integrity(
1029 ResumeLivenessIntegrityCode::ProvenanceOrderIndexDrift,
1030 None,
1031 "liveness plan drifted from canonical storage, dependency, type, and instance products",
1032 ));
1033 }
1034 diagnostics.sort_by(|left, right| {
1035 (left.code, &left.slot, left.message.as_str()).cmp(&(
1036 right.code,
1037 &right.slot,
1038 right.message.as_str(),
1039 ))
1040 });
1041 diagnostics.dedup();
1042 diagnostics
1043}
1044
1045fn classification_slot<'a>(
1046 classification: &ResumeLivenessClassificationRef<'a>,
1047) -> &'a ResumeLivenessSlot {
1048 match classification {
1049 ResumeLivenessClassificationRef::Retained(record) => &record.slot,
1050 ResumeLivenessClassificationRef::Recomputable(record) => &record.slot,
1051 ResumeLivenessClassificationRef::Excluded(record) => &record.slot,
1052 ResumeLivenessClassificationRef::Blocked(record) => &record.slot,
1053 }
1054}
1055
1056fn classification_kind(
1057 classification: &ResumeLivenessClassificationRef<'_>,
1058) -> ResumeLivenessClassificationKind {
1059 match classification {
1060 ResumeLivenessClassificationRef::Retained(_) => ResumeLivenessClassificationKind::Retained,
1061 ResumeLivenessClassificationRef::Recomputable(_) => {
1062 ResumeLivenessClassificationKind::Recomputable
1063 }
1064 ResumeLivenessClassificationRef::Excluded(_) => ResumeLivenessClassificationKind::Excluded,
1065 ResumeLivenessClassificationRef::Blocked(_) => ResumeLivenessClassificationKind::Blocked,
1066 }
1067}
1068
1069fn classification_reason(
1070 classification: &ResumeLivenessClassificationRef<'_>,
1071) -> Option<ResumeRetentionReason> {
1072 match classification {
1073 ResumeLivenessClassificationRef::Retained(record) => Some(record.reason),
1074 ResumeLivenessClassificationRef::Recomputable(record) => Some(record.reason),
1075 ResumeLivenessClassificationRef::Excluded(record) => Some(record.reason),
1076 ResumeLivenessClassificationRef::Blocked(_) => None,
1077 }
1078}
1079
1080fn integrity(
1081 code: ResumeLivenessIntegrityCode,
1082 slot: Option<ResumeExistingSlot>,
1083 message: &str,
1084) -> ResumeLivenessIntegrityDiagnostic {
1085 ResumeLivenessIntegrityDiagnostic {
1086 code,
1087 slot,
1088 message: message.to_string(),
1089 }
1090}
1091
1092#[cfg(test)]
1093mod tests {
1094 use super::*;
1095
1096 #[test]
1097 fn classifies_every_exact_state_computed_context_and_form_slot_once() {
1098 let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
1099 "src/Liveness.tsx",
1100 r#"
1101@component("x-liveness-theme") class Theme extends Component {
1102 @context() color!: string;
1103 render() { return <span />; }
1104}
1105@component("x-liveness-child") class Child extends Component {
1106 count = state(1);
1107 @computed() get doubled() { return this.count * 2; }
1108 @consume(Theme.color) color!: string;
1109 render() { return <span>{this.count}</span>; }
1110}
1111@component("x-liveness-page") @route("/") class Page extends Component {
1112 theme = state("blue");
1113 @provide(Theme.color) providedTheme: string = this.theme;
1114 @form() profile!: Form;
1115 @field(this.profile) name = "";
1116 render() { return <><Child /><Child /><input field={this.name}/></>; }
1117}"#,
1118 ));
1119 let plan = build_resume_liveness_plan(&model);
1120 assert!(validate_resume_liveness_plan(&model, &plan).is_empty());
1121 assert_eq!(
1122 plan.slot_index.len(),
1123 plan.retained.len()
1124 + plan.recomputable.len()
1125 + plan.excluded.len()
1126 + plan.blocked.len()
1127 );
1128 assert_eq!(
1129 plan.slots_for_reason(ResumeRetentionReason::MutableState)
1130 .len(),
1131 3
1132 );
1133 assert_eq!(
1134 plan.slots_for_reason(ResumeRetentionReason::PureEagerComputedCache)
1135 .len(),
1136 2
1137 );
1138 let context_slots = plan.slots_for_reason(ResumeRetentionReason::ContextConsumerDependency);
1139 assert_eq!(context_slots.len(), 1);
1140 assert!(context_slots.iter().all(|slot| {
1141 matches!(slot.existing_slot, ResumeExistingSlot::Context(_))
1142 && slot.direct_dependencies.len() == 1
1143 && slot.transitive_dependencies.len() == 1
1144 }));
1145 assert!(plan
1146 .retained
1147 .iter()
1148 .filter(|record| record.reason == ResumeRetentionReason::FormFieldValue)
1149 .all(|record| matches!(
1150 record.slot.existing_slot,
1151 ResumeExistingSlot::FormFieldValue(_)
1152 )));
1153 assert_eq!(
1154 [
1155 ResumeRetentionReason::FormFieldValue,
1156 ResumeRetentionReason::FormFieldDirty,
1157 ResumeRetentionReason::FormFieldTouched,
1158 ResumeRetentionReason::FormRuleResult,
1159 ResumeRetentionReason::FormAggregateValidation,
1160 ResumeRetentionReason::StableFormSubmission,
1161 ]
1162 .into_iter()
1163 .map(|reason| plan.slots_for_reason(reason).len())
1164 .sum::<usize>(),
1165 6
1166 );
1167 }
1168
1169 #[test]
1170 fn computed_recomputation_carries_direct_and_transitive_exact_slot_evidence() {
1171 let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
1172 "src/ComputedLiveness.tsx",
1173 r#"
1174@component("x-computed-liveness") class Example {
1175 count = state(1);
1176 @computed() get doubled() { return this.count * 2; }
1177 @computed() get label() { return this.doubled + 1; }
1178 render() { return <main />; }
1179}"#,
1180 ));
1181 let plan = build_resume_liveness_plan(&model);
1182 let label = plan
1183 .recomputable
1184 .iter()
1185 .find(|record| {
1186 record
1187 .slot
1188 .existing_slot
1189 .text()
1190 .contains("computed%3Alabel")
1191 })
1192 .expect("label cache");
1193 assert_eq!(label.slot.direct_dependencies.len(), 1);
1194 assert_eq!(label.slot.transitive_dependencies.len(), 2);
1195 assert!(label.proof.pure && label.proof.deterministic);
1196 }
1197
1198 #[test]
1199 fn liveness_plan_is_deterministic_and_integrity_rejects_drift() {
1200 let first = presolve_parser::parse_file(
1201 "src/A.tsx",
1202 r#"@component("x-a") class A { value = state(1); render() { return <b>{this.value}</b>; } }"#,
1203 );
1204 let second = presolve_parser::parse_file(
1205 "src/B.tsx",
1206 r#"@component("x-b") class B { value = state(2); render() { return <i>{this.value}</i>; } }"#,
1207 );
1208 let forward = crate::build_application_semantic_model_for_unit(
1209 &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
1210 );
1211 let reverse = crate::build_application_semantic_model_for_unit(
1212 &crate::CompilationUnit::from_parsed_files(vec![second, first]),
1213 );
1214 assert_eq!(
1215 build_resume_liveness_plan(&forward),
1216 build_resume_liveness_plan(&reverse)
1217 );
1218 let mut drifted = build_resume_liveness_plan(&forward);
1219 drifted.slot_index.clear();
1220 assert!(validate_resume_liveness_plan(&forward, &drifted)
1221 .iter()
1222 .any(|diagnostic| {
1223 diagnostic.code == ResumeLivenessIntegrityCode::ProvenanceOrderIndexDrift
1224 }));
1225
1226 let mut invalid_policy = build_resume_liveness_plan(&forward);
1227 invalid_policy.retained[0].reason = ResumeRetentionReason::FormFieldValue;
1228 invalid_policy.retained[0].slot.boundary_candidate = None;
1229 let diagnostics = validate_resume_liveness_plan(&forward, &invalid_policy);
1230 assert!(diagnostics.iter().any(|diagnostic| {
1231 diagnostic.code == ResumeLivenessIntegrityCode::InvalidRetentionReason
1232 }));
1233 assert!(diagnostics.iter().any(|diagnostic| {
1234 diagnostic.code == ResumeLivenessIntegrityCode::InvalidCandidatePromotion
1235 }));
1236 }
1237
1238 #[test]
1239 fn reserves_the_complete_j2_integrity_range() {
1240 assert_eq!(
1241 [
1242 ResumeLivenessIntegrityCode::DuplicateClassification,
1243 ResumeLivenessIntegrityCode::MissingStorageOwner,
1244 ResumeLivenessIntegrityCode::UnknownDependency,
1245 ResumeLivenessIntegrityCode::InvalidRetentionReason,
1246 ResumeLivenessIntegrityCode::RecomputeWithoutProof,
1247 ResumeLivenessIntegrityCode::RequiredUnsupportedValue,
1248 ResumeLivenessIntegrityCode::InvalidCandidatePromotion,
1249 ResumeLivenessIntegrityCode::ProvenanceOrderIndexDrift,
1250 ]
1251 .map(ResumeLivenessIntegrityCode::code),
1252 [
1253 "PSASM1320",
1254 "PSASM1321",
1255 "PSASM1322",
1256 "PSASM1323",
1257 "PSASM1324",
1258 "PSASM1325",
1259 "PSASM1326",
1260 "PSASM1327",
1261 ]
1262 );
1263 }
1264}