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 method = component.methods.iter().find(|method| {
155                        method.name
156                            == event
157                                .handler
158                                .strip_prefix("this.")
159                                .unwrap_or(&event.handler)
160                    });
161                    if let Some(method) = method {
162                        let Some(action_batch_id) = model
163                            .effect_trigger_plan
164                            .action_batches
165                            .values()
166                            .find(|batch| batch.authored_action_method == method.id)
167                            .map(|batch| batch.id.clone())
168                        else {
169                            continue;
170                        };
171                        targets.entry(target_id.clone()).or_insert_with(|| {
172                            OrdinaryTemplateInstanceTargetRecord {
173                                target_id: target_id.clone(),
174                                component_instance_id: instance.id.clone(),
175                                component_id: instance.component.clone(),
176                                template_entity_id: target_entity.id.clone(),
177                                target_kind: target_kind(target_entity.kind),
178                                provenance: target_entity.provenance.clone(),
179                            }
180                        });
181                        events.push(OrdinaryTemplateInstanceEventRecord {
182                            component_instance_id: instance.id.clone(),
183                            component_id: instance.component.clone(),
184                            target_id,
185                            declaration_event_id: event.id.clone(),
186                            event_type: event.event.clone(),
187                            handler_method_id: method.id.clone(),
188                            action_batch_id: Some(action_batch_id),
189                            arguments: event.arguments.clone(),
190                            existing_event_program_identity: event.id.clone(),
191                            provenance: model
192                                .provenance(&event.id)
193                                .cloned()
194                                .unwrap_or_else(|| event_entity.provenance.clone()),
195                        });
196                    }
197                }
198            }
199        }
200        // Forms retain their Phase-I ownership and programs. This bridge only
201        // attaches their already-canonical control/host entities to the same
202        // ordinary instance target table used by all other executable DOM.
203        for binding in model
204            .form_field_bindings
205            .values()
206            .filter(|binding| binding.component == instance.component)
207        {
208            if !model
209                .optimized_form_ir
210                .optimized
211                .instances
212                .values()
213                .any(|form| form.form == binding.form && form.component_instance == instance.id)
214            {
215                continue;
216            }
217            insert_form_target(
218                &mut targets,
219                instance,
220                binding.control_entity.clone(),
221                OrdinaryTemplateTargetKind::FormControlHost,
222                binding.provenance.clone(),
223            );
224        }
225        for host in model
226            .submission_hosts
227            .values()
228            .filter(|host| host.component == instance.component)
229        {
230            if !model
231                .optimized_form_ir
232                .optimized
233                .instances
234                .values()
235                .any(|form| form.form == host.form && form.component_instance == instance.id)
236            {
237                continue;
238            }
239            insert_form_target(
240                &mut targets,
241                instance,
242                host.owner_template_element.clone(),
243                OrdinaryTemplateTargetKind::FormSubmissionHost,
244                host.provenance.clone(),
245            );
246        }
247    }
248    let mut targets = targets.into_values().collect::<Vec<_>>();
249    targets.sort_by(|left, right| left.target_id.cmp(&right.target_id));
250    bindings.sort_by(|left, right| left.instance_binding_id.cmp(&right.instance_binding_id));
251    events.sort_by(|left, right| {
252        (&left.component_instance_id, &left.declaration_event_id)
253            .cmp(&(&right.component_instance_id, &right.declaration_event_id))
254    });
255    OrdinaryTemplateInstanceRegistry {
256        version: ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION,
257        targets,
258        bindings,
259        events,
260    }
261}
262
263fn binding_kind(kind: TemplateSemanticKind) -> Option<OrdinaryTemplateBindingKind> {
264    match kind {
265        TemplateSemanticKind::AttributeBinding => Some(OrdinaryTemplateBindingKind::Attribute),
266        TemplateSemanticKind::Binding => Some(OrdinaryTemplateBindingKind::Text),
267        TemplateSemanticKind::Conditional => Some(OrdinaryTemplateBindingKind::Conditional),
268        TemplateSemanticKind::List => Some(OrdinaryTemplateBindingKind::List),
269        TemplateSemanticKind::Fragment
270        | TemplateSemanticKind::Element
271        | TemplateSemanticKind::Text
272        | TemplateSemanticKind::Attribute
273        | TemplateSemanticKind::EventAttribute => None,
274    }
275}
276
277const fn target_kind(kind: TemplateSemanticKind) -> OrdinaryTemplateTargetKind {
278    match kind {
279        TemplateSemanticKind::Conditional => OrdinaryTemplateTargetKind::ConditionalBoundary,
280        TemplateSemanticKind::List => OrdinaryTemplateTargetKind::ListBoundary,
281        _ => OrdinaryTemplateTargetKind::Element,
282    }
283}
284
285fn target_entity_for_binding<'a>(
286    entity: &'a crate::TemplateSemanticEntity,
287    entities: &[&'a crate::TemplateSemanticEntity],
288) -> &'a crate::TemplateSemanticEntity {
289    match entity.kind {
290        TemplateSemanticKind::AttributeBinding => containing_element(entity, entities),
291        _ => entity,
292    }
293}
294
295fn target_entity_for_event<'a>(
296    entity: &'a crate::TemplateSemanticEntity,
297    entities: &[&'a crate::TemplateSemanticEntity],
298) -> &'a crate::TemplateSemanticEntity {
299    containing_element(entity, entities)
300}
301
302fn containing_element<'a>(
303    entity: &'a crate::TemplateSemanticEntity,
304    entities: &[&'a crate::TemplateSemanticEntity],
305) -> &'a crate::TemplateSemanticEntity {
306    entities
307        .iter()
308        .copied()
309        .filter(|candidate| {
310            candidate.kind == TemplateSemanticKind::Element
311                && provenance_contains(&candidate.provenance, &entity.provenance)
312        })
313        .min_by_key(|candidate| candidate.provenance.span.end - candidate.provenance.span.start)
314        .unwrap_or(entity)
315}
316
317fn provenance_contains(parent: &SourceProvenance, child: &SourceProvenance) -> bool {
318    parent.path == child.path
319        && parent.span.start <= child.span.start
320        && parent.span.end >= child.span.end
321}
322
323fn provenance_matches_span(
324    provenance: &SourceProvenance,
325    span: &presolve_parser::SourceSpan,
326) -> bool {
327    provenance.span.start == span.start && provenance.span.end == span.end
328}
329
330fn insert_form_target(
331    targets: &mut BTreeMap<TemplateInstanceTargetId, OrdinaryTemplateInstanceTargetRecord>,
332    instance: &crate::ComponentInstance,
333    template_entity_id: SemanticId,
334    target_kind: OrdinaryTemplateTargetKind,
335    provenance: SourceProvenance,
336) {
337    let target_id = TemplateInstanceTargetId::for_component_instance_template_entity(
338        instance.id.clone(),
339        template_entity_id.clone(),
340    );
341    targets
342        .entry(target_id.clone())
343        .or_insert(OrdinaryTemplateInstanceTargetRecord {
344            target_id,
345            component_instance_id: instance.id.clone(),
346            component_id: instance.component.clone(),
347            template_entity_id,
348            target_kind,
349            provenance,
350        });
351}
352
353fn binding_dependencies(
354    model: &ApplicationSemanticModel,
355    binding: &SemanticId,
356) -> (Vec<IrStorageId>, Vec<SemanticId>) {
357    let state_storage_ids = model
358        .references_from(binding)
359        .into_iter()
360        .filter(|reference| reference.kind == SemanticReferenceKind::TemplateState)
361        .map(|reference| IrStorageId::for_semantic_origin(&reference.target))
362        .collect::<BTreeSet<_>>()
363        .into_iter()
364        .collect();
365    let computed_ids = model
366        .references_from(binding)
367        .into_iter()
368        .filter(|reference| reference.kind == SemanticReferenceKind::TemplateComputed)
369        .map(|reference| reference.target.clone())
370        .collect();
371    (state_storage_ids, computed_ids)
372}
373
374/// # Errors
375///
376/// Returns an error when a retained record is malformed, duplicate, or no
377/// longer equals the canonical Phase H/template projection.
378pub fn validate_ordinary_template_instance_registry(
379    model: &ApplicationSemanticModel,
380    registry: &OrdinaryTemplateInstanceRegistry,
381) -> Result<(), String> {
382    if registry.version != ORDINARY_TEMPLATE_INSTANCE_REGISTRY_VERSION {
383        return Err("unsupported ordinary template instance registry version".to_string());
384    }
385    if registry != &build_ordinary_template_instance_registry(model) {
386        return Err(
387            "ordinary template instance registry drifted from canonical products".to_string(),
388        );
389    }
390    let mut targets = BTreeSet::new();
391    let mut bindings = BTreeSet::new();
392    for target in &registry.targets {
393        if target.target_id
394            != TemplateInstanceTargetId::for_component_instance_template_entity(
395                target.component_instance_id.clone(),
396                target.template_entity_id.clone(),
397            )
398            || !targets.insert(target.target_id.clone())
399        {
400            return Err("ordinary template target is duplicate or malformed".to_string());
401        }
402    }
403    for binding in &registry.bindings {
404        if binding.instance_binding_id
405            != TemplateInstanceBindingId::for_component_instance_binding(
406                binding.component_instance_id.clone(),
407                binding.declaration_binding_id.clone(),
408            )
409            || !targets.contains(&binding.target_id)
410            || !bindings.insert(binding.instance_binding_id.clone())
411        {
412            return Err("ordinary template binding is duplicate or malformed".to_string());
413        }
414    }
415    Ok(())
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use crate::{build_application_semantic_model_for_unit, CompilationUnit};
422
423    #[test]
424    fn projects_distinct_targets_and_bindings_for_repeated_instances() {
425        let source = presolve_parser::parse_file(
426            "src/Repeated.tsx",
427            r#"
428@component("x-child") class Child {
429  count = state(1);
430  @action()
431  increment() { this.count++; }
432  render() { return <button title={this.count} onClick={() => this.increment()}>{this.count}</button>; }
433}
434@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }
435"#,
436        );
437        let model =
438            build_application_semantic_model_for_unit(&CompilationUnit::from_parsed_files(vec![
439                source,
440            ]));
441        let registry = build_ordinary_template_instance_registry(&model);
442        let child_targets = registry
443            .targets
444            .iter()
445            .filter(|target| target.component_id.as_str().contains("x-child"))
446            .collect::<Vec<_>>();
447        assert!(child_targets.len() >= 2);
448        assert!(child_targets
449            .windows(2)
450            .any(|pair| pair[0].target_id != pair[1].target_id));
451        let child_events = registry
452            .events
453            .iter()
454            .filter(|event| event.component_id.as_str().contains("x-child"))
455            .collect::<Vec<_>>();
456        assert_eq!(child_events.len(), 2);
457        assert_ne!(child_events[0].target_id, child_events[1].target_id);
458        assert!(registry.bindings.iter().all(|binding| {
459            registry
460                .targets
461                .iter()
462                .any(|target| target.target_id == binding.target_id)
463        }));
464        assert!(validate_ordinary_template_instance_registry(&model, &registry).is_ok());
465    }
466}