Skip to main content

molframe_engine/
compile.rs

1//! Validation, graph rewrites and the reusable physical plan.
2
3use 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/// One node in a compiled physical explanation.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct PhysicalNode {
12    /// Stable physical execution ordinal.
13    pub ordinal: usize,
14    /// Original graph node index.
15    pub node: usize,
16    /// Canonical operation name.
17    pub operation: &'static str,
18    /// Canonical dependency node indices.
19    pub dependencies: Vec<usize>,
20    /// Dominant data-movement cost.
21    pub cost: crate::Cost,
22    /// Conservative retained result bytes.
23    pub estimated_output_bytes: usize,
24}
25
26/// Logical and physical views of a compiled workflow.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Explanation {
29    /// Nodes declared in the logical graph, before dead-node removal.
30    pub logical_node_count: usize,
31    /// Nodes retained by the physical graph.
32    pub physical_nodes: Vec<PhysicalNode>,
33    /// Logical nodes removed as common subexpressions.
34    pub common_subexpressions_eliminated: usize,
35    /// Logical nodes removed because no output reaches them.
36    pub dead_nodes_eliminated: usize,
37    /// Conservative peak retained bytes from output-lifetime planning.
38    pub estimated_peak_bytes: usize,
39    /// Number of retained operations that can share spatial planning state.
40    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/// A validated, optimized workflow reusable across input sets.
68#[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    /// Validates and optimizes this graph into a reusable physical plan.
79    ///
80    /// Compilation performs type and cycle validation, stable topological
81    /// ordering, common-subexpression elimination, dead-node elimination,
82    /// output-lifetime planning and a conservative memory estimate.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error for a graph with no outputs, an invalid dependency,
87    /// an incompatible dependency type or a cycle.
88    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    /// Returns both the logical summary and optimized physical plan.
172    #[must_use]
173    pub const fn explain(&self) -> &Explanation {
174        &self.explanation
175    }
176
177    /// Executes the physical graph in stable topological order.
178    ///
179    /// # Errors
180    ///
181    /// Returns a missing/type input error, cancellation, memory refusal or the
182    /// first kernel failure. No partial result is published.
183    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}