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    if workflow.explanation.estimated_peak_bytes > context.memory_budget().bytes() {
134        return Err(WorkflowError::MemoryRefused {
135            estimated: workflow.explanation.estimated_peak_bytes,
136            budget: context.memory_budget().bytes(),
137        });
138    }
139    let _reservation = context
140        .try_reserve(workflow.explanation.estimated_peak_bytes)
141        .map_err(|_| WorkflowError::MemoryRefused {
142            estimated: workflow.explanation.estimated_peak_bytes,
143            budget: context.memory_budget().bytes(),
144        })?;
145    let output_nodes = workflow
146        .outputs
147        .iter()
148        .map(|(_, node, _)| *node)
149        .collect::<BTreeSet<_>>();
150    let capacity = workflow
151        .nodes
152        .iter()
153        .map(|node| node.index)
154        .max()
155        .map_or(0, |maximum| maximum + 1);
156    let mut values = vec![None; capacity];
157    let mut uses = workflow.use_counts.clone();
158    for node in &workflow.nodes {
159        if context.cancellation().is_cancelled() {
160            return Err(WorkflowError::Cancelled);
161        }
162        let value = if let Some(name) = &node.input_name {
163            let Some(input) = inputs.values.get(name) else {
164                return Err(WorkflowError::MissingInput(name.clone()));
165            };
166            if input.type_id != node.type_id {
167                return Err(WorkflowError::InputType {
168                    name: name.clone(),
169                    expected: node.type_name,
170                });
171            }
172            Arc::clone(&input.value)
173        } else {
174            let arguments = node
175                .dependencies
176                .iter()
177                .map(|dependency| {
178                    values
179                        .get(*dependency)
180                        .and_then(Option::as_ref)
181                        .cloned()
182                        .ok_or(WorkflowError::InternalTypeMismatch {
183                            expected: type_name::<()>(),
184                        })
185                })
186                .collect::<Result<Vec<_>, _>>()?;
187            let Some(kernel) = &node.kernel else {
188                return Err(WorkflowError::InternalTypeMismatch {
189                    expected: node.type_name,
190                });
191            };
192            match kernel(&arguments, context) {
193                Ok(value) => value,
194                Err(error @ WorkflowError::Operation { .. }) => return Err(error),
195                Err(error) => {
196                    return Err(WorkflowError::Operation {
197                        operation: node.operation,
198                        message: error.to_string().into(),
199                    });
200                }
201            }
202        };
203        values[node.index] = Some(value);
204        for dependency in &node.dependencies {
205            uses[*dependency] = uses[*dependency].saturating_sub(1);
206            if uses[*dependency] == 0 && !output_nodes.contains(dependency) {
207                values[*dependency] = None;
208            }
209        }
210    }
211    let mut results = BTreeMap::new();
212    for (name, node, _) in &workflow.outputs {
213        let Some(value) = values.get(*node).and_then(Option::as_ref) else {
214            return Err(WorkflowError::MissingResult(name.clone()));
215        };
216        results.insert(name.clone(), Arc::clone(value));
217    }
218    Ok(WorkflowResults {
219        graph_id: workflow.graph_id,
220        values: results,
221    })
222}