presolve_compiler/
component_initialization.rs1use 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}