github.com/hashicorp/packer@v1.14.3/internal/dag/dag.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package dag
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"strings"
    10  
    11  	"github.com/hashicorp/hcl/v2"
    12  )
    13  
    14  // AcyclicGraph is a specialization of Graph that cannot have cycles.
    15  type AcyclicGraph struct {
    16  	Graph
    17  }
    18  
    19  // WalkFunc is the callback used for walking the graph.
    20  type WalkFunc func(Vertex) hcl.Diagnostics
    21  
    22  // DepthWalkFunc is a walk function that also receives the current depth of the
    23  // walk as an argument
    24  type DepthWalkFunc func(Vertex, int) error
    25  
    26  func (g *AcyclicGraph) DirectedGraph() Grapher {
    27  	return g
    28  }
    29  
    30  // Validate validates the DAG. A DAG is valid if it has no cycles or self-referencing vertex.
    31  func (g *AcyclicGraph) Validate() error {
    32  	// Look for cycles of more than 1 component
    33  	var err error
    34  	cycles := g.Cycles()
    35  	if len(cycles) > 0 {
    36  		for _, cycle := range cycles {
    37  			cycleStr := make([]string, len(cycle))
    38  			for j, vertex := range cycle {
    39  				cycleStr[j] = VertexName(vertex)
    40  			}
    41  
    42  			err = errors.Join(err, fmt.Errorf(
    43  				"Cycle: %s", strings.Join(cycleStr, ", ")))
    44  		}
    45  	}
    46  
    47  	// Look for cycles to self
    48  	for _, e := range g.Edges() {
    49  		if e.Source() == e.Target() {
    50  			err = errors.Join(err, fmt.Errorf(
    51  				"Self reference: %s", VertexName(e.Source())))
    52  		}
    53  	}
    54  
    55  	return err
    56  }
    57  
    58  // Cycles reports any cycles between graph nodes.
    59  // Self-referencing nodes are not reported, and must be detected separately.
    60  func (g *AcyclicGraph) Cycles() [][]Vertex {
    61  	var cycles [][]Vertex
    62  	for _, cycle := range StronglyConnected(&g.Graph) {
    63  		if len(cycle) > 1 {
    64  			cycles = append(cycles, cycle)
    65  		}
    66  	}
    67  	return cycles
    68  }
    69  
    70  type walkType uint64
    71  
    72  const (
    73  	depthFirst walkType = 1 << iota
    74  	breadthFirst
    75  	downOrder
    76  	upOrder
    77  )
    78  
    79  // ReverseTopologicalOrder returns a topological sort of the given graph, with
    80  // target vertices ordered before the sources of their edges. The nodes are not
    81  // sorted, and any valid order may be returned. This function will panic if it
    82  // encounters a cycle.
    83  func (g *AcyclicGraph) ReverseTopologicalOrder() []Vertex {
    84  	return g.topoOrder(downOrder)
    85  }
    86  
    87  func (g *AcyclicGraph) topoOrder(order walkType) []Vertex {
    88  	// Use a dfs-based sorting algorithm, similar to that used in
    89  	// TransitiveReduction.
    90  	sorted := make([]Vertex, 0, len(g.vertices))
    91  
    92  	// tmp track the current working node to check for cycles
    93  	tmp := map[Vertex]bool{}
    94  
    95  	// perm tracks completed nodes to end the recursion
    96  	perm := map[Vertex]bool{}
    97  
    98  	var visit func(v Vertex)
    99  
   100  	visit = func(v Vertex) {
   101  		if perm[v] {
   102  			return
   103  		}
   104  
   105  		if tmp[v] {
   106  			panic("cycle found in dag")
   107  		}
   108  
   109  		tmp[v] = true
   110  		var next Set
   111  		switch {
   112  		case order&downOrder != 0:
   113  			next = g.downEdgesNoCopy(v)
   114  		case order&upOrder != 0:
   115  			next = g.upEdgesNoCopy(v)
   116  		default:
   117  			panic(fmt.Sprintln("invalid order", order))
   118  		}
   119  
   120  		for _, u := range next {
   121  			visit(u)
   122  		}
   123  
   124  		tmp[v] = false
   125  		perm[v] = true
   126  		sorted = append(sorted, v)
   127  	}
   128  
   129  	for _, v := range g.Vertices() {
   130  		visit(v)
   131  	}
   132  
   133  	return sorted
   134  }