Skip to main content

presolve_compiler/
resume_restore.rs

1//! J8 closed restore programs and the fixed application R0-R20 schedule.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6    build_computed_instance_slot_registry, build_ordinary_template_instance_registry,
7    build_resume_boundary_graph, build_resume_liveness_plan, build_resume_schema_registry,
8    build_runtime_component_registry, build_state_instance_storage_registry,
9    lower_components_to_ir, ApplicationSemanticModel, ComponentInstanceId,
10    ComponentStructuralRegionId, ConsumerInstanceId, ContextSourceInstanceId, FieldId,
11    FormInstanceId, InstanceContextValueSlotId, OrdinaryTemplateBindingKind, ProviderInstanceId,
12    ResumeAnchorId, ResumeBoundaryId, ResumeBoundaryOwner, ResumeExistingSlot,
13    ResumeLivenessClassificationRef, ResumeRestoreProgramId, ResumeSlotId, ResumeValueCodec,
14    SemanticId, SourceProvenance, TemplateInstanceBindingId,
15};
16
17pub const RESUME_RESTORE_PLAN_VERSION: u32 = 1;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
20pub enum ResumeRestorePhase {
21    R0ValidateAllArtifactsAndSnapshot,
22    R1AllocateEmptyClosedRuntimeRegistries,
23    R2AllocateBoundaryRuntimeRecords,
24    R3RestoreMutableStateAndResources,
25    R4RestoreRetainedComputedCaches,
26    R5RecomputeComputed,
27    R6RestoreContextProviderValues,
28    R7BindContextConsumers,
29    R8RestoreComponentRuntimeAndSlots,
30    R9RestoreStructuralSelection,
31    R10VerifyDomAnchors,
32    R11RestoreFormFieldValues,
33    R12RestoreFormDirtyAndTouched,
34    R13RestoreFormValidation,
35    R14RestoreStableFormSubmission,
36    R15SynchronizeFormControls,
37    R16InstallDomBindings,
38    R17InstallEffectSubscriptions,
39    R18InstallEventDelegationAndActivation,
40    R19MarkBoundariesReady,
41    R20PublishApplicationReady,
42}
43
44impl ResumeRestorePhase {
45    pub const ALL: [Self; 21] = [
46        Self::R0ValidateAllArtifactsAndSnapshot,
47        Self::R1AllocateEmptyClosedRuntimeRegistries,
48        Self::R2AllocateBoundaryRuntimeRecords,
49        Self::R3RestoreMutableStateAndResources,
50        Self::R4RestoreRetainedComputedCaches,
51        Self::R5RecomputeComputed,
52        Self::R6RestoreContextProviderValues,
53        Self::R7BindContextConsumers,
54        Self::R8RestoreComponentRuntimeAndSlots,
55        Self::R9RestoreStructuralSelection,
56        Self::R10VerifyDomAnchors,
57        Self::R11RestoreFormFieldValues,
58        Self::R12RestoreFormDirtyAndTouched,
59        Self::R13RestoreFormValidation,
60        Self::R14RestoreStableFormSubmission,
61        Self::R15SynchronizeFormControls,
62        Self::R16InstallDomBindings,
63        Self::R17InstallEffectSubscriptions,
64        Self::R18InstallEventDelegationAndActivation,
65        Self::R19MarkBoundariesReady,
66        Self::R20PublishApplicationReady,
67    ];
68}
69
70#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
71pub enum ResumeRestoreInstruction {
72    AllocateBoundaryRuntimeRecord,
73    DecodeValue {
74        slot_id: ResumeSlotId,
75        codec: ResumeValueCodec,
76    },
77    WriteSlot {
78        slot_id: ResumeSlotId,
79    },
80    RestoreStructuralSelection {
81        region_id: crate::ComponentStructuralRegionId,
82        slot_id: ResumeSlotId,
83    },
84    RestoreFormSlot {
85        form_instance_id: FormInstanceId,
86        field_id: Option<FieldId>,
87        slot_id: ResumeSlotId,
88    },
89    RecomputeComputed {
90        component_instance_id: ComponentInstanceId,
91        computed_id: SemanticId,
92    },
93    BindContextConsumer {
94        consumer_instance_id: ConsumerInstanceId,
95        selected_source: ContextSourceInstanceId,
96        provider_instance_id: Option<ProviderInstanceId>,
97        value_slot_id: InstanceContextValueSlotId,
98    },
99    InstallDomBinding {
100        binding_id: TemplateInstanceBindingId,
101        anchor_id: ResumeAnchorId,
102    },
103    InstallEffectSubscription {
104        effect_id: SemanticId,
105    },
106    MarkBoundaryRestored,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct ResumeRestoreInstructionRecord {
111    pub phase: ResumeRestorePhase,
112    pub instruction: ResumeRestoreInstruction,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct ResumeRestoreSlotAssignment {
117    pub slot: ResumeExistingSlot,
118    pub slot_id: ResumeSlotId,
119    pub phase: ResumeRestorePhase,
120    pub retained: bool,
121    pub recomputable: bool,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct ResumeRestoreProgram {
126    pub program_id: ResumeRestoreProgramId,
127    pub boundary_id: ResumeBoundaryId,
128    pub slot_assignments: Vec<ResumeRestoreSlotAssignment>,
129    pub instructions: Vec<ResumeRestoreInstructionRecord>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct ResumeRestoreSchedulePhase {
134    pub phase: ResumeRestorePhase,
135    pub programs: Vec<ResumeRestoreProgramId>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ResumeRestoreApplicationSchedule {
140    pub phases: Vec<ResumeRestoreSchedulePhase>,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
144pub enum ResumeRestoreBlockReason {
145    MissingSlotSchema,
146    UnsupportedRecomputableSlot,
147    MissingComputedProgram,
148    MissingFormSlotOwner,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct ResumeRestoreBlock {
153    pub boundary: Option<ResumeBoundaryId>,
154    pub slot: ResumeExistingSlot,
155    pub reason: ResumeRestoreBlockReason,
156    pub provenance: SourceProvenance,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct ResumeRestorePlan {
161    pub version: u32,
162    pub programs: Vec<ResumeRestoreProgram>,
163    pub schedule: ResumeRestoreApplicationSchedule,
164    pub blocks: Vec<ResumeRestoreBlock>,
165    pub program_index: BTreeMap<ResumeBoundaryId, usize>,
166}
167
168impl ResumeRestorePlan {
169    #[must_use]
170    pub fn program(&self, boundary: &ResumeBoundaryId) -> Option<&ResumeRestoreProgram> {
171        self.program_index
172            .get(boundary)
173            .and_then(|index| self.programs.get(*index))
174    }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
178pub enum ResumeRestoreIntegrityCode {
179    ProgramReference,
180    PhaseOrDuplicateWrite,
181    MissingCompletion,
182    OrderingOrOutputDrift,
183}
184
185impl ResumeRestoreIntegrityCode {
186    #[must_use]
187    pub const fn code(self) -> &'static str {
188        match self {
189            Self::ProgramReference => "PSASM1359",
190            Self::PhaseOrDuplicateWrite => "PSASM1360",
191            Self::MissingCompletion => "PSASM1361",
192            Self::OrderingOrOutputDrift => "PSASM1362",
193        }
194    }
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct ResumeRestoreIntegrityDiagnostic {
199    pub code: ResumeRestoreIntegrityCode,
200    pub boundary: Option<ResumeBoundaryId>,
201    pub message: String,
202}
203
204#[derive(Debug, Clone)]
205struct RestoreAuthorities {
206    computed: BTreeMap<ResumeExistingSlot, (ComponentInstanceId, SemanticId)>,
207    form: BTreeMap<ResumeExistingSlot, (FormInstanceId, Option<FieldId>)>,
208    context_bindings:
209        BTreeMap<ResumeExistingSlot, Vec<crate::RuntimeComponentContextBindingRecord>>,
210    structural: BTreeMap<ComponentStructuralRegionId, ResumeExistingSlot>,
211}
212
213impl RestoreAuthorities {
214    fn new(model: &ApplicationSemanticModel) -> Self {
215        let ir = lower_components_to_ir(model);
216        let mut computed = BTreeMap::new();
217        for record in build_computed_instance_slot_registry(model, &ir).records {
218            let value = (
219                record.component_instance_id.clone(),
220                record.computed_id.clone(),
221            );
222            computed.insert(
223                ResumeExistingSlot::ComputedCache(record.cache_slot_id),
224                value.clone(),
225            );
226            computed.insert(
227                ResumeExistingSlot::ComputedDirty(record.dirty_slot_id),
228                value,
229            );
230        }
231        let mut form = BTreeMap::new();
232        for instance in model.optimized_form_ir.optimized.instances.values() {
233            for (field, slot) in &instance.storage.value {
234                form.insert(
235                    ResumeExistingSlot::FormFieldValue(slot.clone()),
236                    (instance.id.clone(), Some(field.clone())),
237                );
238            }
239            for (field, slot) in &instance.storage.dirty {
240                form.insert(
241                    ResumeExistingSlot::FormFieldDirty(slot.clone()),
242                    (instance.id.clone(), Some(field.clone())),
243                );
244            }
245            for (field, slot) in &instance.storage.touched {
246                form.insert(
247                    ResumeExistingSlot::FormFieldTouched(slot.clone()),
248                    (instance.id.clone(), Some(field.clone())),
249                );
250            }
251            for (field, slot) in &instance.storage.validation {
252                form.insert(
253                    ResumeExistingSlot::FormFieldValidation(slot.clone()),
254                    (instance.id.clone(), Some(field.clone())),
255                );
256            }
257            form.insert(
258                ResumeExistingSlot::FormValidationAggregate(instance.storage.aggregate.clone()),
259                (instance.id.clone(), None),
260            );
261            form.insert(
262                ResumeExistingSlot::FormSubmission(instance.storage.submission.clone()),
263                (instance.id.clone(), None),
264            );
265        }
266        let mut context_bindings =
267            BTreeMap::<ResumeExistingSlot, Vec<crate::RuntimeComponentContextBindingRecord>>::new();
268        for binding in build_runtime_component_registry(model, &model.component_ir_optimization)
269            .instance_context_bindings
270        {
271            context_bindings
272                .entry(ResumeExistingSlot::Context(binding.runtime_slot.clone()))
273                .or_default()
274                .push(binding);
275        }
276        for bindings in context_bindings.values_mut() {
277            bindings.sort_by(|left, right| left.consumer_instance.cmp(&right.consumer_instance));
278        }
279        let state_slots = build_state_instance_storage_registry(model, &ir);
280        let structural = build_ordinary_template_instance_registry(model)
281            .bindings
282            .iter()
283            .filter_map(|binding| {
284                let region_kind = match binding.binding_kind {
285                    OrdinaryTemplateBindingKind::Conditional => "conditional",
286                    OrdinaryTemplateBindingKind::List => "keyed-list",
287                    _ => return None,
288                };
289                let [storage] = binding.state_storage_ids.as_slice() else {
290                    return None;
291                };
292                let state_slot = state_slots.records.iter().find(|record| {
293                    record.component_instance_id == binding.component_instance_id
294                        && record.storage_id == *storage
295                })?;
296                Some((
297                    ComponentStructuralRegionId::for_template_entity(
298                        &binding.declaration_binding_id,
299                        region_kind,
300                    ),
301                    ResumeExistingSlot::State(state_slot.slot_id.clone()),
302                ))
303            })
304            .collect();
305        Self {
306            computed,
307            form,
308            context_bindings,
309            structural,
310        }
311    }
312}
313
314#[must_use]
315#[allow(clippy::too_many_lines)]
316pub fn build_resume_restore_plan(model: &ApplicationSemanticModel) -> ResumeRestorePlan {
317    let graph = build_resume_boundary_graph(model);
318    let liveness = build_resume_liveness_plan(model);
319    let schemas = build_resume_schema_registry(model);
320    let authorities = RestoreAuthorities::new(model);
321    let mut programs = Vec::new();
322    let mut blocks = Vec::new();
323
324    for boundary in &graph.boundaries {
325        let mut by_phase = BTreeMap::<ResumeRestorePhase, Vec<ResumeRestoreInstruction>>::new();
326        by_phase
327            .entry(ResumeRestorePhase::R2AllocateBoundaryRuntimeRecords)
328            .or_default()
329            .push(ResumeRestoreInstruction::AllocateBoundaryRuntimeRecord);
330        let mut slot_assignments = Vec::new();
331        if let Some(schema) = schemas.schema(&boundary.id) {
332            for slot_schema in &schema.slots {
333                let Some(classification) = liveness.classification(&slot_schema.existing_slot)
334                else {
335                    blocks.push(ResumeRestoreBlock {
336                        boundary: Some(boundary.id.clone()),
337                        slot: slot_schema.existing_slot.clone(),
338                        reason: ResumeRestoreBlockReason::MissingSlotSchema,
339                        provenance: slot_schema.provenance.clone(),
340                    });
341                    continue;
342                };
343                match classification {
344                    ResumeLivenessClassificationRef::Retained(_) => {
345                        let phase = restore_phase(&slot_schema.existing_slot);
346                        slot_assignments.push(ResumeRestoreSlotAssignment {
347                            slot: slot_schema.existing_slot.clone(),
348                            slot_id: slot_schema.resume_slot_id.clone(),
349                            phase,
350                            retained: true,
351                            recomputable: false,
352                        });
353                        let instructions = by_phase.entry(phase).or_default();
354                        instructions.push(ResumeRestoreInstruction::DecodeValue {
355                            slot_id: slot_schema.resume_slot_id.clone(),
356                            codec: slot_schema.codec.clone(),
357                        });
358                        if is_form_slot(&slot_schema.existing_slot) {
359                            if let Some((form_instance_id, field_id)) =
360                                authorities.form.get(&slot_schema.existing_slot)
361                            {
362                                instructions.push(ResumeRestoreInstruction::RestoreFormSlot {
363                                    form_instance_id: form_instance_id.clone(),
364                                    field_id: field_id.clone(),
365                                    slot_id: slot_schema.resume_slot_id.clone(),
366                                });
367                            } else {
368                                blocks.push(ResumeRestoreBlock {
369                                    boundary: Some(boundary.id.clone()),
370                                    slot: slot_schema.existing_slot.clone(),
371                                    reason: ResumeRestoreBlockReason::MissingFormSlotOwner,
372                                    provenance: slot_schema.provenance.clone(),
373                                });
374                            }
375                        } else {
376                            instructions.push(ResumeRestoreInstruction::WriteSlot {
377                                slot_id: slot_schema.resume_slot_id.clone(),
378                            });
379                        }
380                    }
381                    ResumeLivenessClassificationRef::Recomputable(_) => {
382                        slot_assignments.push(ResumeRestoreSlotAssignment {
383                            slot: slot_schema.existing_slot.clone(),
384                            slot_id: slot_schema.resume_slot_id.clone(),
385                            phase: ResumeRestorePhase::R5RecomputeComputed,
386                            retained: false,
387                            recomputable: true,
388                        });
389                        if matches!(
390                            slot_schema.existing_slot,
391                            ResumeExistingSlot::ComputedCache(_)
392                        ) {
393                            if let Some((component_instance_id, computed_id)) =
394                                authorities.computed.get(&slot_schema.existing_slot)
395                            {
396                                by_phase
397                                    .entry(ResumeRestorePhase::R5RecomputeComputed)
398                                    .or_default()
399                                    .push(ResumeRestoreInstruction::RecomputeComputed {
400                                        component_instance_id: component_instance_id.clone(),
401                                        computed_id: computed_id.clone(),
402                                    });
403                            } else {
404                                blocks.push(ResumeRestoreBlock {
405                                    boundary: Some(boundary.id.clone()),
406                                    slot: slot_schema.existing_slot.clone(),
407                                    reason: ResumeRestoreBlockReason::MissingComputedProgram,
408                                    provenance: slot_schema.provenance.clone(),
409                                });
410                            }
411                        } else if !matches!(
412                            slot_schema.existing_slot,
413                            ResumeExistingSlot::ComputedDirty(_)
414                        ) {
415                            blocks.push(ResumeRestoreBlock {
416                                boundary: Some(boundary.id.clone()),
417                                slot: slot_schema.existing_slot.clone(),
418                                reason: ResumeRestoreBlockReason::UnsupportedRecomputableSlot,
419                                provenance: slot_schema.provenance.clone(),
420                            });
421                        }
422                    }
423                    ResumeLivenessClassificationRef::Excluded(_)
424                    | ResumeLivenessClassificationRef::Blocked(_) => {
425                        blocks.push(ResumeRestoreBlock {
426                            boundary: Some(boundary.id.clone()),
427                            slot: slot_schema.existing_slot.clone(),
428                            reason: ResumeRestoreBlockReason::MissingSlotSchema,
429                            provenance: slot_schema.provenance.clone(),
430                        });
431                    }
432                }
433            }
434        }
435        for slot in &boundary.owned_slots {
436            if let Some(bindings) = authorities.context_bindings.get(slot) {
437                for binding in bindings {
438                    by_phase
439                        .entry(ResumeRestorePhase::R7BindContextConsumers)
440                        .or_default()
441                        .push(ResumeRestoreInstruction::BindContextConsumer {
442                            consumer_instance_id: binding.consumer_instance.clone(),
443                            selected_source: binding.selected_source.clone(),
444                            provider_instance_id: binding.provider_source.clone(),
445                            value_slot_id: binding.runtime_slot.clone(),
446                        });
447                }
448            }
449        }
450        if let ResumeBoundaryOwner::StructuralRegion { region, .. } = &boundary.owner {
451            if let Some(slot) = authorities.structural.get(region) {
452                by_phase
453                    .entry(ResumeRestorePhase::R9RestoreStructuralSelection)
454                    .or_default()
455                    .push(ResumeRestoreInstruction::RestoreStructuralSelection {
456                        region_id: region.clone(),
457                        slot_id: slot.resume_slot_id(),
458                    });
459            }
460        }
461        by_phase
462            .entry(ResumeRestorePhase::R19MarkBoundariesReady)
463            .or_default()
464            .push(ResumeRestoreInstruction::MarkBoundaryRestored);
465        slot_assignments.sort_by(|left, right| left.slot_id.cmp(&right.slot_id));
466        let instructions = ResumeRestorePhase::ALL
467            .iter()
468            .flat_map(|phase| {
469                by_phase
470                    .remove(phase)
471                    .unwrap_or_default()
472                    .into_iter()
473                    .map(|instruction| ResumeRestoreInstructionRecord {
474                        phase: *phase,
475                        instruction,
476                    })
477            })
478            .collect();
479        programs.push(ResumeRestoreProgram {
480            program_id: ResumeRestoreProgramId::for_boundary(&boundary.id),
481            boundary_id: boundary.id.clone(),
482            slot_assignments,
483            instructions,
484        });
485    }
486
487    blocks.sort_by(|left, right| {
488        (&left.boundary, &left.slot, left.reason).cmp(&(&right.boundary, &right.slot, right.reason))
489    });
490    let schedule = ResumeRestoreApplicationSchedule {
491        phases: ResumeRestorePhase::ALL
492            .iter()
493            .map(|phase| ResumeRestoreSchedulePhase {
494                phase: *phase,
495                programs: programs
496                    .iter()
497                    .filter(|program| {
498                        program
499                            .instructions
500                            .iter()
501                            .any(|instruction| instruction.phase == *phase)
502                    })
503                    .map(|program| program.program_id.clone())
504                    .collect(),
505            })
506            .collect(),
507    };
508    let program_index = programs
509        .iter()
510        .enumerate()
511        .map(|(index, program)| (program.boundary_id.clone(), index))
512        .collect();
513    ResumeRestorePlan {
514        version: RESUME_RESTORE_PLAN_VERSION,
515        programs,
516        schedule,
517        blocks,
518        program_index,
519    }
520}
521
522fn restore_phase(slot: &ResumeExistingSlot) -> ResumeRestorePhase {
523    match slot {
524        ResumeExistingSlot::State(_)
525        | ResumeExistingSlot::ResourceState(_)
526        | ResumeExistingSlot::ResourceData(_)
527        | ResumeExistingSlot::ResourceError(_) => {
528            ResumeRestorePhase::R3RestoreMutableStateAndResources
529        }
530        ResumeExistingSlot::ComputedCache(_) | ResumeExistingSlot::ComputedDirty(_) => {
531            ResumeRestorePhase::R4RestoreRetainedComputedCaches
532        }
533        ResumeExistingSlot::Context(_) => ResumeRestorePhase::R6RestoreContextProviderValues,
534        ResumeExistingSlot::FormFieldValue(_) => ResumeRestorePhase::R11RestoreFormFieldValues,
535        ResumeExistingSlot::FormFieldDirty(_) | ResumeExistingSlot::FormFieldTouched(_) => {
536            ResumeRestorePhase::R12RestoreFormDirtyAndTouched
537        }
538        ResumeExistingSlot::FormFieldValidation(_)
539        | ResumeExistingSlot::FormValidationAggregate(_) => {
540            ResumeRestorePhase::R13RestoreFormValidation
541        }
542        ResumeExistingSlot::FormSubmission(_) => ResumeRestorePhase::R14RestoreStableFormSubmission,
543        ResumeExistingSlot::EffectActivation(_) => {
544            ResumeRestorePhase::R17InstallEffectSubscriptions
545        }
546    }
547}
548
549fn is_form_slot(slot: &ResumeExistingSlot) -> bool {
550    matches!(
551        slot,
552        ResumeExistingSlot::FormFieldValue(_)
553            | ResumeExistingSlot::FormFieldDirty(_)
554            | ResumeExistingSlot::FormFieldTouched(_)
555            | ResumeExistingSlot::FormFieldValidation(_)
556            | ResumeExistingSlot::FormValidationAggregate(_)
557            | ResumeExistingSlot::FormSubmission(_)
558    )
559}
560
561/// # Errors
562///
563/// Returns J8 integrity diagnostics for malformed program references, wrong
564/// phases, duplicate writes, missing completion, parent-order violations, or
565/// deterministic construction drift.
566#[allow(clippy::too_many_lines)]
567pub fn validate_resume_restore_plan(
568    model: &ApplicationSemanticModel,
569    plan: &ResumeRestorePlan,
570) -> Result<(), Vec<ResumeRestoreIntegrityDiagnostic>> {
571    let mut diagnostics = Vec::new();
572    let graph = build_resume_boundary_graph(model);
573    let expected_boundaries = graph
574        .boundaries
575        .iter()
576        .map(|boundary| boundary.id.clone())
577        .collect::<Vec<_>>();
578    let actual_boundaries = plan
579        .programs
580        .iter()
581        .map(|program| program.boundary_id.clone())
582        .collect::<Vec<_>>();
583    if expected_boundaries != actual_boundaries
584        || plan.schedule.phases.len() != ResumeRestorePhase::ALL.len()
585        || plan
586            .schedule
587            .phases
588            .iter()
589            .map(|phase| phase.phase)
590            .collect::<Vec<_>>()
591            != ResumeRestorePhase::ALL
592    {
593        diagnostics.push(restore_diagnostic(
594            ResumeRestoreIntegrityCode::ProgramReference,
595            None,
596            "restore programs or fixed phase schedule do not correspond",
597        ));
598    }
599    let known_programs = plan
600        .programs
601        .iter()
602        .map(|program| program.program_id.clone())
603        .collect::<BTreeSet<_>>();
604    if plan
605        .schedule
606        .phases
607        .iter()
608        .flat_map(|phase| &phase.programs)
609        .any(|program| !known_programs.contains(program))
610    {
611        diagnostics.push(restore_diagnostic(
612            ResumeRestoreIntegrityCode::ProgramReference,
613            None,
614            "restore schedule references an unknown program",
615        ));
616    }
617    for program in &plan.programs {
618        let mut writes = BTreeSet::new();
619        let mut previous_phase = None;
620        for record in &program.instructions {
621            if previous_phase.is_some_and(|phase| phase > record.phase) {
622                diagnostics.push(restore_diagnostic(
623                    ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
624                    Some(program.boundary_id.clone()),
625                    "restore instruction phases are out of fixed order",
626                ));
627            }
628            previous_phase = Some(record.phase);
629            if !instruction_phase_is_valid(program, record) {
630                diagnostics.push(restore_diagnostic(
631                    ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
632                    Some(program.boundary_id.clone()),
633                    "restore instruction is assigned to the wrong fixed phase",
634                ));
635            }
636            if let ResumeRestoreInstruction::WriteSlot { slot_id }
637            | ResumeRestoreInstruction::RestoreFormSlot { slot_id, .. } = &record.instruction
638            {
639                if !writes.insert(slot_id.clone()) {
640                    diagnostics.push(restore_diagnostic(
641                        ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
642                        Some(program.boundary_id.clone()),
643                        "restore program writes one slot more than once",
644                    ));
645                }
646            }
647        }
648        if !matches!(
649            program.instructions.last(),
650            Some(ResumeRestoreInstructionRecord {
651                phase: ResumeRestorePhase::R19MarkBoundariesReady,
652                instruction: ResumeRestoreInstruction::MarkBoundaryRestored,
653            })
654        ) {
655            diagnostics.push(restore_diagnostic(
656                ResumeRestoreIntegrityCode::MissingCompletion,
657                Some(program.boundary_id.clone()),
658                "restore program lacks its final MarkBoundaryRestored",
659            ));
660        }
661    }
662    if plan.version != RESUME_RESTORE_PLAN_VERSION || plan != &build_resume_restore_plan(model) {
663        diagnostics.push(restore_diagnostic(
664            ResumeRestoreIntegrityCode::OrderingOrOutputDrift,
665            None,
666            "restore plan drifted from canonical J2/J3/J6 construction",
667        ));
668    }
669    if diagnostics.is_empty() {
670        Ok(())
671    } else {
672        Err(diagnostics)
673    }
674}
675
676fn instruction_phase_is_valid(
677    program: &ResumeRestoreProgram,
678    record: &ResumeRestoreInstructionRecord,
679) -> bool {
680    match &record.instruction {
681        ResumeRestoreInstruction::AllocateBoundaryRuntimeRecord => {
682            record.phase == ResumeRestorePhase::R2AllocateBoundaryRuntimeRecords
683        }
684        ResumeRestoreInstruction::DecodeValue { slot_id, .. }
685        | ResumeRestoreInstruction::WriteSlot { slot_id }
686        | ResumeRestoreInstruction::RestoreFormSlot { slot_id, .. } => program
687            .slot_assignments
688            .iter()
689            .find(|assignment| &assignment.slot_id == slot_id)
690            .is_some_and(|assignment| assignment.retained && assignment.phase == record.phase),
691        ResumeRestoreInstruction::RestoreStructuralSelection { .. } => {
692            record.phase == ResumeRestorePhase::R9RestoreStructuralSelection
693        }
694        ResumeRestoreInstruction::RecomputeComputed { .. } => {
695            record.phase == ResumeRestorePhase::R5RecomputeComputed
696        }
697        ResumeRestoreInstruction::BindContextConsumer { .. } => {
698            record.phase == ResumeRestorePhase::R7BindContextConsumers
699        }
700        ResumeRestoreInstruction::InstallDomBinding { .. } => {
701            record.phase == ResumeRestorePhase::R16InstallDomBindings
702        }
703        ResumeRestoreInstruction::InstallEffectSubscription { .. } => {
704            record.phase == ResumeRestorePhase::R17InstallEffectSubscriptions
705        }
706        ResumeRestoreInstruction::MarkBoundaryRestored => {
707            record.phase == ResumeRestorePhase::R19MarkBoundariesReady
708        }
709    }
710}
711
712fn restore_diagnostic(
713    code: ResumeRestoreIntegrityCode,
714    boundary: Option<ResumeBoundaryId>,
715    message: &str,
716) -> ResumeRestoreIntegrityDiagnostic {
717    ResumeRestoreIntegrityDiagnostic {
718        code,
719        boundary,
720        message: message.to_string(),
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    fn model(source: &str) -> ApplicationSemanticModel {
729        crate::build_application_semantic_model(&presolve_parser::parse_file(
730            "src/ResumeRestore.tsx",
731            source,
732        ))
733    }
734
735    #[test]
736    fn builds_all_fixed_phases_and_assigns_retained_and_recomputable_slots() {
737        let model = model(
738            r#"@component("x-counter") class Counter {
739  count = state(1);
740  @computed() get doubled() { return this.count * 2; }
741  render() { return <button>{this.doubled}</button>; }
742}"#,
743        );
744        let plan = build_resume_restore_plan(&model);
745        let liveness = build_resume_liveness_plan(&model);
746        assert_eq!(
747            plan.schedule
748                .phases
749                .iter()
750                .map(|phase| phase.phase)
751                .collect::<Vec<_>>(),
752            ResumeRestorePhase::ALL
753        );
754        let assignments = plan
755            .programs
756            .iter()
757            .flat_map(|program| &program.slot_assignments)
758            .map(|assignment| assignment.slot.clone())
759            .collect::<BTreeSet<_>>();
760        assert!(liveness
761            .retained
762            .iter()
763            .all(|record| assignments.contains(&record.slot.existing_slot)));
764        assert!(liveness
765            .recomputable
766            .iter()
767            .all(|record| assignments.contains(&record.slot.existing_slot)));
768        assert!(plan.programs.iter().all(|program| matches!(
769            program.instructions.last(),
770            Some(ResumeRestoreInstructionRecord {
771                phase: ResumeRestorePhase::R19MarkBoundariesReady,
772                instruction: ResumeRestoreInstruction::MarkBoundaryRestored,
773            })
774        )));
775        assert_eq!(validate_resume_restore_plan(&model, &plan), Ok(()));
776    }
777
778    #[test]
779    fn restores_form_slot_classes_in_their_exact_fixed_phases() {
780        let model = model(
781            r#"@component("x-profile") class Profile {
782  @form() profile!: Form;
783  @field(this.profile) name = "";
784  render() { return <input field={this.name} />; }
785}"#,
786        );
787        let plan = build_resume_restore_plan(&model);
788        let assignments = plan
789            .programs
790            .iter()
791            .flat_map(|program| &program.slot_assignments)
792            .map(|assignment| (&assignment.slot, assignment.phase))
793            .collect::<BTreeMap<_, _>>();
794        assert!(assignments.iter().any(|(slot, phase)| matches!(
795            (slot, phase),
796            (
797                ResumeExistingSlot::FormFieldValue(_),
798                ResumeRestorePhase::R11RestoreFormFieldValues
799            )
800        )));
801        assert!(assignments.iter().any(|(slot, phase)| matches!(
802            (slot, phase),
803            (
804                ResumeExistingSlot::FormFieldDirty(_) | ResumeExistingSlot::FormFieldTouched(_),
805                ResumeRestorePhase::R12RestoreFormDirtyAndTouched
806            )
807        )));
808        assert!(assignments.iter().any(|(slot, phase)| matches!(
809            (slot, phase),
810            (
811                ResumeExistingSlot::FormFieldValidation(_)
812                    | ResumeExistingSlot::FormValidationAggregate(_),
813                ResumeRestorePhase::R13RestoreFormValidation
814            )
815        )));
816        assert!(assignments.iter().any(|(slot, phase)| matches!(
817            (slot, phase),
818            (
819                ResumeExistingSlot::FormSubmission(_),
820                ResumeRestorePhase::R14RestoreStableFormSubmission
821            )
822        )));
823    }
824
825    #[test]
826    fn structural_regions_restore_from_their_exact_retained_state_slots() {
827        let model = model(
828            r#"@component("x-leaf") class Leaf extends Component { render() { return <span />; } }
829@component("x-page") @route("/") class Page extends Component {
830  visible = state(true);
831  items = state([{ id: "a" }, { id: "b" }]);
832  render() {
833    return <main>{this.visible ? <div><Leaf /></div> : <aside>Hidden</aside>}<ul>{this.items.map(item => <li key={item.id}><Leaf /></li>)}</ul></main>;
834  }
835  hide() { this.visible = false; }
836  trim() { this.items = [{ id: "b" }]; }
837}"#,
838        );
839        let plan = build_resume_restore_plan(&model);
840        let structural = plan
841            .programs
842            .iter()
843            .flat_map(|program| &program.instructions)
844            .filter_map(|record| match &record.instruction {
845                ResumeRestoreInstruction::RestoreStructuralSelection { region_id, slot_id } => {
846                    Some((record.phase, region_id.to_string(), slot_id.to_string()))
847                }
848                _ => None,
849            })
850            .collect::<Vec<_>>();
851        assert_eq!(structural.len(), 2);
852        assert!(structural
853            .iter()
854            .all(|(phase, _, _)| { *phase == ResumeRestorePhase::R9RestoreStructuralSelection }));
855        assert!(structural
856            .iter()
857            .any(|(_, region, slot)| region.ends_with(":conditional")
858                && slot.contains("state%3Avisible")));
859        assert!(structural
860            .iter()
861            .any(|(_, region, slot)| region.ends_with(":keyed-list")
862                && slot.contains("state%3Aitems")));
863        assert_eq!(validate_resume_restore_plan(&model, &plan), Ok(()));
864    }
865
866    #[test]
867    fn malformed_reference_phase_write_completion_and_parent_order_are_rejected() {
868        let model = model(
869            r#"@component("x-child") class Child { value = state(1); render() { return <span>{this.value}</span>; } }
870@component("x-parent") @route("/") class Parent { render() { return <Child />; } }"#,
871        );
872        let canonical = build_resume_restore_plan(&model);
873
874        let mut dangling = canonical.clone();
875        dangling.programs.pop();
876        let diagnostics =
877            validate_resume_restore_plan(&model, &dangling).expect_err("dangling reference");
878        assert!(diagnostics
879            .iter()
880            .any(|diagnostic| { diagnostic.code == ResumeRestoreIntegrityCode::ProgramReference }));
881
882        let mut missing = canonical.clone();
883        missing.programs[0].instructions.pop();
884        let diagnostics = validate_resume_restore_plan(&model, &missing).expect_err("missing mark");
885        assert!(diagnostics.iter().any(|diagnostic| {
886            diagnostic.code == ResumeRestoreIntegrityCode::MissingCompletion
887        }));
888
889        let mut wrong_phase = canonical.clone();
890        wrong_phase.programs[0].instructions[0].phase = ResumeRestorePhase::R19MarkBoundariesReady;
891        let diagnostics =
892            validate_resume_restore_plan(&model, &wrong_phase).expect_err("wrong phase");
893        assert!(diagnostics.iter().any(|diagnostic| {
894            diagnostic.code == ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite
895                || diagnostic.code == ResumeRestoreIntegrityCode::OrderingOrOutputDrift
896        }));
897
898        let mut duplicate = canonical.clone();
899        let duplicate_write = duplicate
900            .programs
901            .iter()
902            .enumerate()
903            .find_map(|(index, program)| {
904                program
905                    .instructions
906                    .iter()
907                    .find(|record| {
908                        matches!(
909                            record.instruction,
910                            ResumeRestoreInstruction::WriteSlot { .. }
911                        )
912                    })
913                    .cloned()
914                    .map(|record| (index, record))
915            });
916        if let Some((program_index, write)) = duplicate_write {
917            duplicate.programs[program_index]
918                .instructions
919                .insert(1, write);
920        }
921        let diagnostics =
922            validate_resume_restore_plan(&model, &duplicate).expect_err("duplicate write");
923        assert!(diagnostics.iter().any(|diagnostic| {
924            diagnostic.code == ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite
925                || diagnostic.code == ResumeRestoreIntegrityCode::OrderingOrOutputDrift
926        }));
927
928        let mut reversed = canonical;
929        reversed.programs.reverse();
930        let diagnostics =
931            validate_resume_restore_plan(&model, &reversed).expect_err("parent order");
932        assert!(diagnostics
933            .iter()
934            .any(|diagnostic| { diagnostic.code == ResumeRestoreIntegrityCode::ProgramReference }));
935    }
936
937    #[test]
938    fn construction_is_deterministic_under_source_reversal() {
939        let first = presolve_parser::parse_file(
940            "src/A.tsx",
941            r#"@component("x-a") @route("/a") class A { value = state(1); render() { return <button>{this.value}</button>; } }"#,
942        );
943        let second = presolve_parser::parse_file(
944            "src/B.tsx",
945            r#"@component("x-b") @route("/b") class B { enabled = state(true); render() { return <button>{this.enabled}</button>; } }"#,
946        );
947        let forward = crate::build_application_semantic_model_for_unit(
948            &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
949        );
950        let reverse = crate::build_application_semantic_model_for_unit(
951            &crate::CompilationUnit::from_parsed_files(vec![second, first]),
952        );
953        assert_eq!(
954            build_resume_restore_plan(&forward),
955            build_resume_restore_plan(&reverse)
956        );
957    }
958
959    #[test]
960    fn reserves_the_complete_j8_integrity_range() {
961        assert_eq!(
962            [
963                ResumeRestoreIntegrityCode::ProgramReference,
964                ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
965                ResumeRestoreIntegrityCode::MissingCompletion,
966                ResumeRestoreIntegrityCode::OrderingOrOutputDrift,
967            ]
968            .map(ResumeRestoreIntegrityCode::code),
969            ["PSASM1359", "PSASM1360", "PSASM1361", "PSASM1362"]
970        );
971    }
972}