Skip to main content

molframe_engine/
runtime.rs

1//! Typed input ownership and deterministic execution.
2
3use crate::compile::CompiledWorkflow;
4use crate::graph::{Output, Value, WorkflowError};
5use molframe_core::ExecutionContext;
6use std::any::{TypeId, type_name};
7use std::collections::{BTreeMap, BTreeSet};
8use std::sync::Arc;
9
10#[derive(Clone)]
11struct InputValue {
12    value: Value,
13    type_id: TypeId,
14}
15
16/// Owned values supplied to a compiled workflow by input name.
17#[derive(Clone, Default)]
18pub struct WorkflowInputs {
19    values: BTreeMap<Box<str>, InputValue>,
20}
21
22impl std::fmt::Debug for WorkflowInputs {
23    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        formatter
25            .debug_struct("WorkflowInputs")
26            .field("names", &self.values.keys().collect::<Vec<_>>())
27            .finish()
28    }
29}
30
31impl WorkflowInputs {
32    /// Starts an empty named input set.
33    #[must_use]
34    pub const fn new() -> Self {
35        Self {
36            values: BTreeMap::new(),
37        }
38    }
39
40    /// Inserts or replaces one typed value.
41    pub fn insert<T: Send + Sync + 'static>(&mut self, name: impl Into<Box<str>>, value: T) {
42        self.values.insert(
43            name.into(),
44            InputValue {
45                value: Arc::new(value),
46                type_id: TypeId::of::<T>(),
47            },
48        );
49    }
50}
51
52/// Named, owner-backed values returned by one workflow execution.
53#[derive(Clone, Default)]
54pub struct WorkflowResults {
55    graph_id: u64,
56    values: BTreeMap<Box<str>, Value>,
57}
58
59impl std::fmt::Debug for WorkflowResults {
60    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        formatter
62            .debug_struct("WorkflowResults")
63            .field("graph_id", &self.graph_id)
64            .field("names", &self.values.keys().collect::<Vec<_>>())
65            .finish()
66    }
67}
68
69impl WorkflowResults {
70    /// Returns a result through its typed output handle.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error when the handle belongs to another graph, the result
75    /// is missing or its requested type is inconsistent.
76    pub fn get<T: Send + Sync + 'static>(&self, output: &Output<T>) -> Result<&T, WorkflowError> {
77        if output.node.graph != self.graph_id {
78            return Err(WorkflowError::MissingResult(output.name.clone()));
79        }
80        self.get_named(&output.name)
81    }
82
83    /// Clones the result's shared owner without copying its payload.
84    ///
85    /// This is the lifetime-safe path for FFI views that must outlive the
86    /// [`WorkflowResults`] map.
87    ///
88    /// # Errors
89    ///
90    /// Returns the same handle, name, and type errors as [`Self::get`].
91    pub fn get_shared<T: Send + Sync + 'static>(
92        &self,
93        output: &Output<T>,
94    ) -> Result<Arc<T>, WorkflowError> {
95        if output.node.graph != self.graph_id {
96            return Err(WorkflowError::MissingResult(output.name.clone()));
97        }
98        let Some(value) = self.values.get(&output.name) else {
99            return Err(WorkflowError::MissingResult(output.name.clone()));
100        };
101        Arc::clone(value)
102            .downcast::<T>()
103            .map_err(|_| WorkflowError::ResultType {
104                name: output.name.clone(),
105            })
106    }
107
108    /// Returns a result by its stable name and expected Rust type.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error when the name is absent or the requested type differs.
113    pub fn get_named<T: Send + Sync + 'static>(&self, name: &str) -> Result<&T, WorkflowError> {
114        let Some(value) = self.values.get(name) else {
115            return Err(WorkflowError::MissingResult(name.into()));
116        };
117        value
118            .downcast_ref::<T>()
119            .ok_or_else(|| WorkflowError::ResultType { name: name.into() })
120    }
121
122    /// Stable output names in lexical order.
123    pub fn names(&self) -> impl ExactSizeIterator<Item = &str> {
124        self.values.keys().map(AsRef::as_ref)
125    }
126}
127
128pub(crate) fn execute(
129    workflow: &CompiledWorkflow,
130    inputs: &WorkflowInputs,
131    context: &ExecutionContext,
132) -> Result<WorkflowResults, WorkflowError> {
133    // Admission only: kernels reserve their own scratch, so holding the whole
134    // estimate here would charge the same bytes twice and starve them.
135    if workflow.explanation.estimated_peak_bytes > context.available_bytes() {
136        return Err(WorkflowError::MemoryRefused {
137            estimated: workflow.explanation.estimated_peak_bytes,
138            budget: context.memory_budget().bytes(),
139        });
140    }
141    let output_nodes = workflow
142        .outputs
143        .iter()
144        .map(|(_, node, _)| *node)
145        .collect::<BTreeSet<_>>();
146    let capacity = workflow
147        .nodes
148        .iter()
149        .map(|node| node.index)
150        .max()
151        .map_or(0, |maximum| maximum + 1);
152    let mut values = vec![None; capacity];
153    let mut uses = workflow.use_counts.clone();
154    for node in &workflow.nodes {
155        if context.cancellation().is_cancelled() {
156            return Err(WorkflowError::Cancelled);
157        }
158        let value = if let Some(name) = &node.input_name {
159            let Some(input) = inputs.values.get(name) else {
160                return Err(WorkflowError::MissingInput(name.clone()));
161            };
162            if input.type_id != node.type_id {
163                return Err(WorkflowError::InputType {
164                    name: name.clone(),
165                    expected: node.type_name,
166                });
167            }
168            Arc::clone(&input.value)
169        } else {
170            let arguments = node
171                .dependencies
172                .iter()
173                .map(|dependency| {
174                    values
175                        .get(*dependency)
176                        .and_then(Option::as_ref)
177                        .cloned()
178                        .ok_or(WorkflowError::InternalTypeMismatch {
179                            expected: type_name::<()>(),
180                        })
181                })
182                .collect::<Result<Vec<_>, _>>()?;
183            let Some(kernel) = &node.kernel else {
184                return Err(WorkflowError::InternalTypeMismatch {
185                    expected: node.type_name,
186                });
187            };
188            match kernel(&arguments, context) {
189                Ok(value) => value,
190                Err(error @ WorkflowError::Operation { .. }) => return Err(error),
191                Err(error) => {
192                    return Err(WorkflowError::Operation {
193                        operation: node.operation,
194                        message: error.to_string().into(),
195                    });
196                }
197            }
198        };
199        values[node.index] = Some(value);
200        for dependency in &node.dependencies {
201            uses[*dependency] = uses[*dependency].saturating_sub(1);
202            if uses[*dependency] == 0 && !output_nodes.contains(dependency) {
203                values[*dependency] = None;
204            }
205        }
206    }
207    let mut results = BTreeMap::new();
208    for (name, node, _) in &workflow.outputs {
209        let Some(value) = values.get(*node).and_then(Option::as_ref) else {
210            return Err(WorkflowError::MissingResult(name.clone()));
211        };
212        results.insert(name.clone(), Arc::clone(value));
213    }
214    Ok(WorkflowResults {
215        graph_id: workflow.graph_id,
216        values: results,
217    })
218}