Skip to main content

presolve_compiler/
resume_liveness.rs

1//! J2 canonical resumability liveness over existing compiler-owned runtime slots.
2
3use 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}