Skip to main content

presolve_compiler/
ordinary_template_instance.rs

1//! J1-P projection of ordinary template execution onto exact component instances.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use crate::component_graph::render_event_handlers;
6use crate::component_graph::SerializableValue;
7use crate::{
8    ApplicationSemanticModel, ComponentInstanceId, ComponentInstanceStatus, IrStorageId,
9    SemanticId, SemanticReferenceKind, SourceProvenance, TemplateInstanceBindingId,
10    TemplateInstanceTargetId, TemplateSemanticKind,
11};
12
13pub const ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION: u32 = 1;
14
15#[derive(Debug, Clone, Copy, Eq, PartialEq)]
16pub enum OrdinaryTemplateTargetKind {
17    Element,
18    AttributeOrPropertyHost,
19    EventHost,
20    ConditionalBoundary,
21    ListBoundary,
22    FormControlHost,
23    FormSubmissionHost,
24}
25
26#[derive(Debug, Clone, Copy, Eq, PartialEq)]
27pub enum OrdinaryTemplateBindingKind {
28    Text,
29    Attribute,
30    Property,
31    Conditional,
32    List,
33    FormControl,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct OrdinaryTemplateInstanceTargetRecord {
38    pub target_id: TemplateInstanceTargetId,
39    pub component_instance_id: ComponentInstanceId,
40    pub component_id: SemanticId,
41    pub template_entity_id: SemanticId,
42    pub target_kind: OrdinaryTemplateTargetKind,
43    pub provenance: SourceProvenance,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct OrdinaryTemplateInstanceBindingRecord {
48    pub instance_binding_id: TemplateInstanceBindingId,
49    pub component_instance_id: ComponentInstanceId,
50    pub component_id: SemanticId,
51    pub declaration_binding_id: SemanticId,
52    pub target_id: TemplateInstanceTargetId,
53    pub binding_kind: OrdinaryTemplateBindingKind,
54    pub state_storage_ids: Vec<IrStorageId>,
55    pub computed_ids: Vec<SemanticId>,
56    pub existing_program_identity: SemanticId,
57    pub expression: Option<String>,
58    pub attribute_name: Option<String>,
59    pub provenance: SourceProvenance,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct OrdinaryTemplateInstanceEventRecord {
64    pub component_instance_id: ComponentInstanceId,
65    pub component_id: SemanticId,
66    pub target_id: TemplateInstanceTargetId,
67    pub declaration_event_id: SemanticId,
68    pub event_type: String,
69    pub handler_method_id: SemanticId,
70    pub action_batch_id: Option<SemanticId>,
71    pub arguments: Vec<SerializableValue>,
72    pub existing_event_program_identity: SemanticId,
73    pub provenance: SourceProvenance,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct OrdinaryTemplateInstanceRegistry {
78    pub version: u32,
79    pub targets: Vec<OrdinaryTemplateInstanceTargetRecord>,
80    pub bindings: Vec<OrdinaryTemplateInstanceBindingRecord>,
81    pub events: Vec<OrdinaryTemplateInstanceEventRecord>,
82}
83
84#[must_use]
85#[allow(clippy::too_many_lines)]
86pub fn build_ordinary_template_instance_registry(
87    model: &ApplicationSemanticModel,
88) -> OrdinaryTemplateInstanceRegistry {
89    let mut targets = BTreeMap::new();
90    let mut bindings = Vec::new();
91    let mut events = Vec::new();
92    for instance in model.component_instance_plan.instances.values() {
93        if !matches!(
94            instance.status,
95            ComponentInstanceStatus::Planned | ComponentInstanceStatus::StructuralTemplate
96        ) {
97            continue;
98        }
99        let template = instance.component.template();
100        let entities = model.template_entities_for(&template);
101        for entity in &entities {
102            let Some(binding_kind) = binding_kind(entity.kind) else {
103                continue;
104            };
105            let target_entity = target_entity_for_binding(entity, &entities);
106            let target_id = TemplateInstanceTargetId::for_component_instance_template_entity(
107                instance.id.clone(),
108                target_entity.id.clone(),
109            );
110            targets.entry(target_id.clone()).or_insert_with(|| {
111                OrdinaryTemplateInstanceTargetRecord {
112                    target_id: target_id.clone(),
113                    component_instance_id: instance.id.clone(),
114                    component_id: instance.component.clone(),
115                    template_entity_id: target_entity.id.clone(),
116                    target_kind: target_kind(target_entity.kind),
117                    provenance: target_entity.provenance.clone(),
118                }
119            });
120            let (state_storage_ids, computed_ids) = binding_dependencies(model, &entity.id);
121            bindings.push(OrdinaryTemplateInstanceBindingRecord {
122                instance_binding_id: TemplateInstanceBindingId::for_component_instance_binding(
123                    instance.id.clone(),
124                    entity.id.clone(),
125                ),
126                component_instance_id: instance.id.clone(),
127                component_id: instance.component.clone(),
128                declaration_binding_id: entity.id.clone(),
129                target_id,
130                binding_kind,
131                state_storage_ids,
132                computed_ids,
133                existing_program_identity: entity.id.clone(),
134                expression: entity.expression.clone(),
135                attribute_name: entity.attribute_name.clone(),
136                provenance: entity.provenance.clone(),
137            });
138        }
139        if let Some(component) = model.component(&instance.component) {
140            if let Some(render) = &component.render {
141                for event in render_event_handlers(render) {
142                    let Some(event_entity) = entities.iter().find(|entity| {
143                        entity.kind == TemplateSemanticKind::EventAttribute
144                            && provenance_matches_span(&entity.provenance, &event.span)
145                    }) else {
146                        continue;
147                    };
148                    let target_entity = target_entity_for_event(event_entity, &entities);
149                    let target_id =
150                        TemplateInstanceTargetId::for_component_instance_template_entity(
151                            instance.id.clone(),
152                            target_entity.id.clone(),
153                        );
154                    let name = event
155                        .handler
156                        .strip_prefix("this.")
157                        .unwrap_or(&event.handler);
158                    if let Some(endpoint) = component.action_endpoint_id(name) {
159                        let Some(action_batch_id) = model
160                            .effect_trigger_plan
161                            .action_batches
162                            .values()
163                            .find(|batch| batch.authored_action_endpoint == endpoint)
164                            .map(|batch| batch.id.clone())
165                        else {
166                            continue;
167                        };
168                        targets.entry(target_id.clone()).or_insert_with(|| {
169                            OrdinaryTemplateInstanceTargetRecord {
170                                target_id: target_id.clone(),
171                                component_instance_id: instance.id.clone(),
172                                component_id: instance.component.clone(),
173                                template_entity_id: target_entity.id.clone(),
174                                target_kind: target_kind(target_entity.kind),
175                                provenance: target_entity.provenance.clone(),
176                            }
177                        });
178                        events.push(OrdinaryTemplateInstanceEventRecord {
179                            component_instance_id: instance.id.clone(),
180                            component_id: instance.component.clone(),
181                            target_id,
182                            declaration_event_id: event.id.clone(),
183                            event_type: event.event.clone(),
184                            handler_method_id: endpoint,
185                            action_batch_id: Some(action_batch_id),
186                            arguments: event.arguments.clone(),
187                            existing_event_program_identity: event.id.clone(),
188                            provenance: model
189                                .provenance(&event.id)
190                                .cloned()
191                                .unwrap_or_else(|| event_entity.provenance.clone()),
192                        });
193                    }
194                }
195            }
196        }
197        // Forms retain their Phase-I ownership and programs. This bridge only
198        // attaches their already-canonical control/host entities to the same
199        // ordinary instance target table used by all other executable DOM.
200        for binding in model
201            .form_field_bindings
202            .values()
203            .filter(|binding| binding.component == instance.component)
204        {
205            if !model
206                .optimized_form_ir
207                .optimized
208                .instances
209                .values()
210                .any(|form| form.form == binding.form && form.component_instance == instance.id)
211            {
212                continue;
213            }
214            insert_form_target(
215                &mut targets,
216                instance,
217                binding.control_entity.clone(),
218                OrdinaryTemplateTargetKind::FormControlHost,
219                binding.provenance.clone(),
220            );
221        }
222        for host in model
223            .submission_hosts
224            .values()
225            .filter(|host| host.component == instance.component)
226        {
227            if !model
228                .optimized_form_ir
229                .optimized
230                .instances
231                .values()
232                .any(|form| form.form == host.form && form.component_instance == instance.id)
233            {
234                continue;
235            }
236            insert_form_target(
237                &mut targets,
238                instance,
239                host.owner_template_element.clone(),
240                OrdinaryTemplateTargetKind::FormSubmissionHost,
241                host.provenance.clone(),
242            );
243        }
244    }
245    let mut targets = targets.into_values().collect::<Vec<_>>();
246    targets.sort_by(|left, right| left.target_id.cmp(&right.target_id));
247    bindings.sort_by(|left, right| left.instance_binding_id.cmp(&right.instance_binding_id));
248    events.sort_by(|left, right| {
249        (&left.component_instance_id, &left.declaration_event_id)
250            .cmp(&(&right.component_instance_id, &right.declaration_event_id))
251    });
252    OrdinaryTemplateInstanceRegistry {
253        version: ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
254        targets,
255        bindings,
256        events,
257    }
258}
259
260fn binding_kind(kind: TemplateSemanticKind) -> Option<OrdinaryTemplateBindingKind> {
261    match kind {
262        TemplateSemanticKind::AttributeBinding => Some(OrdinaryTemplateBindingKind::Attribute),
263        TemplateSemanticKind::Binding => Some(OrdinaryTemplateBindingKind::Text),
264        TemplateSemanticKind::Conditional => Some(OrdinaryTemplateBindingKind::Conditional),
265        TemplateSemanticKind::List => Some(OrdinaryTemplateBindingKind::List),
266        TemplateSemanticKind::Fragment
267        | TemplateSemanticKind::Element
268        | TemplateSemanticKind::Text
269        | TemplateSemanticKind::Attribute
270        | TemplateSemanticKind::EventAttribute => None,
271    }
272}
273
274const fn target_kind(kind: TemplateSemanticKind) -> OrdinaryTemplateTargetKind {
275    match kind {
276        TemplateSemanticKind::Conditional => OrdinaryTemplateTargetKind::ConditionalBoundary,
277        TemplateSemanticKind::List => OrdinaryTemplateTargetKind::ListBoundary,
278        _ => OrdinaryTemplateTargetKind::Element,
279    }
280}
281
282fn target_entity_for_binding<'a>(
283    entity: &'a crate::TemplateSemanticEntity,
284    entities: &[&'a crate::TemplateSemanticEntity],
285) -> &'a crate::TemplateSemanticEntity {
286    match entity.kind {
287        TemplateSemanticKind::AttributeBinding => containing_element(entity, entities),
288        _ => entity,
289    }
290}
291
292fn target_entity_for_event<'a>(
293    entity: &'a crate::TemplateSemanticEntity,
294    entities: &[&'a crate::TemplateSemanticEntity],
295) -> &'a crate::TemplateSemanticEntity {
296    containing_element(entity, entities)
297}
298
299fn containing_element<'a>(
300    entity: &'a crate::TemplateSemanticEntity,
301    entities: &[&'a crate::TemplateSemanticEntity],
302) -> &'a crate::TemplateSemanticEntity {
303    entities
304        .iter()
305        .copied()
306        .filter(|candidate| {
307            candidate.kind == TemplateSemanticKind::Element
308                && provenance_contains(&candidate.provenance, &entity.provenance)
309        })
310        .min_by_key(|candidate| candidate.provenance.span.end - candidate.provenance.span.start)
311        .unwrap_or(entity)
312}
313
314fn provenance_contains(parent: &SourceProvenance, child: &SourceProvenance) -> bool {
315    parent.path == child.path
316        && parent.span.start <= child.span.start
317        && parent.span.end >= child.span.end
318}
319
320fn provenance_matches_span(
321    provenance: &SourceProvenance,
322    span: &presolve_parser::SourceSpan,
323) -> bool {
324    provenance.span.start == span.start && provenance.span.end == span.end
325}
326
327fn insert_form_target(
328    targets: &mut BTreeMap<TemplateInstanceTargetId, OrdinaryTemplateInstanceTargetRecord>,
329    instance: &crate::ComponentInstance,
330    template_entity_id: SemanticId,
331    target_kind: OrdinaryTemplateTargetKind,
332    provenance: SourceProvenance,
333) {
334    let target_id = TemplateInstanceTargetId::for_component_instance_template_entity(
335        instance.id.clone(),
336        template_entity_id.clone(),
337    );
338    targets
339        .entry(target_id.clone())
340        .or_insert(OrdinaryTemplateInstanceTargetRecord {
341            target_id,
342            component_instance_id: instance.id.clone(),
343            component_id: instance.component.clone(),
344            template_entity_id,
345            target_kind,
346            provenance,
347        });
348}
349
350fn binding_dependencies(
351    model: &ApplicationSemanticModel,
352    binding: &SemanticId,
353) -> (Vec<IrStorageId>, Vec<SemanticId>) {
354    let state_storage_ids = model
355        .references_from(binding)
356        .into_iter()
357        .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
358        .map(|reference| IrStorageId::for_semantic_origin(&reference.target))
359        .collect::<BTreeSet<_>>()
360        .into_iter()
361        .collect();
362    let computed_ids = model
363        .references_from(binding)
364        .into_iter()
365        .filter(|reference| reference.kind == SemanticReferenceKind::TemplateComputed)
366        .map(|reference| reference.target.clone())
367        .collect();
368    (state_storage_ids, computed_ids)
369}
370
371/// # Errors
372///
373/// Returns an error when a retained record is malformed, duplicate, or no
374/// longer equals the canonical Phase H/template projection.
375pub fn validate_ordinary_template_instance_registry(
376    model: &ApplicationSemanticModel,
377    registry: &OrdinaryTemplateInstanceRegistry,
378) -> Result<(), String> {
379    if registry.version != ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION {
380        return Err("unsupported ordinary template instance registry version".to_string());
381    }
382    if registry != &build_ordinary_template_instance_registry(model) {
383        return Err(
384            "ordinary template instance registry drifted from canonical products".to_string(),
385        );
386    }
387    let mut targets = BTreeSet::new();
388    let mut bindings = BTreeSet::new();
389    for target in &registry.targets {
390        if target.target_id
391            != TemplateInstanceTargetId::for_component_instance_template_entity(
392                target.component_instance_id.clone(),
393                target.template_entity_id.clone(),
394            )
395            || !targets.insert(target.target_id.clone())
396        {
397            return Err("ordinary template target is duplicate or malformed".to_string());
398        }
399    }
400    for binding in &registry.bindings {
401        if binding.instance_binding_id
402            != TemplateInstanceBindingId::for_component_instance_binding(
403                binding.component_instance_id.clone(),
404                binding.declaration_binding_id.clone(),
405            )
406            || !targets.contains(&binding.target_id)
407            || !bindings.insert(binding.instance_binding_id.clone())
408        {
409            return Err("ordinary template binding is duplicate or malformed".to_string());
410        }
411    }
412    Ok(())
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use crate::{build_application_semantic_model_for_unit, CompilationUnit};
419
420    #[test]
421    fn projects_distinct_targets_and_bindings_for_repeated_instances() {
422        let source = presolve_parser::parse_file(
423            "src/Repeated.tsx",
424            r#"
425@component("x-child") class Child {
426  count = state(1);
427  @action()
428  increment() { this.count++; }
429  render() { return <button title={this.count} onClick={() => this.increment()}>{this.count}</button>; }
430}
431@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }
432"#,
433        );
434        let model =
435            build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
436                source,
437            ]));
438        let registry = build_ordinary_template_instance_registry(&model);
439        let child_targets = registry
440            .targets
441            .iter()
442            .filter(|target| target.component_id.as_str().contains("x-child"))
443            .collect::<Vec<_>>();
444        assert!(child_targets.len() >= 2);
445        assert!(child_targets
446            .windows(2)
447            .any(|pair| pair[0].target_id != pair[1].target_id));
448        let child_events = registry
449            .events
450            .iter()
451            .filter(|event| event.component_id.as_str().contains("x-child"))
452            .collect::<Vec<_>>();
453        assert_eq!(child_events.len(), 2);
454        assert_ne!(child_events[0].target_id, child_events[1].target_id);
455        assert!(registry.bindings.iter().all(|binding| {
456            registry
457                .targets
458                .iter()
459                .any(|target| target.target_id == binding.target_id)
460        }));
461        assert!(validate_ordinary_template_instance_registry(&model, &registry).is_ok());
462    }
463}