presolve_compiler/
component_ir.rs1use crate::{
2 ApplicationSemanticModel, ComponentInstanceId, InstanceContextValueSlotId, SemanticId,
3 SlotBindingId, SourceProvenance,
4};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ComponentIrOperation {
8 CreateComponentInstance {
9 instance: ComponentInstanceId,
10 component: SemanticId,
11 parent: Option<ComponentInstanceId>,
12 },
13 InitializeComponentInstance {
14 instance: ComponentInstanceId,
15 context_slots: Vec<InstanceContextValueSlotId>,
16 },
17 MaterializeComponentTemplate {
18 instance: ComponentInstanceId,
19 template: SemanticId,
20 },
21 BindSlotContent {
22 binding: SlotBindingId,
23 callee_instance: ComponentInstanceId,
24 slot: crate::SlotId,
25 content_fragment: crate::SlotContentFragmentId,
26 content_owner_instance: ComponentInstanceId,
27 },
28 DestroyComponentInstance {
29 instance: ComponentInstanceId,
30 },
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct ComponentIrInstruction {
35 pub index: usize,
36 pub operation: ComponentIrOperation,
37 pub provenance: SourceProvenance,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct ComponentIrReport {
42 pub instructions: Vec<ComponentIrInstruction>,
43}
44
45#[must_use]
47pub fn lower_component_ir(model: &ApplicationSemanticModel) -> ComponentIrReport {
48 let mut instructions = Vec::new();
49 let mut push = |operation, provenance: SourceProvenance| {
50 let index = instructions.len();
51 instructions.push(ComponentIrInstruction {
52 index,
53 operation,
54 provenance,
55 });
56 };
57 for batch in &model.component_initialization.instance_batches {
58 for instance_id in &batch.instances {
59 let Some(instance) = model.component_instance_plan.instances.get(instance_id) else {
60 continue;
61 };
62 push(
63 ComponentIrOperation::CreateComponentInstance {
64 instance: instance.id.clone(),
65 component: instance.component.clone(),
66 parent: instance.parent_instance.clone(),
67 },
68 instance.provenance.clone(),
69 );
70 let context_slots = model
71 .instance_context
72 .resolutions
73 .values()
74 .filter(|resolution| resolution.consumer_instance.component_instance == instance.id)
75 .filter(|resolution| {
76 model
77 .composition_types
78 .instance_context_bindings
79 .get(&resolution.consumer_instance)
80 .is_some_and(|record| {
81 record.overall == crate::CompositionCompatibility::Compatible
82 })
83 })
84 .filter_map(|resolution| resolution.value_slot.clone())
85 .collect();
86 push(
87 ComponentIrOperation::InitializeComponentInstance {
88 instance: instance.id.clone(),
89 context_slots,
90 },
91 instance.provenance.clone(),
92 );
93 push(
94 ComponentIrOperation::MaterializeComponentTemplate {
95 instance: instance.id.clone(),
96 template: instance.component.template(),
97 },
98 instance.provenance.clone(),
99 );
100 }
101 }
102 for batch in &model.component_initialization.slot_binding_batches {
103 for binding_id in &batch.bindings {
104 let Some(binding) = model.slot_bindings.binding(binding_id) else {
105 continue;
106 };
107 let (Some(slot), Some(content_fragment)) =
108 (binding.slot.clone(), binding.content_fragment.clone())
109 else {
110 continue;
111 };
112 push(
113 ComponentIrOperation::BindSlotContent {
114 binding: binding.id.clone(),
115 callee_instance: binding.callee_instance.clone(),
116 slot,
117 content_fragment,
118 content_owner_instance: binding.content_owner_instance.clone(),
119 },
120 binding.provenance.clone(),
121 );
122 }
123 }
124 ComponentIrReport { instructions }
125}
126
127#[must_use]
128pub fn validate_component_ir(
129 model: &ApplicationSemanticModel,
130 report: &ComponentIrReport,
131) -> Vec<String> {
132 let expected = lower_component_ir(model);
133 (report != &expected)
134 .then(|| "component IR does not match the canonical H10 plan".to_string())
135 .into_iter()
136 .collect()
137}
138
139#[cfg(test)]
140mod tests {
141 use crate::{
142 build_application_semantic_model, lower_component_ir, validate_component_ir,
143 ComponentIrOperation,
144 };
145
146 #[test]
147 fn lowers_ordered_instance_context_and_slot_operations() {
148 let asm = build_application_semantic_model(&presolve_parser::parse_file(
149 "src/Ir.tsx",
150 r#"
151@component("x-card") class Card extends Component { @slot() children!: SlotContent; render() { return <article><slot /></article>; } }
152@component("x-page") class Page extends Component { render() { return <Card><p /></Card>; } }
153"#,
154 ));
155 let report = lower_component_ir(&asm);
156 assert_eq!(report.instructions.len(), 7);
157 assert!(matches!(
158 report.instructions[0].operation,
159 ComponentIrOperation::CreateComponentInstance { .. }
160 ));
161 assert!(matches!(
162 report.instructions[6].operation,
163 ComponentIrOperation::BindSlotContent { .. }
164 ));
165 assert!(validate_component_ir(&asm, &report).is_empty());
166 }
167
168 #[test]
169 fn asm_validation_rejects_mutated_component_ir() {
170 let mut asm = build_application_semantic_model(&presolve_parser::parse_file(
171 "src/IrValidate.tsx",
172 r#"
173@component("x-page") class Page extends Component { render() { return <main />; } }
174"#,
175 ));
176 asm.component_ir.instructions.clear();
177 assert!(crate::validate_application_semantic_model(&asm)
178 .iter()
179 .any(|diagnostic| diagnostic.code == "PSASM1199"));
180 }
181}