Skip to main content

presolve_compiler/
resume_identity.rs

1//! Canonical Phase J resumability identity domains.
2//!
3//! These are compiler-only typed wrappers. They do not serialize a resume
4//! product; J9 alone owns public manifest encoding.
5
6use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12use crate::{
13    ComponentInstanceId, ComponentRootId, ComponentStructuralRegionId, ComputedCacheSlotId,
14    ComputedDirtyFlagId, FormInstanceId, IrStorageId, SemanticId,
15};
16
17macro_rules! resume_id {
18    ($name:ident) => {
19        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
20        #[serde(transparent)]
21        pub struct $name(String);
22
23        impl $name {
24            #[must_use]
25            pub fn as_str(&self) -> &str {
26                &self.0
27            }
28        }
29
30        impl fmt::Display for $name {
31            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32                self.0.fmt(formatter)
33            }
34        }
35
36        impl FromStr for $name {
37            type Err = ResumeIdentityParseError;
38
39            fn from_str(value: &str) -> Result<Self, Self::Err> {
40                if value.is_empty() {
41                    Err(ResumeIdentityParseError)
42                } else {
43                    Ok(Self(value.to_string()))
44                }
45            }
46        }
47    };
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct ResumeIdentityParseError;
52
53impl fmt::Display for ResumeIdentityParseError {
54    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55        formatter.write_str("resume identity must not be empty")
56    }
57}
58
59impl std::error::Error for ResumeIdentityParseError {}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
62pub enum ResumeBoundaryKind {
63    ApplicationRoot,
64    ComponentInstance,
65    StructuralRegion,
66    FormInstance,
67    Interaction,
68}
69
70impl ResumeBoundaryKind {
71    const fn label(self) -> &'static str {
72        match self {
73            Self::ApplicationRoot => "ApplicationRoot",
74            Self::ComponentInstance => "ComponentInstance",
75            Self::StructuralRegion => "StructuralRegion",
76            Self::FormInstance => "FormInstance",
77            Self::Interaction => "Interaction",
78        }
79    }
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
83pub enum ResumeActivationRootKind {
84    Eager,
85    Event,
86    Visible,
87    Manual,
88}
89
90impl ResumeActivationRootKind {
91    const fn label(self) -> &'static str {
92        match self {
93            Self::Eager => "Eager",
94            Self::Event => "Event",
95            Self::Visible => "Visible",
96            Self::Manual => "Manual",
97        }
98    }
99}
100
101resume_id!(ResumeBoundaryId);
102resume_id!(ResumeSlotId);
103resume_id!(ResumeSchemaId);
104resume_id!(ResumeCaptureProgramId);
105resume_id!(ResumeRestoreProgramId);
106resume_id!(ResumeChunkId);
107resume_id!(ResumeChunkGroupId);
108resume_id!(ResumeActivationId);
109resume_id!(ResumeAnchorId);
110resume_id!(ResumeEventId);
111resume_id!(ResumeSnapshotId);
112resume_id!(ResumeBuildId);
113resume_id!(ResumeValueRecordId);
114
115/// Exact ordinary-template target for one authored template entity materialized
116/// by one compiler-planned component instance.
117#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
118pub struct TemplateInstanceTargetId {
119    component_instance_id: ComponentInstanceId,
120    template_entity_id: SemanticId,
121}
122
123impl TemplateInstanceTargetId {
124    #[must_use]
125    pub fn for_component_instance_template_entity(
126        component_instance_id: ComponentInstanceId,
127        template_entity_id: SemanticId,
128    ) -> Self {
129        Self {
130            component_instance_id,
131            template_entity_id,
132        }
133    }
134
135    #[must_use]
136    pub const fn component_instance_id(&self) -> &ComponentInstanceId {
137        &self.component_instance_id
138    }
139
140    #[must_use]
141    pub const fn template_entity_id(&self) -> &SemanticId {
142        &self.template_entity_id
143    }
144}
145
146impl fmt::Display for TemplateInstanceTargetId {
147    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148        write!(
149            formatter,
150            "{}/template-target:{}",
151            self.component_instance_id,
152            percent_encode(self.template_entity_id.as_str())
153        )
154    }
155}
156
157/// Exact ordinary-template binding execution for one component instance.
158#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
159pub struct TemplateInstanceBindingId {
160    component_instance_id: ComponentInstanceId,
161    binding_id: SemanticId,
162}
163
164impl TemplateInstanceBindingId {
165    #[must_use]
166    pub fn for_component_instance_binding(
167        component_instance_id: ComponentInstanceId,
168        binding_id: SemanticId,
169    ) -> Self {
170        Self {
171            component_instance_id,
172            binding_id,
173        }
174    }
175
176    #[must_use]
177    pub const fn component_instance_id(&self) -> &ComponentInstanceId {
178        &self.component_instance_id
179    }
180
181    #[must_use]
182    pub const fn binding_id(&self) -> &SemanticId {
183        &self.binding_id
184    }
185}
186
187impl fmt::Display for TemplateInstanceBindingId {
188    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189        write!(
190            formatter,
191            "{}/template-binding:{}",
192            self.component_instance_id,
193            percent_encode(self.binding_id.as_str())
194        )
195    }
196}
197
198/// Exact runtime cache address for one declaration-level computed cache in one
199/// compiler-planned component instance.
200#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
201pub struct ComputedInstanceCacheSlotId {
202    component_instance_id: ComponentInstanceId,
203    cache_slot_id: ComputedCacheSlotId,
204}
205
206impl ComputedInstanceCacheSlotId {
207    #[must_use]
208    pub fn for_component_instance_cache_slot(
209        component_instance_id: ComponentInstanceId,
210        cache_slot_id: ComputedCacheSlotId,
211    ) -> Self {
212        Self {
213            component_instance_id,
214            cache_slot_id,
215        }
216    }
217
218    #[must_use]
219    pub const fn component_instance_id(&self) -> &ComponentInstanceId {
220        &self.component_instance_id
221    }
222
223    #[must_use]
224    pub const fn cache_slot_id(&self) -> &ComputedCacheSlotId {
225        &self.cache_slot_id
226    }
227}
228
229impl fmt::Display for ComputedInstanceCacheSlotId {
230    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(
232            formatter,
233            "{}/computed-cache:{}",
234            self.component_instance_id,
235            percent_encode(self.cache_slot_id.as_str())
236        )
237    }
238}
239
240/// Exact runtime dirty address for one declaration-level computed dirty flag in
241/// one compiler-planned component instance.
242#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
243pub struct ComputedInstanceDirtySlotId {
244    component_instance_id: ComponentInstanceId,
245    dirty_flag_id: ComputedDirtyFlagId,
246}
247
248impl ComputedInstanceDirtySlotId {
249    #[must_use]
250    pub fn for_component_instance_dirty_flag(
251        component_instance_id: ComponentInstanceId,
252        dirty_flag_id: ComputedDirtyFlagId,
253    ) -> Self {
254        Self {
255            component_instance_id,
256            dirty_flag_id,
257        }
258    }
259
260    #[must_use]
261    pub const fn component_instance_id(&self) -> &ComponentInstanceId {
262        &self.component_instance_id
263    }
264
265    #[must_use]
266    pub const fn dirty_flag_id(&self) -> &ComputedDirtyFlagId {
267        &self.dirty_flag_id
268    }
269}
270
271impl fmt::Display for ComputedInstanceDirtySlotId {
272    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
273        write!(
274            formatter,
275            "{}/computed-dirty:{}",
276            self.component_instance_id,
277            percent_encode(self.dirty_flag_id.as_str())
278        )
279    }
280}
281
282/// Exact runtime State address for one declaration-level IR storage in one
283/// compiler-planned component instance.
284#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
285pub struct StateInstanceSlotId {
286    component_instance_id: ComponentInstanceId,
287    storage_id: IrStorageId,
288}
289
290impl StateInstanceSlotId {
291    #[must_use]
292    pub fn for_component_instance_storage(
293        component_instance_id: ComponentInstanceId,
294        storage_id: IrStorageId,
295    ) -> Self {
296        Self {
297            component_instance_id,
298            storage_id,
299        }
300    }
301
302    #[must_use]
303    pub const fn component_instance_id(&self) -> &ComponentInstanceId {
304        &self.component_instance_id
305    }
306
307    #[must_use]
308    pub const fn storage_id(&self) -> &IrStorageId {
309        &self.storage_id
310    }
311}
312
313impl fmt::Display for StateInstanceSlotId {
314    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315        write!(
316            formatter,
317            "{}/state-slot:{}",
318            self.component_instance_id,
319            percent_encode(self.storage_id.as_str())
320        )
321    }
322}
323
324impl ResumeBoundaryId {
325    #[must_use]
326    pub fn application_root(root: &ComponentRootId) -> Self {
327        Self::new(
328            root.as_str(),
329            ResumeBoundaryKind::ApplicationRoot,
330            root.as_str(),
331        )
332    }
333    #[must_use]
334    pub fn component_instance(instance: &ComponentInstanceId) -> Self {
335        Self::new(
336            instance.as_str(),
337            ResumeBoundaryKind::ComponentInstance,
338            instance.as_str(),
339        )
340    }
341    #[must_use]
342    pub fn structural_region(
343        owner: &ComponentInstanceId,
344        region: &ComponentStructuralRegionId,
345    ) -> Self {
346        Self::new(
347            owner.as_str(),
348            ResumeBoundaryKind::StructuralRegion,
349            region.as_str(),
350        )
351    }
352    #[must_use]
353    pub fn form_instance(instance: &FormInstanceId) -> Self {
354        Self::new(
355            instance.as_str(),
356            ResumeBoundaryKind::FormInstance,
357            instance.as_str(),
358        )
359    }
360    #[must_use]
361    pub fn interaction(owner: &ComponentInstanceId, event: &SemanticId) -> Self {
362        Self::new(
363            owner.as_str(),
364            ResumeBoundaryKind::Interaction,
365            event.as_str(),
366        )
367    }
368    fn new(owner: &str, kind: ResumeBoundaryKind, local: &str) -> Self {
369        Self(format!("resume-boundary:{owner}:{}:{local}", kind.label()))
370    }
371}
372
373impl ResumeSlotId {
374    #[must_use]
375    pub fn for_existing_storage(slot: &str) -> Self {
376        Self(format!("resume-slot:{slot}"))
377    }
378}
379impl ResumeSchemaId {
380    #[must_use]
381    pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
382        Self(format!("resume-schema:{boundary}"))
383    }
384}
385impl ResumeCaptureProgramId {
386    #[must_use]
387    pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
388        Self(format!("resume-capture:{boundary}"))
389    }
390}
391impl ResumeRestoreProgramId {
392    #[must_use]
393    pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
394        Self(format!("resume-restore:{boundary}"))
395    }
396}
397impl ResumeActivationId {
398    #[must_use]
399    pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
400        Self(format!("resume-activation:{boundary}"))
401    }
402}
403impl ResumeEventId {
404    #[must_use]
405    pub fn for_existing_event(event: &str) -> Self {
406        Self(format!("resume-event:{event}"))
407    }
408}
409impl ResumeSnapshotId {
410    #[must_use]
411    pub fn for_build(build: &ResumeBuildId) -> Self {
412        Self(format!("resume-snapshot:{build}"))
413    }
414}
415impl ResumeValueRecordId {
416    #[must_use]
417    pub fn for_slot(slot: &ResumeSlotId) -> Self {
418        Self(format!("resume-value:{slot}"))
419    }
420}
421impl ResumeChunkId {
422    #[must_use]
423    pub fn for_activation_root(kind: ResumeActivationRootKind, root: &str) -> Self {
424        Self(format!("resume-chunk:{}:{root}", kind.label()))
425    }
426}
427impl ResumeChunkGroupId {
428    #[must_use]
429    pub fn for_ordered_chunks(chunks: &[ResumeChunkId]) -> Self {
430        Self(format!(
431            "resume-chunk-group:{:016x}",
432            canonical_hash(
433                &chunks
434                    .iter()
435                    .map(ToString::to_string)
436                    .collect::<Vec<_>>()
437                    .join("\n")
438            )
439        ))
440    }
441}
442impl ResumeAnchorId {
443    #[must_use]
444    pub fn for_target(boundary: &ResumeBoundaryId, kind: &str, target: &str) -> Self {
445        Self(format!(
446            "presolve-r:{:016x}",
447            canonical_hash(&format!("{boundary}\n{kind}\n{target}"))
448        ))
449    }
450}
451impl ResumeBuildId {
452    pub const ZERO_SENTINEL: &'static str =
453        "resume-build:0000000000000000000000000000000000000000000000000000000000000000";
454
455    #[must_use]
456    pub fn for_public_inputs(inputs: &str) -> Self {
457        let digest = Sha256::digest(inputs.as_bytes());
458        Self(format!("resume-build:{digest:x}"))
459    }
460
461    #[must_use]
462    pub fn zero_sentinel() -> Self {
463        Self(Self::ZERO_SENTINEL.to_string())
464    }
465}
466
467fn canonical_hash(value: &str) -> u64 {
468    value
469        .as_bytes()
470        .iter()
471        .fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
472            (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
473        })
474}
475fn percent_encode(value: &str) -> String {
476    value.bytes().fold(String::new(), |mut encoded, byte| {
477        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
478            encoded.push(char::from(byte));
479        } else {
480            use std::fmt::Write as _;
481            write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail");
482        }
483        encoded
484    })
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::{ComponentRootId, FormId};
491
492    #[test]
493    fn j1_resume_identities_are_typed_deterministic_and_instance_qualified() {
494        let component = SemanticId::component(Some("x-one"), "One");
495        let root = ComponentRootId::for_component(&component);
496        let first = ComponentInstanceId::for_root(&root);
497        let second = ComponentInstanceId::for_invocation(
498            &first,
499            &crate::ComponentInvocationId::for_template_entity(&component, "child"),
500        );
501        let form = FormId::for_owner(&component, "profile");
502        let first_form = FormInstanceId::for_component_instance(&first, &form);
503        let second_form = FormInstanceId::for_component_instance(&second, &form);
504        assert_ne!(
505            ResumeBoundaryId::component_instance(&first),
506            ResumeBoundaryId::component_instance(&second)
507        );
508        assert_ne!(
509            ResumeBoundaryId::form_instance(&first_form),
510            ResumeBoundaryId::form_instance(&second_form)
511        );
512        let template_entity = component.template().template_entity("element", "root");
513        let binding = component.template().template_entity("binding", "root.0");
514        assert_ne!(
515            TemplateInstanceTargetId::for_component_instance_template_entity(
516                first.clone(),
517                template_entity.clone(),
518            ),
519            TemplateInstanceTargetId::for_component_instance_template_entity(
520                second.clone(),
521                template_entity,
522            )
523        );
524        assert!(
525            TemplateInstanceBindingId::for_component_instance_binding(first, binding)
526                .to_string()
527                .contains("/template-binding:")
528        );
529        let event = SemanticId::component(Some("x-one-event"), "Event");
530        assert_eq!(
531            ResumeEventId::for_existing_event(event.as_str()).to_string(),
532            format!("resume-event:{event}")
533        );
534        let slot = ResumeSlotId::for_existing_storage("runtime-slot:one");
535        assert_eq!(
536            ResumeValueRecordId::for_slot(&slot).to_string(),
537            "resume-value:resume-slot:runtime-slot:one"
538        );
539        let chunk =
540            ResumeChunkId::for_activation_root(ResumeActivationRootKind::Event, event.as_str());
541        assert_eq!(
542            ResumeChunkGroupId::for_ordered_chunks(std::slice::from_ref(&chunk)),
543            ResumeChunkGroupId::for_ordered_chunks(&[chunk])
544        );
545        assert!("".parse::<ResumeBuildId>().is_err());
546    }
547
548    #[test]
549    fn j1a_state_slot_identity_preserves_the_exact_typed_pair() {
550        let component = SemanticId::component(Some("x-state"), "State");
551        let instance = ComponentInstanceId::for_root(&ComponentRootId::for_component(&component));
552        let storage = IrStorageId::for_semantic_origin(&component.state_field("count/value"));
553        let slot =
554            StateInstanceSlotId::for_component_instance_storage(instance.clone(), storage.clone());
555        assert_eq!(slot.component_instance_id(), &instance);
556        assert_eq!(slot.storage_id(), &storage);
557        assert_eq!(
558            slot.to_string(),
559            format!("{instance}/state-slot:storage%3Acomponent%3Ax-state%2Fstate%3Acount%2Fvalue")
560        );
561    }
562}