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(_) => ResumeRestorePhase::R3RestoreMutableStateAndResources,
525        ResumeExistingSlot::ComputedCache(_) | ResumeExistingSlot::ComputedDirty(_) => {
526            ResumeRestorePhase::R4RestoreRetainedComputedCaches
527        }
528        ResumeExistingSlot::Context(_) => ResumeRestorePhase::R6RestoreContextProviderValues,
529        ResumeExistingSlot::FormFieldValue(_) => ResumeRestorePhase::R11RestoreFormFieldValues,
530        ResumeExistingSlot::FormFieldDirty(_) | ResumeExistingSlot::FormFieldTouched(_) => {
531            ResumeRestorePhase::R12RestoreFormDirtyAndTouched
532        }
533        ResumeExistingSlot::FormFieldValidation(_)
534        | ResumeExistingSlot::FormValidationAggregate(_) => {
535            ResumeRestorePhase::R13RestoreFormValidation
536        }
537        ResumeExistingSlot::FormSubmission(_) => ResumeRestorePhase::R14RestoreStableFormSubmission,
538        ResumeExistingSlot::EffectActivation(_) => {
539            ResumeRestorePhase::R17InstallEffectSubscriptions
540        }
541    }
542}
543
544fn is_form_slot(slot: &ResumeExistingSlot) -> bool {
545    matches!(
546        slot,
547        ResumeExistingSlot::FormFieldValue(_)
548            | ResumeExistingSlot::FormFieldDirty(_)
549            | ResumeExistingSlot::FormFieldTouched(_)
550            | ResumeExistingSlot::FormFieldValidation(_)
551            | ResumeExistingSlot::FormValidationAggregate(_)
552            | ResumeExistingSlot::FormSubmission(_)
553    )
554}
555
556/// # Errors
557///
558/// Returns J8 integrity diagnostics for malformed program references, wrong
559/// phases, duplicate writes, missing completion, parent-order violations, or
560/// deterministic construction drift.
561#[allow(clippy::too_many_lines)]
562pub fn validate_resume_restore_plan(
563    model: &ApplicationSemanticModel,
564    plan: &ResumeRestorePlan,
565) -> Result<(), Vec<ResumeRestoreIntegrityDiagnostic>> {
566    let mut diagnostics = Vec::new();
567    let graph = build_resume_boundary_graph(model);
568    let expected_boundaries = graph
569        .boundaries
570        .iter()
571        .map(|boundary| boundary.id.clone())
572        .collect::<Vec<_>>();
573    let actual_boundaries = plan
574        .programs
575        .iter()
576        .map(|program| program.boundary_id.clone())
577        .collect::<Vec<_>>();
578    if expected_boundaries != actual_boundaries
579        || plan.schedule.phases.len() != ResumeRestorePhase::ALL.len()
580        || plan
581            .schedule
582            .phases
583            .iter()
584            .map(|phase| phase.phase)
585            .collect::<Vec<_>>()
586            != ResumeRestorePhase::ALL
587    {
588        diagnostics.push(restore_diagnostic(
589            ResumeRestoreIntegrityCode::ProgramReference,
590            None,
591            "restore programs or fixed phase schedule do not correspond",
592        ));
593    }
594    let known_programs = plan
595        .programs
596        .iter()
597        .map(|program| program.program_id.clone())
598        .collect::<BTreeSet<_>>();
599    if plan
600        .schedule
601        .phases
602        .iter()
603        .flat_map(|phase| &phase.programs)
604        .any(|program| !known_programs.contains(program))
605    {
606        diagnostics.push(restore_diagnostic(
607            ResumeRestoreIntegrityCode::ProgramReference,
608            None,
609            "restore schedule references an unknown program",
610        ));
611    }
612    for program in &plan.programs {
613        let mut writes = BTreeSet::new();
614        let mut previous_phase = None;
615        for record in &program.instructions {
616            if previous_phase.is_some_and(|phase| phase > record.phase) {
617                diagnostics.push(restore_diagnostic(
618                    ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
619                    Some(program.boundary_id.clone()),
620                    "restore instruction phases are out of fixed order",
621                ));
622            }
623            previous_phase = Some(record.phase);
624            if !instruction_phase_is_valid(program, record) {
625                diagnostics.push(restore_diagnostic(
626                    ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
627                    Some(program.boundary_id.clone()),
628                    "restore instruction is assigned to the wrong fixed phase",
629                ));
630            }
631            if let ResumeRestoreInstruction::WriteSlot { slot_id }
632            | ResumeRestoreInstruction::RestoreFormSlot { slot_id, .. } = &record.instruction
633            {
634                if !writes.insert(slot_id.clone()) {
635                    diagnostics.push(restore_diagnostic(
636                        ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
637                        Some(program.boundary_id.clone()),
638                        "restore program writes one slot more than once",
639                    ));
640                }
641            }
642        }
643        if !matches!(
644            program.instructions.last(),
645            Some(ResumeRestoreInstructionRecord {
646                phase: ResumeRestorePhase::R19MarkBoundariesReady,
647                instruction: ResumeRestoreInstruction::MarkBoundaryRestored,
648            })
649        ) {
650            diagnostics.push(restore_diagnostic(
651                ResumeRestoreIntegrityCode::MissingCompletion,
652                Some(program.boundary_id.clone()),
653                "restore program lacks its final MarkBoundaryRestored",
654            ));
655        }
656    }
657    if plan.version != RESUME_RESTORE_PLAN_VERSION || plan != &build_resume_restore_plan(model) {
658        diagnostics.push(restore_diagnostic(
659            ResumeRestoreIntegrityCode::OrderingOrOutputDrift,
660            None,
661            "restore plan drifted from canonical J2/J3/J6 construction",
662        ));
663    }
664    if diagnostics.is_empty() {
665        Ok(())
666    } else {
667        Err(diagnostics)
668    }
669}
670
671fn instruction_phase_is_valid(
672    program: &ResumeRestoreProgram,
673    record: &ResumeRestoreInstructionRecord,
674) -> bool {
675    match &record.instruction {
676        ResumeRestoreInstruction::AllocateBoundaryRuntimeRecord => {
677            record.phase == ResumeRestorePhase::R2AllocateBoundaryRuntimeRecords
678        }
679        ResumeRestoreInstruction::DecodeValue { slot_id, .. }
680        | ResumeRestoreInstruction::WriteSlot { slot_id }
681        | ResumeRestoreInstruction::RestoreFormSlot { slot_id, .. } => program
682            .slot_assignments
683            .iter()
684            .find(|assignment| &assignment.slot_id == slot_id)
685            .is_some_and(|assignment| assignment.retained && assignment.phase == record.phase),
686        ResumeRestoreInstruction::RestoreStructuralSelection { .. } => {
687            record.phase == ResumeRestorePhase::R9RestoreStructuralSelection
688        }
689        ResumeRestoreInstruction::RecomputeComputed { .. } => {
690            record.phase == ResumeRestorePhase::R5RecomputeComputed
691        }
692        ResumeRestoreInstruction::BindContextConsumer { .. } => {
693            record.phase == ResumeRestorePhase::R7BindContextConsumers
694        }
695        ResumeRestoreInstruction::InstallDomBinding { .. } => {
696            record.phase == ResumeRestorePhase::R16InstallDomBindings
697        }
698        ResumeRestoreInstruction::InstallEffectSubscription { .. } => {
699            record.phase == ResumeRestorePhase::R17InstallEffectSubscriptions
700        }
701        ResumeRestoreInstruction::MarkBoundaryRestored => {
702            record.phase == ResumeRestorePhase::R19MarkBoundariesReady
703        }
704    }
705}
706
707fn restore_diagnostic(
708    code: ResumeRestoreIntegrityCode,
709    boundary: Option<ResumeBoundaryId>,
710    message: &str,
711) -> ResumeRestoreIntegrityDiagnostic {
712    ResumeRestoreIntegrityDiagnostic {
713        code,
714        boundary,
715        message: message.to_string(),
716    }
717}
718
719#[cfg(test)]
720mod tests {
721    use super::*;
722
723    fn model(source: &str) -> ApplicationSemanticModel {
724        crate::build_application_semantic_model(&presolve_parser::parse_file(
725            "src/ResumeRestore.tsx",
726            source,
727        ))
728    }
729
730    #[test]
731    fn builds_all_fixed_phases_and_assigns_retained_and_recomputable_slots() {
732        let model = model(
733            r#"@component("x-counter") class Counter {
734  count = state(1);
735  @computed() get doubled() { return this.count * 2; }
736  render() { return <button>{this.doubled}</button>; }
737}"#,
738        );
739        let plan = build_resume_restore_plan(&model);
740        let liveness = build_resume_liveness_plan(&model);
741        assert_eq!(
742            plan.schedule
743                .phases
744                .iter()
745                .map(|phase| phase.phase)
746                .collect::<Vec<_>>(),
747            ResumeRestorePhase::ALL
748        );
749        let assignments = plan
750            .programs
751            .iter()
752            .flat_map(|program| &program.slot_assignments)
753            .map(|assignment| assignment.slot.clone())
754            .collect::<BTreeSet<_>>();
755        assert!(liveness
756            .retained
757            .iter()
758            .all(|record| assignments.contains(&record.slot.existing_slot)));
759        assert!(liveness
760            .recomputable
761            .iter()
762            .all(|record| assignments.contains(&record.slot.existing_slot)));
763        assert!(plan.programs.iter().all(|program| matches!(
764            program.instructions.last(),
765            Some(ResumeRestoreInstructionRecord {
766                phase: ResumeRestorePhase::R19MarkBoundariesReady,
767                instruction: ResumeRestoreInstruction::MarkBoundaryRestored,
768            })
769        )));
770        assert_eq!(validate_resume_restore_plan(&model, &plan), Ok(()));
771    }
772
773    #[test]
774    fn restores_form_slot_classes_in_their_exact_fixed_phases() {
775        let model = model(
776            r#"@component("x-profile") class Profile {
777  @form() profile!: Form;
778  @field(this.profile) name = "";
779  render() { return <input field={this.name} />; }
780}"#,
781        );
782        let plan = build_resume_restore_plan(&model);
783        let assignments = plan
784            .programs
785            .iter()
786            .flat_map(|program| &program.slot_assignments)
787            .map(|assignment| (&assignment.slot, assignment.phase))
788            .collect::<BTreeMap<_, _>>();
789        assert!(assignments.iter().any(|(slot, phase)| matches!(
790            (slot, phase),
791            (
792                ResumeExistingSlot::FormFieldValue(_),
793                ResumeRestorePhase::R11RestoreFormFieldValues
794            )
795        )));
796        assert!(assignments.iter().any(|(slot, phase)| matches!(
797            (slot, phase),
798            (
799                ResumeExistingSlot::FormFieldDirty(_) | ResumeExistingSlot::FormFieldTouched(_),
800                ResumeRestorePhase::R12RestoreFormDirtyAndTouched
801            )
802        )));
803        assert!(assignments.iter().any(|(slot, phase)| matches!(
804            (slot, phase),
805            (
806                ResumeExistingSlot::FormFieldValidation(_)
807                    | ResumeExistingSlot::FormValidationAggregate(_),
808                ResumeRestorePhase::R13RestoreFormValidation
809            )
810        )));
811        assert!(assignments.iter().any(|(slot, phase)| matches!(
812            (slot, phase),
813            (
814                ResumeExistingSlot::FormSubmission(_),
815                ResumeRestorePhase::R14RestoreStableFormSubmission
816            )
817        )));
818    }
819
820    #[test]
821    fn structural_regions_restore_from_their_exact_retained_state_slots() {
822        let model = model(
823            r#"@component("x-leaf") class Leaf extends Component { render() { return <span />; } }
824@component("x-page") @route("/") class Page extends Component {
825  visible = state(true);
826  items = state([{ id: "a" }, { id: "b" }]);
827  render() {
828    return <main>{this.visible ? <div><Leaf /></div> : <aside>Hidden</aside>}<ul>{this.items.map(item => <li key={item.id}><Leaf /></li>)}</ul></main>;
829  }
830  hide() { this.visible = false; }
831  trim() { this.items = [{ id: "b" }]; }
832}"#,
833        );
834        let plan = build_resume_restore_plan(&model);
835        let structural = plan
836            .programs
837            .iter()
838            .flat_map(|program| &program.instructions)
839            .filter_map(|record| match &record.instruction {
840                ResumeRestoreInstruction::RestoreStructuralSelection { region_id, slot_id } => {
841                    Some((record.phase, region_id.to_string(), slot_id.to_string()))
842                }
843                _ => None,
844            })
845            .collect::<Vec<_>>();
846        assert_eq!(structural.len(), 2);
847        assert!(structural
848            .iter()
849            .all(|(phase, _, _)| { *phase == ResumeRestorePhase::R9RestoreStructuralSelection }));
850        assert!(structural
851            .iter()
852            .any(|(_, region, slot)| region.ends_with(":conditional")
853                && slot.contains("state%3Avisible")));
854        assert!(structural
855            .iter()
856            .any(|(_, region, slot)| region.ends_with(":keyed-list")
857                && slot.contains("state%3Aitems")));
858        assert_eq!(validate_resume_restore_plan(&model, &plan), Ok(()));
859    }
860
861    #[test]
862    fn malformed_reference_phase_write_completion_and_parent_order_are_rejected() {
863        let model = model(
864            r#"@component("x-child") class Child { value = state(1); render() { return <span>{this.value}</span>; } }
865@component("x-parent") @route("/") class Parent { render() { return <Child />; } }"#,
866        );
867        let canonical = build_resume_restore_plan(&model);
868
869        let mut dangling = canonical.clone();
870        dangling.programs.pop();
871        let diagnostics =
872            validate_resume_restore_plan(&model, &dangling).expect_err("dangling reference");
873        assert!(diagnostics
874            .iter()
875            .any(|diagnostic| { diagnostic.code == ResumeRestoreIntegrityCode::ProgramReference }));
876
877        let mut missing = canonical.clone();
878        missing.programs[0].instructions.pop();
879        let diagnostics = validate_resume_restore_plan(&model, &missing).expect_err("missing mark");
880        assert!(diagnostics.iter().any(|diagnostic| {
881            diagnostic.code == ResumeRestoreIntegrityCode::MissingCompletion
882        }));
883
884        let mut wrong_phase = canonical.clone();
885        wrong_phase.programs[0].instructions[0].phase = ResumeRestorePhase::R19MarkBoundariesReady;
886        let diagnostics =
887            validate_resume_restore_plan(&model, &wrong_phase).expect_err("wrong phase");
888        assert!(diagnostics.iter().any(|diagnostic| {
889            diagnostic.code == ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite
890                || diagnostic.code == ResumeRestoreIntegrityCode::OrderingOrOutputDrift
891        }));
892
893        let mut duplicate = canonical.clone();
894        let duplicate_write = duplicate
895            .programs
896            .iter()
897            .enumerate()
898            .find_map(|(index, program)| {
899                program
900                    .instructions
901                    .iter()
902                    .find(|record| {
903                        matches!(
904                            record.instruction,
905                            ResumeRestoreInstruction::WriteSlot { .. }
906                        )
907                    })
908                    .cloned()
909                    .map(|record| (index, record))
910            });
911        if let Some((program_index, write)) = duplicate_write {
912            duplicate.programs[program_index]
913                .instructions
914                .insert(1, write);
915        }
916        let diagnostics =
917            validate_resume_restore_plan(&model, &duplicate).expect_err("duplicate write");
918        assert!(diagnostics.iter().any(|diagnostic| {
919            diagnostic.code == ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite
920                || diagnostic.code == ResumeRestoreIntegrityCode::OrderingOrOutputDrift
921        }));
922
923        let mut reversed = canonical;
924        reversed.programs.reverse();
925        let diagnostics =
926            validate_resume_restore_plan(&model, &reversed).expect_err("parent order");
927        assert!(diagnostics
928            .iter()
929            .any(|diagnostic| { diagnostic.code == ResumeRestoreIntegrityCode::ProgramReference }));
930    }
931
932    #[test]
933    fn construction_is_deterministic_under_source_reversal() {
934        let first = presolve_parser::parse_file(
935            "src/A.tsx",
936            r#"@component("x-a") @route("/a") class A { value = state(1); render() { return <button>{this.value}</button>; } }"#,
937        );
938        let second = presolve_parser::parse_file(
939            "src/B.tsx",
940            r#"@component("x-b") @route("/b") class B { enabled = state(true); render() { return <button>{this.enabled}</button>; } }"#,
941        );
942        let forward = crate::build_application_semantic_model_for_unit(
943            &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
944        );
945        let reverse = crate::build_application_semantic_model_for_unit(
946            &crate::CompilationUnit::from_parsed_files(vec![second, first]),
947        );
948        assert_eq!(
949            build_resume_restore_plan(&forward),
950            build_resume_restore_plan(&reverse)
951        );
952    }
953
954    #[test]
955    fn reserves_the_complete_j8_integrity_range() {
956        assert_eq!(
957            [
958                ResumeRestoreIntegrityCode::ProgramReference,
959                ResumeRestoreIntegrityCode::PhaseOrDuplicateWrite,
960                ResumeRestoreIntegrityCode::MissingCompletion,
961                ResumeRestoreIntegrityCode::OrderingOrOutputDrift,
962            ]
963            .map(ResumeRestoreIntegrityCode::code),
964            ["PSASM1359", "PSASM1360", "PSASM1361", "PSASM1362"]
965        );
966    }
967}