Skip to main content

presolve_compiler/
runtime_component_artifact.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    build_computed_instance_slot_registry, build_ordinary_template_instance_registry,
5    build_runtime_component_registry, build_state_instance_storage_registry,
6    lower_components_to_ir, resume_value_codec, semantic_type_text, ApplicationSemanticModel,
7    OptimizedComponentIrReport, OrdinaryTemplateBindingKind, OrdinaryTemplateTargetKind,
8    ResumeValueCodec, RuntimeComponentRegistry, SerializationCompatibility,
9};
10use crate::{TemplateChild, TemplateNode, TemplateSemanticKind};
11
12pub const RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION: u32 = 20;
13
14/// Public H14 compiler artifact. All executable references are canonical IDs.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct RuntimeComponentArtifact {
17    pub schema_version: u32,
18    pub component_definitions: Vec<SerializedComponentDefinition>,
19    pub instances: Vec<SerializedComponentInstance>,
20    pub initialization_batches: Vec<SerializedComponentBatch>,
21    pub slot_binding_programs: Vec<SerializedSlotBinding>,
22    pub instance_context_bindings: Vec<SerializedInstanceContextBinding>,
23    pub ordinary_template_targets: Vec<SerializedOrdinaryTemplateTarget>,
24    pub ordinary_template_bindings: Vec<SerializedOrdinaryTemplateBinding>,
25    pub ordinary_template_events: Vec<SerializedOrdinaryTemplateEvent>,
26    pub destruction: SerializedDestructionMetadata,
27    pub structural_programs: Vec<SerializedStructuralComponentProgram>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub struct SerializedComponentDefinition {
32    pub component: String,
33    pub template: String,
34    pub slots: Vec<String>,
35    pub boundary: String,
36}
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct SerializedComponentInstance {
39    pub instance: String,
40    pub component: String,
41    pub invocation: Option<String>,
42    pub parent: Option<String>,
43    pub depth: usize,
44    pub initialization_batch: usize,
45    pub ordinary_template_targets: Vec<String>,
46    pub ordinary_template_bindings: Vec<String>,
47    pub ordinary_template_events: Vec<String>,
48    pub state_slots: Vec<SerializedRuntimeStateSlot>,
49    #[serde(skip_serializing_if = "Vec::is_empty")]
50    pub computed_slots: Vec<SerializedRuntimeComputedSlot>,
51    pub context_prefix: String,
52    pub instruction_indices: Vec<usize>,
53    pub structural_region: Option<String>,
54}
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct SerializedRuntimeStateSlot {
57    pub slot_id: String,
58    pub state_id: String,
59    pub storage_id: String,
60    pub initial_value: crate::SerializableValue,
61    pub semantic_type: String,
62    pub serializable: bool,
63    /// The compiler-issued closed resume codec for a serializable structural
64    /// occurrence slot. Static boundary schemas remain the authority for
65    /// static-instance capture.
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub resume_codec: Option<ResumeValueCodec>,
68}
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct SerializedRuntimeComputedSlot {
71    pub computed_id: String,
72    pub cache_slot_id: String,
73    pub dirty_slot_id: String,
74    pub dirty_initial_value: bool,
75}
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct SerializedOrdinaryTemplateTarget {
78    pub id: String,
79    pub component_instance_id: String,
80    pub component_id: String,
81    pub template_entity_id: String,
82    pub kind: String,
83}
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct SerializedOrdinaryTemplateBinding {
86    pub id: String,
87    pub component_instance_id: String,
88    pub component_id: String,
89    pub declaration_binding_id: String,
90    pub target_id: String,
91    pub kind: String,
92    pub state_storage_ids: Vec<String>,
93    pub computed_ids: Vec<String>,
94    pub program_id: String,
95    pub expression: Option<String>,
96    pub attribute_name: Option<String>,
97}
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct SerializedOrdinaryTemplateEvent {
100    pub component_instance_id: String,
101    pub component_id: String,
102    pub target_id: String,
103    pub declaration_event_id: String,
104    pub event_type: String,
105    pub handler_method_id: String,
106    pub action_batch_id: Option<String>,
107    #[serde(
108        default,
109        skip_serializing_if = "Vec::is_empty",
110        with = "crate::component_graph::runtime_arguments_serde"
111    )]
112    pub arguments: Vec<crate::component_graph::SerializableValue>,
113    pub program_id: String,
114}
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116pub struct SerializedComponentBatch {
117    pub index: usize,
118    pub instances: Vec<String>,
119    pub context_sources: Vec<String>,
120    pub slot_bindings: Vec<String>,
121}
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct SerializedSlotBinding {
124    pub binding: String,
125    pub caller_instance: String,
126    pub callee_instance: String,
127    pub slot: String,
128    pub outlet: String,
129    pub fragment: String,
130    pub content_owner_instance: String,
131}
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct SerializedInstanceContextBinding {
134    pub consumer_instance: String,
135    pub selected_source: String,
136    pub provider_source: Option<String>,
137    pub runtime_slot: String,
138}
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140pub struct SerializedDestructionMetadata {
141    pub operation: String,
142    pub enabled: bool,
143}
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145pub struct SerializedStructuralComponentProgram {
146    pub region: String,
147    /// Canonical component that owns the conditional or keyed-list host.
148    pub host_component: String,
149    /// Exact compiler-generated template node ID for the structural host.
150    pub host_node: String,
151    /// Exact semantic template entity for the conditional or keyed-list host.
152    pub host_template_entity: String,
153    /// Compiler-rendered conditional branches for initially static host
154    /// instances. Keyed-list and nested host scopes remain absent until their
155    /// complete compiler input scopes have an authored product.
156    pub conditional_host_fragments: Vec<SerializedStructuralConditionalHostFragments>,
157    /// Compiler-rendered keyed item fragments with exact structural invocation
158    /// anchors. They remain inactive until keyed materialization is admitted.
159    pub keyed_host_fragments: Vec<SerializedStructuralKeyedHostFragment>,
160    pub template_occurrences: Vec<SerializedStructuralTemplateOccurrence>,
161    pub template_instances: Vec<String>,
162    pub destroy_order: Vec<String>,
163    pub create_order: Vec<String>,
164}
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct SerializedStructuralConditionalHostFragments {
167    /// `static-instance` or `structural-occurrence`; selected by the compiler.
168    pub host_scope: String,
169    /// Exact static component instance or structural template instance that
170    /// owns the conditional according to `host_scope`.
171    pub host_instance: String,
172    pub when_true_html: String,
173    pub when_false_html: String,
174    /// Exact compiler-issued occurrence invocations anchored by each branch.
175    pub when_true_invocations: Vec<String>,
176    pub when_false_invocations: Vec<String>,
177    pub slot_projection_bindings: Vec<String>,
178    /// Exact compiler-selected caller-owned ordinary members rendered into
179    /// this host's Slot outlets. The runtime never discovers these from DOM.
180    pub slot_projection_programs: Vec<SerializedStructuralSlotProjectionProgram>,
181}
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct SerializedStructuralKeyedHostFragment {
184    pub host_scope: String,
185    pub host_instance: String,
186    pub item_template_html: String,
187    pub item_invocations: Vec<String>,
188    pub slot_projection_bindings: Vec<String>,
189    /// Exact compiler-selected caller-owned ordinary members rendered into
190    /// this host's Slot outlets. The runtime never discovers these from DOM.
191    pub slot_projection_programs: Vec<SerializedStructuralSlotProjectionProgram>,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195pub struct SerializedStructuralSlotProjectionProgram {
196    pub binding: String,
197    pub caller_instance: String,
198    pub content_owner_instance: String,
199    pub target_ids: Vec<String>,
200    pub binding_ids: Vec<String>,
201    pub event_ids: Vec<String>,
202    pub nested_invocations: Vec<String>,
203}
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub struct SerializedStructuralTemplateOccurrence {
206    pub template_instance: String,
207    pub invocation: String,
208    /// Exact semantic template entity for this invocation marker.
209    pub invocation_template_entity: String,
210    pub component: String,
211    /// Compiler template parent. The runtime substitutes only the opaque
212    /// occurrence identity prefix when restoring a dynamic descendant.
213    pub parent_template_instance: String,
214    /// Compiler template slots; runtime occurrence identity replaces only the
215    /// template-instance prefix when materialization is later admitted.
216    pub state_slots: Vec<SerializedRuntimeStateSlot>,
217    pub computed_slots: Vec<SerializedRuntimeComputedSlot>,
218    /// Compiler-rendered target component template. It remains inactive until
219    /// the materializer consumes it under an opaque occurrence identity.
220    pub template_html: String,
221    pub nested_invocations: Vec<String>,
222    /// Inactive compiler-owned template projection for a future materializer.
223    /// These IDs must be used as emitted; they are never selected from the DOM.
224    pub ordinary_template_targets: Vec<String>,
225    pub ordinary_template_bindings: Vec<String>,
226    pub ordinary_template_events: Vec<String>,
227}
228
229const fn target_kind_text(kind: OrdinaryTemplateTargetKind) -> &'static str {
230    match kind {
231        OrdinaryTemplateTargetKind::Element => "element",
232        OrdinaryTemplateTargetKind::AttributeOrPropertyHost => "attribute_or_property_host",
233        OrdinaryTemplateTargetKind::EventHost => "event_host",
234        OrdinaryTemplateTargetKind::ConditionalBoundary => "conditional_boundary",
235        OrdinaryTemplateTargetKind::ListBoundary => "list_boundary",
236        OrdinaryTemplateTargetKind::FormControlHost => "form_control_host",
237        OrdinaryTemplateTargetKind::FormSubmissionHost => "form_submission_host",
238    }
239}
240
241const fn binding_kind_text(kind: OrdinaryTemplateBindingKind) -> &'static str {
242    match kind {
243        OrdinaryTemplateBindingKind::Text => "text",
244        OrdinaryTemplateBindingKind::Attribute => "attribute",
245        OrdinaryTemplateBindingKind::Property => "property",
246        OrdinaryTemplateBindingKind::Conditional => "conditional",
247        OrdinaryTemplateBindingKind::List => "list",
248        OrdinaryTemplateBindingKind::FormControl => "form_control",
249    }
250}
251
252#[must_use]
253#[allow(clippy::too_many_lines)]
254pub fn build_runtime_component_artifact(
255    model: &ApplicationSemanticModel,
256    optimized: &OptimizedComponentIrReport,
257) -> RuntimeComponentArtifact {
258    let ordinary = build_ordinary_template_instance_registry(model);
259    let ir = lower_components_to_ir(model);
260    let state_slots = build_state_instance_storage_registry(model, &ir);
261    let computed_slots = build_computed_instance_slot_registry(model, &ir);
262    let mut artifact = artifact_from_registry(&build_runtime_component_registry(model, optimized));
263    artifact.ordinary_template_targets = ordinary
264        .targets
265        .iter()
266        .map(|target| SerializedOrdinaryTemplateTarget {
267            id: target.target_id.to_string(),
268            component_instance_id: target.component_instance_id.to_string(),
269            component_id: target.component_id.to_string(),
270            template_entity_id: target.template_entity_id.to_string(),
271            kind: target_kind_text(target.target_kind).to_string(),
272        })
273        .collect();
274    artifact.ordinary_template_bindings = ordinary
275        .bindings
276        .iter()
277        .map(|binding| SerializedOrdinaryTemplateBinding {
278            id: binding.instance_binding_id.to_string(),
279            component_instance_id: binding.component_instance_id.to_string(),
280            component_id: binding.component_id.to_string(),
281            declaration_binding_id: binding.declaration_binding_id.to_string(),
282            target_id: binding.target_id.to_string(),
283            kind: binding_kind_text(binding.binding_kind).to_string(),
284            state_storage_ids: binding
285                .state_storage_ids
286                .iter()
287                .map(ToString::to_string)
288                .collect(),
289            computed_ids: binding
290                .computed_ids
291                .iter()
292                .map(ToString::to_string)
293                .collect(),
294            program_id: binding.existing_program_identity.to_string(),
295            expression: binding.expression.clone(),
296            attribute_name: binding.attribute_name.clone(),
297        })
298        .collect();
299    artifact.ordinary_template_events = ordinary
300        .events
301        .iter()
302        .map(|event| SerializedOrdinaryTemplateEvent {
303            component_instance_id: event.component_instance_id.to_string(),
304            component_id: event.component_id.to_string(),
305            target_id: event.target_id.to_string(),
306            declaration_event_id: event.declaration_event_id.to_string(),
307            event_type: event.event_type.clone(),
308            handler_method_id: event.handler_method_id.to_string(),
309            action_batch_id: event.action_batch_id.as_ref().map(ToString::to_string),
310            arguments: event.arguments.clone(),
311            program_id: event.existing_event_program_identity.to_string(),
312        })
313        .collect();
314    for instance in &mut artifact.instances {
315        instance.ordinary_template_targets = artifact
316            .ordinary_template_targets
317            .iter()
318            .filter(|target| target.component_instance_id == instance.instance)
319            .map(|target| target.id.clone())
320            .collect();
321        instance.ordinary_template_bindings = artifact
322            .ordinary_template_bindings
323            .iter()
324            .filter(|binding| binding.component_instance_id == instance.instance)
325            .map(|binding| binding.id.clone())
326            .collect();
327        instance.ordinary_template_events = artifact
328            .ordinary_template_events
329            .iter()
330            .filter(|event| event.component_instance_id == instance.instance)
331            .map(|event| event.declaration_event_id.clone())
332            .collect();
333        instance.state_slots = state_slots
334            .records
335            .iter()
336            .filter(|slot| slot.component_instance_id.to_string() == instance.instance)
337            .map(|slot| SerializedRuntimeStateSlot {
338                slot_id: slot.slot_id.to_string(),
339                state_id: slot.state_id.to_string(),
340                storage_id: slot.storage_id.to_string(),
341                initial_value: slot.initial_value.clone(),
342                semantic_type: semantic_type_text(&slot.semantic_type),
343                serializable: slot.serialization == SerializationCompatibility::Serializable,
344                resume_codec: (slot.serialization == SerializationCompatibility::Serializable)
345                    .then(|| resume_value_codec(&slot.semantic_type).ok())
346                    .flatten(),
347            })
348            .collect();
349        instance.computed_slots = computed_slots
350            .records
351            .iter()
352            .filter(|slot| slot.component_instance_id.to_string() == instance.instance)
353            .map(|slot| SerializedRuntimeComputedSlot {
354                computed_id: slot.computed_id.to_string(),
355                cache_slot_id: slot.cache_slot_id.to_string(),
356                dirty_slot_id: slot.dirty_slot_id.to_string(),
357                dirty_initial_value: slot.dirty_initial_value,
358            })
359            .collect();
360    }
361    let mut programs = std::collections::BTreeMap::<String, StructuralProgramBuild>::new();
362    for instance in model.component_instance_plan.instances.values() {
363        if instance.status == crate::ComponentInstanceStatus::StructuralTemplate {
364            if let Some(region) = &instance.structural_region {
365                let program = programs
366                    .entry(region.to_string())
367                    .or_insert_with(|| structural_program_build(model, region));
368                program.template_instances.push(instance.id.to_string());
369                program
370                    .template_occurrences
371                    .push(SerializedStructuralTemplateOccurrence {
372                        template_instance: instance.id.to_string(),
373                        invocation: instance
374                            .invocation
375                            .as_ref()
376                            .expect("structural template invocation")
377                            .to_string(),
378                        invocation_template_entity: model
379                            .component_invocations
380                            .get(
381                                instance
382                                    .invocation
383                                    .as_ref()
384                                    .expect("structural template invocation"),
385                            )
386                            .expect("structural template invocation is compiler-owned")
387                            .template_entity
388                            .to_string(),
389                        component: instance.component.to_string(),
390                        parent_template_instance: instance
391                            .parent_instance
392                            .as_ref()
393                            .expect("structural template parent")
394                            .to_string(),
395                        state_slots: state_slots
396                            .records
397                            .iter()
398                            .filter(|slot| slot.component_instance_id == instance.id)
399                            .map(|slot| SerializedRuntimeStateSlot {
400                                slot_id: slot.slot_id.to_string(),
401                                state_id: slot.state_id.to_string(),
402                                storage_id: slot.storage_id.to_string(),
403                                initial_value: slot.initial_value.clone(),
404                                semantic_type: semantic_type_text(&slot.semantic_type),
405                                serializable: slot.serialization
406                                    == SerializationCompatibility::Serializable,
407                                resume_codec: (slot.serialization
408                                    == SerializationCompatibility::Serializable)
409                                    .then(|| resume_value_codec(&slot.semantic_type).ok())
410                                    .flatten(),
411                            })
412                            .collect(),
413                        computed_slots: computed_slots
414                            .records
415                            .iter()
416                            .filter(|slot| slot.component_instance_id == instance.id)
417                            .map(|slot| SerializedRuntimeComputedSlot {
418                                computed_id: slot.computed_id.to_string(),
419                                cache_slot_id: slot.cache_slot_id.to_string(),
420                                dirty_slot_id: slot.dirty_slot_id.to_string(),
421                                dirty_initial_value: slot.dirty_initial_value,
422                            })
423                            .collect(),
424                        template_html: crate::generate_structural_template_instance_html(
425                            model,
426                            &instance.id,
427                        )
428                        .unwrap_or_default(),
429                        nested_invocations:
430                            crate::ordinary_html_codegen::structural_invocations_in_compiler_html(
431                                &crate::generate_structural_template_instance_html(
432                                    model,
433                                    &instance.id,
434                                )
435                                .unwrap_or_default(),
436                            ),
437                        ordinary_template_targets: artifact
438                            .ordinary_template_targets
439                            .iter()
440                            .filter(|target| {
441                                target.component_instance_id == instance.id.to_string()
442                            })
443                            .map(|target| target.id.clone())
444                            .collect(),
445                        ordinary_template_bindings: artifact
446                            .ordinary_template_bindings
447                            .iter()
448                            .filter(|binding| {
449                                binding.component_instance_id == instance.id.to_string()
450                            })
451                            .map(|binding| binding.id.clone())
452                            .collect(),
453                        ordinary_template_events: artifact
454                            .ordinary_template_events
455                            .iter()
456                            .filter(|event| event.component_instance_id == instance.id.to_string())
457                            .map(|event| event.declaration_event_id.clone())
458                            .collect(),
459                    });
460            }
461        }
462    }
463    artifact.structural_programs = programs
464        .into_iter()
465        .map(|(region, program)| SerializedStructuralComponentProgram {
466            region,
467            host_component: program.host_component,
468            host_node: program.host_node,
469            host_template_entity: program.host_template_entity,
470            conditional_host_fragments: crate::generate_structural_conditional_host_fragments(
471                model,
472                &program.region,
473            )
474            .into_iter()
475            .map(|fragments| {
476                let compiler_html =
477                    format!("{}{}", fragments.when_true_html, fragments.when_false_html);
478                SerializedStructuralConditionalHostFragments {
479                    host_scope: fragments.host_scope.artifact_text().to_string(),
480                    host_instance: fragments.host_instance.to_string(),
481                    when_true_html: fragments.when_true_html,
482                    when_false_html: fragments.when_false_html,
483                    when_true_invocations: fragments.when_true_invocations,
484                    when_false_invocations: fragments.when_false_invocations,
485                    slot_projection_programs: structural_slot_projection_programs(
486                        model,
487                        &ordinary,
488                        &fragments.slot_projection_bindings,
489                        &compiler_html,
490                    ),
491                    slot_projection_bindings: fragments.slot_projection_bindings,
492                }
493            })
494            .collect(),
495            keyed_host_fragments: crate::generate_structural_keyed_host_fragments(
496                model,
497                &program.region,
498            )
499            .into_iter()
500            .map(|fragments| {
501                let slot_projection_programs = structural_slot_projection_programs(
502                    model,
503                    &ordinary,
504                    &fragments.slot_projection_bindings,
505                    &fragments.item_template_html,
506                );
507                SerializedStructuralKeyedHostFragment {
508                    host_scope: fragments.host_scope.artifact_text().to_string(),
509                    host_instance: fragments.host_instance.to_string(),
510                    item_template_html: fragments.item_template_html,
511                    item_invocations: fragments.item_invocations,
512                    slot_projection_programs,
513                    slot_projection_bindings: fragments.slot_projection_bindings,
514                }
515            })
516            .collect(),
517            template_occurrences: program.template_occurrences,
518            create_order: program.template_instances.clone(),
519            destroy_order: program.template_instances.iter().rev().cloned().collect(),
520            template_instances: program.template_instances,
521        })
522        .collect();
523    artifact
524}
525
526fn structural_slot_projection_programs(
527    model: &ApplicationSemanticModel,
528    ordinary: &crate::OrdinaryTemplateInstanceRegistry,
529    bindings: &[String],
530    compiler_html: &str,
531) -> Vec<SerializedStructuralSlotProjectionProgram> {
532    let semantic_entities = crate::build_template_semantic_entities(&model.templates);
533    let semantic_entities = semantic_entities
534        .iter()
535        .map(|entity| (&entity.id, entity))
536        .collect::<std::collections::BTreeMap<_, _>>();
537    bindings
538        .iter()
539        .filter_map(|binding_id| {
540            let binding = model
541                .slot_bindings
542                .bindings
543                .values()
544                .find(|binding| binding.id.as_str() == binding_id)?;
545            let fragment = model
546                .slot_content_fragments
547                .get(binding.content_fragment.as_ref()?)?;
548            let roots = fragment
549                .content_template_entities
550                .iter()
551                .filter_map(|entity| semantic_entities.get(entity).copied())
552                .collect::<Vec<_>>();
553            let owns_entity = |entity_id: &crate::SemanticId| {
554                let Some(entity) = semantic_entities.get(entity_id).copied() else {
555                    return false;
556                };
557                roots.iter().any(|root| {
558                    root.provenance.path == entity.provenance.path
559                        && root.provenance.span.start <= entity.provenance.span.start
560                        && entity.provenance.span.end <= root.provenance.span.end
561                })
562            };
563            let rendered_binding_targets = ordinary
564                .bindings
565                .iter()
566                .filter(|record| {
567                    record.component_instance_id == binding.caller_instance
568                        && compiler_html.contains(&record.instance_binding_id.to_string())
569                })
570                .map(|record| record.target_id.to_string())
571                .collect::<std::collections::BTreeSet<_>>();
572            let target_ids = ordinary
573                .targets
574                .iter()
575                .filter(|target| {
576                    target.component_instance_id == binding.caller_instance
577                        && owns_entity(&target.template_entity_id)
578                        && (compiler_html.contains(&target.target_id.to_string())
579                            || rendered_binding_targets.contains(&target.target_id.to_string()))
580                })
581                .map(|target| target.target_id.to_string())
582                .collect::<Vec<_>>();
583            let target_set = target_ids.iter().collect::<std::collections::BTreeSet<_>>();
584            let binding_ids = ordinary
585                .bindings
586                .iter()
587                .filter(|record| {
588                    record.component_instance_id == binding.caller_instance
589                        && target_set.contains(&record.target_id.to_string())
590                })
591                .map(|record| record.instance_binding_id.to_string())
592                .collect::<Vec<_>>();
593            let event_ids = ordinary
594                .events
595                .iter()
596                .filter(|event| {
597                    event.component_instance_id == binding.caller_instance
598                        && target_set.contains(&event.target_id.to_string())
599                })
600                .map(|event| event.declaration_event_id.to_string())
601                .collect::<Vec<_>>();
602            let caller_component = &model
603                .component_instance_plan
604                .instances
605                .get(&binding.caller_instance)?
606                .component;
607            let nested_invocations = model
608                .component_invocations_owned_by(caller_component)
609                .into_iter()
610                .filter(|invocation| {
611                    model
612                        .component_invocations
613                        .get(invocation)
614                        .is_some_and(|record| {
615                            owns_entity(&record.template_entity)
616                                && compiler_html.contains(invocation.as_str())
617                        })
618                })
619                .map(ToString::to_string)
620                .collect();
621            Some(SerializedStructuralSlotProjectionProgram {
622                binding: binding_id.clone(),
623                caller_instance: binding.caller_instance.to_string(),
624                content_owner_instance: binding.content_owner_instance.to_string(),
625                target_ids,
626                binding_ids,
627                event_ids,
628                nested_invocations,
629            })
630        })
631        .collect()
632}
633
634#[derive(Debug, Clone)]
635struct StructuralProgramBuild {
636    region: crate::ComponentStructuralRegionId,
637    host_component: String,
638    host_node: String,
639    host_template_entity: String,
640    template_instances: Vec<String>,
641    template_occurrences: Vec<SerializedStructuralTemplateOccurrence>,
642}
643
644/// Resolve a structural-region ID back to its exact compiler-authored host.
645///
646/// A region ID is derived from the semantic template entity, while the DOM
647/// renderer addresses the same construct by its generated template node ID.
648/// This is the only compiler join between those domains; runtime code must not
649/// infer it from DOM shape, selectors, or user data.
650fn structural_program_build(
651    model: &ApplicationSemanticModel,
652    region: &crate::ComponentStructuralRegionId,
653) -> StructuralProgramBuild {
654    let entity = crate::structural_template_entity_for_region(region, &model.template_entities)
655        .expect("structural component instance references a semantic template host");
656    let template_id = entity
657        .owner
658        .entity_id()
659        .expect("structural template host has a template owner");
660    let template = model
661        .templates
662        .iter()
663        .find(|template| &template.id == template_id)
664        .expect("structural template host has an emitted template");
665    let component = template
666        .owner
667        .entity_id()
668        .expect("emitted template has a component owner");
669    let host_node = structural_host_node(template, entity.kind, entity.provenance.span)
670        .expect("structural template host has an emitted runtime node");
671
672    StructuralProgramBuild {
673        region: region.clone(),
674        host_component: component.to_string(),
675        host_node,
676        host_template_entity: entity.id.to_string(),
677        template_instances: Vec::new(),
678        template_occurrences: Vec::new(),
679    }
680}
681
682fn structural_host_node(
683    template: &TemplateNode,
684    kind: TemplateSemanticKind,
685    span: presolve_parser::SourceSpan,
686) -> Option<String> {
687    fn visit(
688        children: &[TemplateChild],
689        kind: TemplateSemanticKind,
690        span: presolve_parser::SourceSpan,
691    ) -> Option<String> {
692        for child in children {
693            match child {
694                TemplateChild::Element(element) => {
695                    if let Some(id) = visit(&element.children, kind, span) {
696                        return Some(id);
697                    }
698                }
699                TemplateChild::Fragment(fragment) => {
700                    if let Some(id) = visit(&fragment.children, kind, span) {
701                        return Some(id);
702                    }
703                }
704                TemplateChild::Conditional(conditional) => {
705                    if kind == TemplateSemanticKind::Conditional && conditional.span == span {
706                        return Some(conditional.id.0.clone());
707                    }
708                    if let Some(id) = visit(&conditional.when_true, kind, span)
709                        .or_else(|| visit(&conditional.when_false, kind, span))
710                    {
711                        return Some(id);
712                    }
713                }
714                TemplateChild::List(list) => {
715                    if kind == TemplateSemanticKind::List && list.span == span {
716                        return Some(list.id.0.clone());
717                    }
718                    if let Some(id) = visit(&list.item_template, kind, span) {
719                        return Some(id);
720                    }
721                }
722                TemplateChild::Text { .. } | TemplateChild::Binding { .. } => {}
723            }
724        }
725        None
726    }
727
728    template
729        .root
730        .as_ref()
731        .and_then(|root| visit(&root.children, kind, span))
732        .or_else(|| {
733            template
734                .root_fragment
735                .as_ref()
736                .and_then(|fragment| visit(&fragment.children, kind, span))
737        })
738}
739
740#[must_use]
741pub fn artifact_from_registry(registry: &RuntimeComponentRegistry) -> RuntimeComponentArtifact {
742    RuntimeComponentArtifact {
743        schema_version: RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION,
744        component_definitions: registry
745            .component_definitions
746            .iter()
747            .map(|r| SerializedComponentDefinition {
748                component: r.component.to_string(),
749                template: r.template.to_string(),
750                slots: r.declared_slots.iter().map(ToString::to_string).collect(),
751                boundary: "client".to_string(),
752            })
753            .collect(),
754        instances: registry
755            .instances
756            .iter()
757            .map(|r| SerializedComponentInstance {
758                instance: r.instance.to_string(),
759                component: r.component.to_string(),
760                invocation: r.invocation.as_ref().map(ToString::to_string),
761                parent: r.parent.as_ref().map(ToString::to_string),
762                depth: r.depth,
763                initialization_batch: r.initialization_batch,
764                ordinary_template_targets: Vec::new(),
765                ordinary_template_bindings: Vec::new(),
766                ordinary_template_events: Vec::new(),
767                state_slots: Vec::new(),
768                computed_slots: Vec::new(),
769                context_prefix: r.instance_context_prefix.clone(),
770                instruction_indices: r.optimized_instruction_indices.clone(),
771                structural_region: r.structural_region.as_ref().map(ToString::to_string),
772            })
773            .collect(),
774        initialization_batches: registry
775            .initialization_batches
776            .iter()
777            .map(|r| SerializedComponentBatch {
778                index: r.index,
779                instances: r.instances.iter().map(ToString::to_string).collect(),
780                context_sources: r.context_sources.iter().map(ToString::to_string).collect(),
781                slot_bindings: r.slot_bindings.iter().map(ToString::to_string).collect(),
782            })
783            .collect(),
784        slot_binding_programs: registry
785            .slot_bindings
786            .iter()
787            .map(|r| SerializedSlotBinding {
788                binding: r.binding.to_string(),
789                caller_instance: r.caller_instance.to_string(),
790                callee_instance: r.callee_instance.to_string(),
791                slot: r.slot.to_string(),
792                outlet: r.outlet.to_string(),
793                fragment: r.fragment.to_string(),
794                content_owner_instance: r.content_owner_instance.to_string(),
795            })
796            .collect(),
797        instance_context_bindings: registry
798            .instance_context_bindings
799            .iter()
800            .map(|r| SerializedInstanceContextBinding {
801                consumer_instance: r.consumer_instance.to_string(),
802                selected_source: r.selected_source.to_string(),
803                provider_source: r.provider_source.as_ref().map(ToString::to_string),
804                runtime_slot: r.runtime_slot.to_string(),
805            })
806            .collect(),
807        ordinary_template_targets: Vec::new(),
808        ordinary_template_bindings: Vec::new(),
809        ordinary_template_events: Vec::new(),
810        destruction: SerializedDestructionMetadata {
811            operation: "destroy_component_instance".to_string(),
812            enabled: true,
813        },
814        structural_programs: Vec::new(),
815    }
816}
817
818/// # Panics
819///
820/// Panics only if compiler-owned artifact data cannot be serialized.
821#[must_use]
822pub fn runtime_component_artifact_json(artifact: &RuntimeComponentArtifact) -> String {
823    serde_json::to_string_pretty(artifact).expect("component runtime artifact serializes") + "\n"
824}
825
826/// # Errors
827///
828/// Returns an error for an unsupported schema or invalid canonical endpoints/order.
829#[allow(clippy::too_many_lines)]
830pub fn validate_runtime_component_artifact(
831    artifact: &RuntimeComponentArtifact,
832) -> Result<(), String> {
833    if artifact.schema_version != RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION {
834        return Err("unsupported component runtime artifact schema version".to_string());
835    }
836    let structural_occurrence_invocations = artifact
837        .structural_programs
838        .iter()
839        .flat_map(|program| &program.template_occurrences)
840        .map(|occurrence| occurrence.invocation.as_str())
841        .collect::<std::collections::BTreeSet<_>>();
842    if artifact.structural_programs.iter().any(|program| {
843        program.region.is_empty()
844            || program.host_component.is_empty()
845            || program.host_node.is_empty()
846            || program.host_template_entity.is_empty()
847            || program.conditional_host_fragments.iter().any(|fragments| {
848                let known_invocations = program
849                    .template_occurrences
850                    .iter()
851                    .map(|occurrence| occurrence.invocation.as_str())
852                    .collect::<std::collections::BTreeSet<_>>();
853                fragments.host_instance.is_empty()
854                    || !matches!(
855                        fragments.host_scope.as_str(),
856                        "static-instance" | "structural-occurrence"
857                    )
858                    || fragments.when_true_html.is_empty()
859                    || fragments.when_false_html.is_empty()
860                    || fragments.when_true_invocations.len()
861                        != fragments
862                            .when_true_invocations
863                            .iter()
864                            .collect::<std::collections::BTreeSet<_>>()
865                            .len()
866                    || fragments.when_false_invocations.len()
867                        != fragments
868                            .when_false_invocations
869                            .iter()
870                            .collect::<std::collections::BTreeSet<_>>()
871                            .len()
872                    || fragments
873                        .when_true_invocations
874                        .iter()
875                        .chain(&fragments.when_false_invocations)
876                        .any(|invocation| !known_invocations.contains(invocation.as_str()))
877            })
878            || program.keyed_host_fragments.iter().any(|fragments| {
879                let known_invocations = program
880                    .template_occurrences
881                    .iter()
882                    .map(|occurrence| occurrence.invocation.as_str())
883                    .collect::<std::collections::BTreeSet<_>>();
884                fragments.host_instance.is_empty()
885                    || !matches!(
886                        fragments.host_scope.as_str(),
887                        "static-instance" | "structural-occurrence"
888                    )
889                    || fragments.item_template_html.is_empty()
890                    || fragments.item_invocations.len()
891                        != fragments
892                            .item_invocations
893                            .iter()
894                            .collect::<std::collections::BTreeSet<_>>()
895                            .len()
896                    || fragments
897                        .item_invocations
898                        .iter()
899                        .any(|invocation| !known_invocations.contains(invocation.as_str()))
900            })
901            || program.template_occurrences.len() != program.template_instances.len()
902            || program.template_occurrences.iter().any(|occurrence| {
903                occurrence.template_html.is_empty()
904                    || occurrence.invocation_template_entity.is_empty()
905                    || occurrence.nested_invocations.len()
906                        != occurrence
907                            .nested_invocations
908                            .iter()
909                            .collect::<std::collections::BTreeSet<_>>()
910                            .len()
911                    || occurrence.nested_invocations.iter().any(|invocation| {
912                        !structural_occurrence_invocations.contains(invocation.as_str())
913                    })
914            })
915            || program
916                .template_occurrences
917                .iter()
918                .map(|occurrence| occurrence.template_instance.as_str())
919                .collect::<Vec<_>>()
920                != program.template_instances
921            || program.create_order != program.template_instances
922            || program
923                .destroy_order
924                .iter()
925                .rev()
926                .cloned()
927                .collect::<Vec<_>>()
928                != program.template_instances
929    }) {
930        return Err("component artifact has invalid structural program ordering".to_string());
931    }
932    let structural_regions = artifact
933        .structural_programs
934        .iter()
935        .map(|program| program.region.as_str())
936        .collect::<std::collections::BTreeSet<_>>();
937    let structural_hosts = artifact
938        .structural_programs
939        .iter()
940        .map(|program| (program.host_component.as_str(), program.host_node.as_str()))
941        .collect::<std::collections::BTreeSet<_>>();
942    if structural_regions.len() != artifact.structural_programs.len()
943        || structural_hosts.len() != artifact.structural_programs.len()
944    {
945        return Err("component artifact has duplicate structural program addresses".to_string());
946    }
947    let instances = artifact
948        .instances
949        .iter()
950        .map(|r| r.instance.as_str())
951        .collect::<std::collections::BTreeSet<_>>();
952    let instance_components = artifact
953        .instances
954        .iter()
955        .map(|instance| (instance.instance.as_str(), instance.component.as_str()))
956        .collect::<std::collections::BTreeMap<_, _>>();
957    let structural_template_instances = artifact
958        .structural_programs
959        .iter()
960        .flat_map(|program| program.template_instances.iter().map(String::as_str))
961        .collect::<std::collections::BTreeSet<_>>();
962    let structural_template_components = artifact
963        .structural_programs
964        .iter()
965        .flat_map(|program| {
966            program.template_occurrences.iter().map(|occurrence| {
967                (
968                    occurrence.template_instance.as_str(),
969                    occurrence.component.as_str(),
970                )
971            })
972        })
973        .collect::<std::collections::BTreeMap<_, _>>();
974    let slot_binding_callees = artifact
975        .slot_binding_programs
976        .iter()
977        .map(|binding| (binding.binding.as_str(), binding.callee_instance.as_str()))
978        .collect::<std::collections::BTreeMap<_, _>>();
979    let slot_projection_programs_valid =
980        |bindings: &[String],
981         programs: &[SerializedStructuralSlotProjectionProgram],
982         host: &str| {
983            bindings.len() == programs.len()
984                && bindings
985                    .iter()
986                    .collect::<std::collections::BTreeSet<_>>()
987                    .len()
988                    == bindings.len()
989                && programs
990                    .iter()
991                    .map(|program| program.binding.as_str())
992                    .collect::<std::collections::BTreeSet<_>>()
993                    .len()
994                    == programs.len()
995                && programs.iter().all(|program| {
996                    let Some(slot_binding) = artifact
997                        .slot_binding_programs
998                        .iter()
999                        .find(|binding| binding.binding == program.binding)
1000                    else {
1001                        return false;
1002                    };
1003                    let targets = program
1004                        .target_ids
1005                        .iter()
1006                        .collect::<std::collections::BTreeSet<_>>();
1007                    bindings.contains(&program.binding)
1008                        && slot_binding.callee_instance == host
1009                        && slot_binding.caller_instance == program.caller_instance
1010                        && slot_binding.content_owner_instance == program.content_owner_instance
1011                        && targets.len() == program.target_ids.len()
1012                        && program
1013                            .binding_ids
1014                            .iter()
1015                            .collect::<std::collections::BTreeSet<_>>()
1016                            .len()
1017                            == program.binding_ids.len()
1018                        && program
1019                            .event_ids
1020                            .iter()
1021                            .collect::<std::collections::BTreeSet<_>>()
1022                            .len()
1023                            == program.event_ids.len()
1024                        && program
1025                            .nested_invocations
1026                            .iter()
1027                            .collect::<std::collections::BTreeSet<_>>()
1028                            .len()
1029                            == program.nested_invocations.len()
1030                        && program.target_ids.iter().all(|id| {
1031                            artifact.ordinary_template_targets.iter().any(|target| {
1032                                target.id == *id
1033                                    && target.component_instance_id == program.caller_instance
1034                            })
1035                        })
1036                        && program.binding_ids.iter().all(|id| {
1037                            artifact.ordinary_template_bindings.iter().any(|binding| {
1038                                binding.id == *id
1039                                    && binding.component_instance_id == program.caller_instance
1040                                    && targets.contains(&binding.target_id)
1041                            })
1042                        })
1043                        && program.event_ids.iter().all(|id| {
1044                            artifact.ordinary_template_events.iter().any(|event| {
1045                                event.declaration_event_id == *id
1046                                    && event.component_instance_id == program.caller_instance
1047                                    && targets.contains(&event.target_id)
1048                            })
1049                        })
1050                })
1051        };
1052    if artifact.structural_programs.iter().any(|program| {
1053        let host_instances = program
1054            .conditional_host_fragments
1055            .iter()
1056            .map(|fragments| fragments.host_instance.as_str())
1057            .collect::<std::collections::BTreeSet<_>>();
1058        host_instances.len() != program.conditional_host_fragments.len()
1059            || program.conditional_host_fragments.iter().any(|fragments| {
1060                (match fragments.host_scope.as_str() {
1061                    "static-instance" => {
1062                        !instances.contains(fragments.host_instance.as_str())
1063                            || instance_components
1064                                .get(fragments.host_instance.as_str())
1065                                .is_none_or(|component| *component != program.host_component)
1066                    }
1067                    "structural-occurrence" => {
1068                        !structural_template_instances.contains(fragments.host_instance.as_str())
1069                            || structural_template_components
1070                                .get(fragments.host_instance.as_str())
1071                                .is_none_or(|component| *component != program.host_component)
1072                    }
1073                    _ => true,
1074                }) || {
1075                    fragments.slot_projection_bindings.len()
1076                        != fragments
1077                            .slot_projection_bindings
1078                            .iter()
1079                            .collect::<std::collections::BTreeSet<_>>()
1080                            .len()
1081                        || fragments.slot_projection_bindings.iter().any(|binding| {
1082                            slot_binding_callees
1083                                .get(binding.as_str())
1084                                .is_none_or(|callee| *callee != fragments.host_instance)
1085                        })
1086                        || !slot_projection_programs_valid(
1087                            &fragments.slot_projection_bindings,
1088                            &fragments.slot_projection_programs,
1089                            &fragments.host_instance,
1090                        )
1091                }
1092            })
1093    }) {
1094        return Err("component artifact has invalid conditional host fragments".to_string());
1095    }
1096    if artifact.structural_programs.iter().any(|program| {
1097        let host_instances = program
1098            .keyed_host_fragments
1099            .iter()
1100            .map(|fragments| fragments.host_instance.as_str())
1101            .collect::<std::collections::BTreeSet<_>>();
1102        host_instances.len() != program.keyed_host_fragments.len()
1103            || program.keyed_host_fragments.iter().any(|fragments| {
1104                (match fragments.host_scope.as_str() {
1105                    "static-instance" => {
1106                        !instances.contains(fragments.host_instance.as_str())
1107                            || instance_components
1108                                .get(fragments.host_instance.as_str())
1109                                .is_none_or(|component| *component != program.host_component)
1110                    }
1111                    "structural-occurrence" => {
1112                        !structural_template_instances.contains(fragments.host_instance.as_str())
1113                            || structural_template_components
1114                                .get(fragments.host_instance.as_str())
1115                                .is_none_or(|component| *component != program.host_component)
1116                    }
1117                    _ => true,
1118                }) || {
1119                    fragments.slot_projection_bindings.len()
1120                        != fragments
1121                            .slot_projection_bindings
1122                            .iter()
1123                            .collect::<std::collections::BTreeSet<_>>()
1124                            .len()
1125                        || fragments.slot_projection_bindings.iter().any(|binding| {
1126                            slot_binding_callees
1127                                .get(binding.as_str())
1128                                .is_none_or(|callee| *callee != fragments.host_instance)
1129                        })
1130                        || !slot_projection_programs_valid(
1131                            &fragments.slot_projection_bindings,
1132                            &fragments.slot_projection_programs,
1133                            &fragments.host_instance,
1134                        )
1135                }
1136            })
1137    }) {
1138        return Err("component artifact has invalid keyed host fragments".to_string());
1139    }
1140    let target_ids = artifact
1141        .ordinary_template_targets
1142        .iter()
1143        .map(|target| target.id.as_str())
1144        .collect::<std::collections::BTreeSet<_>>();
1145    let binding_ids = artifact
1146        .ordinary_template_bindings
1147        .iter()
1148        .map(|binding| binding.id.as_str())
1149        .collect::<std::collections::BTreeSet<_>>();
1150    let event_keys = artifact
1151        .ordinary_template_events
1152        .iter()
1153        .map(|event| {
1154            (
1155                event.component_instance_id.as_str(),
1156                event.target_id.as_str(),
1157                event.event_type.as_str(),
1158            )
1159        })
1160        .collect::<std::collections::BTreeSet<_>>();
1161    if artifact
1162        .structural_programs
1163        .iter()
1164        .flat_map(|program| {
1165            program.template_occurrences.iter().map(move |occurrence| {
1166                (
1167                    occurrence,
1168                    artifact
1169                        .ordinary_template_targets
1170                        .iter()
1171                        .filter(|target| {
1172                            target.component_instance_id == occurrence.template_instance
1173                        })
1174                        .map(|target| target.id.clone())
1175                        .collect::<Vec<_>>(),
1176                    artifact
1177                        .ordinary_template_bindings
1178                        .iter()
1179                        .filter(|binding| {
1180                            binding.component_instance_id == occurrence.template_instance
1181                        })
1182                        .map(|binding| binding.id.clone())
1183                        .collect::<Vec<_>>(),
1184                    artifact
1185                        .ordinary_template_events
1186                        .iter()
1187                        .filter(|event| event.component_instance_id == occurrence.template_instance)
1188                        .map(|event| event.declaration_event_id.clone())
1189                        .collect::<Vec<_>>(),
1190                )
1191            })
1192        })
1193        .any(|(occurrence, targets, bindings, events)| {
1194            occurrence.ordinary_template_targets != targets
1195                || occurrence.ordinary_template_bindings != bindings
1196                || occurrence.ordinary_template_events != events
1197        })
1198    {
1199        return Err("component artifact has invalid structural template projection".to_string());
1200    }
1201    let mut structural_computed_cache_slots = std::collections::BTreeSet::new();
1202    let mut structural_computed_dirty_slots = std::collections::BTreeSet::new();
1203    let mut structural_computed_instance_pairs = std::collections::BTreeSet::new();
1204    let mut structural_state_slots = std::collections::BTreeSet::new();
1205    let mut structural_state_instance_pairs = std::collections::BTreeSet::new();
1206    if artifact
1207        .structural_programs
1208        .iter()
1209        .flat_map(|program| &program.template_occurrences)
1210        .any(|occurrence| {
1211            occurrence.state_slots.iter().any(|slot| {
1212                slot.slot_id
1213                    != canonical_state_slot_text(&occurrence.template_instance, &slot.storage_id)
1214                    || slot.storage_id != format!("storage:{}", slot.state_id)
1215                    || slot.state_id.is_empty()
1216                    || slot.semantic_type.is_empty()
1217                    || (!slot.serializable && slot.resume_codec.is_some())
1218                    || !structural_state_slots.insert(slot.slot_id.as_str())
1219                    || !structural_state_instance_pairs.insert((
1220                        occurrence.template_instance.as_str(),
1221                        slot.storage_id.as_str(),
1222                    ))
1223            }) || occurrence.computed_slots.iter().any(|slot| {
1224                !slot
1225                    .cache_slot_id
1226                    .starts_with(&format!("{}/computed-cache:", occurrence.template_instance))
1227                    || !slot
1228                        .dirty_slot_id
1229                        .starts_with(&format!("{}/computed-dirty:", occurrence.template_instance))
1230                    || slot.computed_id.is_empty()
1231                    || !structural_computed_cache_slots.insert(slot.cache_slot_id.as_str())
1232                    || !structural_computed_dirty_slots.insert(slot.dirty_slot_id.as_str())
1233                    || !structural_computed_instance_pairs.insert((
1234                        occurrence.template_instance.as_str(),
1235                        slot.computed_id.as_str(),
1236                    ))
1237            })
1238        })
1239    {
1240        return Err("component artifact has invalid structural instance slots".to_string());
1241    }
1242    let mut computed_cache_slots = std::collections::BTreeSet::new();
1243    let mut computed_dirty_slots = std::collections::BTreeSet::new();
1244    let mut computed_instance_pairs = std::collections::BTreeSet::new();
1245    let mut state_slots = std::collections::BTreeSet::new();
1246    let mut state_instance_pairs = std::collections::BTreeSet::new();
1247    if target_ids.len() != artifact.ordinary_template_targets.len()
1248        || binding_ids.len() != artifact.ordinary_template_bindings.len()
1249        || event_keys.len() != artifact.ordinary_template_events.len()
1250        || artifact.ordinary_template_targets.iter().any(|target| {
1251            !instances.contains(target.component_instance_id.as_str())
1252                && !structural_template_instances.contains(target.component_instance_id.as_str())
1253                || !target.id.starts_with(&format!(
1254                    "{}/template-target:",
1255                    target.component_instance_id
1256                ))
1257        })
1258        || artifact.ordinary_template_bindings.iter().any(|binding| {
1259            !instances.contains(binding.component_instance_id.as_str())
1260                && !structural_template_instances.contains(binding.component_instance_id.as_str())
1261                || !target_ids.contains(binding.target_id.as_str())
1262                || !binding.id.starts_with(&format!(
1263                    "{}/template-binding:",
1264                    binding.component_instance_id
1265                ))
1266        })
1267        || artifact.ordinary_template_events.iter().any(|event| {
1268            !instances.contains(event.component_instance_id.as_str())
1269                && !structural_template_instances.contains(event.component_instance_id.as_str())
1270                || !target_ids.contains(event.target_id.as_str())
1271                || event.action_batch_id.is_none()
1272        })
1273        || artifact.instances.iter().any(|instance| {
1274            instance
1275                .ordinary_template_targets
1276                .iter()
1277                .any(|id| !target_ids.contains(id.as_str()))
1278                || instance
1279                    .ordinary_template_bindings
1280                    .iter()
1281                    .any(|id| !binding_ids.contains(id.as_str()))
1282                || instance.ordinary_template_targets
1283                    != artifact
1284                        .ordinary_template_targets
1285                        .iter()
1286                        .filter(|target| target.component_instance_id == instance.instance)
1287                        .map(|target| target.id.clone())
1288                        .collect::<Vec<_>>()
1289                || instance.ordinary_template_bindings
1290                    != artifact
1291                        .ordinary_template_bindings
1292                        .iter()
1293                        .filter(|binding| binding.component_instance_id == instance.instance)
1294                        .map(|binding| binding.id.clone())
1295                        .collect::<Vec<_>>()
1296                || instance.ordinary_template_events
1297                    != artifact
1298                        .ordinary_template_events
1299                        .iter()
1300                        .filter(|event| event.component_instance_id == instance.instance)
1301                        .map(|event| event.declaration_event_id.clone())
1302                        .collect::<Vec<_>>()
1303                || instance.state_slots.iter().any(|slot| {
1304                    slot.slot_id != canonical_state_slot_text(&instance.instance, &slot.storage_id)
1305                        || slot.storage_id != format!("storage:{}", slot.state_id)
1306                        || !state_slots.insert(slot.slot_id.as_str())
1307                        || !state_instance_pairs
1308                            .insert((instance.instance.as_str(), slot.storage_id.as_str()))
1309                })
1310                || instance.computed_slots.iter().any(|slot| {
1311                    !slot
1312                        .cache_slot_id
1313                        .starts_with(&format!("{}/computed-cache:", instance.instance))
1314                        || !slot
1315                            .dirty_slot_id
1316                            .starts_with(&format!("{}/computed-dirty:", instance.instance))
1317                        || !computed_cache_slots.insert(slot.cache_slot_id.as_str())
1318                        || !computed_dirty_slots.insert(slot.dirty_slot_id.as_str())
1319                        || !computed_instance_pairs
1320                            .insert((instance.instance.as_str(), slot.computed_id.as_str()))
1321                })
1322        })
1323    {
1324        return Err("component artifact has invalid ordinary template projection".to_string());
1325    }
1326    if artifact.instances.iter().any(|r| {
1327        r.parent.as_deref().is_some_and(|parent| {
1328            !instances.contains(parent) && !structural_template_instances.contains(parent)
1329        })
1330    }) {
1331        return Err("component artifact has an unknown parent instance".to_string());
1332    }
1333    if artifact
1334        .structural_programs
1335        .iter()
1336        .flat_map(|program| &program.template_occurrences)
1337        .any(|occurrence| {
1338            occurrence.parent_template_instance.is_empty()
1339                || (!instances.contains(occurrence.parent_template_instance.as_str())
1340                    && !structural_template_instances
1341                        .contains(occurrence.parent_template_instance.as_str()))
1342        })
1343    {
1344        return Err("component artifact has an unknown structural template parent".to_string());
1345    }
1346    if artifact.slot_binding_programs.iter().any(|r| {
1347        !instances.contains(r.caller_instance.as_str())
1348            || !instances.contains(r.callee_instance.as_str())
1349    }) {
1350        return Err("component artifact has an unknown Slot-binding endpoint".to_string());
1351    }
1352    if artifact
1353        .initialization_batches
1354        .iter()
1355        .enumerate()
1356        .any(|(index, batch)| {
1357            batch.index != index
1358                || batch.instances.iter().any(|id| {
1359                    !instances.contains(id.as_str())
1360                        && !structural_template_instances.contains(id.as_str())
1361                })
1362        })
1363    {
1364        return Err("component artifact has invalid initialization ordering".to_string());
1365    }
1366    Ok(())
1367}
1368
1369fn canonical_state_slot_text(instance: &str, storage: &str) -> String {
1370    let encoded = storage.bytes().fold(String::new(), |mut encoded, byte| {
1371        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
1372            encoded.push(char::from(byte));
1373        } else {
1374            use std::fmt::Write as _;
1375            write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail");
1376        }
1377        encoded
1378    });
1379    format!("{instance}/state-slot:{encoded}")
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use crate::{
1385        build_application_semantic_model, build_runtime_component_artifact,
1386        runtime_component_artifact_json, validate_runtime_component_artifact,
1387    };
1388    #[test]
1389    fn serializes_deterministically_and_rejects_unknown_endpoints() {
1390        let model = build_application_semantic_model(&presolve_parser::parse_file(
1391            "src/Artifact.tsx",
1392            r#"@component("x-page") class Page extends Component { render() { return <main />; } }"#,
1393        ));
1394        let mut artifact =
1395            build_runtime_component_artifact(&model, &model.component_ir_optimization);
1396        assert!(validate_runtime_component_artifact(&artifact).is_ok());
1397        assert_eq!(
1398            runtime_component_artifact_json(&artifact),
1399            runtime_component_artifact_json(&build_runtime_component_artifact(
1400                &model,
1401                &model.component_ir_optimization
1402            ))
1403        );
1404        artifact.instances[0].parent = Some("missing".to_string());
1405        assert!(validate_runtime_component_artifact(&artifact).is_err());
1406    }
1407
1408    #[test]
1409    fn projects_exact_computed_slots_for_each_repeated_instance() {
1410        let model = build_application_semantic_model(&presolve_parser::parse_file(
1411            "src/RepeatedComputedArtifact.tsx",
1412            r#"@component("x-child") class Child { count = state(1); @computed() get doubled() { return this.count * 2; } render() { return <span>{this.doubled}</span>; } }
1413@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
1414        ));
1415        let mut artifact =
1416            build_runtime_component_artifact(&model, &model.component_ir_optimization);
1417        let slots = artifact
1418            .instances
1419            .iter()
1420            .flat_map(|instance| instance.computed_slots.iter())
1421            .collect::<Vec<_>>();
1422        assert_eq!(slots.len(), 2);
1423        assert_ne!(slots[0].cache_slot_id, slots[1].cache_slot_id);
1424        assert_ne!(slots[0].dirty_slot_id, slots[1].dirty_slot_id);
1425        assert!(validate_runtime_component_artifact(&artifact).is_ok());
1426        let computed_instances = artifact
1427            .instances
1428            .iter()
1429            .enumerate()
1430            .filter(|(_, instance)| !instance.computed_slots.is_empty())
1431            .map(|(index, _)| index)
1432            .collect::<Vec<_>>();
1433        let duplicate = artifact.instances[computed_instances[0]].computed_slots[0].clone();
1434        artifact.instances[computed_instances[1]].computed_slots[0].cache_slot_id =
1435            duplicate.cache_slot_id;
1436        assert!(validate_runtime_component_artifact(&artifact).is_err());
1437    }
1438
1439    #[test]
1440    fn projects_and_validates_exact_state_slots_for_repeated_instances() {
1441        let model = build_application_semantic_model(&presolve_parser::parse_file(
1442            "src/RepeatedStateArtifact.tsx",
1443            r#"@component("x-child") class Child { count = state(1); render() { return <span>{this.count}</span>; } }
1444@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
1445        ));
1446        let mut artifact =
1447            build_runtime_component_artifact(&model, &model.component_ir_optimization);
1448        let state_instances = artifact
1449            .instances
1450            .iter()
1451            .enumerate()
1452            .filter(|(_, instance)| !instance.state_slots.is_empty())
1453            .map(|(index, _)| index)
1454            .collect::<Vec<_>>();
1455        assert_eq!(state_instances.len(), 2);
1456        let first = &artifact.instances[state_instances[0]].state_slots[0];
1457        let second = &artifact.instances[state_instances[1]].state_slots[0];
1458        assert_eq!(first.storage_id, second.storage_id);
1459        assert_ne!(first.slot_id, second.slot_id);
1460        assert!(validate_runtime_component_artifact(&artifact).is_ok());
1461
1462        artifact.instances[state_instances[1]].state_slots[0].slot_id = first.slot_id.clone();
1463        assert!(validate_runtime_component_artifact(&artifact).is_err());
1464    }
1465
1466    #[test]
1467    fn structural_programs_name_the_exact_compiler_owned_runtime_hosts() {
1468        let model = build_application_semantic_model(&presolve_parser::parse_file(
1469            "src/StructuralHostArtifact.tsx",
1470            r#"
1471@component("x-leaf") class Leaf extends Component {
1472  count = state(0);
1473  @action() increment() { this.count++; }
1474  @computed() get doubled() { return this.count * 2; }
1475  render() { return <button onClick={() => this.increment()}>{this.doubled}</button>; }
1476}
1477@component("x-page") class Page extends Component {
1478  visible = state(true);
1479  items = state([{ id: "a" }]);
1480  render() { return <main>{this.visible ? <Leaf /> : <span>Hidden</span>}<ul>{this.items.map(item => <li key={item.id}><Leaf /></li>)}</ul></main>; }
1481}
1482@component("x-idle") class Idle extends Component { render() { return <aside />; } }
1483"#,
1484        ));
1485        let mut artifact =
1486            build_runtime_component_artifact(&model, &model.component_ir_optimization);
1487        let manifest = crate::build_template_manifest_from_asm(&model);
1488
1489        assert_eq!(artifact.structural_programs.len(), 2);
1490        assert!(
1491            validate_runtime_component_artifact(&artifact).is_ok(),
1492            "{:?}",
1493            validate_runtime_component_artifact(&artifact)
1494        );
1495        for program in &artifact.structural_programs {
1496            assert!(!program.host_template_entity.is_empty());
1497            assert_eq!(
1498                program
1499                    .template_occurrences
1500                    .iter()
1501                    .map(|occurrence| occurrence.template_instance.as_str())
1502                    .collect::<Vec<_>>(),
1503                program
1504                    .template_instances
1505                    .iter()
1506                    .map(String::as_str)
1507                    .collect::<Vec<_>>()
1508            );
1509            assert!(program.template_occurrences.iter().all(|occurrence| {
1510                !occurrence.invocation.is_empty()
1511                    && !occurrence.invocation_template_entity.is_empty()
1512                    && !occurrence.component.is_empty()
1513            }));
1514            for occurrence in &program.template_occurrences {
1515                assert_eq!(
1516                    occurrence.ordinary_template_targets,
1517                    artifact
1518                        .ordinary_template_targets
1519                        .iter()
1520                        .filter(
1521                            |target| target.component_instance_id == occurrence.template_instance
1522                        )
1523                        .map(|target| target.id.clone())
1524                        .collect::<Vec<_>>()
1525                );
1526                assert_eq!(
1527                    occurrence.ordinary_template_bindings,
1528                    artifact
1529                        .ordinary_template_bindings
1530                        .iter()
1531                        .filter(
1532                            |binding| binding.component_instance_id == occurrence.template_instance
1533                        )
1534                        .map(|binding| binding.id.clone())
1535                        .collect::<Vec<_>>()
1536                );
1537                assert_eq!(
1538                    occurrence.ordinary_template_events,
1539                    artifact
1540                        .ordinary_template_events
1541                        .iter()
1542                        .filter(|event| event.component_instance_id == occurrence.template_instance)
1543                        .map(|event| event.declaration_event_id.clone())
1544                        .collect::<Vec<_>>()
1545                );
1546                assert!(occurrence.template_html.contains("data-presolve-node"));
1547                assert!(occurrence.state_slots.iter().all(|slot| {
1548                    slot.slot_id
1549                        .starts_with(&format!("{}/state-slot:", occurrence.template_instance))
1550                }));
1551                assert!(occurrence.computed_slots.iter().all(|slot| {
1552                    slot.cache_slot_id
1553                        .starts_with(&format!("{}/computed-cache:", occurrence.template_instance))
1554                        && slot.dirty_slot_id.starts_with(&format!(
1555                            "{}/computed-dirty:",
1556                            occurrence.template_instance
1557                        ))
1558                }));
1559            }
1560            let component = manifest
1561                .components
1562                .iter()
1563                .find(|component| component.component_id == program.host_component)
1564                .expect("structural program host component");
1565            assert!(component.template.nodes.iter().any(|node| {
1566                matches!(
1567                    node,
1568                    crate::template_manifest::ManifestNode::Conditional { id, .. }
1569                        | crate::template_manifest::ManifestNode::List { id, .. }
1570                        if id == &program.host_node
1571                )
1572            }));
1573        }
1574
1575        assert!(artifact
1576            .structural_programs
1577            .iter()
1578            .flat_map(|program| &program.template_occurrences)
1579            .any(|occurrence| {
1580                !occurrence.ordinary_template_targets.is_empty()
1581                    && !occurrence.ordinary_template_bindings.is_empty()
1582                    && !occurrence.ordinary_template_events.is_empty()
1583                    && occurrence
1584                        .template_html
1585                        .contains("__PRESOLVE_STRUCTURAL_OCCURRENCE__")
1586            }));
1587
1588        let conditional = artifact
1589            .structural_programs
1590            .iter()
1591            .find(|program| !program.conditional_host_fragments.is_empty())
1592            .expect("conditional host has compiler-authored fragments");
1593        assert_eq!(conditional.conditional_host_fragments.len(), 1);
1594        let fragments = &conditional.conditional_host_fragments[0];
1595        assert!(artifact
1596            .instances
1597            .iter()
1598            .any(|instance| instance.instance == fragments.host_instance));
1599        assert!(fragments
1600            .when_true_html
1601            .contains("data-presolve-structural-invocation="));
1602        assert_eq!(
1603            fragments.when_true_invocations,
1604            crate::ordinary_html_codegen::structural_invocations_in_compiler_html(
1605                &fragments.when_true_html
1606            )
1607        );
1608        assert_eq!(
1609            fragments.when_false_invocations,
1610            crate::ordinary_html_codegen::structural_invocations_in_compiler_html(
1611                &fragments.when_false_html
1612            )
1613        );
1614        assert!(fragments.when_false_html.contains("Hidden"));
1615
1616        let keyed = artifact
1617            .structural_programs
1618            .iter()
1619            .find(|program| !program.keyed_host_fragments.is_empty())
1620            .expect("keyed host has compiler-authored fragments");
1621        let keyed_fragments = &keyed.keyed_host_fragments[0];
1622        assert!(keyed_fragments
1623            .item_template_html
1624            .contains("__ez_list_key__"));
1625        assert!(keyed_fragments
1626            .item_template_html
1627            .contains("data-presolve-structural-invocation="));
1628        assert_eq!(
1629            keyed_fragments.item_invocations,
1630            crate::ordinary_html_codegen::structural_invocations_in_compiler_html(
1631                &keyed_fragments.item_template_html
1632            )
1633        );
1634
1635        let mut invalid_fragments = artifact.clone();
1636        invalid_fragments
1637            .structural_programs
1638            .iter_mut()
1639            .find(|program| !program.conditional_host_fragments.is_empty())
1640            .expect("conditional host has compiler-authored fragments")
1641            .conditional_host_fragments[0]
1642            .when_false_html
1643            .clear();
1644        assert!(validate_runtime_component_artifact(&invalid_fragments).is_err());
1645
1646        let mut invalid_membership = artifact.clone();
1647        invalid_membership
1648            .structural_programs
1649            .iter_mut()
1650            .find(|program| !program.conditional_host_fragments.is_empty())
1651            .expect("conditional host has compiler-authored fragments")
1652            .conditional_host_fragments[0]
1653            .when_true_invocations
1654            .push("fabricated-invocation".to_string());
1655        assert!(validate_runtime_component_artifact(&invalid_membership).is_err());
1656
1657        let mut invalid_keyed = artifact.clone();
1658        invalid_keyed
1659            .structural_programs
1660            .iter_mut()
1661            .find(|program| !program.keyed_host_fragments.is_empty())
1662            .expect("keyed host has compiler-authored fragments")
1663            .keyed_host_fragments[0]
1664            .item_template_html
1665            .clear();
1666        assert!(validate_runtime_component_artifact(&invalid_keyed).is_err());
1667
1668        let mut invalid_keyed_membership = artifact.clone();
1669        invalid_keyed_membership
1670            .structural_programs
1671            .iter_mut()
1672            .find(|program| !program.keyed_host_fragments.is_empty())
1673            .expect("keyed host has compiler-authored fragments")
1674            .keyed_host_fragments[0]
1675            .item_invocations
1676            .push("fabricated-invocation".to_string());
1677        assert!(validate_runtime_component_artifact(&invalid_keyed_membership).is_err());
1678
1679        let mut invalid_host = artifact.clone();
1680        let idle = invalid_host
1681            .instances
1682            .iter()
1683            .find(|instance| instance.component.ends_with("/component:x-idle"))
1684            .expect("unrelated static host instance")
1685            .instance
1686            .clone();
1687        invalid_host
1688            .structural_programs
1689            .iter_mut()
1690            .find(|program| !program.conditional_host_fragments.is_empty())
1691            .expect("conditional host has compiler-authored fragments")
1692            .conditional_host_fragments[0]
1693            .host_instance = idle;
1694        assert!(validate_runtime_component_artifact(&invalid_host).is_err());
1695
1696        let mut invalid_state_slot = artifact.clone();
1697        invalid_state_slot.structural_programs[0].template_occurrences[0].state_slots[0]
1698            .storage_id = "storage:fabricated".to_string();
1699        assert!(validate_runtime_component_artifact(&invalid_state_slot).is_err());
1700
1701        let mut invalid_computed_slot = artifact.clone();
1702        invalid_computed_slot.structural_programs[0].template_occurrences[0].computed_slots[0]
1703            .cache_slot_id = "fabricated-cache".to_string();
1704        assert!(validate_runtime_component_artifact(&invalid_computed_slot).is_err());
1705
1706        let first = &mut artifact.structural_programs[0].template_occurrences[0];
1707        first
1708            .ordinary_template_targets
1709            .push("fabricated-target".to_string());
1710        assert!(validate_runtime_component_artifact(&artifact).is_err());
1711    }
1712
1713    #[test]
1714    fn structural_host_fragments_preserve_their_compiler_scope() {
1715        let model = build_application_semantic_model(&presolve_parser::parse_file(
1716            "src/NestedStructuralHostArtifact.tsx",
1717            r#"
1718@component("x-leaf") class Leaf { render() { return <small />; } }
1719@component("x-card") class Card {
1720  expanded = state(true);
1721  render() { return <article>{this.expanded ? <section><Leaf />{this.expanded}</section> : <em>Collapsed</em>}</article>; }
1722}
1723@component("x-page") class Page {
1724  shown = state(true);
1725  render() { return <main>{this.shown ? <Card /> : <span>Hidden</span>}</main>; }
1726}
1727"#,
1728        ));
1729        let artifact = build_runtime_component_artifact(&model, &model.component_ir_optimization);
1730
1731        assert!(validate_runtime_component_artifact(&artifact).is_ok());
1732        let fragments = artifact
1733            .structural_programs
1734            .iter()
1735            .flat_map(|program| &program.conditional_host_fragments)
1736            .find(|fragments| fragments.host_scope == "structural-occurrence")
1737            .expect("nested structural occurrence host fragments");
1738        assert!(fragments
1739            .when_true_html
1740            .contains("__PRESOLVE_STRUCTURAL_OCCURRENCE__"));
1741        assert!(fragments.when_false_html.contains("Collapsed"));
1742    }
1743}