// 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;
}
}
}
}