Skip to main content

async_nodes/
graph.rs

1//! Graph topology: nodes, pins, and declarative edges.
2//!
3//! A [`BspGraph`] is the compiled blueprint the engine runs: re-executable nodes, edges as
4//! *data* (each carrying a declarative [`Condition`]), static inputs, and the consumer's
5//! named-condition / merger registries. The fluent builder here is the primary authoring
6//! API; [`crate::def::GraphDef`] is the serializable interchange format derived from it.
7
8use std::marker::PhantomData;
9
10use indexmap::IndexMap;
11use serde::{Deserialize, Serialize};
12
13use super::{
14    condition::{Condition, ConditionMap},
15    merge::MergerMap,
16    node::BspNode,
17    value::BspValue,
18};
19
20/// Programmatic node identifier: positional, stable within one graph build.
21///
22/// The *serializable* definition layer uses stable string ids (see
23/// [`crate::def::NodeDef`]); the graph keeps a string label per node
24/// ([`BspGraph::node_label`]) so definition round-trips are lossless while the hot path
25/// stays on cheap, copyable ids.
26pub type NodeId = usize;
27/// A port index on a node (dataflow profile).
28pub type PortIndex = usize;
29/// Edge identifier: the edge's index in declaration order.
30pub type EdgeId = usize;
31
32/// An output pin on a specific node.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub struct OutputPin {
35    pub node_id: NodeId,
36    pub port: PortIndex,
37}
38
39/// An input pin on a specific node.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub struct InputPin {
42    pub node_id: NodeId,
43    pub port: PortIndex,
44}
45
46/// The value-store port a failing node writes its error to when its recovery policy routes
47/// to an error edge (dataflow profile; see
48/// [`crate::recovery::RecoveryPolicy::RouteToErrorEdge`]).
49pub const ERROR_PORT: PortIndex = usize::MAX;
50
51/// A declarative connection between two nodes — data, not a closure.
52///
53/// After `source` completes, the engine evaluates `condition`; when it matches, the edge
54/// emits an activation token toward `target`. Edges carrying `source_port`/`target_port`
55/// additionally route a pin value (dataflow profile).
56#[derive(Clone)]
57pub struct Edge<S> {
58    /// The node this edge starts from.
59    pub source: NodeId,
60    /// The node this edge points to.
61    pub target: NodeId,
62    /// The declarative predicate deciding whether the edge fires.
63    pub condition: Condition<S>,
64    /// Iteration budget. Present (and validated) on cycle-closing edges: exceeding it
65    /// raises [`crate::error::BspError::LoopBoundExceeded`].
66    pub max_iterations: Option<u64>,
67    /// The output port routed by this edge (dataflow profile only).
68    pub source_port: Option<PortIndex>,
69    /// The input port routed by this edge (dataflow profile only).
70    pub target_port: Option<PortIndex>,
71}
72
73impl<S> Edge<S> {
74    fn control(source: NodeId, target: NodeId) -> Self {
75        Self {
76            source,
77            target,
78            condition: Condition::Always,
79            max_iterations: None,
80            source_port: None,
81            target_port: None,
82        }
83    }
84}
85
86/// A legacy non-generic identifier wrapping a designated node with port-link builder logic.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
88pub struct NodeHandle {
89    pub id: NodeId,
90}
91
92impl NodeHandle {
93    #[must_use]
94    pub const fn new(id: NodeId) -> Self {
95        Self { id }
96    }
97
98    #[must_use]
99    pub const fn output(self, port: PortIndex) -> OutputPin {
100        OutputPin {
101            node_id: self.id,
102            port,
103        }
104    }
105
106    #[must_use]
107    pub const fn input(self, port: PortIndex) -> InputPin {
108        InputPin {
109            node_id: self.id,
110            port,
111        }
112    }
113}
114
115/// A compile-time type-safe identifier wrapping a designated node.
116/// This allows compiler-checked, named port accessors generated by macro attributes.
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct TypedNodeHandle<N> {
119    pub id: NodeId,
120    _marker: PhantomData<fn() -> N>,
121}
122
123impl<N> TypedNodeHandle<N> {
124    #[must_use]
125    pub const fn new(id: NodeId) -> Self {
126        Self {
127            id,
128            _marker: PhantomData,
129        }
130    }
131
132    /// Fetches an output pin dynamically by index (bypasses macro accessors).
133    #[must_use]
134    pub const fn get_output_pin(&self, port: PortIndex) -> OutputPin {
135        OutputPin {
136            node_id: self.id,
137            port,
138        }
139    }
140
141    /// Fetches an input pin dynamically by index (bypasses macro accessors).
142    #[must_use]
143    pub const fn get_input_pin(&self, port: PortIndex) -> InputPin {
144        InputPin {
145            node_id: self.id,
146            port,
147        }
148    }
149
150    #[must_use]
151    pub const fn as_untyped(&self) -> NodeHandle {
152        NodeHandle::new(self.id)
153    }
154}
155
156impl<N> From<TypedNodeHandle<N>> for NodeHandle {
157    fn from(typed: TypedNodeHandle<N>) -> Self {
158        Self::new(typed.id)
159    }
160}
161
162impl<N> serde::Serialize for TypedNodeHandle<N> {
163    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164    where
165        S: serde::Serializer,
166    {
167        self.id.serialize(serializer)
168    }
169}
170
171impl<'de, N> serde::Deserialize<'de> for TypedNodeHandle<N> {
172    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
173    where
174        D: serde::Deserializer<'de>,
175    {
176        let id = NodeId::deserialize(deserializer)?;
177        Ok(Self::new(id))
178    }
179}
180
181/// Builder handle for configuring a freshly added edge, e.g.
182/// `graph.add_edge(a, b).when(Condition::Named("has_more".into())).with_max_iterations(30)`.
183pub struct EdgeBuilder<'a, S> {
184    graph: &'a mut BspGraph<S>,
185    id: EdgeId,
186}
187
188impl<S> EdgeBuilder<'_, S> {
189    /// Sets the edge's condition.
190    pub fn when(self, condition: Condition<S>) -> Self {
191        self.graph.edges[self.id].condition = condition;
192        self
193    }
194
195    /// Sets the edge's iteration budget (expected on cycle-closing edges).
196    pub fn with_max_iterations(self, max: u64) -> Self {
197        self.graph.edges[self.id].max_iterations = Some(max);
198        self
199    }
200
201    /// Attaches dataflow ports to the edge (source output port, target input port).
202    pub fn with_ports(self, source_port: PortIndex, target_port: PortIndex) -> Self {
203        let edge = &mut self.graph.edges[self.id];
204        edge.source_port = Some(source_port);
205        edge.target_port = Some(target_port);
206        self.graph.reindex_edge_ports(self.id);
207        self
208    }
209
210    /// Returns the edge's declaration index.
211    #[must_use]
212    pub const fn id(&self) -> EdgeId {
213        self.id
214    }
215}
216
217/// The blueprint definition containing nodes, edges, and initial inputs.
218///
219/// `S` is the consumer-owned run state the graph's nodes operate on; it defaults to `()`
220/// for pure dataflow graphs.
221pub struct BspGraph<S = ()> {
222    pub(crate) nodes: IndexMap<NodeId, Box<dyn BspNode<S>>>,
223    pub(crate) edges: Vec<Edge<S>>,
224    pub(crate) static_inputs: IndexMap<InputPin, BspValue>,
225    /// Consumer-registered named conditions, resolved during edge evaluation.
226    pub(crate) conditions: ConditionMap<S>,
227    /// Consumer-registered mergers for join nodes.
228    pub(crate) mergers: MergerMap<S>,
229    /// Stable string labels per node, emitted as `NodeDef.id` by `to_def`.
230    pub(crate) labels: IndexMap<NodeId, String>,
231    /// Explicit entry nodes (in addition to all no-incoming-edge source nodes).
232    pub(crate) entry: Vec<NodeId>,
233    /// Original `NodeDef`s retained for lossless `to_def` round-trips of def-built graphs.
234    pub(crate) node_defs: IndexMap<NodeId, crate::def::NodeDef>,
235    /// Precomputed incoming edge indices per node to avoid linear scans at runtime.
236    pub(crate) incoming: IndexMap<NodeId, Vec<EdgeId>>,
237    /// Precomputed dataflow edges per input pin (declaration order preserved).
238    pub(crate) incoming_by_pin: IndexMap<InputPin, Vec<EdgeId>>,
239    /// Precomputed outgoing edge indices per node (declaration order preserved).
240    pub(crate) outgoing: IndexMap<NodeId, Vec<EdgeId>>,
241    next_node_id: NodeId,
242}
243
244impl<S> Default for BspGraph<S> {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250impl<S> BspGraph<S> {
251    /// Instantiates a new empty BSP execution graph.
252    #[must_use]
253    pub fn new() -> Self {
254        Self {
255            nodes: IndexMap::new(),
256            edges: Vec::new(),
257            static_inputs: IndexMap::new(),
258            conditions: ConditionMap::default(),
259            mergers: MergerMap::default(),
260            labels: IndexMap::new(),
261            entry: Vec::new(),
262            node_defs: IndexMap::new(),
263            incoming: IndexMap::new(),
264            incoming_by_pin: IndexMap::new(),
265            outgoing: IndexMap::new(),
266            next_node_id: 1,
267        }
268    }
269
270    /// Registers a node with a self-assigned auto-incrementing identifier.
271    pub fn add_node<N: BspNode<S> + 'static>(&mut self, node: N) -> TypedNodeHandle<N> {
272        let id = self.next_node_id;
273        self.next_node_id += 1;
274        self.nodes.insert(id, Box::new(node));
275        self.labels.insert(id, id.to_string());
276        TypedNodeHandle::new(id)
277    }
278
279    /// Registers a node within the graph's designated manual identifier namespace.
280    pub fn add_node_with_id(&mut self, id: impl Into<NodeId>, node: Box<dyn BspNode<S>>) {
281        let id = id.into();
282        self.nodes.insert(id, node);
283        self.labels.entry(id).or_insert_with(|| id.to_string());
284        if id >= self.next_node_id {
285            self.next_node_id = id + 1;
286        }
287    }
288
289    /// Connects an output pin of a source node to an input pin of a target node.
290    ///
291    /// This is the dataflow profile's wiring primitive: an edge with
292    /// [`Condition::Always`] carrying both ports.
293    pub fn add_link(&mut self, source: OutputPin, target: InputPin) {
294        let id = self.edges.len();
295        let mut edge = Edge::control(source.node_id, target.node_id);
296        edge.source_port = Some(source.port);
297        edge.target_port = Some(target.port);
298        self.push_edge(edge);
299        debug_assert_eq!(id, self.edges.len() - 1);
300    }
301
302    /// Back-compatible helper targeting index indices.
303    pub fn add_link_indices(
304        &mut self,
305        source_node: NodeId,
306        source_port: PortIndex,
307        target_node: NodeId,
308        target_port: PortIndex,
309    ) {
310        self.add_link(
311            OutputPin {
312                node_id: source_node,
313                port: source_port,
314            },
315            InputPin {
316                node_id: target_node,
317                port: target_port,
318            },
319        );
320    }
321
322    /// Adds a control-flow edge between two nodes, returning a builder for the condition
323    /// and iteration budget. The default condition is [`Condition::Always`].
324    pub fn add_edge(
325        &mut self,
326        source: impl Into<NodeId>,
327        target: impl Into<NodeId>,
328    ) -> EdgeBuilder<'_, S> {
329        let id = self.edges.len();
330        self.push_edge(Edge::control(source.into(), target.into()));
331        EdgeBuilder { graph: self, id }
332    }
333
334    /// Adds a control-flow edge with an explicit condition in one call.
335    pub fn add_edge_with(
336        &mut self,
337        source: impl Into<NodeId>,
338        target: impl Into<NodeId>,
339        condition: Condition<S>,
340    ) -> EdgeId {
341        let id = self.edges.len();
342        let mut edge = Edge::control(source.into(), target.into());
343        edge.condition = condition;
344        self.push_edge(edge);
345        id
346    }
347
348    /// Injects an initial static parameter into a specific node's input pin.
349    pub fn set_static_input<T: std::any::Any + Send + Sync>(&mut self, target: InputPin, value: T) {
350        self.static_inputs.insert(target, BspValue::new(value));
351    }
352
353    /// Registers a named condition predicate (the serializable routing mechanism).
354    pub fn register_condition(
355        &mut self,
356        name: impl Into<String>,
357        f: impl Fn(&S, &crate::run::ValueStore) -> bool + Send + Sync + 'static,
358    ) {
359        self.conditions.insert(name.into(), std::sync::Arc::new(f));
360    }
361
362    /// Registers a named merger used by join nodes to fold branch states.
363    pub fn register_merger<M: crate::merge::Merger<S> + 'static>(
364        &mut self,
365        name: impl Into<String>,
366        merger: M,
367    ) {
368        self.mergers
369            .insert(name.into(), std::sync::Arc::new(merger));
370    }
371
372    /// Marks a node as an explicit entry (in addition to no-incoming-edge sources).
373    pub fn add_entry(&mut self, id: impl Into<NodeId>) {
374        self.entry.push(id.into());
375    }
376
377    /// The stable string label for a node (its [`crate::def::NodeDef`] id).
378    #[must_use]
379    pub fn node_label(&self, id: NodeId) -> Option<&str> {
380        self.labels.get(&id).map(String::as_str)
381    }
382
383    /// The nodes of this graph, in declaration order.
384    pub fn node_ids(&self) -> impl Iterator<Item = NodeId> + '_ {
385        self.nodes.keys().copied()
386    }
387
388    /// The edges of this graph, in declaration order.
389    #[must_use]
390    pub fn edges(&self) -> &[Edge<S>] {
391        &self.edges
392    }
393
394    /// The initial frontier: explicit entries plus every node with no incoming edges.
395    pub(crate) fn entries(&self) -> indexmap::IndexSet<NodeId> {
396        let mut entries: indexmap::IndexSet<NodeId> = indexmap::IndexSet::new();
397        for &id in &self.entry {
398            entries.insert(id);
399        }
400        for &id in self.nodes.keys() {
401            if self.incoming.get(&id).is_none_or(Vec::is_empty) {
402                entries.insert(id);
403            }
404        }
405        entries
406    }
407
408    /// Appends an edge and maintains the lookup indexes. Edge ids are declaration indices.
409    pub(crate) fn push_edge(&mut self, edge: Edge<S>) {
410        let id = self.edges.len();
411        if let Some(target_port) = edge.target_port {
412            self.incoming_by_pin
413                .entry(InputPin {
414                    node_id: edge.target,
415                    port: target_port,
416                })
417                .or_default()
418                .push(id);
419        }
420        self.incoming.entry(edge.target).or_default().push(id);
421        self.outgoing.entry(edge.source).or_default().push(id);
422        self.edges.push(edge);
423    }
424
425    /// Re-indexes an edge whose ports were attached after insertion.
426    fn reindex_edge_ports(&mut self, id: EdgeId) {
427        let edge = &self.edges[id];
428        if let Some(target_port) = edge.target_port {
429            let pin = InputPin {
430                node_id: edge.target,
431                port: target_port,
432            };
433            let list = self.incoming_by_pin.entry(pin).or_default();
434            if !list.contains(&id) {
435                list.push(id);
436            }
437        }
438    }
439
440    // =========================================================================
441    // BUILDER PATTERN EXTENSIONS
442    // =========================================================================
443
444    #[must_use]
445    pub fn with_node_id(mut self, id: impl Into<NodeId>, node: Box<dyn BspNode<S>>) -> Self {
446        self.add_node_with_id(id, node);
447        self
448    }
449
450    #[must_use]
451    pub fn with_link(mut self, source: OutputPin, target: InputPin) -> Self {
452        self.add_link(source, target);
453        self
454    }
455
456    #[must_use]
457    pub fn with_static_input<T: std::any::Any + Send + Sync>(
458        mut self,
459        target: InputPin,
460        value: T,
461    ) -> Self {
462        self.set_static_input(target, value);
463        self
464    }
465
466    #[must_use]
467    pub fn with_condition(
468        mut self,
469        name: impl Into<String>,
470        f: impl Fn(&S, &crate::run::ValueStore) -> bool + Send + Sync + 'static,
471    ) -> Self {
472        self.register_condition(name, f);
473        self
474    }
475
476    #[must_use]
477    pub fn with_merger<M: crate::merge::Merger<S> + 'static>(
478        mut self,
479        name: impl Into<String>,
480        merger: M,
481    ) -> Self {
482        self.register_merger(name, merger);
483        self
484    }
485}