Skip to content

Commit f29fc2c

Browse files
committed
Add find_shortest_cycle to pathfinding
1 parent f73738d commit f29fc2c

File tree

1 file changed

+120
-0
lines changed

1 file changed

+120
-0
lines changed

rust/src/graph/pathfinding.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::errors::{GrimpError, GrimpResult};
22
use crate::graph::{Graph, ModuleToken, EMPTY_MODULE_TOKENS};
33
use indexmap::{IndexMap, IndexSet};
4+
use itertools::Itertools;
45
use rustc_hash::{FxHashMap, FxHashSet, FxHasher};
56
use slotmap::SecondaryMap;
67
use std::hash::BuildHasherDefault;
@@ -49,6 +50,23 @@ pub fn find_shortest_path(
4950
)
5051
}
5152

53+
/// Finds the shortest cycle from `modules` to `modules`, via a bidirectional BFS.
54+
pub fn find_shortest_cycle(
55+
graph: &Graph,
56+
modules: &FxHashSet<ModuleToken>,
57+
excluded_modules: &FxHashSet<ModuleToken>,
58+
excluded_imports: &FxHashMap<ModuleToken, FxHashSet<ModuleToken>>,
59+
) -> GrimpResult<Option<Vec<ModuleToken>>> {
60+
// Exclude imports internal to `modules`
61+
let mut excluded_imports = excluded_imports.clone();
62+
for (m1, m2) in modules.iter().tuple_combinations() {
63+
excluded_imports.entry(*m1).or_default().insert(*m2);
64+
excluded_imports.entry(*m2).or_default().insert(*m1);
65+
}
66+
67+
_find_shortest_path(graph, modules, modules, excluded_modules, &excluded_imports)
68+
}
69+
5270
fn _find_shortest_path(
5371
graph: &Graph,
5472
from_modules: &FxHashSet<ModuleToken>,
@@ -140,3 +158,105 @@ fn import_is_excluded(
140158
.contains(to_module)
141159
}
142160
}
161+
162+
#[cfg(test)]
163+
mod test_find_shortest_cycle {
164+
use super::*;
165+
166+
#[test]
167+
fn test_finds_cycle_single_module() -> GrimpResult<()> {
168+
let mut graph = Graph::default();
169+
let foo = graph.get_or_add_module("foo").token;
170+
let bar = graph.get_or_add_module("bar").token;
171+
let baz = graph.get_or_add_module("baz").token;
172+
let x = graph.get_or_add_module("x").token;
173+
let y = graph.get_or_add_module("y").token;
174+
let z = graph.get_or_add_module("z").token;
175+
// Shortest cycle
176+
graph.add_import(foo, bar);
177+
graph.add_import(bar, baz);
178+
graph.add_import(baz, foo);
179+
// Longer cycle
180+
graph.add_import(foo, x);
181+
graph.add_import(x, y);
182+
graph.add_import(y, z);
183+
graph.add_import(z, foo);
184+
185+
let path = find_shortest_cycle(
186+
&graph,
187+
&foo.into(),
188+
&FxHashSet::default(),
189+
&FxHashMap::default(),
190+
)?;
191+
assert_eq!(path, Some(vec![foo, bar, baz, foo]));
192+
193+
graph.remove_import(baz, foo);
194+
195+
let path = find_shortest_cycle(
196+
&graph,
197+
&foo.into(),
198+
&FxHashSet::default(),
199+
&FxHashMap::default(),
200+
)?;
201+
assert_eq!(path, Some(vec![foo, x, y, z, foo]));
202+
203+
Ok(())
204+
}
205+
206+
#[test]
207+
fn test_returns_none_if_no_cycle() -> GrimpResult<()> {
208+
let mut graph = Graph::default();
209+
let foo = graph.get_or_add_module("foo").token;
210+
let bar = graph.get_or_add_module("bar").token;
211+
let baz = graph.get_or_add_module("baz").token;
212+
graph.add_import(foo, bar);
213+
graph.add_import(bar, baz);
214+
215+
let path = find_shortest_cycle(
216+
&graph,
217+
&foo.into(),
218+
&FxHashSet::default(),
219+
&FxHashMap::default(),
220+
)?;
221+
222+
assert_eq!(path, None);
223+
224+
Ok(())
225+
}
226+
227+
#[test]
228+
fn test_finds_cycle_multiple_module() -> GrimpResult<()> {
229+
let mut graph = Graph::default();
230+
231+
graph.get_or_add_module("colors");
232+
let red = graph.get_or_add_module("colors.red").token;
233+
let blue = graph.get_or_add_module("colors.blue").token;
234+
let a = graph.get_or_add_module("a").token;
235+
let b = graph.get_or_add_module("b").token;
236+
let c = graph.get_or_add_module("c").token;
237+
let d = graph.get_or_add_module("d").token;
238+
239+
// The computation should not be confused by these two imports internal to `modules`.
240+
graph.add_import(red, blue);
241+
graph.add_import(blue, red);
242+
// This is the part we expect to find.
243+
graph.add_import(red, a);
244+
graph.add_import(a, b);
245+
graph.add_import(b, blue);
246+
// A longer path.
247+
graph.add_import(a, c);
248+
graph.add_import(c, d);
249+
graph.add_import(d, b);
250+
251+
let path = find_shortest_cycle(
252+
&graph,
253+
&FxHashSet::from_iter([red, blue]),
254+
&FxHashSet::default(),
255+
&FxHashMap::default(),
256+
)?;
257+
258+
assert_eq!(path, Some(vec![red, a, b, blue]));
259+
260+
Ok(())
261+
}
262+
}

0 commit comments

Comments
 (0)