You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On Graph.undirected graphs, all four shortest-path algorithms — Graph.dijkstra, Graph.astar, Graph.bellmanFord, and Graph.floydWarshall — only traverse edges in their stored direction. A path that requires crossing an edge opposite to the direction it was passed to Graph.addEdge is not found. By contrast, Graph.connectedComponents and Graph.neighbors handle undirected edges correctly, so the modules disagree about reachability on the same graph.
Reproduction
Reproduced on effect@3.21.2 and effect@3.21.3 (latest at time of filing); the relevant code is unchanged on main.
Reversing the second addEdge to Graph.addEdge(m, b, c, 1) makes all four algorithms succeed, confirming the result depends on edge storage direction, which should be irrelevant for an undirected graph.
Root cause
(line references against packages/effect/src/Graph.ts on current main)
addEdge does register an undirected edge in both endpoints' adjacency lists (the mutable.type === "undirected" branch, ~L1176). But:
dijkstra (~L2406) and astar (~L2731) compute the neighbor as const neighbor = edge.target unconditionally. When the algorithm expands a node that is the stored target of an undirected edge, the computed "neighbor" is the node itself, so the edge is never crossed in the reverse direction. (In the repro, expanding b over the stored edge c->b relaxes b against itself instead of reaching c.)
bellmanFord (~L2880) relaxes only edge.source -> edge.target regardless of graph kind.
floydWarshall (~L2525) initializes the distance matrix only at dist[edge.source][edge.target].
Meanwhile connectedComponents and neighbors go through the getUndirectedNeighbors helper (~L2055), which correctly returns the other endpoint of each incident edge — hence the inconsistency.
Suggested fix
In dijkstra/astar: when graph.type === "undirected", take the neighbor as the other endpoint (edge.source === currentNode ? edge.target : edge.source), as getUndirectedNeighbors does. (Self-loops need the same care getUndirectedNeighbors already takes after Graph.neighbors() returns self-loops in undirected graphs #5667.)
In bellmanFord: also relax edge.target -> edge.source for undirected graphs. (Note any negative-weight undirected edge then constitutes a negative cycle, which matches the algorithm's semantics.)
In floydWarshall: also initialize dist[edge.target][edge.source] (and the corresponding next/edgeMatrix entries) for undirected graphs.
Summary
On
Graph.undirectedgraphs, all four shortest-path algorithms —Graph.dijkstra,Graph.astar,Graph.bellmanFord, andGraph.floydWarshall— only traverse edges in their stored direction. A path that requires crossing an edge opposite to the direction it was passed toGraph.addEdgeis not found. By contrast,Graph.connectedComponentsandGraph.neighborshandle undirected edges correctly, so the modules disagree about reachability on the same graph.Reproduction
Reproduced on
effect@3.21.2andeffect@3.21.3(latest at time of filing); the relevant code is unchanged onmain.Reversing the second
addEdgetoGraph.addEdge(m, b, c, 1)makes all four algorithms succeed, confirming the result depends on edge storage direction, which should be irrelevant for an undirected graph.Root cause
(line references against
packages/effect/src/Graph.tson currentmain)addEdgedoes register an undirected edge in both endpoints' adjacency lists (themutable.type === "undirected"branch, ~L1176). But:dijkstra(~L2406) andastar(~L2731) compute the neighbor asconst neighbor = edge.targetunconditionally. When the algorithm expands a node that is the stored target of an undirected edge, the computed "neighbor" is the node itself, so the edge is never crossed in the reverse direction. (In the repro, expandingbover the stored edgec->brelaxesbagainst itself instead of reachingc.)bellmanFord(~L2880) relaxes onlyedge.source -> edge.targetregardless of graph kind.floydWarshall(~L2525) initializes the distance matrix only atdist[edge.source][edge.target].Meanwhile
connectedComponentsandneighborsgo through thegetUndirectedNeighborshelper (~L2055), which correctly returns the other endpoint of each incident edge — hence the inconsistency.Suggested fix
dijkstra/astar: whengraph.type === "undirected", take the neighbor as the other endpoint (edge.source === currentNode ? edge.target : edge.source), asgetUndirectedNeighborsdoes. (Self-loops need the same caregetUndirectedNeighborsalready takes afterGraph.neighbors()returns self-loops in undirected graphs #5667.)bellmanFord: also relaxedge.target -> edge.sourcefor undirected graphs. (Note any negative-weight undirected edge then constitutes a negative cycle, which matches the algorithm's semantics.)floydWarshall: also initializedist[edge.target][edge.source](and the correspondingnext/edgeMatrixentries) for undirected graphs.Happy to provide more details if useful.