// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; namespace UnrealBuildTool { /// /// Helper class implementing the standard topological sorting algorithm for directed graphs. /// It generates a flattened sequence of nodes in the order that respects all the given dependencies /// between nodes (edges). So, if we have edges A -> B and B -> C, A will be always before B and C, and /// B will be before C. /// /// In many cases there are more than one possible solutions. This helper just generates one of them. /// The algorithm only works for directed acyclic graphs, as otherwise it's impossible to order the nodes. /// In this implementation we can choose how cycles should be handled (CycleHandling member). By default /// sorting fails, but we may set the class to arbitrarily break cycles and complete the task. /// /// Type of node objects class TopologicalSorter where T : notnull { /// /// Enumeration indicating how graph cycles should be handled. /// public enum CycleMode { /// /// Fail sorting on the first cycle encountered (the algorithmically correct solution). /// Fail, /// /// Break cycles at the point they are detected (allows the algorithm to always complete but may skip some edges). /// Break, /// /// Break cycles at the point they are detected (same as Break but outputs warning logs with information about the cycles). /// BreakWithInfo } /// /// Determines what should be done when a cycle in the graph is encountered. /// public CycleMode CycleHandling = CycleMode.Fail; /// /// UBT's logger object. Necessary if we want to get diagnostics, especially when BreakWithInfo is used. /// public ILogger? Logger = null; /// /// Functor returning a node's name. Necessary for some logging diagnostics, especially when BreakWithInfo is used. /// public Func? NodeToString = null; /// /// List of all graph nodes (each user-provided node T is wrapped in an internal GraphNode). /// private readonly List Nodes = new List(); /// /// When traversing the graph, it represents all the nodes visited in the current sequence. /// For instance, if we go from node A to K and then X, Callstack will store A, K, X. /// It's only needed for diagnostic purposes i.e. when we find a cycle we use Callstack /// to output all the cycle's nodes. /// private Stack Callstack = new Stack(); /// /// The resulting sequence of nodes (T, provided by the user) in the topological order. /// private List Result = new List(); /// /// Initialize the sorter object and create the internal graph representation. /// The list of all the graph nodes doesn't need to be provided because it's internally generated /// from Edges (union of all edges' Item1 and Item2. /// /// List of connections between nodes public TopologicalSorter(List> Edges) { CreateGraph(Edges); } /// /// Sort the graph to generate a flat sequence of nodes respecting all graph dependencies. /// /// True on success, false if failed (most likely a cycle encountered) public bool Sort() { Result.Clear(); Callstack.Clear(); ClearNodesStates(); foreach (GraphNode Node in Nodes) { Callstack.Push(Node); if (!Visit(Node)) { return false; } Callstack.Pop(); } Result.Reverse(); return true; } /// /// Get the list of sorted nodes (valid after successfully calling Sort). /// /// public List GetResult() { return Result; } /// /// Process a single node. /// /// /// True on success, false if failed (most likely a cycle encountered) private bool Visit(GraphNode Node) { if (Node.Mark == Mark.Done) { // This node has already been processed. return true; } if (Node.Mark == Mark.InProgress) { // This node is being processed i.e. we've detected a cycle. return HandleCycle(Node); } Node.Mark = Mark.InProgress; foreach (GraphNode Next in Node.Links) { Callstack.Push(Next); if (!Visit(Next)) { return false; } Callstack.Pop(); } Node.Mark = Mark.Done; Result.Add(Node.Data); return true; } /// /// Create the internal graph structure for the provided list of edges. /// /// List of tuples representing connections between nodes private void CreateGraph(List> Edges) { HashSet AllNodes = new HashSet(); // Create a collection of all nodes based on the ones used in Edges. foreach (Tuple Edge in Edges) { AllNodes.Add(Edge.Item1); AllNodes.Add(Edge.Item2); } Dictionary NodeToGraphNode = new Dictionary(); // Create graph node objects. foreach (T Node in AllNodes) { GraphNode GraphNode = new GraphNode(Node); Nodes.Add(GraphNode); NodeToGraphNode.Add(Node, GraphNode); } // Add edges to the graph. foreach (Tuple Edge in Edges) { GraphNode? NodeSrc; GraphNode? NodeDst; if (!NodeToGraphNode.TryGetValue(Edge.Item1, out NodeSrc)) { throw new Exception($"TopologicalSorter: Failed to build graph (source node {Edge.Item1} from an edge unknown)!"); } if (!NodeToGraphNode.TryGetValue(Edge.Item2, out NodeDst)) { throw new Exception($"TopologicalSorter: Failed to build graph (source node {Edge.Item2} from an edge unknown)!"); } NodeSrc.Links.Add(NodeDst); } } /// /// Clear the state (in-progress, done) of all graph nodes. /// private void ClearNodesStates() { foreach (GraphNode Node in Nodes) { Node.Mark = Mark.None; } } /// /// Member executed when a graph cycle is encountered. /// /// /// True if operation should continue, false if a cycle is considered an error private bool HandleCycle(GraphNode Node) { switch (CycleHandling) { case CycleMode.Fail: // We stop further processing and fail the whole sort. Logger?.LogError("TopologicalSorter: Cycle found in graph ({NodesInCycle})", GetNodesInCycleString()); return false; case CycleMode.Break: // Treat the 'in-progress' node as if it's been 'done' i.e. break the cycle at this point. return true; case CycleMode.BreakWithInfo: // As in Break but output log information about the cycle. Logger?.LogWarning("TopologicalSorter: Cycle found in graph ({NodesInCycle})", GetNodesInCycleString()); return true; } return false; } /// /// Returns all nodes being part of a cycle (call valid only if we've really detected a cycle). /// /// private IEnumerable GetNodesInCycle() { GraphNode[] CallstackNodes = Callstack.ToArray(); // First, find where in the callstack the cycle starts. int CycleStart = 1; for (; CycleStart < CallstackNodes.Length; ++CycleStart) { if (CallstackNodes[CycleStart] == CallstackNodes[0]) { break; } } return CallstackNodes.Take(CycleStart + 1).Reverse(); } /// /// Returns a string with a list of names of nodes forming a graph cycle (call valid only if we've really detected a cycle). /// /// private string GetNodesInCycleString() { if (NodeToString == null) { return ""; } IEnumerable CycleNodes = GetNodesInCycle(); return String.Join(", ", CycleNodes.Select(Node => NodeToString(Node.Data))); } /// /// Graph node's state wrt the traversal. /// private enum Mark { None = 0, InProgress = 1, Done = 2 } /// /// Internal helper representing a graph node (wrapping the user-provided node and storing internal state). /// private class GraphNode { /// /// User-provided node object. /// public T Data; /// /// List of graph nodes we can visit starting from this one. /// public List Links = new List(); /// /// Traversal state (in-progress, done). /// public Mark Mark; public GraphNode(T Data) { this.Data = Data; Mark = Mark.None; } } } }