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#[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 #[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 pub host_component: String,
145 pub host_node: String,
147 pub host_template_entity: String,
149 pub conditional_host_fragments: Vec<SerializedStructuralConditionalHostFragments>,
153 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 pub host_scope: String,
165 pub host_instance: String,
168 pub when_true_html: String,
169 pub when_false_html: String,
170 pub when_true_invocations: Vec<String>,
172 pub when_false_invocations: Vec<String>,
173 pub slot_projection_bindings: Vec<String>,
174 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 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 pub invocation_template_entity: String,
206 pub component: String,
207 pub parent_template_instance: String,
210 pub state_slots: Vec<SerializedRuntimeStateSlot>,
213 pub computed_slots: Vec<SerializedRuntimeComputedSlot>,
214 pub template_html: String,
217 pub nested_invocations: Vec<String>,
218 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
640fn 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#[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#[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}