mz_ore/graph.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License in the LICENSE file at the
6// root of this repository, or online at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Graph utilities.
17
18use std::collections::BTreeSet;
19
20/// A non-recursive implementation of a fallible depth-first traversal
21/// starting from `root`.
22///
23/// Assumes that nodes in the graph all have unique node ids.
24///
25/// `at_enter` runs when entering a node. It is expected to return an in-order
26/// list of the children of the node. You can omit children from the list
27/// returned if you want to skip traversing the subgraphs corresponding to
28/// those children. If no children are omitted, `at_enter` can be thought
29/// of as a function that processes the nodes of the graph in pre-order.
30///
31/// `at_exit` runs when exiting a node. It can be thought of as a function that
32/// processes the nodes of the graph in post-order.
33///
34/// This function only enters and exits a node at most once and thus is safe to
35/// run even if the graph contains a cycle.
36pub fn try_nonrecursive_dft<Graph, NodeId, AtEnter, AtExit, E>(
37 graph: &Graph,
38 root: NodeId,
39 at_enter: &mut AtEnter,
40 at_exit: &mut AtExit,
41) -> Result<(), E>
42where
43 NodeId: std::cmp::Ord,
44 AtEnter: FnMut(&Graph, &NodeId) -> Result<Vec<NodeId>, E>,
45 AtExit: FnMut(&Graph, &NodeId) -> Result<(), E>,
46{
47 // All nodes that have been entered but not exited. Last node in the vec is
48 // the node that we most recently entered.
49 let mut entered = Vec::new();
50 // All nodes that have been exited.
51 let mut exited = BTreeSet::new();
52
53 // Pseudocode for the recursive version of this function would look like:
54 // ```
55 // children = at_enter(graph, node)
56 // foreach child in children:
57 // recursive_call(graph, child)
58 // atexit(graph, node)
59 // ```
60 // In this non-recursive implementation, you can think of the call stack as
61 // been replaced by `entered`. Every time an object is pushed into `entered`
62 // would have been a time you would have pushed a recursive call onto the
63 // call stack. Likewise, times an object is popped from `entered` would have
64 // been times when recursive calls leave the stack.
65
66 // Enter from the root.
67 let children = at_enter(graph, &root)?;
68 entered_node(&mut entered, root, children);
69 while !entered.is_empty() {
70 if let Some(to_enter) = find_next_child_to_enter(&mut entered, &exited) {
71 let children = at_enter(graph, &to_enter)?;
72 entered_node(&mut entered, to_enter, children);
73 } else {
74 // If this node has no more children to descend into,
75 // exit the current node and run `at_exit`.
76 let (to_exit, _) = entered.pop().unwrap();
77 at_exit(graph, &to_exit)?;
78 exited.insert(to_exit);
79 }
80 }
81 Ok(())
82}
83
84/// Same as [`try_nonrecursive_dft`], but allows changes to be made to the graph.
85pub fn try_nonrecursive_dft_mut<Graph, NodeId, AtEnter, AtExit, E>(
86 graph: &mut Graph,
87 root: NodeId,
88 at_enter: &mut AtEnter,
89 at_exit: &mut AtExit,
90) -> Result<(), E>
91where
92 NodeId: std::cmp::Ord + Clone,
93 AtEnter: FnMut(&mut Graph, &NodeId) -> Result<Vec<NodeId>, E>,
94 AtExit: FnMut(&mut Graph, &NodeId) -> Result<(), E>,
95{
96 // Code in this method is identical to the code in `nonrecursive_dft`.
97 let mut entered = Vec::new();
98 let mut exited = BTreeSet::new();
99
100 let children = at_enter(graph, &root)?;
101 entered_node(&mut entered, root, children);
102 while !entered.is_empty() {
103 if let Some(to_enter) = find_next_child_to_enter(&mut entered, &exited) {
104 let children = at_enter(graph, &to_enter)?;
105 entered_node(&mut entered, to_enter, children);
106 } else {
107 let (to_exit, _) = entered.pop().unwrap();
108 at_exit(graph, &to_exit)?;
109 exited.insert(to_exit);
110 }
111 }
112 Ok(())
113}
114
115/// A non-recursive implementation of an infallible depth-first traversal
116/// starting from `root`.
117///
118/// Assumes that nodes in the graph all have unique node ids.
119///
120/// `at_enter` runs when entering a node. It is expected to return an in-order
121/// list of the children of the node. You can omit children from the list
122/// returned if you want to skip the traversing subgraphs corresponding to
123/// those children. If no children are omitted, `at_enter` can be thought
124/// of as a function that processes the nodes of the graph in pre-order.
125///
126/// `at_exit` runs when exiting a node. It can be thought of as a function that
127/// processes the nodes of the graph in post-order.
128///
129/// This function only enters and exits a node at most once and thus is safe to
130/// run even if the graph contains a cycle.
131pub fn nonrecursive_dft<Graph, NodeId, AtEnter, AtExit>(
132 graph: &Graph,
133 root: NodeId,
134 at_enter: &mut AtEnter,
135 at_exit: &mut AtExit,
136) where
137 NodeId: std::cmp::Ord,
138 AtEnter: FnMut(&Graph, &NodeId) -> Vec<NodeId>,
139 AtExit: FnMut(&Graph, &NodeId) -> (),
140{
141 // All nodes that have been entered but not exited. Last node in the vec is
142 // the node that we most recently entered.
143 let mut entered = Vec::new();
144 // All nodes that have been exited.
145 let mut exited = BTreeSet::new();
146
147 // Pseudocode for the recursive version of this function would look like:
148 // ```
149 // atenter(graph, node)
150 // foreach child in children(graph, node):
151 // recursive_call(graph, child)
152 // atexit(graph, node)
153 // ```
154 // In this non-recursive implementation, you can think of the call stack as
155 // been replaced by `entered`. Every time an object is pushed into `entered`
156 // would have been a time you would have pushed a recursive call onto the
157 // call stack. Likewise, times an object is popped from `entered` would have
158 // been times when recursive calls leave the stack.
159
160 // Enter from the root.
161 let children = at_enter(graph, &root);
162 entered_node(&mut entered, root, children);
163 while !entered.is_empty() {
164 if let Some(to_enter) = find_next_child_to_enter(&mut entered, &exited) {
165 let children = at_enter(graph, &to_enter);
166 entered_node(&mut entered, to_enter, children);
167 } else {
168 // If this node has no more children to descend into,
169 // exit the current node and run `at_exit`.
170 let (to_exit, _) = entered.pop().unwrap();
171 at_exit(graph, &to_exit);
172 exited.insert(to_exit);
173 }
174 }
175}
176
177/// Same as [`nonrecursive_dft`], but allows changes to be made to the graph.
178pub fn nonrecursive_dft_mut<Graph, NodeId, AtEnter, AtExit>(
179 graph: &mut Graph,
180 root: NodeId,
181 at_enter: &mut AtEnter,
182 at_exit: &mut AtExit,
183) where
184 NodeId: std::cmp::Ord + Clone,
185 AtEnter: FnMut(&mut Graph, &NodeId) -> Vec<NodeId>,
186 AtExit: FnMut(&mut Graph, &NodeId) -> (),
187{
188 // Code in this method is identical to the code in `nonrecursive_dft`.
189 let mut entered = Vec::new();
190 let mut exited = BTreeSet::new();
191
192 let children = at_enter(graph, &root);
193 entered_node(&mut entered, root, children);
194 while !entered.is_empty() {
195 if let Some(to_enter) = find_next_child_to_enter(&mut entered, &exited) {
196 let children = at_enter(graph, &to_enter);
197 entered_node(&mut entered, to_enter, children);
198 } else {
199 let (to_exit, _) = entered.pop().unwrap();
200 at_exit(graph, &to_exit);
201 exited.insert(to_exit);
202 }
203 }
204}
205
206/// Add to `entered` that we have entered `node` and `node` has `children`.
207fn entered_node<NodeId>(
208 entered: &mut Vec<(NodeId, Vec<NodeId>)>,
209 node: NodeId,
210 mut children: Vec<NodeId>,
211) where
212 NodeId: std::cmp::Ord,
213{
214 // Reverse children because `find_next_child_to_enter` will traverse the
215 // list of children by popping them out from the back.
216 children.reverse();
217 entered.push((node, children))
218}
219
220/// Find the next child node, if any, that we have not entered.
221fn find_next_child_to_enter<NodeId>(
222 entered: &mut Vec<(NodeId, Vec<NodeId>)>,
223 exited: &BTreeSet<NodeId>,
224) -> Option<NodeId>
225where
226 NodeId: std::cmp::Ord,
227{
228 let (_, children) = entered.last_mut().unwrap();
229 while let Some(child) = children.pop() {
230 if !exited.contains(&child) {
231 return Some(child);
232 }
233 }
234 None
235}