Skip to main content

presolve_compiler/
component_initialization.rs

1use std::collections::BTreeMap;
2
3use crate::{
4    ComponentInstanceId, ComponentInstancePlan, CompositionCompatibility, CompositionTypeProducts,
5    ContextSourceInstanceId, InstanceContextRegistry, IrReactiveEdge, IrReactiveEdgeKind,
6    IrReactiveGraph, IrReactiveNode, IrReactiveNodeKind, IrUpdateScheduler, SlotBindingId,
7    SlotBindingRegistry,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ComponentInstanceBatch {
12    pub index: usize,
13    pub instances: Vec<ComponentInstanceId>,
14    pub context_sources: Vec<ContextSourceInstanceId>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SlotBindingBatch {
19    pub index: usize,
20    pub bindings: Vec<SlotBindingId>,
21    pub prerequisite_instances: Vec<ComponentInstanceId>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Default)]
25pub struct ComponentInitializationPlan {
26    pub root_instances: Vec<ComponentInstanceId>,
27    pub instance_batches: Vec<ComponentInstanceBatch>,
28    pub slot_binding_batches: Vec<SlotBindingBatch>,
29    pub blocked_instances: Vec<ComponentInstanceId>,
30}
31
32#[must_use]
33#[allow(clippy::too_many_lines)]
34pub fn plan_component_initialization(
35    instances: &ComponentInstancePlan,
36    bindings: &SlotBindingRegistry,
37    types: &CompositionTypeProducts,
38    instance_context: &InstanceContextRegistry,
39) -> ComponentInitializationPlan {
40    let nodes = instances
41        .instances
42        .values()
43        .map(|instance| {
44            (
45                instance.id.as_str().to_string(),
46                IrReactiveNode {
47                    id: instance.id.as_str().to_string(),
48                    kind: IrReactiveNodeKind::Template,
49                    provenance: instance.provenance.clone(),
50                },
51            )
52        })
53        .collect();
54    let edges = instances
55        .instances
56        .values()
57        .filter_map(|instance| {
58            Some(IrReactiveEdge {
59                source: instance.parent_instance.as_ref()?.as_str().to_string(),
60                target: instance.id.as_str().to_string(),
61                kind: IrReactiveEdgeKind::Invalidates,
62                provenance: instance.provenance.clone(),
63            })
64        })
65        .collect();
66    let scheduler = IrUpdateScheduler::new(IrReactiveGraph { nodes, edges });
67    let instance_batches = scheduler
68        .update_batches()
69        .into_iter()
70        .enumerate()
71        .map(|(index, ids)| {
72            let instances = ids
73                .into_iter()
74                .filter_map(|id| {
75                    instances
76                        .instances
77                        .keys()
78                        .find(|instance| instance.as_str() == id)
79                        .cloned()
80                })
81                .collect::<Vec<_>>();
82            let context_sources = instance_context
83                .resolutions
84                .values()
85                .filter(|resolution| {
86                    instances.contains(&resolution.consumer_instance.component_instance)
87                })
88                .filter(|resolution| {
89                    types
90                        .instance_context_bindings
91                        .get(&resolution.consumer_instance)
92                        .is_some_and(|record| {
93                            record.overall == CompositionCompatibility::Compatible
94                        })
95                })
96                .filter_map(|resolution| resolution.selected_source.clone())
97                .collect();
98            ComponentInstanceBatch {
99                index,
100                instances,
101                context_sources,
102            }
103        })
104        .collect::<Vec<_>>();
105    let batch_by_instance = instance_batches
106        .iter()
107        .flat_map(|batch| {
108            batch
109                .instances
110                .iter()
111                .cloned()
112                .map(move |instance| (instance, batch.index))
113        })
114        .collect::<BTreeMap<_, _>>();
115    let mut slot_batches = BTreeMap::<usize, Vec<SlotBindingId>>::new();
116    let mut prerequisites = BTreeMap::<usize, Vec<ComponentInstanceId>>::new();
117    for binding in bindings.bindings.values() {
118        if types
119            .slot_bindings
120            .get(&binding.id)
121            .is_none_or(|record| record.overall != CompositionCompatibility::Compatible)
122            || binding.content_fragment.is_none()
123        {
124            continue;
125        }
126        let Some(index) = [
127            batch_by_instance.get(&binding.caller_instance),
128            batch_by_instance.get(&binding.callee_instance),
129        ]
130        .into_iter()
131        .flatten()
132        .max()
133        .map(|index| index + 1) else {
134            continue;
135        };
136        slot_batches
137            .entry(index)
138            .or_default()
139            .push(binding.id.clone());
140        prerequisites.entry(index).or_default().extend([
141            binding.caller_instance.clone(),
142            binding.callee_instance.clone(),
143        ]);
144    }
145    let slot_binding_batches = slot_batches
146        .into_iter()
147        .map(|(index, mut bindings)| {
148            bindings.sort();
149            let mut prerequisite_instances = prerequisites.remove(&index).unwrap_or_default();
150            prerequisite_instances.sort();
151            prerequisite_instances.dedup();
152            SlotBindingBatch {
153                index,
154                bindings,
155                prerequisite_instances,
156            }
157        })
158        .collect();
159
160    ComponentInitializationPlan {
161        root_instances: instances
162            .instances
163            .values()
164            .filter(|instance| instance.parent_instance.is_none())
165            .map(|instance| instance.id.clone())
166            .collect(),
167        instance_batches,
168        slot_binding_batches,
169        blocked_instances: instances.blocked.keys().cloned().collect(),
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use crate::{build_application_semantic_model, validate_application_semantic_model};
176
177    #[test]
178    fn schedules_parents_before_children_and_groups_independent_siblings() {
179        let asm = build_application_semantic_model(&presolve_parser::parse_file(
180            "src/Initialization.tsx",
181            r#"
182@component("x-card") class Card extends Component {
183  @slot() children!: SlotContent;
184  render() { return <article><slot /></article>; }
185}
186@component("x-page") class Page extends Component {
187  render() { return <main><Card><p>one</p></Card><Card><p>two</p></Card></main>; }
188}
189"#,
190        ));
191        let plan = &asm.component_initialization;
192        assert_eq!(plan.root_instances.len(), 1);
193        assert_eq!(plan.instance_batches.len(), 2);
194        assert_eq!(plan.instance_batches[0].instances.len(), 1);
195        assert_eq!(plan.instance_batches[1].instances.len(), 2);
196        assert_eq!(plan.slot_binding_batches.len(), 1);
197        assert_eq!(plan.slot_binding_batches[0].bindings.len(), 2);
198        assert_eq!(plan.slot_binding_batches[0].prerequisite_instances.len(), 3);
199        assert!(plan.blocked_instances.is_empty());
200        assert!(validate_application_semantic_model(&asm).is_empty());
201    }
202
203    #[test]
204    fn excludes_blocked_and_ineligible_bindings_and_keeps_context_ready_with_instance() {
205        let asm = build_application_semantic_model(&presolve_parser::parse_file(
206            "src/InitializationBlocked.tsx",
207            r#"
208@component("x-theme") class Theme extends Component { @context() color!: string; render() { return <div />; } }
209@component("x-leaf") class Leaf extends Component { @consume(Theme.color) color!: string; render() { return <span />; } }
210@component("x-card") class Card extends Component { @provide(Theme.color) color: string = "blue"; render() { return <Leaf />; } }
211@component("x-page") class Page extends Component { render() { return <main><Card /><Missing><p /></Missing></main>; } }
212"#,
213        ));
214        let plan = &asm.component_initialization;
215        assert_eq!(plan.blocked_instances.len(), 1);
216        assert!(plan.slot_binding_batches.is_empty());
217        assert!(plan
218            .instance_batches
219            .iter()
220            .any(|batch| !batch.context_sources.is_empty()));
221    }
222
223    #[test]
224    fn validation_rejects_mutated_initialization_plan() {
225        let mut asm = build_application_semantic_model(&presolve_parser::parse_file(
226            "src/ValidateInitialization.tsx",
227            r#"@component("x-page") class Page extends Component { render() { return <main />; } }"#,
228        ));
229        asm.component_initialization.root_instances.clear();
230        assert!(validate_application_semantic_model(&asm)
231            .iter()
232            .any(|diagnostic| diagnostic.code == "PSASM1198"));
233    }
234}