Skip to main content

sim_incremental_core/dataflow/
graph.rs

1//! Immutable graph construction and neutral located-code adaptation.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    hash::Hash,
6};
7
8use crate::{FingerprintValue, ValueFingerprint};
9
10/// The semantic class of an edge.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub enum EdgeClass<C> {
13    /// Ordinary value or fact propagation.
14    Data,
15    /// Ordering or control-flow propagation.
16    Control,
17    /// A consumer-defined edge class.
18    Custom(C),
19}
20
21/// The direction in which an edge propagates.
22#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
23pub enum GraphDirection {
24    /// Propagate from the edge's source to its target.
25    Forward,
26    /// Propagate from the edge's target to its source.
27    Reverse,
28}
29
30/// A node's declared relationship to the graph boundary.
31#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub enum Boundary {
33    /// The node is internal to the graph.
34    Internal,
35    /// The node may receive facts from outside the graph.
36    Input,
37    /// The node may send facts outside the graph.
38    Output,
39    /// The node is both an input and an output.
40    InputOutput,
41}
42
43impl Boundary {
44    fn is_input(self) -> bool {
45        matches!(self, Self::Input | Self::InputOutput)
46    }
47
48    fn is_output(self) -> bool {
49        matches!(self, Self::Output | Self::InputOutput)
50    }
51}
52
53/// A node supplied to checked graph construction.
54#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55pub struct NodeSpec<N, L> {
56    /// Stable node identity.
57    pub id: N,
58    /// Consumer-neutral source or artifact location.
59    pub location: L,
60    /// Declared graph-boundary role.
61    pub boundary: Boundary,
62}
63
64/// An edge supplied to checked graph construction.
65#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
66pub struct EdgeSpec<E, N, C> {
67    /// Stable edge identity.
68    pub id: E,
69    /// Declared source node.
70    pub source: N,
71    /// Declared target node.
72    pub target: N,
73    /// Semantic edge class.
74    pub class: EdgeClass<C>,
75    /// Propagation direction.
76    pub direction: GraphDirection,
77}
78
79/// One immutable graph node.
80#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
81pub struct Node<N, L> {
82    id: N,
83    location: L,
84    boundary: Boundary,
85}
86
87impl<N, L> Node<N, L> {
88    /// Returns the stable node identity.
89    pub fn id(&self) -> &N {
90        &self.id
91    }
92
93    /// Returns the consumer-neutral location.
94    pub fn location(&self) -> &L {
95        &self.location
96    }
97
98    /// Returns the declared boundary role.
99    pub fn boundary(&self) -> Boundary {
100        self.boundary
101    }
102}
103
104/// One immutable graph edge.
105#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub struct Edge<E, N, C> {
107    id: E,
108    source: N,
109    target: N,
110    class: EdgeClass<C>,
111    direction: GraphDirection,
112}
113
114impl<E, N, C> Edge<E, N, C> {
115    /// Returns the stable edge identity.
116    pub fn id(&self) -> &E {
117        &self.id
118    }
119
120    /// Returns the declared source node.
121    pub fn source(&self) -> &N {
122        &self.source
123    }
124
125    /// Returns the declared target node.
126    pub fn target(&self) -> &N {
127        &self.target
128    }
129
130    /// Returns the semantic edge class.
131    pub fn class(&self) -> &EdgeClass<C> {
132        &self.class
133    }
134
135    /// Returns the propagation direction.
136    pub fn direction(&self) -> GraphDirection {
137        self.direction
138    }
139
140    pub(super) fn predecessor_and_successor(&self) -> (&N, &N) {
141        match self.direction {
142            GraphDirection::Forward => (&self.source, &self.target),
143            GraphDirection::Reverse => (&self.target, &self.source),
144        }
145    }
146}
147
148/// A checked graph-construction refusal.
149#[derive(Clone, Debug, Eq, PartialEq)]
150pub enum GraphBuildError<N, E> {
151    /// A stable node identity was supplied more than once.
152    DuplicateNode(N),
153    /// A stable edge identity was supplied more than once.
154    DuplicateEdge(E),
155    /// An edge names a node absent from the graph.
156    MissingNode {
157        /// Rejected edge identity.
158        edge: E,
159        /// Missing endpoint identity.
160        node: N,
161    },
162    /// An input boundary has an in-graph predecessor.
163    InputHasPredecessor(N),
164    /// An output boundary has an in-graph successor.
165    OutputHasSuccessor(N),
166    /// A graph without nodes cannot carry a meaningful identity.
167    Empty,
168}
169
170/// Immutable, canonically ordered, content-identified dataflow structure.
171///
172/// The generic parameters represent graph identities, locations, and edge
173/// classes only. Adapters may project machine code, syntax trees, or other
174/// located artifacts into these neutral values without storing their source
175/// types in the graph.
176#[derive(Clone, Debug, Eq, PartialEq)]
177pub struct DataflowGraph<N, E, L, C> {
178    nodes: BTreeMap<N, Node<N, L>>,
179    edges: BTreeMap<E, Edge<E, N, C>>,
180    predecessors: BTreeMap<N, Box<[E]>>,
181    successors: BTreeMap<N, Box<[E]>>,
182    fingerprint: ValueFingerprint,
183}
184
185impl<N, E, L, C> DataflowGraph<N, E, L, C>
186where
187    N: Clone + Hash + Ord,
188    E: Clone + Hash + Ord,
189    L: Hash + Ord,
190    C: Hash + Ord,
191{
192    /// Validates and freezes graph declarations in canonical identity order.
193    pub fn build(
194        nodes: impl IntoIterator<Item = NodeSpec<N, L>>,
195        edges: impl IntoIterator<Item = EdgeSpec<E, N, C>>,
196    ) -> Result<Self, GraphBuildError<N, E>> {
197        let mut frozen_nodes = BTreeMap::new();
198        for node in nodes {
199            let id = node.id.clone();
200            let node = Node {
201                id: node.id,
202                location: node.location,
203                boundary: node.boundary,
204            };
205            if frozen_nodes.insert(id.clone(), node).is_some() {
206                return Err(GraphBuildError::DuplicateNode(id));
207            }
208        }
209        if frozen_nodes.is_empty() {
210            return Err(GraphBuildError::Empty);
211        }
212
213        let mut frozen_edges = BTreeMap::new();
214        for edge in edges {
215            let id = edge.id.clone();
216            for endpoint in [&edge.source, &edge.target] {
217                if !frozen_nodes.contains_key(endpoint) {
218                    return Err(GraphBuildError::MissingNode {
219                        edge: id,
220                        node: endpoint.clone(),
221                    });
222                }
223            }
224            let edge = Edge {
225                id: edge.id,
226                source: edge.source,
227                target: edge.target,
228                class: edge.class,
229                direction: edge.direction,
230            };
231            if frozen_edges.insert(id.clone(), edge).is_some() {
232                return Err(GraphBuildError::DuplicateEdge(id));
233            }
234        }
235
236        let mut predecessors = frozen_nodes
237            .keys()
238            .cloned()
239            .map(|id| (id, BTreeSet::new()))
240            .collect::<BTreeMap<_, _>>();
241        let mut successors = predecessors.clone();
242        for (edge_id, edge) in &frozen_edges {
243            let (predecessor, successor) = edge.predecessor_and_successor();
244            successors
245                .get_mut(predecessor)
246                .expect("validated edge source exists")
247                .insert(edge_id.clone());
248            predecessors
249                .get_mut(successor)
250                .expect("validated edge target exists")
251                .insert(edge_id.clone());
252        }
253        for (id, node) in &frozen_nodes {
254            if node.boundary.is_input() && !predecessors[id].is_empty() {
255                return Err(GraphBuildError::InputHasPredecessor(id.clone()));
256            }
257            if node.boundary.is_output() && !successors[id].is_empty() {
258                return Err(GraphBuildError::OutputHasSuccessor(id.clone()));
259            }
260        }
261
262        let fingerprint = (&frozen_nodes, &frozen_edges).incremental_fingerprint();
263        Ok(Self {
264            nodes: frozen_nodes,
265            edges: frozen_edges,
266            predecessors: freeze_index(predecessors),
267            successors: freeze_index(successors),
268            fingerprint,
269        })
270    }
271
272    /// Returns a node by stable identity.
273    pub fn node(&self, id: &N) -> Option<&Node<N, L>> {
274        self.nodes.get(id)
275    }
276
277    /// Returns an edge by stable identity.
278    pub fn edge(&self, id: &E) -> Option<&Edge<E, N, C>> {
279        self.edges.get(id)
280    }
281
282    /// Iterates nodes in stable identity order.
283    pub fn nodes(&self) -> impl ExactSizeIterator<Item = &Node<N, L>> {
284        self.nodes.values()
285    }
286
287    /// Iterates edges in stable identity order.
288    pub fn edges(&self) -> impl ExactSizeIterator<Item = &Edge<E, N, C>> {
289        self.edges.values()
290    }
291
292    /// Returns incoming edge identities in stable order.
293    pub fn predecessors(&self, node: &N) -> Option<&[E]> {
294        self.predecessors.get(node).map(Box::as_ref)
295    }
296
297    /// Returns outgoing edge identities in stable order.
298    pub fn successors(&self, node: &N) -> Option<&[E]> {
299        self.successors.get(node).map(Box::as_ref)
300    }
301
302    /// Returns the canonical content fingerprint.
303    pub fn fingerprint(&self) -> ValueFingerprint {
304        self.fingerprint
305    }
306}
307
308fn freeze_index<K: Ord, V: Ord>(index: BTreeMap<K, BTreeSet<V>>) -> BTreeMap<K, Box<[V]>> {
309    index
310        .into_iter()
311        .map(|(key, values)| (key, values.into_iter().collect()))
312        .collect()
313}
314
315/// Projects an external located representation into neutral graph declarations.
316///
317/// Implement this trait on a small adapter that borrows an external located-code
318/// object. Only the returned identities, locations, classes, and directions are
319/// retained by [`DataflowGraph`].
320pub trait LocatedGraphAdapter {
321    /// Stable node identity.
322    type NodeId: Clone + Hash + Ord;
323    /// Stable edge identity.
324    type EdgeId: Clone + Hash + Ord;
325    /// Neutral location value.
326    type Location: Hash + Ord;
327    /// Consumer edge-class extension.
328    type Class: Hash + Ord;
329
330    /// Produces located node declarations.
331    fn nodes(&self) -> Vec<NodeSpec<Self::NodeId, Self::Location>>;
332
333    /// Produces directed edge declarations.
334    fn edges(&self) -> Vec<EdgeSpec<Self::EdgeId, Self::NodeId, Self::Class>>;
335
336    /// Validates and freezes the projected graph.
337    fn build_graph(&self) -> AdapterBuildResult<Self> {
338        DataflowGraph::build(self.nodes(), self.edges())
339    }
340}
341
342/// The neutral graph produced by a particular located adapter.
343pub type AdaptedGraph<A> = DataflowGraph<
344    <A as LocatedGraphAdapter>::NodeId,
345    <A as LocatedGraphAdapter>::EdgeId,
346    <A as LocatedGraphAdapter>::Location,
347    <A as LocatedGraphAdapter>::Class,
348>;
349
350/// The checked construction result produced by a located adapter.
351pub type AdapterBuildResult<A> = Result<
352    AdaptedGraph<A>,
353    GraphBuildError<<A as LocatedGraphAdapter>::NodeId, <A as LocatedGraphAdapter>::EdgeId>,
354>;
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    fn node(id: u8, boundary: Boundary) -> NodeSpec<u8, (u8, u8)> {
361        NodeSpec {
362            id,
363            location: (id, id + 1),
364            boundary,
365        }
366    }
367
368    fn edge(id: u8, source: u8, target: u8) -> EdgeSpec<u8, u8, &'static str> {
369        EdgeSpec {
370            id,
371            source,
372            target,
373            class: EdgeClass::Data,
374            direction: GraphDirection::Forward,
375        }
376    }
377
378    #[test]
379    fn insertion_order_does_not_change_structure_or_fingerprint() {
380        let left = DataflowGraph::build(
381            [
382                node(1, Boundary::Input),
383                node(2, Boundary::Internal),
384                node(3, Boundary::Output),
385            ],
386            [edge(10, 1, 2), edge(20, 2, 3)],
387        )
388        .unwrap();
389        let right = DataflowGraph::build(
390            [
391                node(3, Boundary::Output),
392                node(1, Boundary::Input),
393                node(2, Boundary::Internal),
394            ],
395            [edge(20, 2, 3), edge(10, 1, 2)],
396        )
397        .unwrap();
398
399        assert_eq!(left, right);
400        assert_eq!(left.fingerprint(), right.fingerprint());
401        assert_eq!(left.successors(&1), Some([10].as_slice()));
402        assert_eq!(left.predecessors(&3), Some([20].as_slice()));
403    }
404
405    #[test]
406    fn rejects_duplicate_missing_and_invalid_boundary_declarations() {
407        assert_eq!(
408            DataflowGraph::<_, u8, _, &str>::build(
409                [node(1, Boundary::Internal), node(1, Boundary::Internal)],
410                [],
411            ),
412            Err(GraphBuildError::DuplicateNode(1))
413        );
414        assert!(matches!(
415            DataflowGraph::build([node(1, Boundary::Internal)], [edge(7, 1, 2)]),
416            Err(GraphBuildError::MissingNode { edge: 7, node: 2 })
417        ));
418        assert_eq!(
419            DataflowGraph::build(
420                [node(1, Boundary::Internal), node(2, Boundary::Input)],
421                [edge(7, 1, 2)],
422            ),
423            Err(GraphBuildError::InputHasPredecessor(2))
424        );
425        assert_eq!(
426            DataflowGraph::build(
427                [node(1, Boundary::Output), node(2, Boundary::Internal)],
428                [edge(7, 1, 2)],
429            ),
430            Err(GraphBuildError::OutputHasSuccessor(1))
431        );
432    }
433
434    #[test]
435    fn graph_public_surface_remains_representation_neutral() {
436        let source = include_str!("graph.rs");
437        let public_surface = source
438            .lines()
439            .filter(|line| line.trim_start().starts_with("pub "))
440            .collect::<String>();
441        for forbidden in ["Machine", "Jvm", "JVM", "LocatedCode"] {
442            assert!(
443                !public_surface.contains(forbidden),
444                "public graph surface names {forbidden}"
445            );
446        }
447    }
448}