Skip to main content

presolve_compiler/
component_ir.rs

1use 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/// Lower only executable H10 initialization/binding plans into observable IR.
46#[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}