Skip to content

Commit 3a8978c

Browse files
adonovangopherbot
authored andcommitted
cmd/digraph: fix bug in allpaths
The previous algorithm forgot to follow all paths through the target node. Use a simpler algorithm. Thanks to Gemini for one of the test cases. Fixes golang/go#74842 Change-Id: I7bb9290c3e78c7094e4a840b894b0906c25cb42c Reviewed-on: https://go-review.googlesource.com/c/tools/+/692675 Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Russ Cox <[email protected]> Auto-Submit: Alan Donovan <[email protected]>
1 parent bae51bd commit 3a8978c

File tree

3 files changed

+43
-23
lines changed

3 files changed

+43
-23
lines changed

cmd/digraph/digraph.go

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ func (l nodelist) println(sep string) {
6767

6868
type nodeset map[string]bool
6969

70+
func singleton(x string) nodeset { return nodeset{x: true} }
71+
7072
func (s nodeset) sort() nodelist {
7173
nodes := make(nodelist, len(s))
7274
var i int
@@ -191,26 +193,17 @@ func (g graph) sccs() []nodeset {
191193
}
192194

193195
func (g graph) allpaths(from, to string) error {
194-
// Mark all nodes to "to".
195-
seen := make(nodeset) // value of seen[x] indicates whether x is on some path to "to"
196-
var visit func(node string) bool
197-
visit = func(node string) bool {
198-
reachesTo, ok := seen[node]
199-
if !ok {
200-
reachesTo = node == to
201-
seen[node] = reachesTo
202-
for e := range g[node] {
203-
if visit(e) {
204-
reachesTo = true
205-
}
206-
}
207-
if reachesTo && node != to {
208-
seen[node] = true
209-
}
196+
// We intersect the forward closure of 'from' with
197+
// the reverse closure of 'to'. This is not the most
198+
// efficient implementation, but it's the clearest,
199+
// and the previous one had bugs.
200+
seen := g.reachableFrom(singleton(from))
201+
rev := g.transpose().reachableFrom(singleton(to))
202+
for n := range seen {
203+
if !rev[n] {
204+
delete(seen, n)
210205
}
211-
return reachesTo
212206
}
213-
visit(from)
214207

215208
// For each marked node, collect its marked successors.
216209
var edges []string
@@ -241,7 +234,7 @@ func (g graph) somepath(from, to string) error {
241234
tail *path
242235
}
243236

244-
seen := nodeset{from: true}
237+
seen := singleton(from)
245238

246239
var queue []*path
247240
queue = append(queue, &path{node: from, tail: nil})
@@ -469,14 +462,14 @@ func digraph(cmd string, args []string) error {
469462
}
470463

471464
edges := make(map[string]struct{})
472-
for from := range g.reachableFrom(nodeset{node: true}) {
465+
for from := range g.reachableFrom(singleton(node)) {
473466
for to := range g[from] {
474467
edges[fmt.Sprintf("%s %s", from, to)] = struct{}{}
475468
}
476469
}
477470

478471
gtrans := g.transpose()
479-
for from := range gtrans.reachableFrom(nodeset{node: true}) {
472+
for from := range gtrans.reachableFrom(singleton(node)) {
480473
for to := range gtrans[from] {
481474
edges[fmt.Sprintf("%s %s", to, from)] = struct{}{}
482475
}

cmd/digraph/digraph_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,33 @@ func TestAllpaths(t *testing.T) {
156156
to: "H",
157157
want: "A B\nA C\nB D\nC D\nD E\nE F\nE G\nF H\nG H\n",
158158
},
159+
{
160+
// C <--> B --> A --> D <--> E
161+
// ⋃
162+
name: "non-regression test for #74842",
163+
in: "A D\nB A\nB B\nB C\nC B\nD E\nE D",
164+
to: "D",
165+
want: "A D\nD E\nE D\n",
166+
},
167+
{
168+
// A --> B --> D
169+
// ^
170+
// v
171+
// C[123]
172+
name: "regression test for #74842",
173+
in: "A B\nB C1\nB C2\nB C3\nB D\nC1 B\nC2 B\nC3 B\n",
174+
to: "D",
175+
want: "A B\nB C1\nB C2\nB C3\nB D\nC1 B\nC2 B\nC3 B\n",
176+
},
177+
{
178+
// A -------> B --> D
179+
// \--> C ---^ |
180+
// ^----------+
181+
name: "another regression test for #74842",
182+
in: "A B\nA C\nB D\nC B\nD C\n",
183+
to: "D",
184+
want: "A B\nA C\nB D\nC B\nD C\n",
185+
},
159186
} {
160187
t.Run(test.name, func(t *testing.T) {
161188
stdin = strings.NewReader(test.in)

cmd/digraph/doc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ The supported commands are:
2828
reverse <node> ...
2929
the set of nodes that transitively reach the specified nodes
3030
somepath <node> <node>
31-
the list of nodes on some arbitrary path from the first node to the second
31+
the list of edges on some arbitrary path from the first node to the second
3232
allpaths <node> <node>
33-
the set of nodes on all paths from the first node to the second
33+
the set of edges on all paths from the first node to the second
3434
sccs
3535
all strongly connected components (one per line)
3636
scc <node>

0 commit comments

Comments
 (0)