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