Skip to main content

sim_lib_topology/
compile.rs

1//! Graph validation and lowering to deterministic compile plans.
2//!
3//! `compile_graph` validates topology graph data and lowers it to a
4//! `CompiledGraph` with stable node and edge indexes.
5
6use std::collections::BTreeMap;
7
8use sim_kernel::{Cx, Result, Symbol};
9
10use crate::{EdgeId, Graph, NodeId, PortMode, PortRef, validate::validate_graph};
11
12/// Deterministic graph plan produced from validated topology data.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct CompiledGraph {
15    /// Graph name copied from source data.
16    pub name: Symbol,
17    /// Compiled nodes in source declaration order.
18    pub nodes: Vec<CompiledNode>,
19    /// Compiled edges in source declaration order.
20    pub edges: Vec<CompiledEdge>,
21    /// Stable lookup from node id to source-order node index.
22    pub node_index_by_id: BTreeMap<NodeId, usize>,
23    /// Stable lookup from edge id to source-order edge index.
24    pub edge_index_by_id: BTreeMap<EdgeId, usize>,
25    /// Public input node indexes in source order.
26    pub input_nodes: Vec<usize>,
27    /// Public output node indexes in source order.
28    pub output_nodes: Vec<usize>,
29    /// Incoming edge indexes for each node, preserving source edge order.
30    pub incoming_edges: Vec<Vec<usize>>,
31    /// Outgoing edge indexes for each node, sorted by priority then source order.
32    pub outgoing_edges: Vec<Vec<usize>>,
33    /// Whether each node is reachable from any public input node.
34    pub reachable_from_inputs: Vec<bool>,
35    /// Whether each node participates in a directed cycle.
36    pub cyclic_nodes: Vec<bool>,
37    /// Whether each edge participates in a directed cycle.
38    pub cycle_edges: Vec<bool>,
39}
40
41/// Compiled node metadata with a stable source index.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct CompiledNode {
44    /// Source-order node index.
45    pub source_index: usize,
46    /// Source node id.
47    pub id: NodeId,
48    /// Source node verb.
49    pub verb: Symbol,
50    /// Whether the node declares any stream input or output port.
51    pub has_stream_ports: bool,
52}
53
54/// Compiled edge metadata with stable endpoint indexes.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct CompiledEdge {
57    /// Source-order edge index.
58    pub source_index: usize,
59    /// Source edge id.
60    pub id: EdgeId,
61    /// Source port reference.
62    pub from: PortRef,
63    /// Destination port reference.
64    pub to: PortRef,
65    /// Source-order node index for `from`.
66    pub from_node: usize,
67    /// Source-order node index for `to`.
68    pub to_node: usize,
69    /// Routing priority copied from source data.
70    pub priority: i64,
71    /// Per-edge visit cap copied from source data.
72    pub max_visits: Option<u32>,
73}
74
75/// Validates `graph` and lowers it to a [`CompiledGraph`] with stable node and
76/// edge indexes.
77///
78/// This is the compile stage of the topology pipeline (see the crate-level
79/// documentation); the resulting plan is deterministic for a given graph.
80pub fn compile_graph(cx: &mut Cx, graph: &Graph) -> Result<CompiledGraph> {
81    validate_graph(cx, graph)?;
82
83    let node_index_by_id = node_indexes(graph);
84    let input_nodes = boundary_nodes(graph, "in");
85    let output_nodes = boundary_nodes(graph, "out");
86    let nodes = compile_nodes(graph);
87    let (edges, edge_index_by_id) = compile_edges(graph, &node_index_by_id);
88    let (incoming_edges, outgoing_edges) = edge_lists(graph.nodes.len(), &edges);
89    let reachable_from_inputs =
90        reachable_from_inputs(graph.nodes.len(), &edges, &outgoing_edges, &input_nodes);
91    let (cyclic_nodes, cycle_edges) = cycle_metadata(graph.nodes.len(), &edges, &outgoing_edges);
92
93    Ok(CompiledGraph {
94        name: graph.name.clone(),
95        nodes,
96        edges,
97        node_index_by_id,
98        edge_index_by_id,
99        input_nodes,
100        output_nodes,
101        incoming_edges,
102        outgoing_edges,
103        reachable_from_inputs,
104        cyclic_nodes,
105        cycle_edges,
106    })
107}
108
109fn node_indexes(graph: &Graph) -> BTreeMap<NodeId, usize> {
110    graph
111        .nodes
112        .iter()
113        .enumerate()
114        .map(|(index, node)| (node.id.clone(), index))
115        .collect()
116}
117
118fn boundary_nodes(graph: &Graph, verb: &str) -> Vec<usize> {
119    graph
120        .nodes
121        .iter()
122        .enumerate()
123        .filter_map(|(index, node)| (node.verb.name.as_ref() == verb).then_some(index))
124        .collect()
125}
126
127fn compile_nodes(graph: &Graph) -> Vec<CompiledNode> {
128    graph
129        .nodes
130        .iter()
131        .enumerate()
132        .map(|(source_index, node)| CompiledNode {
133            source_index,
134            id: node.id.clone(),
135            verb: node.verb.clone(),
136            has_stream_ports: node
137                .inputs
138                .iter()
139                .chain(node.outputs.iter())
140                .any(|port| port.mode == PortMode::Stream),
141        })
142        .collect()
143}
144
145fn compile_edges(
146    graph: &Graph,
147    node_index_by_id: &BTreeMap<NodeId, usize>,
148) -> (Vec<CompiledEdge>, BTreeMap<EdgeId, usize>) {
149    let mut edge_index_by_id = BTreeMap::new();
150    let edges = graph
151        .edges
152        .iter()
153        .enumerate()
154        .map(|(source_index, edge)| {
155            edge_index_by_id.insert(edge.id, source_index);
156            CompiledEdge {
157                source_index,
158                id: edge.id,
159                from: edge.from.clone(),
160                to: edge.to.clone(),
161                from_node: node_index_by_id[&edge.from.node],
162                to_node: node_index_by_id[&edge.to.node],
163                priority: edge.priority,
164                max_visits: edge.max_visits,
165            }
166        })
167        .collect();
168    (edges, edge_index_by_id)
169}
170
171fn edge_lists(node_count: usize, edges: &[CompiledEdge]) -> (Vec<Vec<usize>>, Vec<Vec<usize>>) {
172    let mut incoming = vec![Vec::new(); node_count];
173    let mut outgoing = vec![Vec::new(); node_count];
174
175    for edge in edges {
176        incoming[edge.to_node].push(edge.source_index);
177        outgoing[edge.from_node].push(edge.source_index);
178    }
179
180    for edge_list in &mut outgoing {
181        edge_list.sort_by_key(|edge_index| (edges[*edge_index].priority, *edge_index));
182    }
183
184    (incoming, outgoing)
185}
186
187fn reachable_from_inputs(
188    node_count: usize,
189    edges: &[CompiledEdge],
190    outgoing_edges: &[Vec<usize>],
191    input_nodes: &[usize],
192) -> Vec<bool> {
193    let mut reachable = vec![false; node_count];
194    for input in input_nodes {
195        visit_reachable(*input, edges, outgoing_edges, &mut reachable);
196    }
197    reachable
198}
199
200fn visit_reachable(
201    node: usize,
202    edges: &[CompiledEdge],
203    outgoing_edges: &[Vec<usize>],
204    reachable: &mut [bool],
205) {
206    if reachable[node] {
207        return;
208    }
209    reachable[node] = true;
210    for edge_index in &outgoing_edges[node] {
211        visit_reachable(edges[*edge_index].to_node, edges, outgoing_edges, reachable);
212    }
213}
214
215fn cycle_metadata(
216    node_count: usize,
217    edges: &[CompiledEdge],
218    outgoing_edges: &[Vec<usize>],
219) -> (Vec<bool>, Vec<bool>) {
220    let reachability = transitive_reachability(node_count, edges, outgoing_edges);
221    let mut cyclic_nodes = vec![false; node_count];
222    let mut cycle_edges = vec![false; edges.len()];
223
224    for edge in edges {
225        if edge.from_node == edge.to_node || reachability[edge.to_node][edge.from_node] {
226            cycle_edges[edge.source_index] = true;
227            cyclic_nodes[edge.from_node] = true;
228            cyclic_nodes[edge.to_node] = true;
229        }
230    }
231
232    (cyclic_nodes, cycle_edges)
233}
234
235fn transitive_reachability(
236    node_count: usize,
237    edges: &[CompiledEdge],
238    outgoing_edges: &[Vec<usize>],
239) -> Vec<Vec<bool>> {
240    let mut reachability = vec![vec![false; node_count]; node_count];
241    for start in 0..node_count {
242        visit_from(start, start, edges, outgoing_edges, &mut reachability);
243    }
244    reachability
245}
246
247fn visit_from(
248    start: usize,
249    node: usize,
250    edges: &[CompiledEdge],
251    outgoing_edges: &[Vec<usize>],
252    reachability: &mut [Vec<bool>],
253) {
254    for edge_index in &outgoing_edges[node] {
255        let next = edges[*edge_index].to_node;
256        if reachability[start][next] {
257            continue;
258        }
259        reachability[start][next] = true;
260        visit_from(start, next, edges, outgoing_edges, reachability);
261    }
262}