Files
revng-revng/lib/Yield/SVG.cpp
Ivan Krysak c6e38cd485 Return to using constant name builders
Because of how name builders used to lazy gather namespaces on the first
requested name, the objects were self mutating. As such only non-const
references could be used to pass them around.

Since that is no longer the case, this restores most of lost const
qualifiers.
2025-04-17 11:19:17 +03:00

638 lines
25 KiB
C++

/// \file SVG.cpp
//
// This file is distributed under the MIT License. See LICENSE.md for details.
//
#include <unordered_map>
#include "llvm/ADT/PostOrderIterator.h"
#include "llvm/Support/FormatVariadic.h"
#include "revng/GraphLayout/SugiyamaStyle/Compute.h"
#include "revng/Model/Binary.h"
#include "revng/Model/NameBuilder.h"
#include "revng/PTML/Tag.h"
#include "revng/Support/GraphAlgorithms.h"
#include "revng/Yield/CallGraphs/CallGraphSlices.h"
#include "revng/Yield/ControlFlow/Configuration.h"
#include "revng/Yield/ControlFlow/Extraction.h"
#include "revng/Yield/ControlFlow/NodeSizeCalculation.h"
#include "revng/Yield/CrossRelations/CrossRelations.h"
#include "revng/Yield/PTML.h"
#include "revng/Yield/SVG.h"
using ptml::Tag;
namespace tags {
static constexpr auto UnconditionalEdge = "unconditional";
static constexpr auto CallEdge = "call";
static constexpr auto TakenEdge = "taken";
static constexpr auto RefusedEdge = "refused";
static constexpr auto NodeBody = "node-body";
static constexpr auto NodeContents = "node-contents";
static constexpr auto UnconditionalArrowHead = "unconditional-arrow-head";
static constexpr auto CallArrowHead = "call-arrow-head";
static constexpr auto TakenArrowHead = "taken-arrow-head";
static constexpr auto RefusedArrowHead = "refused-arrow-head";
} // namespace tags
static llvm::StringRef edgeTypeAsString(const yield::cfg::Edge &Edge) {
switch (Edge.Type) {
case yield::cfg::EdgeType::Unconditional:
return tags::UnconditionalEdge;
case yield::cfg::EdgeType::Call:
return tags::CallEdge;
case yield::cfg::EdgeType::Taken:
return tags::TakenEdge;
case yield::cfg::EdgeType::Refused:
return tags::RefusedEdge;
default:
revng_abort("Unknown edge type");
}
}
static llvm::StringRef edgeTypeAsString(const yield::calls::Edge &Edge) {
// TODO: we might want to use separate set of tags for call graphs.
return Edge.IsBackwards ? tags::RefusedEdge : tags::TakenEdge;
}
template<uintmax_t Numerator = 8, uintmax_t Denominator = 10>
static std::string cubicBend(const yield::layout::Point &From,
const yield::layout::Point &To,
bool VerticalCurves,
std::ratio<Numerator, Denominator> &&Bend = {}) {
using Coordinate = yield::layout::Coordinate;
constexpr Coordinate Factor = Coordinate(Numerator) / Denominator;
Coordinate XModifier = Factor * (To.X - From.X);
Coordinate YModifier = Factor * (To.Y - From.Y);
if (VerticalCurves)
XModifier = 0;
else
YModifier = 0;
return llvm::formatv("C {0} {1} {2} {3} {4} {5} ",
From.X + XModifier,
-From.Y - YModifier,
To.X - XModifier,
-To.Y + YModifier,
To.X,
-To.Y);
}
static std::string edge(const ptml::MarkupBuilder &B,
const yield::layout::Path &Path,
const llvm::StringRef Type,
bool UseOrthogonalBends = true,
bool UseVerticalCurves = false) {
std::string Points;
revng_assert(!Path.empty());
const auto &First = Path.front();
Points += llvm::formatv("M {0} {1} ", First.X, -First.Y);
if (UseOrthogonalBends) {
for (size_t Index = 1; Index < Path.size(); ++Index)
Points += llvm::formatv("L {0} {1} ", Path[Index].X, -Path[Index].Y);
} else {
revng_assert(Path.size() >= 2);
for (auto Iter = Path.begin(); Iter != std::prev(Path.end()); ++Iter)
Points += cubicBend(*Iter, *std::next(Iter), UseVerticalCurves);
}
revng_assert(!Points.empty());
revng_assert(Points.back() == ' ');
Points.pop_back(); // Remove an extra space at the end.
std::string Marker = llvm::formatv("url(#{0}-arrow-head)", Type);
return B.getTag("path")
.addAttribute("class", std::string(Type) += "-edge")
.addAttribute("d", std::move(Points))
.addAttribute("marker-end", std::move(Marker))
.addAttribute("fill", "none")
.toString();
}
template<typename NodeData, typename EdgeData = Empty>
std::string node(const ptml::MarkupBuilder &B,
const yield::layout::OutputNode<NodeData, EdgeData> *Node,
std::string &&Content,
const yield::cfg::Configuration &Configuration) {
yield::layout::Size HalfSize{ Node->Size.W / 2, Node->Size.H / 2 };
yield::layout::Point TopLeft{ Node->Center.X - HalfSize.W,
-Node->Center.Y - HalfSize.H };
Tag Body = B.getTag("body", std::move(Content));
Body.addAttribute("xmlns", R"(http://www.w3.org/1999/xhtml)");
Tag Text = B.getTag("foreignObject", Body.toString());
Text.addAttribute("class", ::tags::NodeContents)
.addAttribute("x", std::to_string(TopLeft.X))
.addAttribute("y", std::to_string(TopLeft.Y))
.addAttribute("width", std::to_string(Node->Size.W))
.addAttribute("height", std::to_string(Node->Size.H));
Tag Border = B.getTag("rect");
Border.addAttribute("class", ::tags::NodeBody)
.addAttribute("x", std::to_string(TopLeft.X))
.addAttribute("y", std::to_string(TopLeft.Y))
.addAttribute("rx", std::to_string(Configuration.NodeCornerRoundingFactor))
.addAttribute("ry", std::to_string(Configuration.NodeCornerRoundingFactor))
.addAttribute("width", std::to_string(Node->Size.W))
.addAttribute("height", std::to_string(Node->Size.H));
return Text.toString() + Border.toString();
}
struct Viewbox {
yield::layout::Point TopLeft = { -1, -1 };
yield::layout::Point BottomRight = { +1, +1 };
};
template<typename NodeData, typename EdgeData = Empty>
static Viewbox
makeViewbox(const yield::layout::OutputNode<NodeData, EdgeData> *Node) {
yield::layout::Size HalfSize{ Node->Size.W / 2, Node->Size.H / 2 };
yield::layout::Point TopLeft{ Node->Center.X - HalfSize.W,
-Node->Center.Y - HalfSize.H };
yield::layout::Point BottomRight{ Node->Center.X + HalfSize.W,
-Node->Center.Y + HalfSize.H };
return Viewbox{ .TopLeft = std::move(TopLeft),
.BottomRight = std::move(BottomRight) };
}
static void expandViewbox(Viewbox &LHS, const Viewbox &RHS) {
if (RHS.TopLeft.X < LHS.TopLeft.X)
LHS.TopLeft.X = RHS.TopLeft.X;
if (RHS.TopLeft.Y < LHS.TopLeft.Y)
LHS.TopLeft.Y = RHS.TopLeft.Y;
if (RHS.BottomRight.X > LHS.BottomRight.X)
LHS.BottomRight.X = RHS.BottomRight.X;
if (RHS.BottomRight.Y > LHS.BottomRight.Y)
LHS.BottomRight.Y = RHS.BottomRight.Y;
}
static void expandViewbox(Viewbox &Box, const yield::layout::Point &Point) {
if (Box.TopLeft.X > Point.X)
Box.TopLeft.X = Point.X;
if (Box.TopLeft.Y > -Point.Y)
Box.TopLeft.Y = -Point.Y;
if (Box.BottomRight.X < Point.X)
Box.BottomRight.X = Point.X;
if (Box.BottomRight.Y < -Point.Y)
Box.BottomRight.Y = -Point.Y;
}
template<SpecializationOf<yield::layout::OutputGraph> GraphType>
Viewbox calculateViewbox(const GraphType &Graph) {
revng_assert(Graph.size() != 0);
// Ensure every node fits.
Viewbox Result = makeViewbox(*Graph.nodes().begin());
for (const auto *Node : Graph.nodes())
expandViewbox(Result, makeViewbox(Node));
// Ensure every edge point fits.
for (const auto *From : Graph.nodes())
for (auto &&[To, Label] : From->successor_edges())
for (const auto &Point : Label->Path)
expandViewbox(Result, Point);
// Add some extra padding for a good measure.
Result.TopLeft.X -= 50;
Result.TopLeft.Y -= 50;
Result.BottomRight.X += 50;
Result.BottomRight.Y += 50;
return Result;
}
/// A really simple arrow head marker generator.
///
/// \param B: PTML builder that should be used to make ptml::Tags.
/// \param Name: the id of the marker as referred to by the objects using it.
/// \param Size: the size of the marker. It sets both width and height to force
/// the marker to be square-shaped.
/// \param Concave: the size of the concave at the rear side of the arrow.
/// \param Shift: the position of the arrow "origin". It is set to 0 by default
/// (arrow origin the same as its tip), positive values shift arrow back,
/// leaving some space between the tip and its target, negative values shift it
/// closer to the target possibly causing an overlap.
static std::string arrowHead(const ptml::MarkupBuilder &B,
llvm::StringRef Name,
float Size,
float Concave,
float Shift = 0) {
std::string Points = llvm::formatv("{0}, {1} {3}, {2} {0}, {0} {1}, {2}",
"0",
std::to_string(Size),
std::to_string(Size / 2),
std::to_string(Concave));
return B
.getTag("marker",
B.getTag("polygon")
.addAttribute("points", std::move(Points))
.toString())
.addAttribute("id", Name)
.addAttribute("markerWidth", std::to_string(Size))
.addAttribute("markerHeight", std::to_string(Size))
.addAttribute("refX", std::to_string(Size - Shift))
.addAttribute("refY", std::to_string(Size / 2))
.addAttribute("refY", std::to_string(Size / 2))
.addAttribute("orient", "auto")
.toString();
}
static std::string duplicateArrowHeadsImpl(const ptml::MarkupBuilder &B,
float Size,
float Dip,
float Shift = 0) {
return arrowHead(B, tags::UnconditionalArrowHead, Size, Dip, Shift)
+ arrowHead(B, tags::CallArrowHead, Size, Dip, Shift)
+ arrowHead(B, tags::TakenArrowHead, Size, Dip, Shift)
+ arrowHead(B, tags::RefusedArrowHead, Size, Dip, Shift);
}
static std::string
defaultArrowHeads(const ptml::MarkupBuilder &B,
const yield::cfg::Configuration &Configuration) {
if (Configuration.UseOrthogonalBends == true)
return duplicateArrowHeadsImpl(B, 8, 3, 0);
else
return duplicateArrowHeadsImpl(B, 8, 3, 2);
}
constexpr bool isVertical(yield::layout::sugiyama::Orientation Orientation) {
return Orientation == yield::layout::sugiyama::Orientation::TopToBottom
|| Orientation == yield::layout::sugiyama::Orientation::BottomToTop;
}
template<typename CallableType, typename NodeType>
concept NodeExporter = requires(CallableType &&Callable, const NodeType &Node) {
{ Callable(Node) } -> convertible_to<std::string>;
};
template<bool ShouldEmitEmptyNodes,
SpecializationOf<yield::layout::OutputGraph> PostLayoutGraph,
NodeExporter<typename PostLayoutGraph::Node> ContentsLambda>
static std::string exportGraph(const ptml::MarkupBuilder &B,
const PostLayoutGraph &Graph,
const yield::cfg::Configuration &Configuration,
yield::layout::sugiyama::Orientation Orientation,
ContentsLambda &&NodeContents) {
std::string Result;
// Short circuit the execution for an empty graph.
if (Graph.size() == 0)
return Result;
// Export all the edges.
for (const auto *From : Graph.nodes()) {
if (ShouldEmitEmptyNodes || !From->isEmpty()) {
for (auto &&[To, Edge] : From->successor_edges()) {
if (ShouldEmitEmptyNodes || !To->isEmpty()) {
revng_assert(Edge != nullptr);
Result += edge(B,
Edge->Path,
edgeTypeAsString(*Edge),
Configuration.UseOrthogonalBends,
isVertical(Orientation));
}
}
}
}
// Export all the nodes.
for (const auto *Node : Graph.nodes())
if (ShouldEmitEmptyNodes || !Node->isEmpty())
Result += node(B, Node, NodeContents(*Node), Configuration);
Viewbox Box = calculateViewbox(Graph);
std::string SerializedBox = llvm::formatv("{0} {1} {2} {3}",
Box.TopLeft.X,
Box.TopLeft.Y,
Box.BottomRight.X - Box.TopLeft.X,
Box.BottomRight.Y - Box.TopLeft.Y);
Tag ArrowHeads = B.getTag("defs", defaultArrowHeads(B, Configuration));
return B.getTag("svg", ArrowHeads.toString() + std::move(Result))
.addAttribute("xmlns", R"(http://www.w3.org/2000/svg)")
.addAttribute("viewbox", std::move(SerializedBox))
.addAttribute("width", std::to_string(Box.BottomRight.X - Box.TopLeft.X))
.addAttribute("height", std::to_string(Box.BottomRight.Y - Box.TopLeft.Y))
.toString();
}
namespace yield::layout::sugiyama {
/// A helper for invoking sugiyama style layouter with the configuration
/// filled in based on the relevant cfg::Configuration.
///
/// \tparam Node The type of the data attached to each graph node
/// \tparam Edge The type of the data attached to each graph edge
///
/// \param Graph An input graph
/// \param CFG An object describing the desired CFG configuration
/// \param LayoutOrientation The direction of the desired layout
/// \param Ranking The ranking strategy
/// \param UseSimpleTreeOptimization A flag deciding whether simple tree
/// optimization should be used.
///
/// \return The laid out version of the graph corresponding to \ref Graph
template<typename Node, typename Edge = Empty>
inline std::optional<OutputGraph<Node, Edge>>
compute(const InputGraph<Node, Edge> &Graph,
const cfg::Configuration &CFG,
Orientation LayoutOrientation = Orientation::TopToBottom,
RankingStrategy Ranking = RankingStrategy::DisjointDepthFirstSearch,
bool UseSimpleTreeOptimization = false) {
return compute(Graph,
Configuration{
.Ranking = Ranking,
.Orientation = LayoutOrientation,
.UseOrthogonalBends = CFG.UseOrthogonalBends,
.PreserveLinearSegments = CFG.PreserveLinearSegments,
.UseSimpleTreeOptimization = UseSimpleTreeOptimization,
.VirtualNodeWeight = CFG.VirtualNodeWeight,
.NodeMarginSize = CFG.ExternalNodeMarginSize,
.EdgeMarginSize = CFG.EdgeMarginSize });
}
} // namespace yield::layout::sugiyama
std::string
yield::svg::controlFlowGraph(const ::ptml::MarkupBuilder &B,
const yield::Function &InternalFunction,
const model::Binary &Binary) {
constexpr auto Configuration = cfg::Configuration::getDefault();
using Pre = cfg::PreLayoutGraph;
Pre Graph = cfg::extractFromInternal(InternalFunction, Binary, Configuration);
model::CNameBuilder NB = Binary;
// This is somewhat crude, as it forces each node's text to be manifested
// twice: once without any ptml tags and once containing them.
// TODO: clean this up if it ever becomes a performance concern.
::ptml::MarkupBuilder Tagless{ .IsInTaglessMode = true };
for (yield::cfg::PreLayoutNode *Node : Graph.nodes()) {
if (Node->isEmpty()) {
Node->Size = yield::cfg::emptyNodeSize(Configuration);
} else {
auto NodeContents = yield::ptml::controlFlowNode(Tagless,
Node->getBasicBlock(),
InternalFunction,
Binary);
Node->Size = yield::cfg::nodeSize(NodeContents, Configuration);
}
}
constexpr auto TopToBottom = layout::sugiyama::Orientation::TopToBottom;
using Post = std::optional<cfg::PostLayoutGraph>;
Post Result = layout::sugiyama::compute(Graph, Configuration, TopToBottom);
revng_assert(Result.has_value());
auto Content = [&](const yield::cfg::PostLayoutNode &Node) {
if (!Node.isEmpty())
return yield::ptml::controlFlowNode(B,
Node.getBasicBlock(),
InternalFunction,
Binary);
else
return std::string{};
};
return exportGraph<true>(B, *Result, Configuration, TopToBottom, Content);
}
struct LabelNodeHelper {
const ptml::MarkupBuilder &B;
const model::Binary &Binary;
const model::AssemblyNameBuilder &NameBuilder;
const yield::cfg::Configuration Configuration;
std::optional<llvm::StringRef> RootNodeLocation = std::nullopt;
void computeSizes(yield::calls::PreLayoutGraph &Graph) {
for (auto *Node : Graph.nodes()) {
if (Node->isEmpty()) {
Node->Size = yield::cfg::emptyNodeSize(Configuration);
} else {
size_t NameLength = 0;
if (std::optional<model::Function::Key> Key = Node->getFunction()) {
auto Iterator = Binary.Functions().find(std::get<0>(*Key));
revng_assert(Iterator != Binary.Functions().end());
NameLength = NameBuilder.name(*Iterator).size();
} else if (auto DynamicFunctionKey = Node->getDynamicFunction()) {
const std::string &Key = std::get<0>(*DynamicFunctionKey);
auto Iterator = Binary.ImportedDynamicFunctions().find(Key);
revng_assert(Iterator != Binary.ImportedDynamicFunctions().end());
NameLength = NameBuilder.name(*Iterator).size();
} else {
revng_abort("Unsupported node type.");
}
revng_assert(NameLength != 0);
Node->Size = yield::cfg::nodeSize(NameLength, 1, Configuration);
}
}
}
std::string operator()(const yield::calls::PostLayoutNode &Node) const {
if (Node.isEmpty())
return "";
llvm::StringRef Location = Node.getLocationString();
if (Node.IsShallow)
return yield::ptml::shallowFunctionLink(B, Location, Binary, NameBuilder);
if (!RootNodeLocation.has_value() || *RootNodeLocation == Location)
return yield::ptml::functionNameDefinition(B,
Location,
Binary,
NameBuilder);
else
return yield::ptml::functionLink(B, Location, Binary, NameBuilder);
}
};
using CrossRelations = yield::crossrelations::CrossRelations;
std::string yield::svg::callGraph(const ::ptml::MarkupBuilder &B,
const CrossRelations &Relations,
const model::Binary &Binary) {
// TODO: make configuration accessible from outside.
auto Configuration = cfg::Configuration::getDefault();
Configuration.UseOrthogonalBends = false;
constexpr auto LeftToRight = layout::sugiyama::Orientation::LeftToRight;
constexpr auto BFS = layout::sugiyama::RankingStrategy::BreadthFirstSearch;
model::AssemblyNameBuilder NameBuilder = Binary;
LabelNodeHelper Helper{ B, Binary, NameBuilder, Configuration };
yield::calls::PreLayoutGraph Result = Relations.toYieldGraph();
auto EntryPoints = entryPoints(&Result);
revng_assert(!EntryPoints.empty());
if (EntryPoints.size() > 1) {
// Add an artificial "root" node to make sure there's a single entry point.
yield::calls::PreLayoutNode *Root = Result.addNode();
for (yield::calls::PreLayoutNode *Entry : EntryPoints)
Root->addSuccessor(Entry);
Result.setEntryNode(Root);
} else {
Result.setEntryNode(EntryPoints.front());
}
auto Tree = calls::makeCalleeTree(Result);
Helper.computeSizes(Tree);
namespace sugiyama = layout::sugiyama;
auto LT = sugiyama::compute(Tree, Configuration, LeftToRight, BFS, true);
revng_assert(LT.has_value());
return exportGraph<false>(B, *LT, Configuration, LeftToRight, Helper);
}
static auto flipPoint(yield::layout::Point const &Point) {
return yield::layout::Point{ -Point.X, -Point.Y };
};
static auto calculateDelta(yield::layout::Point const &LHS,
yield::layout::Point const &RHS) {
return yield::layout::Point{ RHS.X - LHS.X, RHS.Y - LHS.Y };
}
static auto translatePoint(yield::layout::Point const &Point,
yield::layout::Point const &Delta) {
return yield::layout::Point{ Point.X + Delta.X, Point.Y + Delta.Y };
}
static auto convertPoint(yield::layout::Point const &Point,
yield::layout::Point const &Delta) {
return translatePoint(flipPoint(Point), Delta);
}
static yield::calls::PostLayoutGraph
combineHalvesHelper(llvm::StringRef SlicePoint,
yield::calls::PostLayoutGraph &&ForwardsSlice,
yield::calls::PostLayoutGraph &&BackwardsSlice) {
revng_assert(ForwardsSlice.size() != 0 && BackwardsSlice.size() != 0);
auto IsSlicePoint = [&SlicePoint](const auto *Node) {
return Node->getLocationString() == SlicePoint;
};
auto ForwardsIterator = llvm::find_if(ForwardsSlice.nodes(), IsSlicePoint);
revng_assert(ForwardsIterator != ForwardsSlice.nodes().end());
auto *ForwardsSlicePoint = *ForwardsIterator;
revng_assert(ForwardsSlicePoint != nullptr);
auto BackwardsIterator = llvm::find_if(BackwardsSlice.nodes(), IsSlicePoint);
revng_assert(BackwardsIterator != BackwardsSlice.nodes().end());
auto *BackwardsSlicePoint = *BackwardsIterator;
revng_assert(BackwardsSlicePoint != nullptr);
// Find the distance all the nodes of one of the graphs need to be shifted so
// that the `SlicePoint`s overlap.
auto Delta = calculateDelta(flipPoint((*BackwardsIterator)->Center),
(*ForwardsIterator)->Center);
// Ready the backwards part of the graph
for (auto *From : BackwardsSlice.nodes()) {
From->Center = convertPoint(From->Center, Delta);
for (auto &&[Neighbor, Label] : From->successor_edges())
for (auto &Point : Label->Path)
Point = convertPoint(Point, Delta);
}
// Define a map for faster node lookup.
using PostNode = yield::calls::PostLayoutGraph::Node;
llvm::DenseMap<PostNode *, PostNode *> Lookup;
auto AccessLookup = [&Lookup](PostNode *Key) {
auto Iterator = Lookup.find(Key);
revng_assert(Iterator != Lookup.end() && Iterator->second != nullptr);
return Iterator->second;
};
// Move the nodes from the backwards slice into the forwards one.
for (auto *Node : BackwardsSlice.nodes()) {
revng_assert(Node != nullptr);
if (Node != BackwardsSlicePoint) {
auto NewNode = ForwardsSlice.addNode(Node->moveData());
auto &&[Iterator, Success] = Lookup.try_emplace(Node, NewNode);
revng_assert(Success == true);
} else {
auto &&[Iterator, Success] = Lookup.try_emplace(BackwardsSlicePoint,
ForwardsSlicePoint);
revng_assert(Success == true);
}
}
// Move all the edges while also inverting their direction.
for (auto *From : BackwardsSlice.nodes()) {
for (auto &&[To, Label] : From->successor_edges()) {
std::reverse(Label->Path.begin(), Label->Path.end());
AccessLookup(To)->addSuccessor(AccessLookup(From), std::move(*Label));
}
}
return std::move(ForwardsSlice);
}
std::string yield::svg::callGraphSlice(const ::ptml::MarkupBuilder &B,
llvm::StringRef SlicePoint,
const CrossRelations &Relations,
const model::Binary &Binary) {
// TODO: make configuration accessible from outside.
auto Configuration = cfg::Configuration::getDefault();
Configuration.UseOrthogonalBends = false;
constexpr auto LeftToRight = layout::sugiyama::Orientation::LeftToRight;
constexpr auto BFS = layout::sugiyama::RankingStrategy::BreadthFirstSearch;
model::AssemblyNameBuilder NameBuilder = Binary;
LabelNodeHelper Helper{ B, Binary, NameBuilder, Configuration, SlicePoint };
// Ready the forwards facing part of the slice
auto Forward = calls::makeCalleeTree(Relations.toYieldGraph(), SlicePoint);
for (auto *From : Forward.nodes())
for (auto &&[To, Label] : From->successor_edges())
Label->IsBackwards = false;
Helper.computeSizes(Forward);
auto LaidOutForwardsGraph = layout::sugiyama::compute(Forward,
Configuration,
LeftToRight,
BFS,
true);
revng_assert(LaidOutForwardsGraph.has_value());
// Ready the backwards facing part of the slice
auto Backwards = calls::makeCallerTree(Relations.toYieldGraph(), SlicePoint);
for (auto *From : Backwards.nodes())
for (auto &&[To, Label] : From->successor_edges())
Label->IsBackwards = true;
Helper.computeSizes(Backwards);
auto LaidOutBackwardsGraph = layout::sugiyama::compute(Backwards,
Configuration,
LeftToRight,
BFS,
true);
revng_assert(LaidOutBackwardsGraph.has_value());
// Consume the halves to produce a combined graph and export it.
auto CombinedGraph = combineHalvesHelper(SlicePoint,
std::move(*LaidOutForwardsGraph),
std::move(*LaidOutBackwardsGraph));
return exportGraph<false>(B,
CombinedGraph,
Configuration,
LeftToRight,
Helper);
}