diff --git a/cmd/digraph/digraph.go b/cmd/digraph/digraph.go
index 0e9caeb..b11ab77 100644
--- a/cmd/digraph/digraph.go
+++ b/cmd/digraph/digraph.go
@@ -152,17 +152,7 @@
}
func (g digraph) allpaths(from, to string) error {
- // We intersect the forward closure of 'from' with
- // the reverse closure of 'to'. This is not the most
- // efficient implementation, but it's the clearest,
- // and the previous one had bugs.
- seen := nodeset(graph.Reachable(g, from))
- rev := nodeset(graph.Reachable(g.transpose(), to))
- for n := range seen {
- if !rev[n] {
- delete(seen, n)
- }
- }
+ seen := graph.AllPaths(g, from, to)
// For each marked node, collect its marked successors.
var edges []string
diff --git a/internal/graph/allpaths.go b/internal/graph/allpaths.go
new file mode 100644
index 0000000..4ad97fc
--- /dev/null
+++ b/internal/graph/allpaths.go
@@ -0,0 +1,24 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package graph
+
+// AllPaths returns the set of nodes that are part of at least one path from src to dst.
+func AllPaths[NodeID comparable](g Graph[NodeID], src, dst NodeID) map[NodeID]bool {
+ // We intersect the forward closure of 'src' with
+ // the reverse closure of 'dst'. This is not the most
+ // efficient implementation, but it's the clearest,
+ // and the previous one had bugs.
+
+ fwd := Reachable(g, src)
+ rev := Reachable(NewTranspose(g), dst)
+
+ // Intersection
+ for n := range fwd {
+ if !rev[n] {
+ delete(fwd, n)
+ }
+ }
+ return fwd
+}
diff --git a/internal/graph/allpaths_test.go b/internal/graph/allpaths_test.go
new file mode 100644
index 0000000..1f814ad
--- /dev/null
+++ b/internal/graph/allpaths_test.go
@@ -0,0 +1,42 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package graph
+
+import (
+ "maps"
+ "slices"
+ "testing"
+)
+
+func TestAllPaths(t *testing.T) {
+ g := stringGraph{
+ "A": {"B", "C"},
+ "B": {"D"},
+ "C": {"D", "E"},
+ "D": {"F"},
+ "E": {},
+ "F": {},
+ }
+
+ // Paths in g:
+ //
+ // A->B->D->F
+ // A->C->D->F
+ // A->C->E
+
+ // AllPaths(A, F) should include A, B, C, D, F. E is not on path to F.
+ got := slices.Sorted(maps.Keys(AllPaths(g, "A", "F")))
+ want := []string{"A", "B", "C", "D", "F"}
+ if !slices.Equal(got, want) {
+ t.Errorf("AllPaths(A, F) = %v, want %v", got, want)
+ }
+
+ // AllPaths(A, E) -> A, C, E. B is not on path.
+ got = slices.Sorted(maps.Keys(AllPaths(g, "A", "E")))
+ want = []string{"A", "C", "E"}
+ if !slices.Equal(got, want) {
+ t.Errorf("AllPaths(A, E) = %v, want %v", got, want)
+ }
+}