Crates.io | topo_sort |
lib.rs | topo_sort |
version | 0.4.0 |
source | src |
created_at | 2021-12-03 03:59:44.984712 |
updated_at | 2021-12-17 21:38:48.282051 |
description | A 'cycle-safe' topological sort for a set of nodes with dependencies |
homepage | |
repository | https://github.com/nu11ptr/topo_sort |
max_upload_size | |
id | 491551 |
size | 46,897 |
A "cycle-safe" topological sort for a set of nodes with dependencies in Rust.
Basically, it allows sorting a list by its dependencies while checking for
cycles in the graph. If a cycle is detected, a CycleError
is returned from the
iterator (or SortResults::Partial
is returned if using the to/into_vec
APIs)
.
Topological sorts are used to sort the nodes of a unidirectional graph. Typical applications would include anything that requires dependency based sorting. For example, it could be used to find the correct order of compilation dependencies, or it could be used to find module cycles in languages that don't allow cycles. The applications are limitless.
[dependencies]
topo_sort = "0.3"
A basic example:
use topo_sort::{SortResults, TopoSort};
fn main() {
let mut topo_sort = TopoSort::with_capacity(5);
// read as "C" depends on "A" and "B"
topo_sort.insert("C", vec!["A", "B"]);
topo_sort.insert("E", vec!["B", "C"]);
topo_sort.insert("A", vec![]);
topo_sort.insert("D", vec!["A", "C", "E"]);
topo_sort.insert("B", vec!["A"]);
match topo_sort.into_vec_nodes() {
SortResults::Full(nodes) => assert_eq!(vec!["A", "B", "C", "E", "D"], nodes),
SortResults::Partial(_) => panic!("unexpected cycle!"),
}
}
...or using iteration:
use topo_sort::TopoSort;
fn main() {
let mut topo_sort = TopoSort::with_capacity(5);
topo_sort.insert("C", vec!["A", "B"]);
topo_sort.insert("E", vec!["B", "C"]);
topo_sort.insert("A", vec![]);
topo_sort.insert("D", vec!["A", "C", "E"]);
topo_sort.insert("B", vec!["A"]);
let mut nodes = Vec::with_capacity(5);
for node in &topo_sort {
// We check for cycle errors before usage
match node {
Ok(node) => nodes.push(*node),
Err(_) => panic!("Unexpected cycle!"),
}
}
assert_eq!(vec!["A", "B", "C", "E", "D"], nodes);
}
Cycle detection:
use topo_sort::TopoSort;
fn main() {
let mut topo_sort = TopoSort::with_capacity(3);
topo_sort.insert(1, vec![2, 3]);
topo_sort.insert(2, vec![3]);
assert_eq!(vec![2, 1], topo_sort.try_owned_vec_nodes().unwrap());
topo_sort.insert(3, vec![1]); // cycle
assert!(topo_sort.try_vec_nodes().is_err());
}
owned
methods)Eq
and Hash
implemented on nodes
owned
methods that require Clone
std
Vec
Using TopoSort
is a basic two step process:
TopoSort
Vec
Result
so cycles can be detected every iterationto/into_vec
functions - returns a SortResults
enum with a Vec
of
full (no cycle) or partial results (when cycle detected)try_[into]_vec
functions - returns a Vec
wrapped in a Result
(full
or no results)This is implemented using Kahn's algorithm. While basic caution was taken not to do anything too egregious performance-wise, the author's use cases are not performance sensitive, and it has not been optimized. Still, the author tried to make reasonable trade offs and make it flexible for a variety of use cases, not just the author's.
The crate uses two tiny unsafe
blocks which use the addresses of HashMap
keys in a new HashMap
. This was necessary to avoid cloning inserted data on
owned iteration by self referencing the struct. Since there is no removal in
regular iteration (iter()
or for
loop using &
), this should be safe as
there is no chance of the data moving during borrowed iteration. During
owned/consuming iteration (into_iter()
or for
without &
), we remove the
entries as we go. If Rust's HashMap
were to change and shrink during removals,
this iterator could break. If this makes you uncomfortable, simply don't use
consuming iteration.
It has been tested with Miri and passes
all tests. (MIRIFLAGS=-Zmiri-tag-raw-pointers cargo miri test
)
This project is licensed optionally under either: