diff --git a/lang/interpret/interpret.go b/lang/interpret/interpret.go index 4b457e44..617557b4 100644 --- a/lang/interpret/interpret.go +++ b/lang/interpret/interpret.go @@ -291,7 +291,14 @@ func (obj *Interpreter) Interpret(ast interfaces.Stmt, table map[interfaces.Func // ensure that we have a DAG! if _, err := graph.TopologicalSort(); err != nil { - // TODO: print information on the cycles + errNotAcyclic, ok := err.(*pgraph.ErrNotAcyclic) + if !ok { + return nil, err // programming error + } + obj.Logf("%s", err) + for _, vertex := range errNotAcyclic.Cycle { + obj.Logf("* %s", vertex) + } return nil, errwrap.Wrapf(err, "resource graph has cycles") } diff --git a/pgraph/pgraph.go b/pgraph/pgraph.go index 2285ab97..07c1585e 100644 --- a/pgraph/pgraph.go +++ b/pgraph/pgraph.go @@ -31,7 +31,6 @@ package pgraph import ( - "errors" "fmt" "sort" "strings" @@ -40,7 +39,15 @@ import ( ) // ErrNotAcyclic specifies that a particular graph was not found to be a dag. -var ErrNotAcyclic = errors.New("not a dag") +type ErrNotAcyclic struct { + Cycle []Vertex +} + +// Error lets this satisfy the error interface. +func (obj *ErrNotAcyclic) Error() string { + //return fmt.Sprintf("not a dag: %v", obj.Cycle) + return "not a dag" +} // Graph is the graph structure in this library. The graph abstract data type // (ADT) is defined as follows: @@ -667,7 +674,12 @@ func (g *Graph) TopologicalSort() ([]Vertex, error) { // kahn's algorithm if in > 0 { for n := range g.adjacency[c] { if remaining[n] > 0 { - return nil, ErrNotAcyclic + cycle := g.findCycleDFS(c) + if len(cycle) == 0 { + // Hopefully this doesn't happen! + return nil, fmt.Errorf("programming error") + } + return nil, &ErrNotAcyclic{Cycle: cycle} } } } @@ -676,6 +688,61 @@ func (g *Graph) TopologicalSort() ([]Vertex, error) { // kahn's algorithm return L, nil } +// findCycleDFS is a helper for the TopologicalSort functions. +// XXX: A professional should look over this function and try and find issues. +func (g *Graph) findCycleDFS(start Vertex) []Vertex { + visited := make(map[Vertex]bool) + stack := make(map[Vertex]bool) + var path []Vertex + var result []Vertex + found := false + + var dfs func(Vertex) bool + dfs = func(v Vertex) bool { + if found { + return true + } + visited[v] = true + stack[v] = true + path = append(path, v) + + for n := range g.adjacency[v] { + if !visited[n] { + if dfs(n) { + return true + } + } else if stack[n] { + // cycle detected + idx := len(path) - 1 + for idx >= 0 && path[idx] != n { + idx-- + } + if idx >= 0 { + result = append([]Vertex{}, path[idx:]...) + result = append(result, n) // close the cycle + found = true + return true + } + } + } + + stack[v] = false + path = path[:len(path)-1] + return false + } + + // run DFS from all potentially cyclic nodes + for v := range g.adjacency { + if !visited[v] { + if dfs(v) { + break + } + } + } + + return result +} + // DeterministicTopologicalSort returns the sort of graph vertices in a stable // topological sort order. It's slower than the TopologicalSort implementation, // but guarantees that two identical graphs produce the same sort each time. @@ -731,7 +798,12 @@ func (g *Graph) DeterministicTopologicalSort() ([]Vertex, error) { // kahn's alg if in > 0 { for n := range g.adjacency[c] { if remaining[n] > 0 { - return nil, ErrNotAcyclic + cycle := g.findCycleDFS(c) + if len(cycle) == 0 { + // Hopefully this doesn't happen! + return nil, fmt.Errorf("programming error") + } + return nil, &ErrNotAcyclic{Cycle: cycle} } } } diff --git a/pgraph/pgraph_test.go b/pgraph/pgraph_test.go index 291a1243..349ae988 100644 --- a/pgraph/pgraph_test.go +++ b/pgraph/pgraph_test.go @@ -464,6 +464,56 @@ func TestTopoSort2(t *testing.T) { } } +func TestTopoSort3(t *testing.T) { + G, _ := NewGraph("g11") + v1 := NV("v1") + v2 := NV("v2") + v3 := NV("v3") + v4 := NV("v4") + v5 := NV("v5") + v6 := NV("v6") + e1 := NE("e1") + e2 := NE("e2") + e3 := NE("e3") + e4 := NE("e4") + e5 := NE("e5") + e6 := NE("e6") + G.AddEdge(v1, v2, e1) + G.AddEdge(v2, v3, e2) + G.AddEdge(v3, v4, e3) + G.AddEdge(v4, v5, e4) + G.AddEdge(v5, v6, e5) + G.AddEdge(v4, v2, e6) // cycle + + G.ExecGraphviz("/tmp/g.dot") + + _, err := G.TopologicalSort() + if err == nil { + t.Errorf("topological sort passed, but graph is cyclic") + return + } + errNotAcyclic, ok := err.(*ErrNotAcyclic) + if !ok { + t.Errorf("wrong kind of error, got: %v", err) + return + } + cycle := errNotAcyclic.Cycle + + t.Logf("cycle: %v", cycle) + if len(cycle) < 2 { + t.Errorf("cycle is too short") + } + cycle1 := []Vertex{v2, v3, v4, v2} + cycle2 := []Vertex{v3, v4, v2, v3} + cycle3 := []Vertex{v4, v2, v3, v4} + b1 := reflect.DeepEqual(cycle, cycle1) + b2 := reflect.DeepEqual(cycle, cycle2) + b3 := reflect.DeepEqual(cycle, cycle3) + if !b1 && !b2 && !b3 { + t.Errorf("cycle didn't match") + } +} + // empty func TestReachability0(t *testing.T) { {