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