// 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 ( ) 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 ( *Graph) ( string) bool { if , := .byLabel[]; { return false } .byLabel[] = len(.Nodes) .Nodes = append(.Nodes, ) .edges[] = map[string]bool{} return true } func ( *Graph) (, string) { .edges[][] = true } func ( *Graph) (, string) { delete(.edges[], ) } func ( *Graph) (, string) bool { return .edges[] != nil && .edges[][] } func ( *Graph) ( string) []string { := make([]string, 0, 16) for := range .edges[] { = append(, ) } slices.SortFunc(, func(, string) int { return cmp.Compare(.byLabel[], .byLabel[]) }) return } // 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 ( string) (*Graph, error) { := newGraph() := []rule{} , := parseRules() if != nil { return nil, } // TODO: Add line numbers to errors. var []string := func( string, ...any) { = append(, fmt.Sprintf(, ...)) } for , := range { if .op == "!<" { = append(, ) continue } for , := range .def { if == "NONE" { ("NONE cannot be a predecessor") continue } if !.addNode() { ("multiple definitions for %s", ) } for , := range .less { if == "NONE" { continue } if , := .byLabel[]; ! { ("use of %s before its definition", ) } else { .AddEdge(, ) } } } } // Check for missing definition. for , := range .edges { for := range { if .edges[] == nil { ("missing definition for %s", ) } } } // Complete transitive closure. for , := range .Nodes { for , := range .Nodes { for , := range .Nodes { if != && != && .HasEdge(, ) && .HasEdge(, ) { if == { // 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. ("graph cycle: %s < %s < %s", , , ) } .AddEdge(, ) } } } } // Check negative assertions against completed allowed graph. for , := range { for , := range .less { for , := range .def { if .HasEdge(, ) { ("graph edge assertion failed: %s !< %s", , ) } } } } if len() > 0 { return nil, fmt.Errorf("%s", strings.Join(, "\n")) } return , 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 ( syntaxError) () string { return string() } // parseRules parses the rules of a DAG. func parseRules( string) ( []rule, error) { defer func() { := recover() switch e := .(type) { case nil: return case syntaxError: = default: panic() } }() := &rulesParser{lineno: 1, text: } var []string var string for { , := .nextList() if == "" { if == nil { break } .syntaxError("unexpected EOF") } if != nil { = append(, rule{, , }) } = if == ";" { = nil = "" continue } if != "<" && != "!<" { .syntaxError("missing <") } = } return , } // A rulesParser parses the depsRules syntax described above. type rulesParser struct { lineno int lastWord string text string } // syntaxError reports a parsing error. func ( *rulesParser) ( string) { panic(syntaxError(fmt.Sprintf("parsing graph: line %d: syntax error: %s near %s", .lineno, , .lastWord))) } // nextList parses and returns a comma-separated list of names. func ( *rulesParser) () ( []string, string) { for { := .nextToken() switch { case "": if len() == 0 { return nil, "" } fallthrough case ",", "<", "!<", ";": .syntaxError("bad list syntax") } = append(, ) = .nextToken() if != "," { return , } } } // nextToken returns the next token in the deps rules, // one of ";" "," "<" "!<" or a name. func ( *rulesParser) () string { for { if .text == "" { return "" } switch .text[0] { case ';', ',', '<': := .text[:1] .text = .text[1:] return case '!': if len(.text) < 2 || .text[1] != '<' { .syntaxError("unexpected token !") } .text = .text[2:] return "!<" case '#': := strings.Index(.text, "\n") if < 0 { = len(.text) } .text = .text[:] continue case '\n': .lineno++ fallthrough case ' ', '\t': .text = .text[1:] continue default: := strings.IndexAny(.text, "!;,<#\n \t") if < 0 { = len(.text) } := .text[:] .text = .text[:] .lastWord = return } } }