Safegraph - A graph manipulation library with fully type-safe, zero-cost, stable graph APIs

Safegraph is a graph library focused on type-safe, stable graph manipulation APIs. It prevents common indexing mistakes using Rust's type system and zero-cost abstractions.
In this library, "stability" means an index keeps referring to the same logical node or edge after it is obtained. Many graph libraries break this guarantee when removing nodes or edges by compacting internal arrays, which can invalidate old indices.
use Graph;
use VecGraph;
let g: = graph!;
assert_eq!;
assert_eq!;
Safegraph provides two main graph manipulation interfaces:
scoped interface
This API provides stable indices inside a lexical scope. It uses a GhostCell-like lifetime pattern so node/edge indices cannot escape their valid context. This gives compile-time safety without runtime index validation overhead.
use BTreeGraph;
use UniqueEdge;
use Graph;
let g: = graph!;
g.scope;
scope_mut allows mutation inside a scope. Combined with graph!, you can
insert nodes/edges and remove them with compile-time index safety:
use Graph;
use VecGraph;
let mut g = default;
g.scope_mut;
assert_eq!;
assert_eq!;
stabilize() interface
This API converts a graph into a tombstone-versioned stable wrapper. It preserves index identity across mutations and validates index availability at runtime.
use Graph;
use VecGraph;
let mut g = default.stabilize;
graph!;
g.remove_node;
assert!;
assert!;
let n2 = g.insert_node.unwrap;
assert!;
graph! Macro
graph! supports two modes:
-
Expression mode (no input graph):
let g = graph!( ... );- Builds and returns a new graph (
Default::default()+ insertions). - No
StableNode/StableEdgeassertion is emitted.
-
Statement mode (with input graph):
graph!(input_graph_expr => ...);- Mutates the provided graph expression in place.
- Does not return the graph.
- Exports named node/edge bindings (
ident/ident {expr}) into the caller scope. - Emits
StableNode/StableEdgeassertions when named node/edge bindings are used.
Examples:
use VecGraph;
use Graph;
// Expression mode: returns graph value
let g: = graph!;
assert_eq!;
use Graph;
use VecGraph;
// Statement mode: mutates existing graph and exports bound idents
let mut g = default.stabilize;
graph!;
assert_eq!;
assert_eq!;
ident {expr} rule:
- You can specify both a binding name and an explicit payload expression.
- The same node/edge ident must not specify
{expr}more than once in one macro call.
Core Concepts
Stability
In safegraph, stability means:
StableNode: aNodeIxkeeps referring to the same logical node during the lifetime.StableEdge: anEdgeIxkeeps referring to the same logical edge during the lifetime.
Without stability, indices are unstable: after insertion/removal, previously saved indices may no longer be valid, or refers different nodes / edges from it originally refers.
Unstable graph example:
use Graph;
use VecGraph;
let mut g = default;
let n = unsafe ;
g.remove_node;
assert!; // old index is invalid
g.push; // insert another node
assert_eq!; // refering another node
Stable wrapper example:
use Graph;
use VecGraph;
let mut g = default.stabilize;
let n = g.insert_node.unwrap;
g.remove_node;
assert!; // removed generation is invalid
let n2 = g.insert_node.unwrap;
assert!;
assert_eq!; // new generation points to new payload
This is why some APIs/macros require StableNode / StableEdge: they need indices that remain meaningful under mutation.
Usage Examples
Creating and Manipulating Graphs
use Graph;
use BTreeGraph;
let mut graph: = default;
graph.insert_node.unwrap;
graph.insert_node.unwrap;
graph.insert_node.unwrap;
// Add weighted edges
graph.insert_edge.unwrap;
graph.insert_edge.unwrap;
graph.insert_edge.unwrap;
// Query the graph
assert_eq!;
assert_eq!;
// Iterate over outgoing edges
for edge_tag in graph.edge_indices_from
Using Graph Algorithms
use tarjan_scc;
use dijkstra;
use toposort;
use BTreeGraph;
use Graph;
let mut graph = default;
graph.insert_node.unwrap;
graph.insert_node.unwrap;
graph.insert_node.unwrap;
graph.insert_edge.unwrap;
graph.insert_edge.unwrap;
graph.insert_edge.unwrap;
// Find strongly connected components
let sccs: = tarjan_scc.collect;
println!;
// Topological sort (returns Err for cyclic graphs)
assert!;
// Shortest paths
let dists = dijkstra;
Benchmark
Three of the contenders are the same VecGraph accessed three safe ways:
sg_vec_scoped (via scope()/scope_mut(); contains_*_index is always
true, so no bounds check), sg_vec_stabilized (via Graph::stabilize();
versioned/tombstoned stable indices, no scope), and sg_vec_checked
(graph-level node()/edge(), which assert contains = a real bounds
check, no scope). The rest: sg_flat (FlatAdjEdgeGraph), sg_btree
(payload-keyed BTreeGraph), pg (petgraph DiGraph), pg_stable
(petgraph StableDiGraph).
creation (build n nodes + 5n edges)
| backend | 100 | 1 000 | 10 000 |
|---|---|---|---|
| sg_vec_scoped | 1.95µs | 19.94µs | 249.66µs |
| sg_vec_stabilized | 3.82µs | 43.69µs | 472.48µs |
| sg_vec_checked | 1.89µs | 17.94µs | 222.23µs |
| sg_flat | 7.65µs | 82.53µs | 1.09ms |
| sg_btree | 64.10µs | 1.37ms | 19.48ms |
| pg | 1.71µs | 15.07µs | 197.41µs |
| pg_stable | 3.99µs | 37.17µs | 373.84µs |
traversal (sum every node + outgoing edge payload)
| backend | 100 | 1 000 | 10 000 |
|---|---|---|---|
| sg_vec_scoped | 293ns | 3.57µs | 300.33µs |
| sg_vec_stabilized | 326ns | 5.63µs | 277.03µs |
| sg_vec_checked | 215ns | 3.51µs | 255.94µs |
| sg_flat | 821ns | 9.82µs | 124.22µs |
| sg_btree | 6.00µs | 329.96µs | 5.60ms |
| pg | 247ns | 4.22µs | 303.04µs |
| pg_stable | 399ns | 6.25µs | 274.91µs |
random_access (look up n nodes by index in a shuffled order)
| backend | 100 | 1 000 | 10 000 |
|---|---|---|---|
| sg_vec_scoped | 35ns | 271ns | 3.26µs |
| sg_vec_stabilized | 59ns | 574ns | 6.95µs |
| sg_vec_checked | 49ns | 421ns | 4.36µs |
| sg_flat | 47ns | 489ns | 5.89µs |
| sg_btree | 686ns | 16.42µs | 937.67µs |
| pg | 45ns | 358ns | 6.26µs |
| pg_stable | 64ns | 573ns | 8.55µs |
remove_edges (identify half the edges by predicate, then remove)
| backend | 100 | 1 000 | 10 000 |
|---|---|---|---|
| sg_vec_scoped | 8.46µs | 84.91µs | 1.52ms |
| sg_vec_stabilized | 3.89µs | 30.08µs | 379.34µs |
| sg_vec_checked | 4.01µs | 35.23µs | 1.13ms |
| sg_flat | 7.65µs | 83.64µs | 1.19ms |
| sg_btree | 83.75µs | 1.50ms | 21.94ms |
| pg | 2.72µs | 32.12µs | 1.16ms |
| pg_stable | 3.31µs | 61.83µs | 1.31ms |
| pg_stable_loop | 3.39µs | 59.22µs | 1.50ms |
remove_nodes (remove a quarter of the nodes, cascading their edges)
| backend | 100 | 1 000 | 10 000 |
|---|---|---|---|
| sg_vec_scoped | 3.91µs | 63.52µs | 1.51ms |
| sg_vec_stabilized | 4.17µs | 62.30µs | 559.24µs |
| sg_vec_checked | 3.73µs | 73.27µs | 1.55ms |
| sg_flat | 57.22µs | 7.66ms | 734.78ms ⚠ |
| sg_btree | 85.16µs | 1.57ms | 18.69ms |
| pg | 4.22µs | 55.76µs | 1.25ms |
| pg_stable | 4.16µs | 22.45µs | 695.91µs |
| pg_stable_loop | 2.92µs | 29.26µs | — |
memory (live heap bytes of one built graph)
| backend | 100 | 1 000 | 10 000 |
|---|---|---|---|
| sg_vec_scoped | 14.0 KiB | 208.0 KiB | 1.75 MiB |
| sg_vec_stabilized | 19.0 KiB | 280.0 KiB | 2.38 MiB |
| sg_vec_checked | 14.0 KiB | 208.0 KiB | 1.75 MiB |
| sg_flat | 14.5 KiB | 137.7 KiB | 1.53 MiB |
| sg_btree | 59.5 KiB | 601.9 KiB | 5.88 MiB |
| pg | 14.0 KiB | 208.0 KiB | 1.75 MiB |
| pg_stable | 19.0 KiB | 280.0 KiB | 2.38 MiB |