Skip to main content

presolve_compiler/
runtime_effect_instance.rs

1use crate::{
2    ApplicationSemanticModel, ComponentInstanceId, ComponentInstanceStatus,
3    ComponentStructuralRegionId, EffectDeclaration, EffectInstanceId, EffectValidation, SemanticId,
4};
5
6pub const RUNTIME_EFFECT_INSTANCE_REGISTRY_VERSION: u32 = 1;
7
8/// Compiler-owned join of a V2 effect declaration with one planned component
9/// instance. This is metadata only; capability execution remains unavailable
10/// until instance-context programs are emitted.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct RuntimeEffectInstanceRecord {
13    pub id: EffectInstanceId,
14    pub effect: SemanticId,
15    pub component_instance: ComponentInstanceId,
16    pub parent_instance: Option<ComponentInstanceId>,
17    pub depth: usize,
18    pub declaration_order: u32,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct RuntimeEffectInstanceRegistry {
23    pub version: u32,
24    pub records: Vec<RuntimeEffectInstanceRecord>,
25}
26
27/// Inactive compiler template for a V2 effect below a structural component
28/// region. Runtime activation must issue a distinct occurrence identity.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct RuntimeEffectStructuralTemplateRecord {
31    pub template_instance: ComponentInstanceId,
32    pub effect: SemanticId,
33    pub parent_instance: Option<ComponentInstanceId>,
34    pub structural_region: ComponentStructuralRegionId,
35    pub depth: usize,
36    pub declaration_order: u32,
37}
38
39#[must_use]
40pub fn build_runtime_effect_structural_template_registry(
41    model: &ApplicationSemanticModel,
42) -> Vec<RuntimeEffectStructuralTemplateRecord> {
43    let mut records = model
44        .component_instance_plan
45        .instances
46        .values()
47        .filter(|instance| instance.status == ComponentInstanceStatus::StructuralTemplate)
48        .filter_map(|instance| Some((instance, instance.structural_region.as_ref()?)))
49        .flat_map(|(instance, region)| {
50            model
51                .effects
52                .values()
53                .filter(move |effect| {
54                    matches!(effect.declaration, EffectDeclaration::V2Field)
55                        && effect.owner.entity_id() == Some(&instance.component)
56                        && effect.validation == EffectValidation::Valid
57                })
58                .map(move |effect| RuntimeEffectStructuralTemplateRecord {
59                    template_instance: instance.id.clone(),
60                    effect: effect.id.clone(),
61                    parent_instance: instance.parent_instance.clone(),
62                    structural_region: region.clone(),
63                    depth: instance.depth,
64                    declaration_order: effect
65                        .declaration_order
66                        .expect("V2 effect fields retain declaration order"),
67                })
68        })
69        .collect::<Vec<_>>();
70    records.sort_by(|left, right| {
71        (
72            &left.structural_region,
73            &left.template_instance,
74            left.declaration_order,
75            &left.effect,
76        )
77            .cmp(&(
78                &right.structural_region,
79                &right.template_instance,
80                right.declaration_order,
81                &right.effect,
82            ))
83    });
84    records
85}
86
87/// Project valid V2 field effects onto the canonical planned instance topology.
88///
89/// This function joins only canonical effect and component-instance facts. It
90/// deliberately excludes structural templates and legacy decorator effects.
91#[must_use]
92pub fn build_runtime_effect_instance_registry(
93    model: &ApplicationSemanticModel,
94) -> RuntimeEffectInstanceRegistry {
95    let mut records = model
96        .component_instance_plan
97        .instances
98        .values()
99        .filter(|instance| instance.status == ComponentInstanceStatus::Planned)
100        .flat_map(|instance| {
101            model
102                .effects
103                .values()
104                .filter(move |effect| {
105                    matches!(effect.declaration, EffectDeclaration::V2Field)
106                        && effect.owner.entity_id() == Some(&instance.component)
107                        && effect.validation == EffectValidation::Valid
108                })
109                .map(move |effect| RuntimeEffectInstanceRecord {
110                    id: EffectInstanceId::for_component_instance(&instance.id, &effect.id),
111                    effect: effect.id.clone(),
112                    component_instance: instance.id.clone(),
113                    parent_instance: instance.parent_instance.clone(),
114                    depth: instance.depth,
115                    declaration_order: effect
116                        .declaration_order
117                        .expect("V2 effect fields should retain declaration order"),
118                })
119        })
120        .collect::<Vec<_>>();
121    records.sort_by(|left, right| {
122        (
123            &left.component_instance,
124            left.declaration_order,
125            &left.effect,
126        )
127            .cmp(&(
128                &right.component_instance,
129                right.declaration_order,
130                &right.effect,
131            ))
132    });
133    RuntimeEffectInstanceRegistry {
134        version: RUNTIME_EFFECT_INSTANCE_REGISTRY_VERSION,
135        records,
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use crate::{
142        build_application_semantic_model, build_runtime_effect_instance_registry,
143        build_runtime_effect_structural_template_registry, EffectDeclaration,
144    };
145
146    #[test]
147    fn projects_repeated_nested_component_instances_without_name_inference() {
148        let mut model = build_application_semantic_model(&presolve_parser::parse_file(
149            "src/EffectInstances.tsx",
150            r#"
151@component("x-card") class Card extends Component {
152  @effect() report() { document.title = "card"; }
153  render() { return <article />; }
154}
155@component("x-page") class Page extends Component {
156  render() { return <main><Card /><Card /></main>; }
157}
158"#,
159        ));
160        let effect = model.components[0].id.effect("report");
161        let record = model.effects.get_mut(&effect).expect("Card effect");
162        record.declaration = EffectDeclaration::V2Field;
163        record.declaration_order = Some(0);
164        let registry = build_runtime_effect_instance_registry(&model);
165
166        assert_eq!(registry.version, 1);
167        assert_eq!(registry.records.len(), 2);
168        assert!(registry
169            .records
170            .iter()
171            .all(|record| record.effect == effect));
172        assert_ne!(registry.records[0].id, registry.records[1].id);
173        assert!(registry
174            .records
175            .iter()
176            .all(|record| record.parent_instance.is_some()));
177        assert!(registry.records.iter().all(|record| record.depth == 1));
178    }
179
180    #[test]
181    fn projects_inactive_effect_templates_for_structural_component_occurrences() {
182        let mut model = build_application_semantic_model(&presolve_parser::parse_file(
183            "src/StructuralEffects.tsx",
184            r#"
185@component("x-card") class Card extends Component { @effect() report() { document.title = "card"; } render() { return <article />; } }
186@component("x-page") class Page extends Component { visible = state(true); render() { return <main>{this.visible ? <Card /> : <span />}</main>; } }
187"#,
188        ));
189        let effect = model.components[0].id.effect("report");
190        let record = model.effects.get_mut(&effect).unwrap();
191        record.declaration = EffectDeclaration::V2Field;
192        record.declaration_order = Some(0);
193        let templates = build_runtime_effect_structural_template_registry(&model);
194        assert_eq!(templates.len(), 1);
195        assert_eq!(templates[0].effect, effect);
196        assert!(templates[0].parent_instance.is_some());
197    }
198}