Skip to main content

limen_core/graph/
validate.rs

1//! Graph validation interface.
2
3use crate::edge::link::EdgeDescriptor;
4use crate::errors::GraphError;
5use crate::node::link::NodeDescriptor;
6use crate::node::source::EXTERNAL_INGRESS_NODE;
7use crate::node::NodeKind;
8
9// no_std + alloc: bring in the `vec!` macro only
10#[cfg(all(feature = "alloc", not(feature = "std")))]
11use alloc::vec;
12
13/// An interface for descriptor validation (borrowed/owned/buffer).
14pub trait GraphValidator {
15    /// Validates the wiring of the graph.
16    fn validate(&self) -> Result<(), GraphError>;
17}
18
19/// Owned, no-alloc descriptor (arrays are stored by value).
20#[derive(Debug, Clone)]
21pub struct GraphDescBuf<const N: usize, const E: usize> {
22    /// Nodes descriptors.
23    nodes: [NodeDescriptor; N],
24    /// Edge descriptors.
25    edges: [EdgeDescriptor; E],
26}
27
28impl<const N: usize, const E: usize> GraphDescBuf<N, E> {
29    /// Construct a new `GraphDescBuf` owning node and edge descriptor arrays.
30    #[inline]
31    pub fn new(nodes: [NodeDescriptor; N], edges: [EdgeDescriptor; E]) -> Self {
32        Self { nodes, edges }
33    }
34
35    /// Borrow the node descriptors.
36    #[inline]
37    pub fn nodes(&self) -> &[NodeDescriptor; N] {
38        &self.nodes
39    }
40
41    /// Borrow the edge descriptors.
42    #[inline]
43    pub fn edges(&self) -> &[EdgeDescriptor; E] {
44        &self.edges
45    }
46}
47
48impl<const N: usize, const E: usize> GraphValidator for GraphDescBuf<N, E> {
49    #[inline]
50    fn validate(&self) -> Result<(), GraphError> {
51        validate_ports(&self.nodes, &self.edges)?;
52        #[cfg(not(feature = "alloc"))]
53        {
54            validate_acyclic_buf::<N>(&self.nodes, &self.edges)?;
55        }
56        #[cfg(feature = "alloc")]
57        {
58            validate_acyclic_alloc(&self.nodes, &self.edges)?;
59        }
60
61        Ok(())
62    }
63}
64
65/// Validates graph ports, including any number of synthetic ingress "monitor edges".
66///
67/// A monitor edge is an `EdgeDescriptor` whose `upstream.node == EXTERNAL_INGRESS_NODE`.
68/// It *does not* consume a real input port on the downstream node and is only valid
69/// when the downstream node `kind == Source`. At most **one** monitor edge may target
70/// each individual Source node.
71pub fn validate_ports(
72    nodes: &[NodeDescriptor],
73    edges: &[EdgeDescriptor],
74) -> Result<(), GraphError> {
75    let n = nodes.len();
76
77    // (A) Node id ↔ index must match exactly (0..N) and kind/arity constraints.
78    for (i, nd) in nodes.iter().enumerate() {
79        if nd.id().as_usize() != &i {
80            return Err(GraphError::IncompatiblePorts);
81        }
82        match nd.kind() {
83            NodeKind::Source => {
84                if nd.in_ports() != &0 || nd.out_ports() < &1 {
85                    return Err(GraphError::IncompatiblePorts);
86                }
87            }
88            NodeKind::Sink => {
89                if nd.in_ports() < &1 || nd.out_ports() != &0 {
90                    return Err(GraphError::IncompatiblePorts);
91                }
92            }
93            NodeKind::Split => {
94                if nd.in_ports() < &1 || nd.out_ports() < &2 {
95                    return Err(GraphError::IncompatiblePorts);
96                }
97            }
98            NodeKind::Join => {
99                if nd.in_ports() < &2 || nd.out_ports() < &1 {
100                    return Err(GraphError::IncompatiblePorts);
101                }
102            }
103            NodeKind::Process | NodeKind::Model => {
104                if nd.in_ports() < &1 || nd.out_ports() < &1 {
105                    return Err(GraphError::IncompatiblePorts);
106                }
107            }
108            NodeKind::External => {
109                // No fixed arity constraints here.
110            }
111        }
112    }
113
114    // (B) Edge ids, endpoints, and port bounds.
115    // Monitor edges (from EXTERNAL_INGRESS_NODE) are allowed to target any Source.
116    // They do not consume a real port, so we bypass downstream port checks for them.
117    for (i, ed) in edges.iter().enumerate() {
118        // strict id match
119        if ed.id().as_usize() != &i {
120            return Err(GraphError::IncompatiblePorts);
121        }
122
123        let is_monitor = ed.upstream().node() == &EXTERNAL_INGRESS_NODE;
124        let t = ed.downstream().node().as_usize();
125
126        if is_monitor {
127            // Downstream must be real and a Source.
128            if t >= &n {
129                return Err(GraphError::IncompatiblePorts);
130            }
131            if nodes[*t].kind() != &NodeKind::Source {
132                return Err(GraphError::IncompatiblePorts);
133            }
134
135            // Enforce "at most one monitor edge per Source node" via nested scan.
136            for (j, other) in edges.iter().enumerate() {
137                if j == i {
138                    continue;
139                }
140                if other.upstream().node() == &EXTERNAL_INGRESS_NODE
141                    && other.downstream().node().as_usize() == t
142                {
143                    return Err(GraphError::IncompatiblePorts);
144                }
145            }
146
147            // No port-bound checks for monitor edges; they don't consume real ports.
148            continue;
149        }
150
151        // Regular edge: both endpoints must be real nodes in range.
152        let f = ed.upstream().node().as_usize();
153        if f >= &n || t >= &n {
154            return Err(GraphError::IncompatiblePorts);
155        }
156
157        let nf = &nodes[*f];
158        let nt = &nodes[*t];
159
160        // Regular port bounds.
161        if *ed.upstream().port().as_usize() >= *nf.out_ports() as usize {
162            return Err(GraphError::IncompatiblePorts);
163        }
164        if *ed.downstream().port().as_usize() >= *nt.in_ports() as usize {
165            return Err(GraphError::IncompatiblePorts);
166        }
167    }
168
169    // (C) Each (to_node, to_port) must be unique among REAL edges.
170    // Monitor edges are excluded because they do not occupy a real input port.
171    for (i, ei) in edges.iter().enumerate() {
172        if ei.upstream().node() == &EXTERNAL_INGRESS_NODE {
173            continue; // skip monitors
174        }
175        for ej in edges.iter().skip(i + 1) {
176            if ej.upstream().node() == &EXTERNAL_INGRESS_NODE {
177                continue; // skip monitors
178            }
179            if ei.downstream().node().as_usize() == ej.downstream().node().as_usize()
180                && ei.downstream().port().as_usize() == ej.downstream().port().as_usize()
181            {
182                return Err(GraphError::IncompatiblePorts);
183            }
184        }
185    }
186
187    Ok(())
188}
189
190/// Validate acyclicity without allocation using fixed-size arrays on the stack.
191///
192/// This variant is intended for [`GraphDescBuf`] where `N` is known at compile-time.
193///
194/// Monitor edges are ignored for cycle detection (they have no real upstream node
195/// and cannot introduce a cycle).
196pub fn validate_acyclic_buf<const N: usize>(
197    _nodes: &[NodeDescriptor; N],
198    edges: &[EdgeDescriptor],
199) -> Result<(), GraphError> {
200    // Simple Kahn's algorithm with stack-allocated arrays.
201    let mut indeg = [0usize; N];
202
203    // Validate both ends and build indegree.
204    for e in edges {
205        if e.upstream().node() == &EXTERNAL_INGRESS_NODE {
206            continue; // ignore monitor edge
207        }
208        let u = e.upstream().node().as_usize();
209        let v = e.downstream().node().as_usize();
210        if u >= &N || v >= &N {
211            return Err(GraphError::IncompatiblePorts);
212        }
213        indeg[*v] += 1;
214    }
215
216    let mut stack = [0usize; N];
217    let mut top = 0usize;
218
219    // Seed with zero indegree nodes.
220    for (i, &deg) in indeg.iter().enumerate() {
221        if deg == 0 {
222            stack[top] = i;
223            top += 1;
224        }
225    }
226
227    let mut visited = 0usize;
228    while top > 0 {
229        top -= 1;
230        let u = stack[top];
231        visited += 1;
232
233        // Decrement indegree of real outgoing edges from u.
234        for e in edges.iter().filter(|e| {
235            e.upstream().node() != &EXTERNAL_INGRESS_NODE && e.upstream().node().as_usize() == &u
236        }) {
237            let v = e.downstream().node().as_usize();
238            debug_assert!(v < &N, "downstream index validated earlier");
239            indeg[*v] -= 1;
240            if indeg[*v] == 0 {
241                stack[top] = *v;
242                top += 1;
243            }
244        }
245    }
246
247    if visited != N {
248        return Err(GraphError::Cyclic);
249    }
250    Ok(())
251}
252
253/// Validate acyclicity using Kahn's algorithm (requires `alloc`).
254#[cfg(feature = "alloc")]
255pub fn validate_acyclic_alloc(
256    nodes: &[NodeDescriptor],
257    edges: &[EdgeDescriptor],
258) -> Result<(), GraphError> {
259    use alloc::vec::Vec;
260
261    let n = nodes.len();
262    let mut indeg = vec![0usize; n];
263
264    // Validate indices and build indegree for real edges only.
265    for e in edges {
266        if e.upstream().node() == &EXTERNAL_INGRESS_NODE {
267            continue;
268        }
269        let u = e.upstream().node().as_usize();
270        let v = e.downstream().node().as_usize();
271        if u >= &n || v >= &n {
272            return Err(GraphError::IncompatiblePorts);
273        }
274        indeg[*v] += 1;
275    }
276
277    // Seed stack with zero-indegree nodes.
278    let mut stack = Vec::with_capacity(n);
279    for (i, &deg) in indeg.iter().take(n).enumerate() {
280        if deg == 0 {
281            stack.push(i);
282        }
283    }
284
285    // Kahn's main loop.
286    let mut visited = 0usize;
287    while let Some(u) = stack.pop() {
288        visited += 1;
289        for e in edges.iter().filter(|e| {
290            e.upstream().node() != &EXTERNAL_INGRESS_NODE && e.upstream().node().as_usize() == &u
291        }) {
292            let v = e.downstream().node().as_usize();
293            indeg[*v] -= 1;
294            if indeg[*v] == 0 {
295                stack.push(*v);
296            }
297        }
298    }
299
300    if visited != n {
301        return Err(GraphError::Cyclic);
302    }
303    Ok(())
304}