1use crate::graph::{GraphNode, Kernel, Workflow, WorkflowError};
4use crate::runtime::{WorkflowInputs, WorkflowResults};
5use molframe_core::ExecutionContext;
6use std::any::TypeId;
7use std::collections::{BTreeSet, HashMap, VecDeque};
8
9#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct PhysicalNode {
12 pub ordinal: usize,
14 pub node: usize,
16 pub operation: &'static str,
18 pub dependencies: Vec<usize>,
20 pub cost: crate::Cost,
22 pub estimated_output_bytes: usize,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Explanation {
29 pub logical_node_count: usize,
31 pub physical_nodes: Vec<PhysicalNode>,
33 pub common_subexpressions_eliminated: usize,
35 pub dead_nodes_eliminated: usize,
37 pub estimated_peak_bytes: usize,
39 pub spatial_consumers: usize,
41}
42
43#[derive(Clone)]
44pub(crate) struct CompiledNode {
45 pub(crate) index: usize,
46 pub(crate) dependencies: Vec<usize>,
47 pub(crate) input_name: Option<Box<str>>,
48 pub(crate) type_id: TypeId,
49 pub(crate) type_name: &'static str,
50 pub(crate) operation: &'static str,
51 pub(crate) kernel: Option<Kernel>,
52}
53
54impl std::fmt::Debug for CompiledNode {
55 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 formatter
57 .debug_struct("CompiledNode")
58 .field("index", &self.index)
59 .field("dependencies", &self.dependencies)
60 .field("input_name", &self.input_name)
61 .field("type_name", &self.type_name)
62 .field("operation", &self.operation)
63 .finish_non_exhaustive()
64 }
65}
66
67#[derive(Clone, Debug)]
69pub struct CompiledWorkflow {
70 pub(crate) graph_id: u64,
71 pub(crate) nodes: Vec<CompiledNode>,
72 pub(crate) outputs: Vec<(Box<str>, usize, TypeId)>,
73 pub(crate) use_counts: Vec<usize>,
74 pub(crate) explanation: Explanation,
75}
76
77impl Workflow {
78 pub fn compile(&self) -> Result<CompiledWorkflow, WorkflowError> {
89 if self.outputs.is_empty() {
90 return Err(WorkflowError::NoOutputs);
91 }
92 validate_types(&self.nodes)?;
93 let logical_order = stable_topological_order(&self.nodes)?;
94 let aliases = common_subexpressions(&self.nodes, &logical_order);
95 let canonical_outputs = self
96 .outputs
97 .iter()
98 .map(|(name, node, type_id)| (name.clone(), aliases[*node], *type_id))
99 .collect::<Vec<_>>();
100 let active = reachable_nodes(&self.nodes, &aliases, &canonical_outputs);
101 let physical_order = logical_order
102 .iter()
103 .copied()
104 .filter(|node| active.contains(&aliases[*node]) && aliases[*node] == *node)
105 .collect::<Vec<_>>();
106 let nodes = physical_order
107 .iter()
108 .map(|index| {
109 let node = &self.nodes[*index];
110 CompiledNode {
111 index: *index,
112 dependencies: node
113 .dependencies
114 .iter()
115 .map(|dependency| aliases[*dependency])
116 .collect(),
117 input_name: node.input_name.clone(),
118 type_id: node.type_id,
119 type_name: node.type_name,
120 operation: node.metadata.name,
121 kernel: node.kernel.clone(),
122 }
123 })
124 .collect::<Vec<_>>();
125 let use_counts = use_counts(self.nodes.len(), &nodes, &canonical_outputs);
126 let estimated_peak_bytes =
127 estimate_peak(&self.nodes, &nodes, &canonical_outputs, &use_counts);
128 let physical_nodes = nodes
129 .iter()
130 .enumerate()
131 .map(|(ordinal, node)| PhysicalNode {
132 ordinal,
133 node: node.index,
134 operation: node.operation,
135 dependencies: node.dependencies.clone(),
136 cost: self.nodes[node.index].metadata.cost,
137 estimated_output_bytes: self.nodes[node.index].metadata.estimated_output_bytes,
138 })
139 .collect();
140 let common_subexpressions_eliminated = aliases
141 .iter()
142 .enumerate()
143 .filter(|(node, canonical)| *node != **canonical)
144 .count();
145 let dead_nodes_eliminated = self
146 .nodes
147 .len()
148 .saturating_sub(nodes.len() + common_subexpressions_eliminated);
149 let spatial_consumers = nodes
150 .iter()
151 .filter(|node| self.nodes[node.index].metadata.uses_spatial_index)
152 .count();
153 Ok(CompiledWorkflow {
154 graph_id: self.id,
155 nodes,
156 outputs: canonical_outputs,
157 use_counts,
158 explanation: Explanation {
159 logical_node_count: self.nodes.len(),
160 physical_nodes,
161 common_subexpressions_eliminated,
162 dead_nodes_eliminated,
163 estimated_peak_bytes,
164 spatial_consumers,
165 },
166 })
167 }
168}
169
170impl CompiledWorkflow {
171 #[must_use]
173 pub const fn explain(&self) -> &Explanation {
174 &self.explanation
175 }
176
177 pub fn run(
184 &self,
185 inputs: &WorkflowInputs,
186 context: &ExecutionContext,
187 ) -> Result<WorkflowResults, WorkflowError> {
188 crate::runtime::execute(self, inputs, context)
189 }
190}
191
192fn validate_types(nodes: &[GraphNode]) -> Result<(), WorkflowError> {
193 for (index, node) in nodes.iter().enumerate() {
194 if node.dependencies.len() != node.expected_inputs.len() {
195 return Err(WorkflowError::TypeMismatch { node: index });
196 }
197 for (dependency, expected) in node.dependencies.iter().zip(&node.expected_inputs) {
198 let Some(actual) = nodes.get(*dependency) else {
199 return Err(WorkflowError::MissingDependency {
200 node: index,
201 dependency: *dependency,
202 });
203 };
204 if actual.type_id != *expected {
205 return Err(WorkflowError::TypeMismatch { node: index });
206 }
207 }
208 }
209 Ok(())
210}
211
212fn stable_topological_order(nodes: &[GraphNode]) -> Result<Vec<usize>, WorkflowError> {
213 let mut incoming = nodes
214 .iter()
215 .map(|node| node.dependencies.len())
216 .collect::<Vec<_>>();
217 let mut outgoing = vec![Vec::new(); nodes.len()];
218 for (node, record) in nodes.iter().enumerate() {
219 for dependency in &record.dependencies {
220 outgoing[*dependency].push(node);
221 }
222 }
223 let mut ready = incoming
224 .iter()
225 .enumerate()
226 .filter_map(|(index, count)| (*count == 0).then_some(index))
227 .collect::<BTreeSet<_>>();
228 let mut order = Vec::with_capacity(nodes.len());
229 while let Some(index) = ready.pop_first() {
230 order.push(index);
231 for dependent in &outgoing[index] {
232 incoming[*dependent] -= 1;
233 if incoming[*dependent] == 0 {
234 ready.insert(*dependent);
235 }
236 }
237 }
238 if order.len() == nodes.len() {
239 Ok(order)
240 } else {
241 Err(WorkflowError::Cycle)
242 }
243}
244
245fn common_subexpressions(nodes: &[GraphNode], order: &[usize]) -> Vec<usize> {
246 let mut aliases = (0..nodes.len()).collect::<Vec<_>>();
247 let mut seen: HashMap<(Box<str>, Vec<usize>, TypeId), usize> = HashMap::new();
248 for index in order {
249 let node = &nodes[*index];
250 let Some(key) = &node.metadata.cse_key else {
251 continue;
252 };
253 let dependencies = node
254 .dependencies
255 .iter()
256 .map(|dependency| aliases[*dependency])
257 .collect::<Vec<_>>();
258 let signature = (key.clone(), dependencies, node.type_id);
259 if let Some(canonical) = seen.get(&signature) {
260 aliases[*index] = *canonical;
261 } else {
262 seen.insert(signature, *index);
263 }
264 }
265 aliases
266}
267
268fn reachable_nodes(
269 nodes: &[GraphNode],
270 aliases: &[usize],
271 outputs: &[(Box<str>, usize, TypeId)],
272) -> BTreeSet<usize> {
273 let mut active = BTreeSet::new();
274 let mut queue = outputs
275 .iter()
276 .map(|(_, node, _)| *node)
277 .collect::<VecDeque<_>>();
278 while let Some(index) = queue.pop_front() {
279 if !active.insert(index) {
280 continue;
281 }
282 for dependency in &nodes[index].dependencies {
283 queue.push_back(aliases[*dependency]);
284 }
285 }
286 active
287}
288
289fn use_counts(
290 node_count: usize,
291 nodes: &[CompiledNode],
292 outputs: &[(Box<str>, usize, TypeId)],
293) -> Vec<usize> {
294 let mut counts = vec![0; node_count];
295 for node in nodes {
296 for dependency in &node.dependencies {
297 counts[*dependency] += 1;
298 }
299 }
300 for (_, node, _) in outputs {
301 counts[*node] += 1;
302 }
303 counts
304}
305
306fn estimate_peak(
307 logical: &[GraphNode],
308 nodes: &[CompiledNode],
309 outputs: &[(Box<str>, usize, TypeId)],
310 initial_counts: &[usize],
311) -> usize {
312 let output_nodes = outputs
313 .iter()
314 .map(|(_, node, _)| *node)
315 .collect::<BTreeSet<_>>();
316 let mut counts = initial_counts.to_vec();
317 let mut live = 0usize;
318 let mut peak = 0usize;
319 for node in nodes {
320 live = live.saturating_add(logical[node.index].metadata.estimated_output_bytes);
321 peak = peak.max(live);
322 for dependency in &node.dependencies {
323 counts[*dependency] = counts[*dependency].saturating_sub(1);
324 if counts[*dependency] == 0 && !output_nodes.contains(dependency) {
325 live = live.saturating_sub(logical[*dependency].metadata.estimated_output_bytes);
326 }
327 }
328 }
329 peak
330}