import (
"fmt"
+ "sort"
"strings"
)
-// Parse returns a map m such that m[p][d] == true when there is a
-// path from p to d.
-func Parse(dag string) (map[string]map[string]bool, error) {
- allowed := map[string]map[string]bool{"NONE": {}}
+type Graph struct {
+ Nodes []string
+ byLabel map[string]int
+ edges map[string]map[string]bool
+}
+
+func newGraph() *Graph {
+ return &Graph{byLabel: map[string]int{}, edges: map[string]map[string]bool{}}
+}
+
+func (g *Graph) addNode(label string) bool {
+ if _, ok := g.byLabel[label]; ok {
+ return false
+ }
+ g.byLabel[label] = len(g.Nodes)
+ g.Nodes = append(g.Nodes, label)
+ g.edges[label] = map[string]bool{}
+ return true
+}
+
+func (g *Graph) AddEdge(from, to string) {
+ g.edges[from][to] = true
+}
+
+func (g *Graph) HasEdge(from, to string) bool {
+ return g.edges[from] != nil && g.edges[from][to]
+}
+
+func (g *Graph) Edges(from string) []string {
+ edges := make([]string, 0, 16)
+ for k := range g.edges[from] {
+ edges = append(edges, k)
+ }
+ sort.Slice(edges, func(i, j int) bool { return g.byLabel[edges[i]] < g.byLabel[edges[j]] })
+ return edges
+}
+
+// Parse parses the DAG language and returns the transitive closure of
+// the described graph. In the returned graph, there is an edge from "b"
+// to "a" if b < a (or a > b) in the partial order.
+func Parse(dag string) (*Graph, error) {
+ g := newGraph()
disallowed := []rule{}
rules, err := parseRules(dag)
continue
}
for _, def := range r.def {
- if allowed[def] != nil {
+ if def == "NONE" {
+ errorf("NONE cannot be a predecessor")
+ continue
+ }
+ if !g.addNode(def) {
errorf("multiple definitions for %s", def)
}
- allowed[def] = make(map[string]bool)
for _, less := range r.less {
- if allowed[less] == nil {
+ if less == "NONE" {
+ continue
+ }
+ if _, ok := g.byLabel[less]; !ok {
errorf("use of %s before its definition", less)
+ } else {
+ g.AddEdge(def, less)
}
- allowed[def][less] = true
}
}
}
// Check for missing definition.
- for _, tos := range allowed {
+ for _, tos := range g.edges {
for to := range tos {
- if allowed[to] == nil {
+ if g.edges[to] == nil {
errorf("missing definition for %s", to)
}
}
}
// Complete transitive closure.
- for k := range allowed {
- for i := range allowed {
- for j := range allowed {
- if i != k && k != j && allowed[i][k] && allowed[k][j] {
+ for _, k := range g.Nodes {
+ for _, i := range g.Nodes {
+ for _, j := range g.Nodes {
+ if i != k && k != j && g.HasEdge(i, k) && g.HasEdge(k, j) {
if i == j {
// Can only happen along with a "use of X before deps" error above,
// but this error is more specific - it makes clear that reordering the
// rules will not be enough to fix the problem.
errorf("graph cycle: %s < %s < %s", j, k, i)
}
- allowed[i][j] = true
+ g.AddEdge(i, j)
}
}
}
for _, bad := range disallowed {
for _, less := range bad.less {
for _, def := range bad.def {
- if allowed[def][less] {
+ if g.HasEdge(def, less) {
errorf("graph edge assertion failed: %s !< %s", less, def)
}
}
return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
}
- return allowed, nil
+ return g, nil
}
// A rule is a line in the DAG language where "less < def" or "less !< def".
--- /dev/null
+// Copyright 2022 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 dag
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+)
+
+const diamond = `
+NONE < a < b, c < d;
+`
+
+func mustParse(t *testing.T, dag string) *Graph {
+ t.Helper()
+ g, err := Parse(dag)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return g
+}
+
+func wantEdges(t *testing.T, g *Graph, edges string) {
+ t.Helper()
+
+ wantEdges := strings.Fields(edges)
+ wantEdgeMap := make(map[string]bool)
+ for _, e := range wantEdges {
+ wantEdgeMap[e] = true
+ }
+
+ for _, n1 := range g.Nodes {
+ for _, n2 := range g.Nodes {
+ got := g.HasEdge(n1, n2)
+ want := wantEdgeMap[n1+"->"+n2]
+ if got && want {
+ t.Logf("%s->%s", n1, n2)
+ } else if got && !want {
+ t.Errorf("%s->%s present but not expected", n1, n2)
+ } else if want && !got {
+ t.Errorf("%s->%s missing but expected", n1, n2)
+ }
+ }
+ }
+}
+
+func TestParse(t *testing.T) {
+ // Basic smoke test for graph parsing.
+ g := mustParse(t, diamond)
+
+ wantNodes := strings.Fields("a b c d")
+ if !reflect.DeepEqual(wantNodes, g.Nodes) {
+ t.Fatalf("want nodes %v, got %v", wantNodes, g.Nodes)
+ }
+
+ // Parse returns the transitive closure, so it adds d->a.
+ wantEdges(t, g, "b->a c->a d->a d->b d->c")
+}