package rules import ( "fmt" "slices" "strings" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" ) // The module graph's cycle rule (novox/hq ADR 0603 §2, ADR 0614 §3, §6), judged by the checking program (ADR 0656 §5): // the check refuses a change whose module graph has a cycle and names it; a cycle the trunk already has among the same // modules is the trunk's, said in the notes. The running checker gathers the graph's ordering edges of the catalogue as // the change leaves it and as the trunk has it, worked out with the contract's graph (sdk/graph: bases, the provisions // and bus state a module awaits, the verb edges the graph keeps), as module names and the edge's kind, never the // manifests themselves, so the facts stay within the run input's bound. // The kinds of an ordering edge. const ( EdgeBase = "base" EdgeAwaits = "awaits" EdgeInvokes = "invokes" ) // Edge is one ordering edge: From waits for To. Only is EdgeChange when only the change's graph has it, EdgeTrunk when // only the trunk's does, empty when both have it. type Edge struct { From string `json:"from"` To string `json:"to"` Kind string `json:"kind"` Only string `json:"only,omitempty"` } // Where only one graph has an edge. const ( EdgeChange = "change" EdgeTrunk = "trunk" ) // Graph is the module graph's facts: every ordering edge of either graph, between modules of the catalogue. Trunk says // whether the trunk's graph was read; when it was not, every edge is the change's and no cycle is the trunk's. type Graph struct { Edges []Edge `json:"edges"` Trunk bool `json:"trunk"` } // JudgeGraph is the cycle rule over the graph's facts: the failure of a cycle the change makes, or the note of no cycle // or of a cycle the trunk already has. func JudgeGraph(g Graph) (*FailedFirst, []string) { cycle := firstCycle(g.Edges, func(e Edge) bool { return e.Only != EdgeTrunk || !g.Trunk }) if cycle == nil { return nil, []string{"the change's module graph has no cycle"} } words := strings.Join(cycle, " -> ") if g.Trunk { among := firstCycle(g.Edges, func(e Edge) bool { return e.Only != EdgeChange && slices.Contains(cycle, e.From) && slices.Contains(cycle, e.To) }) if among != nil { return nil, []string{"the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)"} } } return &FailedFirst{Machine: buildrun.GoBuildModule, Words: fmt.Sprintf("the change's module graph has the cycle %s: these modules would "+ "each wait for the other for good, through their bases, the provisions and bus state they await, or the verbs they invoke (ADR 0603 §2, ADR 0614 §6)", words)}, nil } // firstCycle is the first cycle of the edges kept, as the modules it passes through with the first named again at its // end, visiting the modules and each one's edges in sorted order (as sdk/graph's Cycle does), or nil for none. func firstCycle(edges []Edge, keep func(Edge) bool) []string { next := map[string][]string{} for _, e := range edges { if keep(e) && !slices.Contains(next[e.From], e.To) { next[e.From] = append(next[e.From], e.To) } } var names []string for name, to := range next { slices.Sort(to) names = append(names, name) } slices.Sort(names) const ( unseen = iota onPath done ) state := map[string]int{} var path []string var visit func(string) []string visit = func(name string) []string { state[name] = onPath path = append(path, name) for _, to := range next[name] { switch state[to] { case onPath: i := slices.Index(path, to) return append(slices.Clone(path[i:]), to) case unseen: if c := visit(to); c != nil { return c } } } path = path[:len(path)-1] state[name] = done return nil } for _, name := range names { if state[name] == unseen { if c := visit(name); c != nil { return c } } } return nil }