Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Variable-Length Paths

Prefix a pattern node with .. and the edge becomes a variable-length path. Everything else about the pattern stays the same — a path drops in anywhere an edge can.

..n(1).dfs()                                              // deepest-first simple paths
..n(1).bfs().len(3..10)                                   // shortest-first, 3–9 edges
..n(1).navigate(Dijkstra::counted())                      // cheapest by hop count
..n(1).navigate(Dijkstra::weighted(|ev: &EV| ev.cost))    // cheapest by edge weight
..n(1).navigate(AStar::new(|ev| ev.cost, |n| h(n))).all() // every simple path, cost-ordered
..n(1).dfs().guard(|p| !p.contains(N(2)))                 // prune paths mid-walk
..n(1).drive(|n| Some(Explore::One(0)))                   // steer expansion per depth

Traversal: dfs / bfs / drive

dfs() yields deepest trails first, bfs() shortest first. Both are lazy — each next() computes exactly one complete path by backtracking.

drive(f) steers a DFS: at each expansion step, the closure receives the number of candidate continuations n and answers with an Explore decision — or None to prune the tip.

Length bounds: len

.len(bounds) constrains the number of edges in the path. Any usize range works, and a bare usize means an exact length:

.len(3)       // exactly 3 edges
.len(2..5)    // 2, 3 or 4
.len(2..=4)   // same, inclusive
.len(1..)     // at least one (the default)
.len(0..)     // include the trivial self-path when from == to

navigate(nav) orders results by cost instead of traversal order. The navigator supplies a non-negative per-edge cost; A* adds an admissible per-node heuristic that steers exploration without changing which path is cheapest.

let cfg = Config::new(())
    .navigate(Dijkstra::weighted(|ms: &u32| *ms as f64));

let (cost, path) = g
    .path_navigate(N(0), N(3), outgoing, cfg, &constraint)
    .next()
    .unwrap();

Two result modes:

ModeMeaning
.one() (default)The single cheapest path. Textbook Dijkstra/A* node dominance — O((V+E) log V).
.all()Every trail, in non-decreasing cost. No dominance; the frontier can grow — you decide when to stop pulling.

Guards

.guard(pred) rejects paths (including partial ones) during the walk. The predicate receives the path tip-first as a lazy, allocation-free view — decide from a short suffix and return early:

.dfs().guard(|p| !p.contains(id::N(2)))

Steering with Explore

The drive closure picks which continuation indices 0..n to expand:

DecisionMeaning
Explore::Allexpand every candidate
Explore::One(i)only the i-th
Explore::Len(l)the first min(l, n)
Explore::Steps(s)exactly the listed indices
Noneprune this tip

Inside a pattern, the path’s endpoints are pattern nodes and the morphism governs the intermediates: a Mono path can’t reuse nodes bound elsewhere in the match, a SubIso path additionally rejects branched intermediates. Bind both endpoints to the same node and paths become cycle detection:

let session = search![&g,
    get(Homo) { N(0), N(1) },            // endpoints may collapse
    get(Mono) { n(0) >> ..n(1).dfs().len(1..) }
].unwrap();

let cycles = session.iter()
    .filter(|m| m[0] == m[1] && !m.path(0).is_empty())
    .count();

Consuming path results

One Match fixes the endpoints — but several concrete paths may realize the same binding. They hang off the match, per path edge:

m.path(0)          // &[id::N] — full node sequence of the first path edge
m.path_count(0)    // how many alternatives were found
m.next_paths()     // step to the next combination (odometer, rightmost first)

next_paths_injective() additionally keeps intermediates across different path edges mutually distinct — that is how Mono/SubIso semantics extend to multi-path matches.

Standalone iterators are plain: g.path_search(..) yields Vec<id::N> per next(), g.path_navigate(..) yields (cost, Vec<id::N>) in non-decreasing cost order.