| // 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 implements a language for expressing directed acyclic | |
| // graphs. | |
| // | |
| // The general syntax of a rule is: | |
| // | |
| // a, b < c, d; | |
| // | |
| // which means c and d come after a and b in the partial order | |
| // (that is, there are edges from c and d to a and b), | |
| // but doesn't provide a relative order between a vs b or c vs d. | |
| // | |
| // The rules can chain together, as in: | |
| // | |
| // e < f, g < h; | |
| // | |
| // which is equivalent to | |
| // | |
| // e < f, g; | |
| // f, g < h; | |
| // | |
| // Except for the special bottom element "NONE", each name | |
| // must appear exactly once on the right-hand side of any rule. | |
| // That rule serves as the definition of the allowed successor | |
| // for that name. The definition must appear before any uses | |
| // of the name on the left-hand side of a rule. (That is, the | |
| // rules themselves must be ordered according to the partial | |
| // order, for easier reading by people.) | |
| // | |
| // Negative assertions double-check the partial order: | |
| // | |
| // i !< j | |
| // | |
| // means that it must NOT be the case that i < j. | |
| // Negative assertions may appear anywhere in the rules, | |
| // even before i and j have been defined. | |
| // | |
| // Comments begin with #. | |
| package dag | |
| import ( | |
| "cmp" | |
| "fmt" | |
| "slices" | |
| "strings" | |
| ) | |
| 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) DelEdge(from, to string) { | |
| delete(g.edges[from], to) | |
| } | |
| 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) | |
| } | |
| slices.SortFunc(edges, func(a, b string) int { | |
| return cmp.Compare(g.byLabel[a], g.byLabel[b]) | |
| }) | |
| 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) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // TODO: Add line numbers to errors. | |
| var errors []string | |
| errorf := func(format string, a ...any) { | |
| errors = append(errors, fmt.Sprintf(format, a...)) | |
| } | |
| for _, r := range rules { | |
| if r.op == "!<" { | |
| disallowed = append(disallowed, r) | |
| continue | |
| } | |
| for _, def := range r.def { | |
| if def == "NONE" { | |
| errorf("NONE cannot be a predecessor") | |
| continue | |
| } | |
| if !g.addNode(def) { | |
| errorf("multiple definitions for %s", def) | |
| } | |
| for _, less := range r.less { | |
| if less == "NONE" { | |
| continue | |
| } | |
| if _, ok := g.byLabel[less]; !ok { | |
| errorf("use of %s before its definition", less) | |
| } else { | |
| g.AddEdge(def, less) | |
| } | |
| } | |
| } | |
| } | |
| // Check for missing definition. | |
| for _, tos := range g.edges { | |
| for to := range tos { | |
| if g.edges[to] == nil { | |
| errorf("missing definition for %s", to) | |
| } | |
| } | |
| } | |
| // Complete transitive closure. | |
| 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) | |
| } | |
| g.AddEdge(i, j) | |
| } | |
| } | |
| } | |
| } | |
| // Check negative assertions against completed allowed graph. | |
| for _, bad := range disallowed { | |
| for _, less := range bad.less { | |
| for _, def := range bad.def { | |
| if g.HasEdge(def, less) { | |
| errorf("graph edge assertion failed: %s !< %s", less, def) | |
| } | |
| } | |
| } | |
| } | |
| if len(errors) > 0 { | |
| return nil, fmt.Errorf("%s", strings.Join(errors, "\n")) | |
| } | |
| return g, nil | |
| } | |
| // A rule is a line in the DAG language where "less < def" or "less !< def". | |
| type rule struct { | |
| less []string | |
| op string // Either "<" or "!<" | |
| def []string | |
| } | |
| type syntaxError string | |
| func (e syntaxError) Error() string { | |
| return string(e) | |
| } | |
| // parseRules parses the rules of a DAG. | |
| func parseRules(rules string) (out []rule, err error) { | |
| defer func() { | |
| e := recover() | |
| switch e := e.(type) { | |
| case nil: | |
| return | |
| case syntaxError: | |
| err = e | |
| default: | |
| panic(e) | |
| } | |
| }() | |
| p := &rulesParser{lineno: 1, text: rules} | |
| var prev []string | |
| var op string | |
| for { | |
| list, tok := p.nextList() | |
| if tok == "" { | |
| if prev == nil { | |
| break | |
| } | |
| p.syntaxError("unexpected EOF") | |
| } | |
| if prev != nil { | |
| out = append(out, rule{prev, op, list}) | |
| } | |
| prev = list | |
| if tok == ";" { | |
| prev = nil | |
| op = "" | |
| continue | |
| } | |
| if tok != "<" && tok != "!<" { | |
| p.syntaxError("missing <") | |
| } | |
| op = tok | |
| } | |
| return out, err | |
| } | |
| // A rulesParser parses the depsRules syntax described above. | |
| type rulesParser struct { | |
| lineno int | |
| lastWord string | |
| text string | |
| } | |
| // syntaxError reports a parsing error. | |
| func (p *rulesParser) syntaxError(msg string) { | |
| panic(syntaxError(fmt.Sprintf("parsing graph: line %d: syntax error: %s near %s", p.lineno, msg, p.lastWord))) | |
| } | |
| // nextList parses and returns a comma-separated list of names. | |
| func (p *rulesParser) nextList() (list []string, token string) { | |
| for { | |
| tok := p.nextToken() | |
| switch tok { | |
| case "": | |
| if len(list) == 0 { | |
| return nil, "" | |
| } | |
| fallthrough | |
| case ",", "<", "!<", ";": | |
| p.syntaxError("bad list syntax") | |
| } | |
| list = append(list, tok) | |
| tok = p.nextToken() | |
| if tok != "," { | |
| return list, tok | |
| } | |
| } | |
| } | |
| // nextToken returns the next token in the deps rules, | |
| // one of ";" "," "<" "!<" or a name. | |
| func (p *rulesParser) nextToken() string { | |
| for { | |
| if p.text == "" { | |
| return "" | |
| } | |
| switch p.text[0] { | |
| case ';', ',', '<': | |
| t := p.text[:1] | |
| p.text = p.text[1:] | |
| return t | |
| case '!': | |
| if len(p.text) < 2 || p.text[1] != '<' { | |
| p.syntaxError("unexpected token !") | |
| } | |
| p.text = p.text[2:] | |
| return "!<" | |
| case '#': | |
| i := strings.Index(p.text, "\n") | |
| if i < 0 { | |
| i = len(p.text) | |
| } | |
| p.text = p.text[i:] | |
| continue | |
| case '\n': | |
| p.lineno++ | |
| fallthrough | |
| case ' ', '\t': | |
| p.text = p.text[1:] | |
| continue | |
| default: | |
| i := strings.IndexAny(p.text, "!;,<#\n \t") | |
| if i < 0 { | |
| i = len(p.text) | |
| } | |
| t := p.text[:i] | |
| p.text = p.text[i:] | |
| p.lastWord = t | |
| return t | |
| } | |
| } | |
| } |