blob: f8cedc023b86e08ca36127c4a8f1c845247d00b0 [file] [log] [blame]
/* Helper code for graphviz output.
Copyright (C) 2019-2025 Free Software Foundation, Inc.
Contributed by David Malcolm <dmalcolm@redhat.com>.
This file is part of GCC.
GCC is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
GCC is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with GCC; see the file COPYING3. If not see
<http://www.gnu.org/licenses/>. */
#define INCLUDE_MAP
#define INCLUDE_STRING
#define INCLUDE_VECTOR
#include "config.h"
#include "system.h"
#include "coretypes.h"
#include "graphviz.h"
#include "xml.h"
#include "xml-printer.h"
#include "pex.h"
#include "selftest.h"
dot::writer::writer (pretty_printer &pp)
: m_pp (pp),
m_indent (0)
{
}
/* Print the current indent to the underlying pp. */
void
dot::writer::write_indent ()
{
for (int i = 0; i < m_indent * 4; ++i)
pp_space (get_pp ());
}
graphviz_out::graphviz_out (pretty_printer *pp)
: writer (*pp)
{
gcc_assert (pp);
}
/* Formatted print of FMT. */
void
graphviz_out::print (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
text_info text (fmt, &ap, errno);
pp_format (get_pp (), &text);
pp_output_formatted_text (get_pp ());
va_end (ap);
}
/* Formatted print of FMT. The text is indented by the current
indent, and a newline is added. */
void
graphviz_out::println (const char *fmt, ...)
{
va_list ap;
write_indent ();
va_start (ap, fmt);
text_info text (fmt, &ap, errno);
pp_format (get_pp (), &text);
pp_output_formatted_text (get_pp ());
va_end (ap);
pp_newline (get_pp ());
}
/* Write the start of an HTML-like row via <TR>, writing to the stream
so that followup text can be escaped. */
void
graphviz_out::begin_tr ()
{
pp_string (get_pp (), "<TR>");
pp_write_text_to_stream (get_pp ());
}
/* Write the end of an HTML-like row via </TR>, writing to the stream
so that followup text can be escaped. */
void
graphviz_out::end_tr ()
{
pp_string (get_pp (), "</TR>");
pp_write_text_to_stream (get_pp ());
}
/* Write the start of an HTML-like <TD>, writing to the stream
so that followup text can be escaped. */
void
graphviz_out::begin_td ()
{
pp_string (get_pp (), "<TD ALIGN=\"LEFT\">");
pp_write_text_to_stream (get_pp ());
}
/* Write the end of an HTML-like </TD>, writing to the stream
so that followup text can be escaped. */
void
graphviz_out::end_td ()
{
pp_string (get_pp (), "</TD>");
pp_write_text_to_stream (get_pp ());
}
/* Write the start of an HTML-like row via <TR><TD>, writing to the stream
so that followup text can be escaped. */
void
graphviz_out::begin_trtd ()
{
pp_string (get_pp (), "<TR><TD ALIGN=\"LEFT\">");
pp_write_text_to_stream (get_pp ());
}
/* Write the end of an HTML-like row via </TD></TR>, writing to the stream
so that followup text can be escaped. */
void
graphviz_out::end_tdtr ()
{
pp_string (get_pp (), "</TD></TR>");
pp_write_text_to_stream (get_pp ());
}
namespace dot {
// Definitions
// struct ast_node
void
ast_node::dump () const
{
pretty_printer pp;
pp.set_output_stream (stderr);
writer w (pp);
print (w);
pp_newline (&pp);
pp_flush (&pp);
}
// struct id
id::id (std::string str)
: m_str (std::move (str)),
m_kind (is_identifier_p (m_str.c_str ())
? kind::identifier
: kind::quoted)
{
}
id::id (const xml::node &n)
: m_kind (kind::html)
{
pretty_printer pp;
n.write_as_xml (&pp, 0, true);
m_str = pp_formatted_text (&pp);
}
void
id::print (writer &w) const
{
switch (m_kind)
{
default:
gcc_unreachable ();
case kind::identifier:
w.write_string (m_str.c_str ());
break;
case kind::quoted:
w.write_character ('"');
for (auto ch : m_str)
if (ch == '"')
w.write_string ("\\\"");
else
w.write_character (ch);
w.write_character ('"');
break;
case kind::html:
w.write_character ('<');
w.write_string (m_str.c_str ());
w.write_character ('>');
break;
}
}
bool
id::is_identifier_p (const char *str)
{
const char initial_ch = *str;
if (initial_ch != '_' && !ISALPHA (initial_ch))
return false;
for (const char *iter = str + 1; *iter; ++iter)
{
const char iter_ch = *iter;
if (iter_ch != '_' && !ISALNUM (iter_ch))
return false;
}
return true;
}
// struct kv_pair
void
kv_pair::print (writer &w) const
{
m_key.print (w);
w.write_character ('=');
m_value.print (w);
}
// struct attr_list
void
attr_list::print (writer &w) const
{
if (m_kvs.empty ())
return;
w.write_string (" [");
for (auto iter = m_kvs.begin (); iter != m_kvs.end (); ++iter)
{
if (iter != m_kvs.begin ())
w.write_string ("; ");
iter->print (w);
}
w.write_string ("]");
}
// struct stmt_list
void
stmt_list::print (writer &w) const
{
for (auto &stmt : m_stmts)
{
w.write_indent ();
stmt->print (w);
w.write_string (";");
w.write_newline ();
}
}
void
stmt_list::add_edge (node_id src_id, node_id dst_id)
{
m_stmts.push_back
(std::make_unique<dot::edge_stmt>
(std::move (src_id), std::move (dst_id)));
}
void
stmt_list::add_attr (id key, id value)
{
add_stmt
(std::make_unique <kv_stmt> (kv_pair (std::move (key),
std::move (value))));
}
// struct graph
void
graph::print (writer &w) const
{
w.write_indent ();
w.write_string ("digraph ");
if (m_id)
{
m_id->print (w);
w.write_character (' ');
}
w.write_string ("{");
w.write_newline ();
w.indent ();
m_stmt_list.print (w);
w.outdent ();
w.write_indent ();
w.write_string ("}");
w.write_newline ();
}
// struct stmt_with_attr_list : public stmt
void
stmt_with_attr_list::set_label (dot::id value)
{
m_attrs.add (dot::id ("label"), std::move (value));
}
// struct node_stmt : public stmt_with_attr_list
void
node_stmt::print (writer &w) const
{
m_id.print (w);
m_attrs.print (w);
}
// struct attr_stmt : public stmt_with_attr_list
void
attr_stmt::print (writer &w) const
{
switch (m_kind)
{
default:
gcc_unreachable ();
case kind::graph:
w.write_string ("graph");
break;
case kind::node:
w.write_string ("node");
break;
case kind::edge:
w.write_string ("edge");
break;
}
m_attrs.print (w);
}
// struct kv_stmt : public stmt
void
kv_stmt::print (writer &w) const
{
m_kv.print (w);
}
// struct node_id
void
node_id::print (writer &w) const
{
m_id.print (w);
if (m_port)
m_port->print (w);
}
// struct port
void
port::print (writer &w) const
{
if (m_id)
{
w.write_character (':');
m_id->print (w);
}
if (m_compass_pt)
{
w.write_character (':');
switch (*m_compass_pt)
{
default:
gcc_unreachable ();
case compass_pt::n:
w.write_string ("n");
break;
case compass_pt::ne:
w.write_string ("ne");
break;
case compass_pt::e:
w.write_string ("e");
break;
case compass_pt::se:
w.write_string ("se");
break;
case compass_pt::s:
w.write_string ("s");
break;
case compass_pt::sw:
w.write_string ("sw");
break;
case compass_pt::w:
w.write_string ("w");
break;
case compass_pt::nw:
w.write_string ("nw");
break;
case compass_pt::c:
w.write_string ("c");
break;
}
}
}
// struct edge_stmt : public stmt_with_attr_list
void
edge_stmt::print (writer &w) const
{
for (auto iter = m_node_ids.begin (); iter != m_node_ids.end (); ++iter)
{
if (iter != m_node_ids.begin ())
w.write_string (" -> ");
iter->print (w);
}
m_attrs.print (w);
}
// struct subgraph : public stmt
void
subgraph::print (writer &w) const
{
w.write_newline ();
w.write_indent ();
w.write_string ("subgraph ");
m_id.print (w);
w.write_string (" {");
w.write_newline ();
w.indent ();
m_stmt_list.print (w);
w.outdent ();
w.write_newline ();
w.write_indent ();
w.write_string ("}"); // newline and semicolon added by stmt_list
}
/* Convert G to graphviz source, attempt to invoke "dot -Tsvg" on it
as a subprocess, and get the SVG source from stdout, or nullptr
if there was a problem. */
static std::unique_ptr<std::string>
make_svg_document_buffer_from_graph (const graph &g)
{
/* Ideally there would be a way of doing this without
invoking dot as a subprocess. */
std::vector<std::string> args;
args.push_back ("dot");
args.push_back ("-Tsvg");
pex p (0, "dot", nullptr);
{
auto pipe_stdin = p.input_file (true, nullptr);
gcc_assert (pipe_stdin.m_file);
pretty_printer pp;
pp.set_output_stream (pipe_stdin.m_file);
writer w (pp);
g.print (w);
pp_flush (&pp);
}
int err = 0;
const char * errmsg
= p.run (PEX_SEARCH,
"dot", args, nullptr, nullptr, &err);
auto pipe_stdout = p.read_output ();
auto content = pipe_stdout.read_all ();
if (errmsg)
return nullptr;
if (err)
return nullptr;
std::string result;
result.reserve (content->size () + 1);
for (auto &iter : *content)
result.push_back (iter);
return std::make_unique<std::string> (std::move (result));
}
/* Convert G to graphviz source, attempt to invoke "dot -Tsvg" on it
as a subprocess, and get the SVG source from stdout, and extract
the "svg" subtree as an xml::raw node.
Note that this
(a) invokes "dot" as a subprocess
(b) assumes that we trust the output from "dot".
Return nullptr if there was a problem. */
std::unique_ptr<xml::node>
make_svg_from_graph (const graph &g)
{
auto svg_src = make_svg_document_buffer_from_graph (g);
if (!svg_src)
return nullptr;
/* Skip past the XML header to the parts we care about. */
auto pos = svg_src->find ("<!-- Generated by graphviz");
if (pos == svg_src->npos)
return nullptr;
auto substring = std::string (*svg_src, pos);
return std::make_unique<xml::raw> (std::move (substring));
}
} // namespace dot
#if CHECKING_P
namespace selftest {
static void
test_ids ()
{
ASSERT_TRUE (dot::id::is_identifier_p ("foo"));
ASSERT_FALSE (dot::id::is_identifier_p ("hello world"));
ASSERT_TRUE (dot::id::is_identifier_p ("foo42"));
ASSERT_FALSE (dot::id::is_identifier_p ("42"));
ASSERT_TRUE (dot::id::is_identifier_p ("_"));
}
static void
test_trivial_graph ()
{
dot::graph g;
// node "a"
{
g.add_stmt (std::make_unique<dot::node_stmt> (dot::id ("a")));
}
// node "b"
{
auto n = std::make_unique<dot::node_stmt> (dot::id ("b"));
n->m_attrs.add (dot::id ("label"), dot::id ("This is node b"));
n->m_attrs.add (dot::id ("color"), dot::id ("green"));
g.add_stmt (std::move (n));
}
// an edge between them
{
auto e = std::make_unique<dot::edge_stmt> (dot::id ("a"),
dot::id ("b"));
e->m_attrs.add (dot::id ("label"), dot::id ("I'm an edge"));
g.add_stmt (std::move (e));
}
pretty_printer pp;
dot::writer w (pp);
g.print (w);
ASSERT_STREQ
(pp_formatted_text (&pp),
("digraph {\n"
" a;\n"
" b [label=\"This is node b\"; color=green];\n"
" a -> b [label=\"I'm an edge\"];\n"
"}\n"));
}
/* Recreating the HTML record example from
https://graphviz.org/doc/info/shapes.html#html */
static void
test_layout_example ()
{
dot::graph g (dot::id ("structs"));
// "node [shape=plaintext]\n"
{
auto attr_stmt
= std::make_unique<dot::attr_stmt> (dot::attr_stmt::kind::node);
attr_stmt->m_attrs.add (dot::id ("shape"), dot::id ("plaintext"));
g.add_stmt (std::move (attr_stmt));
}
// struct1
{
auto n = std::make_unique<dot::node_stmt> (dot::id ("struct1"));
xml::element table ("TABLE", false);
xml::printer xp (table);
xp.set_attr ("BORDER", "0");
xp.set_attr ("CELLBORDER", "1");
xp.set_attr ("CELLSPACING", "0");
xp.push_tag ("TR", true);
xp.push_tag ("TD", false);
xp.add_text ("left");
xp.pop_tag ("TD");
xp.push_tag ("TD", false);
xp.set_attr ("PORT", "f1");
xp.add_text ("mid dle");
xp.pop_tag ("TD");
xp.push_tag ("TD", false);
xp.set_attr ("PORT", "f2");
xp.add_text ("right");
xp.pop_tag ("TD");
n->set_label (table);
g.add_stmt (std::move (n));
}
// struct2
{
auto n = std::make_unique<dot::node_stmt> (dot::id ("struct2"));
xml::element table ("TABLE", false);
xml::printer xp (table);
xp.set_attr ("BORDER", "0");
xp.set_attr ("CELLBORDER", "1");
xp.set_attr ("CELLSPACING", "0");
xp.push_tag ("TR", true);
xp.push_tag ("TD", false);
xp.set_attr ("PORT", "f0");
xp.add_text ("one");
xp.pop_tag ("TD");
xp.push_tag ("TD", false);
xp.add_text ("two");
xp.pop_tag ("TD");
n->set_label (table);
g.add_stmt (std::move (n));
}
// struct3
{
auto n = std::make_unique<dot::node_stmt> (dot::id ("struct3"));
xml::element table ("TABLE", false);
xml::printer xp (table);
xp.set_attr ("BORDER", "0");
xp.set_attr ("CELLBORDER", "1");
xp.set_attr ("CELLSPACING", "0");
xp.set_attr ("CELLPADDING", "4");
xp.push_tag ("TR", false);
xp.push_tag ("TD", true);
xp.set_attr ("ROWSPAN", "3");
xp.add_text ("hello");
xp.append (std::make_unique<xml::element> ("BR", false));
xp.add_text ("world");
xp.pop_tag ("TD");
xp.push_tag ("TD", true);
xp.set_attr ("COLSPAN", "3");
xp.add_text ("b");
xp.pop_tag ("TD");
xp.push_tag ("TD", true);
xp.set_attr ("ROWSPAN", "3");
xp.add_text ("g");
xp.pop_tag ("TD");
xp.push_tag ("TD", true);
xp.set_attr ("ROWSPAN", "3");
xp.add_text ("h");
xp.pop_tag ("TD");
xp.pop_tag ("TR");
xp.push_tag ("TR", false);
xp.push_tag ("TD", true);
xp.add_text ("c");
xp.pop_tag ("TD");
xp.push_tag ("TD", true);
xp.set_attr ("PORT", "here");
xp.add_text ("d");
xp.pop_tag ("TD");
xp.push_tag ("TD", true);
xp.add_text ("e");
xp.pop_tag ("TD");
xp.pop_tag ("TR");
xp.push_tag ("TR", false);
xp.push_tag ("TD", true);
xp.set_attr ("COLSPAN", "3");
xp.add_text ("f");
xp.pop_tag ("TD");
n->set_label (table);
g.add_stmt (std::move (n));
}
g.m_stmt_list.add_edge
(dot::node_id (dot::id ("struct1"),
dot::port (dot::id ("f1"))),
dot::node_id (dot::id ("struct2"),
dot::port (dot::id ("f0"))));
g.m_stmt_list.add_edge
(dot::node_id (dot::id ("struct1"),
dot::port (dot::id ("f2"))),
dot::node_id (dot::id ("struct3"),
dot::port (dot::id ("here"))));
pretty_printer pp;
dot::writer w (pp);
g.print (w);
/* There are some whitespace differences with the example in the
GraphViz docs. */
ASSERT_STREQ
(pp_formatted_text (&pp),
("digraph structs {\n"
" node [shape=plaintext];\n" // added semicolon
" struct1 [label=<<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n"
" <TR><TD>left</TD><TD PORT=\"f1\">mid dle</TD><TD PORT=\"f2\">right</TD></TR>\n"
"</TABLE>\n"
">];\n"
" struct2 [label=<<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\">\n"
" <TR><TD PORT=\"f0\">one</TD><TD>two</TD></TR>\n"
"</TABLE>\n"
">];\n"
" struct3 [label=<<TABLE BORDER=\"0\" CELLBORDER=\"1\" CELLSPACING=\"0\" CELLPADDING=\"4\">\n"
" <TR>\n"
" <TD ROWSPAN=\"3\">hello<BR/>world</TD>\n"
" <TD COLSPAN=\"3\">b</TD>\n"
" <TD ROWSPAN=\"3\">g</TD>\n"
" <TD ROWSPAN=\"3\">h</TD>\n"
" </TR>\n"
" <TR>\n"
" <TD>c</TD>\n"
" <TD PORT=\"here\">d</TD>\n"
" <TD>e</TD>\n"
" </TR>\n"
" <TR>\n"
" <TD COLSPAN=\"3\">f</TD>\n"
" </TR>\n"
"</TABLE>\n"
">];\n"
" struct1:f1 -> struct2:f0;\n"
" struct1:f2 -> struct3:here;\n"
"}\n"));
}
/* Run all of the selftests within this file. */
void
graphviz_cc_tests ()
{
test_ids ();
test_trivial_graph ();
test_layout_example ();
}
} // namespace selftest
#endif /* CHECKING_P */