Skip to main content

presolve_compiler/
runtime_component_artifact.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{
4    build_computed_instance_slot_registry, build_ordinary_template_instance_registry,
5    build_runtime_component_registry, build_state_instance_storage_registry,
6    lower_components_to_ir, semantic_type_text, ApplicationSemanticModel,
7    OptimizedComponentIrReport, OrdinaryTemplateBindingKind, OrdinaryTemplateTargetKind,
8    RuntimeComponentRegistry, SerializationCompatibility,
9};
10
11pub const RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION: u32 = 4;
12
13/// Public H14 compiler artifact. All executable references are canonical IDs.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct RuntimeComponentArtifact {
16    pub schema_version: u32,
17    pub component_definitions: Vec<SerializedComponentDefinition>,
18    pub instances: Vec<SerializedComponentInstance>,
19    pub initialization_batches: Vec<SerializedComponentBatch>,
20    pub slot_binding_programs: Vec<SerializedSlotBinding>,
21    pub instance_context_bindings: Vec<SerializedInstanceContextBinding>,
22    pub ordinary_template_targets: Vec<SerializedOrdinaryTemplateTarget>,
23    pub ordinary_template_bindings: Vec<SerializedOrdinaryTemplateBinding>,
24    pub ordinary_template_events: Vec<SerializedOrdinaryTemplateEvent>,
25    pub destruction: SerializedDestructionMetadata,
26    pub structural_programs: Vec<SerializedStructuralComponentProgram>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct SerializedComponentDefinition {
31    pub component: String,
32    pub template: String,
33    pub slots: Vec<String>,
34    pub boundary: String,
35}
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct SerializedComponentInstance {
38    pub instance: String,
39    pub component: String,
40    pub invocation: Option<String>,
41    pub parent: Option<String>,
42    pub depth: usize,
43    pub initialization_batch: usize,
44    pub ordinary_template_targets: Vec<String>,
45    pub ordinary_template_bindings: Vec<String>,
46    pub ordinary_template_events: Vec<String>,
47    pub state_slots: Vec<SerializedRuntimeStateSlot>,
48    #[serde(skip_serializing_if = "Vec::is_empty")]
49    pub computed_slots: Vec<SerializedRuntimeComputedSlot>,
50    pub context_prefix: String,
51    pub instruction_indices: Vec<usize>,
52    pub structural_region: Option<String>,
53}
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct SerializedRuntimeStateSlot {
56    pub slot_id: String,
57    pub state_id: String,
58    pub storage_id: String,
59    pub initial_value: crate::SerializableValue,
60    pub semantic_type: String,
61    pub serializable: bool,
62}
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct SerializedRuntimeComputedSlot {
65    pub computed_id: String,
66    pub cache_slot_id: String,
67    pub dirty_slot_id: String,
68    pub dirty_initial_value: bool,
69}
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct SerializedOrdinaryTemplateTarget {
72    pub id: String,
73    pub component_instance_id: String,
74    pub component_id: String,
75    pub template_entity_id: String,
76    pub kind: String,
77}
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct SerializedOrdinaryTemplateBinding {
80    pub id: String,
81    pub component_instance_id: String,
82    pub component_id: String,
83    pub declaration_binding_id: String,
84    pub target_id: String,
85    pub kind: String,
86    pub state_storage_ids: Vec<String>,
87    pub computed_ids: Vec<String>,
88    pub program_id: String,
89    pub expression: Option<String>,
90    pub attribute_name: Option<String>,
91}
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93pub struct SerializedOrdinaryTemplateEvent {
94    pub component_instance_id: String,
95    pub component_id: String,
96    pub target_id: String,
97    pub declaration_event_id: String,
98    pub event_type: String,
99    pub handler_method_id: String,
100    pub action_batch_id: Option<String>,
101    #[serde(default, skip_serializing_if = "Vec::is_empty")]
102    pub arguments: Vec<crate::component_graph::SerializableValue>,
103    pub program_id: String,
104}
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct SerializedComponentBatch {
107    pub index: usize,
108    pub instances: Vec<String>,
109    pub context_sources: Vec<String>,
110    pub slot_bindings: Vec<String>,
111}
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct SerializedSlotBinding {
114    pub binding: String,
115    pub caller_instance: String,
116    pub callee_instance: String,
117    pub slot: String,
118    pub outlet: String,
119    pub fragment: String,
120    pub content_owner_instance: String,
121}
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct SerializedInstanceContextBinding {
124    pub consumer_instance: String,
125    pub selected_source: String,
126    pub provider_source: Option<String>,
127    pub runtime_slot: String,
128}
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct SerializedDestructionMetadata {
131    pub operation: String,
132    pub enabled: bool,
133}
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct SerializedStructuralComponentProgram {
136    pub region: String,
137    pub template_instances: Vec<String>,
138    pub destroy_order: Vec<String>,
139    pub create_order: Vec<String>,
140}
141
142const fn target_kind_text(kind: OrdinaryTemplateTargetKind) -> &'static str {
143    match kind {
144        OrdinaryTemplateTargetKind::Element => "element",
145        OrdinaryTemplateTargetKind::AttributeOrPropertyHost => "attribute_or_property_host",
146        OrdinaryTemplateTargetKind::EventHost => "event_host",
147        OrdinaryTemplateTargetKind::ConditionalBoundary => "conditional_boundary",
148        OrdinaryTemplateTargetKind::ListBoundary => "list_boundary",
149        OrdinaryTemplateTargetKind::FormControlHost => "form_control_host",
150        OrdinaryTemplateTargetKind::FormSubmissionHost => "form_submission_host",
151    }
152}
153
154const fn binding_kind_text(kind: OrdinaryTemplateBindingKind) -> &'static str {
155    match kind {
156        OrdinaryTemplateBindingKind::Text => "text",
157        OrdinaryTemplateBindingKind::Attribute => "attribute",
158        OrdinaryTemplateBindingKind::Property => "property",
159        OrdinaryTemplateBindingKind::Conditional => "conditional",
160        OrdinaryTemplateBindingKind::List => "list",
161        OrdinaryTemplateBindingKind::FormControl => "form_control",
162    }
163}
164
165#[must_use]
166#[allow(clippy::too_many_lines)]
167pub fn build_runtime_component_artifact(
168    model: &ApplicationSemanticModel,
169    optimized: &OptimizedComponentIrReport,
170) -> RuntimeComponentArtifact {
171    let ordinary = build_ordinary_template_instance_registry(model);
172    let ir = lower_components_to_ir(model);
173    let state_slots = build_state_instance_storage_registry(model, &ir);
174    let computed_slots = build_computed_instance_slot_registry(model, &ir);
175    let mut artifact = artifact_from_registry(&build_runtime_component_registry(model, optimized));
176    artifact.ordinary_template_targets = ordinary
177        .targets
178        .iter()
179        .map(|target| SerializedOrdinaryTemplateTarget {
180            id: target.target_id.to_string(),
181            component_instance_id: target.component_instance_id.to_string(),
182            component_id: target.component_id.to_string(),
183            template_entity_id: target.template_entity_id.to_string(),
184            kind: target_kind_text(target.target_kind).to_string(),
185        })
186        .collect();
187    artifact.ordinary_template_bindings = ordinary
188        .bindings
189        .iter()
190        .map(|binding| SerializedOrdinaryTemplateBinding {
191            id: binding.instance_binding_id.to_string(),
192            component_instance_id: binding.component_instance_id.to_string(),
193            component_id: binding.component_id.to_string(),
194            declaration_binding_id: binding.declaration_binding_id.to_string(),
195            target_id: binding.target_id.to_string(),
196            kind: binding_kind_text(binding.binding_kind).to_string(),
197            state_storage_ids: binding
198                .state_storage_ids
199                .iter()
200                .map(ToString::to_string)
201                .collect(),
202            computed_ids: binding
203                .computed_ids
204                .iter()
205                .map(ToString::to_string)
206                .collect(),
207            program_id: binding.existing_program_identity.to_string(),
208            expression: binding.expression.clone(),
209            attribute_name: binding.attribute_name.clone(),
210        })
211        .collect();
212    artifact.ordinary_template_events = ordinary
213        .events
214        .iter()
215        .map(|event| SerializedOrdinaryTemplateEvent {
216            component_instance_id: event.component_instance_id.to_string(),
217            component_id: event.component_id.to_string(),
218            target_id: event.target_id.to_string(),
219            declaration_event_id: event.declaration_event_id.to_string(),
220            event_type: event.event_type.clone(),
221            handler_method_id: event.handler_method_id.to_string(),
222            action_batch_id: event.action_batch_id.as_ref().map(ToString::to_string),
223            arguments: event.arguments.clone(),
224            program_id: event.existing_event_program_identity.to_string(),
225        })
226        .collect();
227    for instance in &mut artifact.instances {
228        instance.ordinary_template_targets = artifact
229            .ordinary_template_targets
230            .iter()
231            .filter(|target| target.component_instance_id == instance.instance)
232            .map(|target| target.id.clone())
233            .collect();
234        instance.ordinary_template_bindings = artifact
235            .ordinary_template_bindings
236            .iter()
237            .filter(|binding| binding.component_instance_id == instance.instance)
238            .map(|binding| binding.id.clone())
239            .collect();
240        instance.ordinary_template_events = artifact
241            .ordinary_template_events
242            .iter()
243            .filter(|event| event.component_instance_id == instance.instance)
244            .map(|event| event.declaration_event_id.clone())
245            .collect();
246        instance.state_slots = state_slots
247            .records
248            .iter()
249            .filter(|slot| slot.component_instance_id.to_string() == instance.instance)
250            .map(|slot| SerializedRuntimeStateSlot {
251                slot_id: slot.slot_id.to_string(),
252                state_id: slot.state_id.to_string(),
253                storage_id: slot.storage_id.to_string(),
254                initial_value: slot.initial_value.clone(),
255                semantic_type: semantic_type_text(&slot.semantic_type),
256                serializable: slot.serialization == SerializationCompatibility::Serializable,
257            })
258            .collect();
259        instance.computed_slots = computed_slots
260            .records
261            .iter()
262            .filter(|slot| slot.component_instance_id.to_string() == instance.instance)
263            .map(|slot| SerializedRuntimeComputedSlot {
264                computed_id: slot.computed_id.to_string(),
265                cache_slot_id: slot.cache_slot_id.to_string(),
266                dirty_slot_id: slot.dirty_slot_id.to_string(),
267                dirty_initial_value: slot.dirty_initial_value,
268            })
269            .collect();
270    }
271    let mut programs = std::collections::BTreeMap::<String, Vec<String>>::new();
272    for instance in model.component_instance_plan.instances.values() {
273        if instance.status == crate::ComponentInstanceStatus::StructuralTemplate {
274            if let Some(region) = &instance.structural_region {
275                programs
276                    .entry(region.to_string())
277                    .or_default()
278                    .push(instance.id.to_string());
279            }
280        }
281    }
282    artifact.structural_programs = programs
283        .into_iter()
284        .map(
285            |(region, template_instances)| SerializedStructuralComponentProgram {
286                region,
287                create_order: template_instances.clone(),
288                destroy_order: template_instances.iter().rev().cloned().collect(),
289                template_instances,
290            },
291        )
292        .collect();
293    artifact
294}
295
296#[must_use]
297pub fn artifact_from_registry(registry: &RuntimeComponentRegistry) -> RuntimeComponentArtifact {
298    RuntimeComponentArtifact {
299        schema_version: RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION,
300        component_definitions: registry
301            .component_definitions
302            .iter()
303            .map(|r| SerializedComponentDefinition {
304                component: r.component.to_string(),
305                template: r.template.to_string(),
306                slots: r.declared_slots.iter().map(ToString::to_string).collect(),
307                boundary: "client".to_string(),
308            })
309            .collect(),
310        instances: registry
311            .instances
312            .iter()
313            .map(|r| SerializedComponentInstance {
314                instance: r.instance.to_string(),
315                component: r.component.to_string(),
316                invocation: r.invocation.as_ref().map(ToString::to_string),
317                parent: r.parent.as_ref().map(ToString::to_string),
318                depth: r.depth,
319                initialization_batch: r.initialization_batch,
320                ordinary_template_targets: Vec::new(),
321                ordinary_template_bindings: Vec::new(),
322                ordinary_template_events: Vec::new(),
323                state_slots: Vec::new(),
324                computed_slots: Vec::new(),
325                context_prefix: r.instance_context_prefix.clone(),
326                instruction_indices: r.optimized_instruction_indices.clone(),
327                structural_region: r.structural_region.as_ref().map(ToString::to_string),
328            })
329            .collect(),
330        initialization_batches: registry
331            .initialization_batches
332            .iter()
333            .map(|r| SerializedComponentBatch {
334                index: r.index,
335                instances: r.instances.iter().map(ToString::to_string).collect(),
336                context_sources: r.context_sources.iter().map(ToString::to_string).collect(),
337                slot_bindings: r.slot_bindings.iter().map(ToString::to_string).collect(),
338            })
339            .collect(),
340        slot_binding_programs: registry
341            .slot_bindings
342            .iter()
343            .map(|r| SerializedSlotBinding {
344                binding: r.binding.to_string(),
345                caller_instance: r.caller_instance.to_string(),
346                callee_instance: r.callee_instance.to_string(),
347                slot: r.slot.to_string(),
348                outlet: r.outlet.to_string(),
349                fragment: r.fragment.to_string(),
350                content_owner_instance: r.content_owner_instance.to_string(),
351            })
352            .collect(),
353        instance_context_bindings: registry
354            .instance_context_bindings
355            .iter()
356            .map(|r| SerializedInstanceContextBinding {
357                consumer_instance: r.consumer_instance.to_string(),
358                selected_source: r.selected_source.to_string(),
359                provider_source: r.provider_source.as_ref().map(ToString::to_string),
360                runtime_slot: r.runtime_slot.to_string(),
361            })
362            .collect(),
363        ordinary_template_targets: Vec::new(),
364        ordinary_template_bindings: Vec::new(),
365        ordinary_template_events: Vec::new(),
366        destruction: SerializedDestructionMetadata {
367            operation: "destroy_component_instance".to_string(),
368            enabled: true,
369        },
370        structural_programs: Vec::new(),
371    }
372}
373
374/// # Panics
375///
376/// Panics only if compiler-owned artifact data cannot be serialized.
377#[must_use]
378pub fn runtime_component_artifact_json(artifact: &RuntimeComponentArtifact) -> String {
379    serde_json::to_string_pretty(artifact).expect("component runtime artifact serializes") + "\n"
380}
381
382/// # Errors
383///
384/// Returns an error for an unsupported schema or invalid canonical endpoints/order.
385#[allow(clippy::too_many_lines)]
386pub fn validate_runtime_component_artifact(
387    artifact: &RuntimeComponentArtifact,
388) -> Result<(), String> {
389    if artifact.schema_version != RUNTIME_COMPONENT_ARTIFACT_SCHEMA_VERSION {
390        return Err("unsupported component runtime artifact schema version".to_string());
391    }
392    if artifact.structural_programs.iter().any(|program| {
393        program.create_order != program.template_instances
394            || program
395                .destroy_order
396                .iter()
397                .rev()
398                .cloned()
399                .collect::<Vec<_>>()
400                != program.template_instances
401    }) {
402        return Err("component artifact has invalid structural program ordering".to_string());
403    }
404    let instances = artifact
405        .instances
406        .iter()
407        .map(|r| r.instance.as_str())
408        .collect::<std::collections::BTreeSet<_>>();
409    let target_ids = artifact
410        .ordinary_template_targets
411        .iter()
412        .map(|target| target.id.as_str())
413        .collect::<std::collections::BTreeSet<_>>();
414    let binding_ids = artifact
415        .ordinary_template_bindings
416        .iter()
417        .map(|binding| binding.id.as_str())
418        .collect::<std::collections::BTreeSet<_>>();
419    let event_keys = artifact
420        .ordinary_template_events
421        .iter()
422        .map(|event| {
423            (
424                event.component_instance_id.as_str(),
425                event.target_id.as_str(),
426                event.event_type.as_str(),
427            )
428        })
429        .collect::<std::collections::BTreeSet<_>>();
430    let mut computed_cache_slots = std::collections::BTreeSet::new();
431    let mut computed_dirty_slots = std::collections::BTreeSet::new();
432    let mut computed_instance_pairs = std::collections::BTreeSet::new();
433    let mut state_slots = std::collections::BTreeSet::new();
434    let mut state_instance_pairs = std::collections::BTreeSet::new();
435    if target_ids.len() != artifact.ordinary_template_targets.len()
436        || binding_ids.len() != artifact.ordinary_template_bindings.len()
437        || event_keys.len() != artifact.ordinary_template_events.len()
438        || artifact.ordinary_template_targets.iter().any(|target| {
439            !instances.contains(target.component_instance_id.as_str())
440                || !target.id.starts_with(&format!(
441                    "{}/template-target:",
442                    target.component_instance_id
443                ))
444        })
445        || artifact.ordinary_template_bindings.iter().any(|binding| {
446            !instances.contains(binding.component_instance_id.as_str())
447                || !target_ids.contains(binding.target_id.as_str())
448                || !binding.id.starts_with(&format!(
449                    "{}/template-binding:",
450                    binding.component_instance_id
451                ))
452        })
453        || artifact.ordinary_template_events.iter().any(|event| {
454            !instances.contains(event.component_instance_id.as_str())
455                || !target_ids.contains(event.target_id.as_str())
456                || event.action_batch_id.is_none()
457        })
458        || artifact.instances.iter().any(|instance| {
459            instance
460                .ordinary_template_targets
461                .iter()
462                .any(|id| !target_ids.contains(id.as_str()))
463                || instance
464                    .ordinary_template_bindings
465                    .iter()
466                    .any(|id| !binding_ids.contains(id.as_str()))
467                || instance.ordinary_template_targets
468                    != artifact
469                        .ordinary_template_targets
470                        .iter()
471                        .filter(|target| target.component_instance_id == instance.instance)
472                        .map(|target| target.id.clone())
473                        .collect::<Vec<_>>()
474                || instance.ordinary_template_bindings
475                    != artifact
476                        .ordinary_template_bindings
477                        .iter()
478                        .filter(|binding| binding.component_instance_id == instance.instance)
479                        .map(|binding| binding.id.clone())
480                        .collect::<Vec<_>>()
481                || instance.ordinary_template_events
482                    != artifact
483                        .ordinary_template_events
484                        .iter()
485                        .filter(|event| event.component_instance_id == instance.instance)
486                        .map(|event| event.declaration_event_id.clone())
487                        .collect::<Vec<_>>()
488                || instance.state_slots.iter().any(|slot| {
489                    slot.slot_id != canonical_state_slot_text(&instance.instance, &slot.storage_id)
490                        || slot.storage_id != format!("storage:{}", slot.state_id)
491                        || !state_slots.insert(slot.slot_id.as_str())
492                        || !state_instance_pairs
493                            .insert((instance.instance.as_str(), slot.storage_id.as_str()))
494                })
495                || instance.computed_slots.iter().any(|slot| {
496                    !slot
497                        .cache_slot_id
498                        .starts_with(&format!("{}/computed-cache:", instance.instance))
499                        || !slot
500                            .dirty_slot_id
501                            .starts_with(&format!("{}/computed-dirty:", instance.instance))
502                        || !computed_cache_slots.insert(slot.cache_slot_id.as_str())
503                        || !computed_dirty_slots.insert(slot.dirty_slot_id.as_str())
504                        || !computed_instance_pairs
505                            .insert((instance.instance.as_str(), slot.computed_id.as_str()))
506                })
507        })
508    {
509        return Err("component artifact has invalid ordinary template projection".to_string());
510    }
511    if artifact.instances.iter().any(|r| {
512        r.parent
513            .as_deref()
514            .is_some_and(|parent| !instances.contains(parent))
515    }) {
516        return Err("component artifact has an unknown parent instance".to_string());
517    }
518    if artifact.slot_binding_programs.iter().any(|r| {
519        !instances.contains(r.caller_instance.as_str())
520            || !instances.contains(r.callee_instance.as_str())
521    }) {
522        return Err("component artifact has an unknown Slot-binding endpoint".to_string());
523    }
524    if artifact
525        .initialization_batches
526        .iter()
527        .enumerate()
528        .any(|(index, batch)| {
529            batch.index != index
530                || batch
531                    .instances
532                    .iter()
533                    .any(|id| !instances.contains(id.as_str()))
534        })
535    {
536        return Err("component artifact has invalid initialization ordering".to_string());
537    }
538    Ok(())
539}
540
541fn canonical_state_slot_text(instance: &str, storage: &str) -> String {
542    let encoded = storage.bytes().fold(String::new(), |mut encoded, byte| {
543        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
544            encoded.push(char::from(byte));
545        } else {
546            use std::fmt::Write as _;
547            write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail");
548        }
549        encoded
550    });
551    format!("{instance}/state-slot:{encoded}")
552}
553
554#[cfg(test)]
555mod tests {
556    use crate::{
557        build_application_semantic_model, build_runtime_component_artifact,
558        runtime_component_artifact_json, validate_runtime_component_artifact,
559    };
560    #[test]
561    fn serializes_deterministically_and_rejects_unknown_endpoints() {
562        let model = build_application_semantic_model(&presolve_parser::parse_file(
563            "src/Artifact.tsx",
564            r#"@component("x-page") class Page extends Component { render() { return <main />; } }"#,
565        ));
566        let mut artifact =
567            build_runtime_component_artifact(&model, &model.component_ir_optimization);
568        assert!(validate_runtime_component_artifact(&artifact).is_ok());
569        assert_eq!(
570            runtime_component_artifact_json(&artifact),
571            runtime_component_artifact_json(&build_runtime_component_artifact(
572                &model,
573                &model.component_ir_optimization
574            ))
575        );
576        artifact.instances[0].parent = Some("missing".to_string());
577        assert!(validate_runtime_component_artifact(&artifact).is_err());
578    }
579
580    #[test]
581    fn projects_exact_computed_slots_for_each_repeated_instance() {
582        let model = build_application_semantic_model(&presolve_parser::parse_file(
583            "src/RepeatedComputedArtifact.tsx",
584            r#"@component("x-child") class Child { count = state(1); @computed() get doubled() { return this.count * 2; } render() { return <span>{this.doubled}</span>; } }
585@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
586        ));
587        let mut artifact =
588            build_runtime_component_artifact(&model, &model.component_ir_optimization);
589        let slots = artifact
590            .instances
591            .iter()
592            .flat_map(|instance| instance.computed_slots.iter())
593            .collect::<Vec<_>>();
594        assert_eq!(slots.len(), 2);
595        assert_ne!(slots[0].cache_slot_id, slots[1].cache_slot_id);
596        assert_ne!(slots[0].dirty_slot_id, slots[1].dirty_slot_id);
597        assert!(validate_runtime_component_artifact(&artifact).is_ok());
598        let computed_instances = artifact
599            .instances
600            .iter()
601            .enumerate()
602            .filter(|(_, instance)| !instance.computed_slots.is_empty())
603            .map(|(index, _)| index)
604            .collect::<Vec<_>>();
605        let duplicate = artifact.instances[computed_instances[0]].computed_slots[0].clone();
606        artifact.instances[computed_instances[1]].computed_slots[0].cache_slot_id =
607            duplicate.cache_slot_id;
608        assert!(validate_runtime_component_artifact(&artifact).is_err());
609    }
610
611    #[test]
612    fn projects_and_validates_exact_state_slots_for_repeated_instances() {
613        let model = build_application_semantic_model(&presolve_parser::parse_file(
614            "src/RepeatedStateArtifact.tsx",
615            r#"@component("x-child") class Child { count = state(1); render() { return <span>{this.count}</span>; } }
616@component("x-parent") class Parent { render() { return <><Child /><Child /></>; } }"#,
617        ));
618        let mut artifact =
619            build_runtime_component_artifact(&model, &model.component_ir_optimization);
620        let state_instances = artifact
621            .instances
622            .iter()
623            .enumerate()
624            .filter(|(_, instance)| !instance.state_slots.is_empty())
625            .map(|(index, _)| index)
626            .collect::<Vec<_>>();
627        assert_eq!(state_instances.len(), 2);
628        let first = &artifact.instances[state_instances[0]].state_slots[0];
629        let second = &artifact.instances[state_instances[1]].state_slots[0];
630        assert_eq!(first.storage_id, second.storage_id);
631        assert_ne!(first.slot_id, second.slot_id);
632        assert!(validate_runtime_component_artifact(&artifact).is_ok());
633
634        artifact.instances[state_instances[1]].state_slots[0].slot_id = first.slot_id.clone();
635        assert!(validate_runtime_component_artifact(&artifact).is_err());
636    }
637}