Skip to main content

linkrs_query/executor/streaming/operators/
algorithms.rs

1use std::cmp::Ordering;
2use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
3use std::sync::Arc;
4
5use crate::executor::expression::evaluator::traits::ExpressionContext;
6use crate::executor::expression::evaluator::ExpressionEvaluator;
7use crate::executor::streaming::context::ValueRowContext;
8use crate::executor::streaming::query_registry::CancelToken;
9use crate::executor::streaming::slot::SlotLayout;
10use crate::executor::traversal::graph_reader::{edge_key, EdgeKey};
11use crate::planning::plan::core::nodes::traversal::path_algorithms::PathUniqueness;
12use crate::storage::QueryStorage;
13use linkrs_core::error::QueryError;
14use linkrs_core::types::expr::Expression;
15use linkrs_core::types::storage_ids::VertexId;
16use linkrs_core::{EdgeDirection, NPath, Path, Value, Vertex};
17
18/// Materialize one path endpoint into a tagged vertex.
19///
20/// Tagged values pass through untouched. Bare ids need a label: the plan
21/// override wins, otherwise the role tag derived from the edge schemas.
22/// A bare id with no resolvable label is an explicit error rather than a
23/// silently skipped pair; a storage read failure surfaces instead of
24/// masquerading as a missing vertex.
25pub(crate) fn materialize_path_endpoint(
26    storage: &dyn QueryStorage,
27    space_name: &str,
28    value: &Value,
29    override_tag: &str,
30    role_tag: Option<&str>,
31) -> Result<Option<Vertex>, QueryError> {
32    if let Value::Vertex(vertex) = value {
33        return Ok(Some((**vertex).clone()));
34    }
35    let Ok(vid) = VertexId::try_from(value) else {
36        return Ok(None);
37    };
38    let tag = if !override_tag.is_empty() {
39        override_tag.to_string()
40    } else if let Some(tag) = role_tag {
41        tag.to_string()
42    } else {
43        return Err(QueryError::execution(
44            "Path endpoint is a bare id: OVER must list edge types whose schema resolves one label per path endpoint"
45                .to_string(),
46        ));
47    };
48    storage
49        .get_vertex(space_name, &tag, &vid)
50        .map_err(|error| QueryError::execution(format!("Failed to read path endpoint: {error}")))
51}
52
53/// Derive the (start, end) endpoint labels for a label-less path search.
54///
55/// A plan-provided label wins for both roles. Otherwise every listed edge
56/// type contributes its source label to the start role and its destination
57/// label to the end role, oriented by the traversal direction, and each
58/// role must resolve to exactly one label across all edge types.
59/// `Both` traversal needs that edge type's endpoints to share one label.
60/// An empty edge type list, unknown edge types, untyped schemas or an
61/// ambiguous role yield `None` rather than a guessed label; callers only
62/// need a label when an endpoint arrives as a bare id.
63pub(crate) fn path_endpoint_tags(
64    storage: &dyn QueryStorage,
65    space_name: &str,
66    edge_types: &[String],
67    direction: EdgeDirection,
68    override_tag: &str,
69) -> Option<(String, String)> {
70    if !override_tag.is_empty() {
71        return Some((override_tag.to_string(), override_tag.to_string()));
72    }
73    if edge_types.is_empty() {
74        return None;
75    }
76    let mut start_tags: Vec<String> = Vec::new();
77    let mut end_tags: Vec<String> = Vec::new();
78    for edge_type in edge_types {
79        let (src_tag, dst_tag) =
80            crate::executor::traversal::graph_reader::resolve_edge_endpoint_tags(
81                storage, space_name, edge_type,
82            )?;
83        let (start, end) = match direction {
84            EdgeDirection::Out => (src_tag, dst_tag),
85            EdgeDirection::In => (dst_tag, src_tag),
86            EdgeDirection::Both => {
87                if src_tag != dst_tag {
88                    return None;
89                }
90                (src_tag.clone(), dst_tag)
91            }
92        };
93        push_unique_label(&mut start_tags, start);
94        push_unique_label(&mut end_tags, end);
95    }
96    match (start_tags.as_slice(), end_tags.as_slice()) {
97        ([start], [end]) => Some((start.clone(), end.clone())),
98        _ => None,
99    }
100}
101
102fn push_unique_label(labels: &mut Vec<String>, label: String) {
103    if !label.is_empty() && !labels.iter().any(|kept| kept == &label) {
104        labels.push(label);
105    }
106}
107
108pub(crate) struct BidirBfsConfig<'a> {
109    pub(crate) space_name: &'a str,
110    pub(crate) vertex_tag: &'a str,
111    pub(crate) edge_type_filter: Option<&'a [String]>,
112    pub(crate) max_depth: usize,
113    pub(crate) single_shortest: bool,
114    pub(crate) limit: usize,
115    pub(crate) direction: EdgeDirection,
116}
117
118pub(crate) fn path_endpoint_pairs(
119    row: &[Value],
120    layout: Arc<SlotLayout>,
121    start_vertices: &[Value],
122    target_vertices: &[Value],
123    target_expression: Option<&Expression>,
124) -> Result<Vec<(Value, Value)>, QueryError> {
125    let context = ValueRowContext::new(row.to_vec(), layout.clone());
126    let row_start = context.get_variable("vid").or_else(|| row.first().cloned());
127    let row_target = if let Some(expression) = target_expression {
128        let mut expression_context = ValueRowContext::new(row.to_vec(), layout);
129        Some(
130            ExpressionEvaluator::evaluate(expression, &mut expression_context).map_err(
131                |error| QueryError::execution(format!("Path target evaluation failed: {error}")),
132            )?,
133        )
134    } else {
135        context
136            .get_variable("dst_vid")
137            .or_else(|| row.get(1).cloned())
138    };
139    let starts: Vec<Value> = if start_vertices.is_empty() {
140        row_start.into_iter().collect()
141    } else {
142        start_vertices.to_vec()
143    };
144    let targets: Vec<Value> = if target_vertices.is_empty() {
145        row_target.into_iter().collect()
146    } else {
147        target_vertices.to_vec()
148    };
149    Ok(starts
150        .into_iter()
151        .flat_map(|start| {
152            targets
153                .iter()
154                .cloned()
155                .map(move |target| (start.clone(), target))
156        })
157        .collect())
158}
159
160pub(crate) fn bidir_bfs_shortest_path(
161    storage: &dyn QueryStorage,
162    start_vertex: &Vertex,
163    end_vertex: &Vertex,
164    cfg: BidirBfsConfig,
165    cancel_token: Option<&CancelToken>,
166) -> Result<Vec<Path>, QueryError> {
167    let mut result_paths = Vec::new();
168    let mut best_len: Option<usize> = None;
169    let mut seen_paths: HashSet<String> = HashSet::new();
170    let mut left_depth: usize = 0;
171    let mut right_depth: usize = 0;
172
173    let mut left_visited: HashMap<VertexId, Arc<NPath>> = HashMap::new();
174    let mut right_visited: HashMap<VertexId, Arc<NPath>> = HashMap::new();
175    let mut left_queue: VecDeque<(VertexId, Arc<NPath>)> = VecDeque::new();
176    let mut right_queue: VecDeque<(VertexId, Arc<NPath>)> = VecDeque::new();
177
178    let start_id = start_vertex.vid;
179    let end_id = end_vertex.vid;
180    let np = Arc::new(NPath::new(Arc::new(start_vertex.clone())));
181    left_queue.push_back((start_id, np.clone()));
182    left_visited.insert(start_id, np);
183    let np = Arc::new(NPath::new(Arc::new(end_vertex.clone())));
184    right_queue.push_back((end_id, np.clone()));
185    right_visited.insert(end_id, np);
186
187    let dir_out = EdgeDirection::Out;
188    let dir_in = EdgeDirection::In;
189    let forward_dir = match cfg.direction {
190        EdgeDirection::Out => dir_out,
191        EdgeDirection::In => dir_in,
192        EdgeDirection::Both => dir_out,
193    };
194    let backward_dir = match cfg.direction {
195        EdgeDirection::Out => dir_in,
196        EdgeDirection::In => dir_out,
197        EdgeDirection::Both => dir_in,
198    };
199
200    while !left_queue.is_empty() && !right_queue.is_empty() {
201        if let Some(token) = cancel_token {
202            if token.is_cancelled() {
203                return Err(QueryError::execution("Query cancelled".to_string()));
204            }
205        }
206        if cfg.single_shortest && !result_paths.is_empty() {
207            break;
208        }
209        if result_paths.len() >= cfg.limit {
210            break;
211        }
212        // When collecting all shortest paths, stop as soon as both sides
213        // have been expanded past the best length found so far: any future
214        // meeting would be strictly longer than the best path.
215        if !cfg.single_shortest {
216            if let Some(best) = best_len {
217                if best <= left_depth && best <= right_depth {
218                    break;
219                }
220            }
221        }
222
223        let left_level = left_queue.len();
224        let mut left_next: Vec<(VertexId, Arc<NPath>)> = Vec::new();
225        for _ in 0..left_level {
226            if let Some((current_id, current_npath)) = left_queue.pop_front() {
227                if current_npath.len() >= cfg.max_depth {
228                    continue;
229                }
230                let edges = storage.get_node_edges(
231                    cfg.space_name,
232                    &current_id,
233                    forward_dir,
234                    cfg.edge_type_filter.unwrap_or(&[]),
235                )?;
236                for edge in &edges {
237                    let neighbor_id = edge.dst();
238                    if left_visited.contains_key(neighbor_id) {
239                        continue;
240                    }
241                    let Some(neighbor_tag) =
242                        crate::executor::traversal::graph_reader::resolve_neighbor_tag(
243                            storage,
244                            cfg.space_name,
245                            edge,
246                            neighbor_id,
247                            cfg.vertex_tag,
248                        )
249                    else {
250                        continue;
251                    };
252                    // An invisible neighbor is legitimately skipped, but a read
253                    // failure must abort: treating it as "no such vertex"
254                    // would silently drop paths from the result.
255                    if let Some(neighbor_vertex) =
256                        storage.get_vertex(cfg.space_name, &neighbor_tag, neighbor_id)?
257                    {
258                        let new_npath = Arc::new(NPath::extend(
259                            current_npath.clone(),
260                            Arc::new((*edge).clone()),
261                            Arc::new(neighbor_vertex),
262                        ));
263                        left_next.push((*neighbor_id, new_npath.clone()));
264                        left_visited.insert(*neighbor_id, new_npath);
265                    }
266                }
267            }
268        }
269        for (id, np) in left_next.drain(..) {
270            left_queue.push_back((id, np));
271        }
272        left_depth += 1;
273
274        if cfg.single_shortest && !result_paths.is_empty() {
275            break;
276        }
277        if result_paths.len() >= cfg.limit {
278            break;
279        }
280        if !cfg.single_shortest {
281            if let Some(best) = best_len {
282                if best <= left_depth && best <= right_depth {
283                    break;
284                }
285            }
286        }
287
288        let right_level = right_queue.len();
289        let mut right_next: Vec<(VertexId, Arc<NPath>)> = Vec::new();
290        for _ in 0..right_level {
291            if let Some((current_id, current_npath)) = right_queue.pop_front() {
292                if current_npath.len() >= cfg.max_depth {
293                    continue;
294                }
295
296                if let Some(left_npath) = left_visited.get(&current_id) {
297                    let total_len = left_npath.len() + current_npath.len();
298                    if total_len <= cfg.max_depth {
299                        let mut left_path = left_npath.to_path();
300                        let mut right_path = current_npath.to_path();
301                        right_path.reverse();
302                        left_path.steps.extend(right_path.steps);
303                        if push_shortest_path(
304                            left_path,
305                            &mut best_len,
306                            &mut seen_paths,
307                            &mut result_paths,
308                            cfg.limit,
309                        ) {
310                            break;
311                        }
312                    }
313                    continue;
314                }
315
316                let edges = storage.get_node_edges(
317                    cfg.space_name,
318                    &current_id,
319                    backward_dir,
320                    cfg.edge_type_filter.unwrap_or(&[]),
321                )?;
322                for edge in &edges {
323                    let neighbor_id = if edge.dst() == &current_id {
324                        edge.src()
325                    } else {
326                        edge.dst()
327                    };
328                    if right_visited.contains_key(neighbor_id) {
329                        continue;
330                    }
331                    let Some(neighbor_tag) =
332                        crate::executor::traversal::graph_reader::resolve_neighbor_tag(
333                            storage,
334                            cfg.space_name,
335                            edge,
336                            neighbor_id,
337                            cfg.vertex_tag,
338                        )
339                    else {
340                        continue;
341                    };
342                    if let Some(neighbor_vertex) =
343                        storage.get_vertex(cfg.space_name, &neighbor_tag, neighbor_id)?
344                    {
345                        // The right side walks backward from the end vertex,
346                        // so store the edge flipped: the resulting path then
347                        // keeps edge endpoints and step dsts consistent.
348                        let mut backward_edge = (*edge).clone();
349                        std::mem::swap(&mut backward_edge.src, &mut backward_edge.dst);
350                        let new_npath = Arc::new(NPath::extend(
351                            current_npath.clone(),
352                            Arc::new(backward_edge),
353                            Arc::new(neighbor_vertex),
354                        ));
355                        right_next.push((*neighbor_id, new_npath.clone()));
356                        right_visited.insert(*neighbor_id, new_npath);
357                    }
358                }
359            }
360        }
361        for (id, np) in right_next.drain(..) {
362            if let Some(left_npath) = left_visited.get(&id) {
363                let total_len = left_npath.len() + np.len();
364                if total_len <= cfg.max_depth {
365                    let mut left_path = left_npath.to_path();
366                    let mut right_path = np.to_path();
367                    right_path.reverse();
368                    left_path.steps.extend(right_path.steps);
369                    if push_shortest_path(
370                        left_path,
371                        &mut best_len,
372                        &mut seen_paths,
373                        &mut result_paths,
374                        cfg.limit,
375                    ) {
376                        break;
377                    }
378                }
379            } else {
380                right_queue.push_back((id, np));
381            }
382        }
383        right_depth += 1;
384    }
385
386    if cfg.single_shortest && !result_paths.is_empty() {
387        result_paths.sort_by_key(|a| a.steps.len());
388        result_paths.truncate(1);
389    }
390    result_paths.truncate(cfg.limit);
391    Ok(result_paths)
392}
393
394/// Add a candidate path to the result set, keeping only paths of the
395/// minimal length found so far. Returns `true` when the result limit is
396/// reached and the caller should stop searching.
397fn push_shortest_path(
398    path: Path,
399    best_len: &mut Option<usize>,
400    seen: &mut HashSet<String>,
401    result: &mut Vec<Path>,
402    limit: usize,
403) -> bool {
404    let total = path.steps.len();
405    match *best_len {
406        Some(best) if total > best => return false,
407        Some(best) if total < best => {
408            *best_len = Some(total);
409            result.clear();
410        }
411        None => *best_len = Some(total),
412        Some(_) => {}
413    }
414    if seen.insert(format!("{:?}", path)) {
415        result.push(path);
416    }
417    result.len() >= limit
418}
419
420pub(crate) struct AllPathsConfig<'a> {
421    pub(crate) space_name: &'a str,
422    pub(crate) vertex_tag: &'a str,
423    pub(crate) edge_types: &'a [String],
424    pub(crate) direction: EdgeDirection,
425    pub(crate) min_depth: usize,
426    pub(crate) max_depth: usize,
427    pub(crate) path_uniqueness: PathUniqueness,
428    pub(crate) result_cap: usize,
429}
430
431/// Heap entry for the weighted (Dijkstra) shortest path. Ordered by ascending
432/// cost, then ascending depth; `Ord` inverts both so the `BinaryHeap`
433/// (a max-heap) pops the cheapest, then fewest-hop entry first.
434struct WeightedPathItem {
435    cost: f64,
436    depth: usize,
437    vertex_id: VertexId,
438    npath: Arc<NPath>,
439}
440
441impl PartialEq for WeightedPathItem {
442    fn eq(&self, other: &Self) -> bool {
443        self.cost == other.cost && self.depth == other.depth
444    }
445}
446impl Eq for WeightedPathItem {}
447impl PartialOrd for WeightedPathItem {
448    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
449        Some(self.cmp(other))
450    }
451}
452impl Ord for WeightedPathItem {
453    fn cmp(&self, other: &Self) -> Ordering {
454        other
455            .cost
456            .partial_cmp(&self.cost)
457            .unwrap_or(Ordering::Equal)
458            .then_with(|| other.depth.cmp(&self.depth))
459    }
460}
461
462/// Weighted shortest path (Dijkstra) between two vertices.
463///
464/// Runs from `start_vertex`, finalizing the cheapest path to
465/// `end_vertex` within `cfg.max_depth` hops. The edge weight is read by
466/// `read_edge_weight` (shared with the MATCH `*WEIGHTED` runtime). Ties on
467/// cost resolve to the fewer-hop path; ties on both resolve to whichever
468/// the heap finalizes first, keeping the result deterministic.
469pub(crate) fn weighted_shortest_path(
470    storage: &dyn QueryStorage,
471    start_vertex: &Vertex,
472    end_vertex: &Vertex,
473    space_name: &str,
474    vertex_tag: &str,
475    edge_type_filter: Option<&[String]>,
476    direction: EdgeDirection,
477    max_depth: usize,
478    weight_prop: &str,
479    cancel_token: Option<&CancelToken>,
480) -> Result<Option<Path>, QueryError> {
481    if let Some(token) = cancel_token {
482        if token.is_cancelled() {
483            return Err(QueryError::execution("Query cancelled".to_string()));
484        }
485    }
486
487    let start_id = start_vertex.vid;
488    let end_id = end_vertex.vid;
489
490    let mut finalized: HashSet<VertexId> = HashSet::new();
491    let mut dist: HashMap<VertexId, f64> = HashMap::new();
492    let mut heap: BinaryHeap<WeightedPathItem> = BinaryHeap::new();
493    let np = Arc::new(NPath::new(Arc::new(start_vertex.clone())));
494    dist.insert(start_id, 0.0);
495    heap.push(WeightedPathItem {
496        cost: 0.0,
497        depth: 0,
498        vertex_id: start_id,
499        npath: np,
500    });
501
502    while let Some(top) = heap.pop() {
503        if let Some(token) = cancel_token {
504            if token.is_cancelled() {
505                return Err(QueryError::execution("Query cancelled".to_string()));
506            }
507        }
508        // Skip stale heap entries superseded by a cheaper finalized cost.
509        if finalized.contains(&top.vertex_id) {
510            continue;
511        }
512        finalized.insert(top.vertex_id);
513
514        if top.vertex_id == end_id {
515            return Ok(Some(top.npath.to_path()));
516        }
517        if top.depth >= max_depth {
518            continue;
519        }
520
521        let edges = storage.get_node_edges(
522            space_name,
523            &top.vertex_id,
524            direction,
525            edge_type_filter.unwrap_or(&[]),
526        )?;
527        for edge in &edges {
528            let neighbor_id = if direction == EdgeDirection::In {
529                edge.src()
530            } else if direction == EdgeDirection::Out {
531                edge.dst()
532            } else if edge.dst() == &top.vertex_id {
533                edge.src()
534            } else {
535                edge.dst()
536            };
537            if finalized.contains(neighbor_id) {
538                continue;
539            }
540            let next_cost = top.cost
541                + crate::executor::traversal::weighted::read_edge_weight(edge, weight_prop);
542            let better = match dist.get(neighbor_id) {
543                Some(old) => next_cost < *old,
544                None => true,
545            };
546            if !better {
547                continue;
548            }
549            let Some(neighbor_tag) = crate::executor::traversal::graph_reader::resolve_neighbor_tag(
550                storage,
551                space_name,
552                edge,
553                neighbor_id,
554                vertex_tag,
555            ) else {
556                continue;
557            };
558            if let Some(neighbor_vertex) =
559                storage.get_vertex(space_name, &neighbor_tag, neighbor_id)?
560            {
561                dist.insert(*neighbor_id, next_cost);
562                heap.push(WeightedPathItem {
563                    cost: next_cost,
564                    depth: top.depth + 1,
565                    vertex_id: *neighbor_id,
566                    npath: Arc::new(NPath::extend(
567                        top.npath.clone(),
568                        Arc::new((*edge).clone()),
569                        Arc::new(neighbor_vertex),
570                    )),
571                });
572            }
573        }
574    }
575    Ok(None)
576}
577
578pub(crate) fn enumerate_all_paths(
579    storage: &dyn QueryStorage,
580    start_vertex: &Vertex,
581    end_vertex: &Vertex,
582    cfg: AllPathsConfig<'_>,
583    cancel_token: Option<&CancelToken>,
584) -> Result<Vec<Path>, QueryError> {
585    let start_id = start_vertex.vid;
586    let end_id = end_vertex.vid;
587    let mut stack = vec![WalkState {
588        current_id: start_id,
589        path: Arc::new(NPath::new(Arc::new(start_vertex.clone()))),
590        visited_vertices: HashSet::from([start_id]),
591        visited_edges: HashSet::new(),
592    }];
593    let mut paths = Vec::new();
594
595    while let Some(state) = stack.pop() {
596        if let Some(token) = cancel_token {
597            if token.is_cancelled() {
598                return Err(QueryError::execution("Query cancelled".to_string()));
599            }
600        }
601        let depth = state.path.len();
602        if state.current_id == end_id && depth >= cfg.min_depth {
603            paths.push(state.path.to_path());
604            if paths.len() >= cfg.result_cap {
605                break;
606            }
607        }
608        if depth >= cfg.max_depth {
609            continue;
610        }
611        let edges = storage
612            .get_node_edges(
613                cfg.space_name,
614                &state.current_id,
615                cfg.direction,
616                cfg.edge_types,
617            )
618            .map_err(|error| {
619                QueryError::execution(format!("Failed to read path edges: {error}"))
620            })?;
621        for edge in edges {
622            let next_id = match cfg.direction {
623                EdgeDirection::Out => *edge.dst(),
624                EdgeDirection::In => *edge.src(),
625                EdgeDirection::Both => {
626                    if edge.src() == &state.current_id {
627                        *edge.dst()
628                    } else {
629                        *edge.src()
630                    }
631                }
632            };
633            // A self-loop lands back on the current vertex; a normal edge
634            // may revisit an earlier vertex. The repeat rules are governed
635            // by the WITH LOOP / WITH TRAIL / WITH WALK policy.
636            let is_self_loop = edge.src() == edge.dst();
637            let repeats_vertex = state.visited_vertices.contains(&next_id);
638            let key = edge_key(&edge);
639            match cfg.path_uniqueness {
640                PathUniqueness::VertexUnique => {
641                    if repeats_vertex {
642                        continue;
643                    }
644                }
645                PathUniqueness::AllowSelfLoop => {
646                    if repeats_vertex && !is_self_loop {
647                        continue;
648                    }
649                }
650                PathUniqueness::EdgeUnique => {
651                    if state.visited_edges.contains(&key) {
652                        continue;
653                    }
654                }
655                PathUniqueness::Free => {}
656            }
657            // A self-loop adds no new reachable vertex, so under policies
658            // with no edge-level constraint it is emitted as a terminal
659            // extension instead of being pushed back on the stack (which
660            // would loop on the same edge forever). Under EdgeUnique the
661            // burned edge bounds the repetition, so the hop continues
662            // like any other.
663            let terminal_extension = is_self_loop
664                && matches!(
665                    cfg.path_uniqueness,
666                    PathUniqueness::AllowSelfLoop | PathUniqueness::Free
667                );
668            // A path ends at the target: normal edges leaving the target are
669            // not followed. A self-loop on the target is still taken as a
670            // terminal extension, otherwise WITH LOOP could never surface a
671            // path that uses one.
672            if state.current_id == end_id && !terminal_extension {
673                continue;
674            }
675            let Some(neighbor_tag) = crate::executor::traversal::graph_reader::resolve_neighbor_tag(
676                storage,
677                cfg.space_name,
678                &edge,
679                &next_id,
680                cfg.vertex_tag,
681            ) else {
682                continue;
683            };
684            let Some(vertex) = storage
685                .get_vertex(cfg.space_name, &neighbor_tag, &next_id)
686                .map_err(|error| {
687                    QueryError::execution(format!("Failed to read path vertex: {error}"))
688                })?
689            else {
690                continue;
691            };
692            let next_path = Arc::new(NPath::extend(
693                state.path.clone(),
694                Arc::new(edge),
695                Arc::new(vertex),
696            ));
697            if terminal_extension {
698                if next_id == end_id && depth + 1 >= cfg.min_depth {
699                    paths.push(next_path.to_path());
700                    if paths.len() >= cfg.result_cap {
701                        break;
702                    }
703                }
704                continue;
705            }
706            let mut next_vertices = state.visited_vertices.clone();
707            next_vertices.insert(next_id);
708            let mut next_edges = state.visited_edges.clone();
709            next_edges.insert(key);
710            stack.push(WalkState {
711                current_id: next_id,
712                path: next_path,
713                visited_vertices: next_vertices,
714                visited_edges: next_edges,
715            });
716        }
717    }
718    Ok(paths)
719}
720
721/// Enumeration state of one candidate path: the current vertex plus the
722/// bookkeeping the repeat policy prunes with. `visited_vertices` backs the
723/// vertex-unique policies, `visited_edges` backs the edge-unique one.
724struct WalkState {
725    current_id: VertexId,
726    path: Arc<NPath>,
727    visited_vertices: HashSet<VertexId>,
728    visited_edges: HashSet<EdgeKey>,
729}