/// \file SVG.cpp // // This file is distributed under the MIT License. See LICENSE.md for details. // #include #include "llvm/ADT/PostOrderIterator.h" #include "llvm/Support/FormatVariadic.h" #include "revng/GraphLayout/SugiyamaStyle/Compute.h" #include "revng/Model/Binary.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 static std::string cubicBend(const yield::layout::Point &From, const yield::layout::Point &To, bool VerticalCurves, std::ratio &&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 std::string node(const ptml::MarkupBuilder &B, const yield::layout::OutputNode *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 static Viewbox makeViewbox(const yield::layout::OutputNode *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 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 (const 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 concept NodeExporter = requires(CallableType &&Callable, const NodeType &Node) { { Callable(Node) } -> convertible_to; }; template PostLayoutGraph, NodeExporter 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 (const 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 inline std::optional> compute(const InputGraph &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::NameBuilder NB = Binary; cfg::calculateNodeSizes(Graph, InternalFunction, Binary, NB, Configuration); constexpr auto TopToBottom = layout::sugiyama::Orientation::TopToBottom; using Post = std::optional; 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(B, *Result, Configuration, TopToBottom, Content); } struct LabelNodeHelper { const ptml::MarkupBuilder &B; const model::Binary &Binary; model::NameBuilder &NameBuilder; const yield::cfg::Configuration Configuration; std::optional RootNodeLocation = std::nullopt; void computeSizes(yield::calls::PreLayoutGraph &Graph) { for (auto *Node : Graph.nodes()) { if (!Node->isEmpty()) { // A normal node size_t NameLength = 0; if (std::optional 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::layout::Size{ NameLength * Configuration.LabelFontSize * Configuration.HorizontalFontFactor, 1 * Configuration.LabelFontSize * Configuration.VerticalFontFactor }; } else { // An entry node. Node->Size = yield::layout::Size{ 30, 30 }; } Node->Size.W += Configuration.InternalNodeMarginSize * 2; Node->Size.H += Configuration.InternalNodeMarginSize * 2; } } 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::NameBuilder 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(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 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::NameBuilder 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(B, CombinedGraph, Configuration, LeftToRight, Helper); }