Skip to main content

presolve_compiler/
resume_chunk.rs

1//! J5 deterministic resume chunk roots and canonical program closures.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::{
6    build_resume_activation_plan, build_resume_boundary_graph, ApplicationSemanticModel,
7    ResumeActivationPolicy, ResumeActivationPrerequisite, ResumeActivationRootKind,
8    ResumeBoundaryActivationProgram, ResumeBoundaryId, ResumeChunkId, ResumeExistingSlot,
9    SemanticId, SourceProvenance,
10};
11
12pub const RESUME_CHUNK_GRAPH_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum ResumeChunkRootKind {
16    Eager,
17    Interaction,
18    Visible,
19    Manual,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
23pub enum ResumeChunkProgram {
24    RuntimeBootstrap,
25    RuntimeRegistries,
26    EventDelegation,
27    PostRestoreRecomputation(ResumeExistingSlot),
28    ImmediateFormRuntime(crate::FormInstanceId),
29    OrdinaryEvent {
30        event: SemanticId,
31        handler: SemanticId,
32        action_batch: SemanticId,
33        program: SemanticId,
34    },
35    FormSubmit {
36        submission_host: crate::SubmissionHostId,
37        submission_plan: crate::SubmissionPlanId,
38        submit_action: SemanticId,
39        action_batch: SemanticId,
40        serialization_plan: crate::SerializationPlanId,
41    },
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct ResumeChunkProgramInclusion {
46    pub chunk_id: ResumeChunkId,
47    pub program: ResumeChunkProgram,
48    pub required_boundaries: Vec<ResumeBoundaryId>,
49    pub provenance: SourceProvenance,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ResumeChunkModulePlan {
54    pub module_path_stem: String,
55    pub canonical_module_bytes: String,
56    pub content_hash: String,
57    pub module_path: String,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ResumeChunk {
62    pub id: ResumeChunkId,
63    pub root_kind: ResumeChunkRootKind,
64    pub root_boundary: Option<ResumeBoundaryId>,
65    pub required_boundaries: Vec<ResumeBoundaryId>,
66    pub programs: Vec<ResumeChunkProgram>,
67    pub dependency_chunks: Vec<ResumeChunkId>,
68    pub module: ResumeChunkModulePlan,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
72pub enum ResumeChunkBlockReason {
73    MissingActivationBoundary,
74    MissingProgram,
75    UnsupportedActivationPolicy,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ResumeChunkBlock {
80    pub root_boundary: Option<ResumeBoundaryId>,
81    pub reason: ResumeChunkBlockReason,
82    pub provenance: SourceProvenance,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ResumeChunkGraph {
87    pub version: u32,
88    pub eager_chunk: ResumeChunkId,
89    pub chunks: Vec<ResumeChunk>,
90    pub inclusions: Vec<ResumeChunkProgramInclusion>,
91    pub blocks: Vec<ResumeChunkBlock>,
92    pub chunk_index: BTreeMap<ResumeChunkId, usize>,
93}
94
95impl ResumeChunkGraph {
96    #[must_use]
97    pub fn chunk(&self, id: &ResumeChunkId) -> Option<&ResumeChunk> {
98        self.chunk_index
99            .get(id)
100            .and_then(|index| self.chunks.get(*index))
101    }
102
103    #[must_use]
104    pub fn chunk_for_root(&self, boundary: &ResumeBoundaryId) -> Option<&ResumeChunk> {
105        self.chunks
106            .iter()
107            .find(|chunk| chunk.root_boundary.as_ref() == Some(boundary))
108    }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
112pub enum ResumeChunkIntegrityCode {
113    DuplicateInclusion,
114    MissingProgram,
115    DependencyCycle,
116    RootCorrespondence,
117    UnrelatedProgram,
118    OrderingOrOutputDrift,
119}
120
121impl ResumeChunkIntegrityCode {
122    #[must_use]
123    pub const fn code(self) -> &'static str {
124        match self {
125            Self::DuplicateInclusion => "PSASM1343",
126            Self::MissingProgram => "PSASM1344",
127            Self::DependencyCycle => "PSASM1345",
128            Self::RootCorrespondence => "PSASM1346",
129            Self::UnrelatedProgram => "PSASM1347",
130            Self::OrderingOrOutputDrift => "PSASM1348",
131        }
132    }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct ResumeChunkIntegrityDiagnostic {
137    pub code: ResumeChunkIntegrityCode,
138    pub chunk: Option<ResumeChunkId>,
139    pub message: String,
140}
141
142#[must_use]
143#[allow(clippy::too_many_lines)]
144/// # Panics
145///
146/// Panics only when the canonical J3 graph lacks its required application
147/// root boundary, which indicates an earlier compiler integrity failure.
148pub fn build_resume_chunk_graph(model: &ApplicationSemanticModel) -> ResumeChunkGraph {
149    let activation = build_resume_activation_plan(model);
150    let boundaries = build_resume_boundary_graph(model);
151    let eager_id =
152        ResumeChunkId::for_activation_root(ResumeActivationRootKind::Eager, "application");
153    let eager_provenance = boundaries
154        .boundaries
155        .iter()
156        .find(|boundary| boundary.kind == crate::ResumeBoundaryKind::ApplicationRoot)
157        .map(|boundary| boundary.provenance.clone())
158        .expect("canonical application root boundary");
159    let mut eager_programs = vec![
160        ResumeChunkProgram::RuntimeBootstrap,
161        ResumeChunkProgram::RuntimeRegistries,
162        ResumeChunkProgram::EventDelegation,
163    ];
164    let mut eager_boundaries = Vec::new();
165    for decision in activation
166        .decisions
167        .iter()
168        .filter(|decision| decision.policy == ResumeActivationPolicy::Eager)
169    {
170        eager_boundaries.push(decision.boundary.clone());
171        for prerequisite in &decision.prerequisites {
172            match prerequisite {
173                ResumeActivationPrerequisite::PostRestoreRecomputation(slot)
174                | ResumeActivationPrerequisite::RecomputableSlot(slot) => {
175                    eager_programs.push(ResumeChunkProgram::PostRestoreRecomputation(slot.clone()));
176                }
177                ResumeActivationPrerequisite::ImmediateFormRuntime(form) => {
178                    eager_programs.push(ResumeChunkProgram::ImmediateFormRuntime(form.clone()));
179                }
180                ResumeActivationPrerequisite::ApplicationBootstrap
181                | ResumeActivationPrerequisite::RuntimeRegistryInstallation
182                | ResumeActivationPrerequisite::EventDelegationInstallation
183                | ResumeActivationPrerequisite::ExactInteraction(_)
184                | ResumeActivationPrerequisite::RequiredBoundary(_)
185                | ResumeActivationPrerequisite::RetainedSlot(_) => {}
186            }
187        }
188    }
189    eager_boundaries.sort();
190    eager_boundaries.dedup();
191    eager_programs.sort();
192    eager_programs.dedup();
193    let mut chunks = vec![chunk(
194        eager_id.clone(),
195        ResumeChunkRootKind::Eager,
196        None,
197        eager_boundaries.clone(),
198        eager_programs.clone(),
199    )];
200    let mut inclusions = eager_programs
201        .iter()
202        .cloned()
203        .map(|program| ResumeChunkProgramInclusion {
204            chunk_id: eager_id.clone(),
205            program,
206            required_boundaries: eager_boundaries.clone(),
207            provenance: eager_provenance.clone(),
208        })
209        .collect::<Vec<_>>();
210    let mut blocks = Vec::new();
211
212    for decision in activation.decisions.iter().filter(|decision| {
213        matches!(
214            decision.policy,
215            ResumeActivationPolicy::Interaction
216                | ResumeActivationPolicy::Visible
217                | ResumeActivationPolicy::Manual
218        )
219    }) {
220        let Some(reference) = boundaries
221            .activation_references
222            .iter()
223            .find(|reference| reference.interaction_boundary == decision.boundary)
224        else {
225            blocks.push(ResumeChunkBlock {
226                root_boundary: Some(decision.boundary.clone()),
227                reason: ResumeChunkBlockReason::MissingActivationBoundary,
228                provenance: decision.provenance.clone(),
229            });
230            continue;
231        };
232        let (root_kind, activation_kind) = match decision.policy {
233            ResumeActivationPolicy::Interaction => (
234                ResumeChunkRootKind::Interaction,
235                ResumeActivationRootKind::Event,
236            ),
237            ResumeActivationPolicy::Visible => (
238                ResumeChunkRootKind::Visible,
239                ResumeActivationRootKind::Visible,
240            ),
241            ResumeActivationPolicy::Manual => (
242                ResumeChunkRootKind::Manual,
243                ResumeActivationRootKind::Manual,
244            ),
245            ResumeActivationPolicy::Eager | ResumeActivationPolicy::None => unreachable!(),
246        };
247        let id = ResumeChunkId::for_activation_root(activation_kind, decision.boundary.as_str());
248        let program = match &reference.program {
249            ResumeBoundaryActivationProgram::OrdinaryEvent {
250                declaration_event,
251                handler_method,
252                action_batch,
253                existing_program,
254                ..
255            } => ResumeChunkProgram::OrdinaryEvent {
256                event: declaration_event.clone(),
257                handler: handler_method.clone(),
258                action_batch: action_batch.clone(),
259                program: existing_program.clone(),
260            },
261            ResumeBoundaryActivationProgram::FormSubmit {
262                submission_host,
263                submission_plan,
264                submit_action,
265                action_batch,
266                serialization_plan,
267                ..
268            } => ResumeChunkProgram::FormSubmit {
269                submission_host: submission_host.clone(),
270                submission_plan: submission_plan.clone(),
271                submit_action: submit_action.clone(),
272                action_batch: action_batch.clone(),
273                serialization_plan: serialization_plan.clone(),
274            },
275        };
276        let mut required_boundaries = reference.required_boundaries.clone();
277        required_boundaries.sort();
278        required_boundaries.dedup();
279        chunks.push(chunk(
280            id.clone(),
281            root_kind,
282            Some(decision.boundary.clone()),
283            required_boundaries.clone(),
284            vec![program.clone()],
285        ));
286        inclusions.push(ResumeChunkProgramInclusion {
287            chunk_id: id,
288            program,
289            required_boundaries,
290            provenance: reference.provenance.clone(),
291        });
292    }
293
294    chunks.sort_by(|left, right| {
295        (
296            left.root_kind != ResumeChunkRootKind::Eager,
297            &left.root_boundary,
298            &left.id,
299        )
300            .cmp(&(
301                right.root_kind != ResumeChunkRootKind::Eager,
302                &right.root_boundary,
303                &right.id,
304            ))
305    });
306    inclusions.sort_by(|left, right| {
307        (&left.chunk_id, &left.program).cmp(&(&right.chunk_id, &right.program))
308    });
309    blocks.sort_by(|left, right| {
310        (&left.root_boundary, left.reason).cmp(&(&right.root_boundary, right.reason))
311    });
312    let chunk_index = chunks
313        .iter()
314        .enumerate()
315        .map(|(index, chunk)| (chunk.id.clone(), index))
316        .collect();
317    ResumeChunkGraph {
318        version: RESUME_CHUNK_GRAPH_VERSION,
319        eager_chunk: eager_id,
320        chunks,
321        inclusions,
322        blocks,
323        chunk_index,
324    }
325}
326
327fn chunk(
328    id: ResumeChunkId,
329    root_kind: ResumeChunkRootKind,
330    root_boundary: Option<ResumeBoundaryId>,
331    required_boundaries: Vec<ResumeBoundaryId>,
332    programs: Vec<ResumeChunkProgram>,
333) -> ResumeChunk {
334    let kind = match root_kind {
335        ResumeChunkRootKind::Eager => "boot",
336        ResumeChunkRootKind::Interaction => "event",
337        ResumeChunkRootKind::Visible => "visible",
338        ResumeChunkRootKind::Manual => "manual",
339    };
340    let short = safe_stem(
341        root_boundary
342            .as_ref()
343            .map_or("application", ResumeBoundaryId::as_str),
344    );
345    let canonical_module_bytes = format!(
346        "// chunk={id}\n// kind={root_kind:?}\n// root={}\n// boundaries={}\n// programs={}\nexport {{}};\n",
347        root_boundary.as_ref().map_or("", ResumeBoundaryId::as_str),
348        required_boundaries
349            .iter()
350            .map(ToString::to_string)
351            .collect::<Vec<_>>()
352            .join(","),
353        programs
354            .iter()
355            .map(|program| format!("{program:?}"))
356            .collect::<Vec<_>>()
357            .join(",")
358    );
359    let build_hash = crate::ResumeBuildId::for_public_inputs(&canonical_module_bytes);
360    let content_hash = build_hash
361        .as_str()
362        .strip_prefix("resume-build:")
363        .unwrap_or(build_hash.as_str())
364        .to_string();
365    let module_path_stem = format!("{kind}.{short}");
366    let module_path = format!("{module_path_stem}.{content_hash}.js");
367    ResumeChunk {
368        id,
369        root_kind,
370        root_boundary,
371        required_boundaries,
372        programs,
373        dependency_chunks: Vec::new(),
374        module: ResumeChunkModulePlan {
375            module_path_stem,
376            canonical_module_bytes,
377            content_hash,
378            module_path,
379        },
380    }
381}
382
383fn safe_stem(value: &str) -> String {
384    let mut stem = value
385        .chars()
386        .map(|character| {
387            if character.is_ascii_alphanumeric() {
388                character
389            } else {
390                '-'
391            }
392        })
393        .collect::<String>();
394    while stem.contains("--") {
395        stem = stem.replace("--", "-");
396    }
397    stem.trim_matches('-').chars().take(48).collect()
398}
399
400#[must_use]
401pub fn validate_resume_chunk_graph(
402    model: &ApplicationSemanticModel,
403    graph: &ResumeChunkGraph,
404) -> Vec<ResumeChunkIntegrityDiagnostic> {
405    let canonical = build_resume_chunk_graph(model);
406    let activation = build_resume_activation_plan(model);
407    let mut diagnostics = Vec::new();
408    let mut inclusions = BTreeSet::new();
409    for inclusion in &graph.inclusions {
410        if !inclusions.insert((inclusion.chunk_id.clone(), inclusion.program.clone())) {
411            diagnostics.push(integrity(
412                ResumeChunkIntegrityCode::DuplicateInclusion,
413                Some(inclusion.chunk_id.clone()),
414                "generated program was included more than once in one chunk",
415            ));
416        }
417        if graph.chunk(&inclusion.chunk_id).is_none() {
418            diagnostics.push(integrity(
419                ResumeChunkIntegrityCode::MissingProgram,
420                Some(inclusion.chunk_id.clone()),
421                "program inclusion references an unknown chunk",
422            ));
423        }
424    }
425    for chunk in &graph.chunks {
426        if !chunk.dependency_chunks.is_empty() {
427            diagnostics.push(integrity(
428                ResumeChunkIntegrityCode::DependencyCycle,
429                Some(chunk.id.clone()),
430                "Phase J v1 root chunks cannot depend on other lazy chunks",
431            ));
432        }
433        if chunk.programs.iter().any(|program| {
434            !graph
435                .inclusions
436                .iter()
437                .any(|inclusion| inclusion.chunk_id == chunk.id && inclusion.program == *program)
438        }) {
439            diagnostics.push(integrity(
440                ResumeChunkIntegrityCode::MissingProgram,
441                Some(chunk.id.clone()),
442                "chunk program has no reciprocal inclusion record",
443            ));
444        }
445    }
446    for decision in activation.decisions.iter().filter(|decision| {
447        decision.policy != ResumeActivationPolicy::None
448            && decision.policy != ResumeActivationPolicy::Eager
449    }) {
450        if graph.chunk_for_root(&decision.boundary).is_none() {
451            diagnostics.push(integrity(
452                ResumeChunkIntegrityCode::RootCorrespondence,
453                None,
454                "non-eager activation has no exact lazy root chunk",
455            ));
456        }
457    }
458    if graph.version != RESUME_CHUNK_GRAPH_VERSION || graph != &canonical {
459        diagnostics.push(integrity(
460            ResumeChunkIntegrityCode::OrderingOrOutputDrift,
461            None,
462            "chunk graph drifted from canonical roots, closure, module bytes, or order",
463        ));
464    }
465    diagnostics.sort_by(|left, right| {
466        (left.code, &left.chunk, left.message.as_str()).cmp(&(
467            right.code,
468            &right.chunk,
469            right.message.as_str(),
470        ))
471    });
472    diagnostics.dedup();
473    diagnostics
474}
475
476fn integrity(
477    code: ResumeChunkIntegrityCode,
478    chunk: Option<ResumeChunkId>,
479    message: &str,
480) -> ResumeChunkIntegrityDiagnostic {
481    ResumeChunkIntegrityDiagnostic {
482        code,
483        chunk,
484        message: message.to_string(),
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn creates_one_eager_and_one_isolated_chunk_per_interaction() {
494        let model = crate::build_application_semantic_model(&presolve_parser::parse_file(
495            "src/Chunks.tsx",
496            r#"
497@component("x-chunk-child") class Child {
498  a = state(1); b = state(2);
499  @action() first() { this.a++; }
500  @action() second() { this.b++; }
501  render() { return <><button onClick={() => this.first()}>A</button><button onClick={() => this.second()}>B</button></>; }
502}
503@component("x-chunk-page") @route("/") class Page { render() { return <Child />; } }
504"#,
505        ));
506        let graph = build_resume_chunk_graph(&model);
507        assert!(validate_resume_chunk_graph(&model, &graph).is_empty());
508        assert_eq!(graph.chunks.len(), 3);
509        assert_eq!(
510            graph
511                .chunks
512                .iter()
513                .filter(|chunk| chunk.root_kind == ResumeChunkRootKind::Eager)
514                .count(),
515            1
516        );
517        let lazy = graph
518            .chunks
519            .iter()
520            .filter(|chunk| chunk.root_kind == ResumeChunkRootKind::Interaction)
521            .collect::<Vec<_>>();
522        assert_eq!(lazy.len(), 2);
523        assert!(lazy.iter().all(|chunk| {
524            chunk.programs.len() == 1
525                && chunk.dependency_chunks.is_empty()
526                && chunk.module.module_path.starts_with("event.")
527        }));
528        assert_ne!(lazy[0].programs, lazy[1].programs);
529    }
530
531    #[test]
532    fn chunk_output_is_deterministic_under_source_reversal() {
533        let first = presolve_parser::parse_file(
534            "src/A.tsx",
535            r#"@component("x-a") @route("/a") class A { @action() go() {} render() { return <button onClick={() => this.go()}>A</button>; } }"#,
536        );
537        let second = presolve_parser::parse_file(
538            "src/B.tsx",
539            r#"@component("x-b") @route("/b") class B { @action() go() {} render() { return <button onClick={() => this.go()}>B</button>; } }"#,
540        );
541        let forward = crate::build_application_semantic_model_for_unit(
542            &crate::CompilationUnit::from_parsed_files(vec![first.clone(), second.clone()]),
543        );
544        let reverse = crate::build_application_semantic_model_for_unit(
545            &crate::CompilationUnit::from_parsed_files(vec![second, first]),
546        );
547        assert_eq!(
548            build_resume_chunk_graph(&forward),
549            build_resume_chunk_graph(&reverse)
550        );
551    }
552
553    #[test]
554    fn reserves_the_complete_j5_integrity_range() {
555        assert_eq!(
556            [
557                ResumeChunkIntegrityCode::DuplicateInclusion,
558                ResumeChunkIntegrityCode::MissingProgram,
559                ResumeChunkIntegrityCode::DependencyCycle,
560                ResumeChunkIntegrityCode::RootCorrespondence,
561                ResumeChunkIntegrityCode::UnrelatedProgram,
562                ResumeChunkIntegrityCode::OrderingOrOutputDrift,
563            ]
564            .map(ResumeChunkIntegrityCode::code),
565            [
566                "PSASM1343",
567                "PSASM1344",
568                "PSASM1345",
569                "PSASM1346",
570                "PSASM1347",
571                "PSASM1348",
572            ]
573        );
574    }
575}