This patch adds a new tree-like representation for use when building
diagnostic messages.

The version in this patch is rather simplistic, and is purely to support
various awkward cases in upcoming patches I have that rework the insides
of the analyzer.  I hope to eventually extend the markup support to
allow capturing richer markup in diagnostic sink output (e.g. HTML
output, markdown in SARIF, etc), but I'm deliberately keeping the scope
of this patch quite narrow.

gcc/ChangeLog:
        * Makefile.in (OBJS-libcommon): Add diagnostics/markup.o.
        * diagnostics/diagnostics-selftests.cc
        (diagnostics::selftest::run_diagnostics_selftests): Call
        markup_cc_tests.
        * diagnostics/diagnostics-selftests.h
        (diagnostics::selftest::markup_cc_tests): New decl.
        * diagnostics/markup.cc: New file.
        * diagnostics/markup.h: New file.

Signed-off-by: David Malcolm <[email protected]>
---
 gcc/Makefile.in                          |   1 +
 gcc/diagnostics/diagnostics-selftests.cc |   1 +
 gcc/diagnostics/diagnostics-selftests.h  |   1 +
 gcc/diagnostics/markup.cc                | 734 +++++++++++++++++++++++
 gcc/diagnostics/markup.h                 | 596 ++++++++++++++++++
 5 files changed, 1333 insertions(+)
 create mode 100644 gcc/diagnostics/markup.cc
 create mode 100644 gcc/diagnostics/markup.h

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index c6059206a95..7d9536d8da4 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1908,6 +1908,7 @@ OBJS-libcommon = \
        diagnostics/lazy-paths.o \
        diagnostics/logging.o \
        diagnostics/macro-unwinding.o \
+       diagnostics/markup.o \
        diagnostics/option-classifier.o \
        diagnostics/paths.o \
        diagnostics/paths-output.o \
diff --git a/gcc/diagnostics/diagnostics-selftests.cc 
b/gcc/diagnostics/diagnostics-selftests.cc
index bb371e79763..ad794d5eff3 100644
--- a/gcc/diagnostics/diagnostics-selftests.cc
+++ b/gcc/diagnostics/diagnostics-selftests.cc
@@ -42,6 +42,7 @@ run_diagnostics_selftests ()
   color_cc_tests ();
   file_cache_cc_tests ();
   source_printing_cc_tests ();
+  markup_cc_tests ();
   html_sink_cc_tests ();
   sarif_sink_cc_tests ();
   digraphs_cc_tests ();
diff --git a/gcc/diagnostics/diagnostics-selftests.h 
b/gcc/diagnostics/diagnostics-selftests.h
index da1460f324b..73b556324b0 100644
--- a/gcc/diagnostics/diagnostics-selftests.h
+++ b/gcc/diagnostics/diagnostics-selftests.h
@@ -39,6 +39,7 @@ extern void digraphs_cc_tests ();
 extern void file_cache_cc_tests ();
 extern void html_sink_cc_tests ();
 extern void lazy_paths_cc_tests ();
+extern void markup_cc_tests ();
 extern void output_spec_cc_tests ();
 extern void paths_output_cc_tests ();
 extern void sarif_sink_cc_tests ();
diff --git a/gcc/diagnostics/markup.cc b/gcc/diagnostics/markup.cc
new file mode 100644
index 00000000000..a73486d776b
--- /dev/null
+++ b/gcc/diagnostics/markup.cc
@@ -0,0 +1,734 @@
+/* A tree-like representation for use when building diagnostic messages.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <[email protected]>.
+
+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/>.  */
+
+#include "config.h"
+#define INCLUDE_MAP
+#define INCLUDE_STRING
+#define INCLUDE_VECTOR
+#include "system.h"
+#include "coretypes.h"
+
+#include "diagnostics/markup.h"
+
+#include "text-art/dump-widget-info.h"
+
+#include "selftest.h"
+
+namespace diagnostics {
+namespace markup {
+
+static bool
+child_needs_parens_p (const node &parent, const node &child)
+{
+  if (parent.get_kind () == kind::inline_code)
+    return false;
+
+  switch (child.get_kind ())
+    {
+    case kind::text:
+      return false;
+    case kind::compound:
+    case kind::math_negation:
+    case kind::math_subtraction:
+    case kind::math_sum:
+    case kind::math_product:
+    case kind::math_half_open_interval:
+      return true;
+
+    case kind::code_element_expr:
+    case kind::code_field_expr:
+    case kind::code_infix_binary_op:
+      return false;
+
+    case kind::inline_code:
+      return false;
+    }
+
+  return true;
+}
+
+/* Lower CHILD of PARENT and add to DST_NODE,
+   wrapping lowered node in parentheses if necessary.  */
+
+static void
+lower_and_append_with_any_parens (compound_node &dst_node,
+                                 const node &parent,
+                                 const node &child,
+                                 const lowering_options &opts)
+{
+  bool needs_parens = child_needs_parens_p (parent, child);
+
+  if (needs_parens)
+    dst_node.add_child ("(");
+
+  dst_node.add_child (child.lower (opts));
+
+  if (needs_parens)
+    dst_node.add_child (")");
+}
+
+
+// class node
+
+static void
+dump_node_tree_to_pp (const node &n,
+                     text_art::theme *theme,
+                     pretty_printer *pp)
+{
+  if (!theme)
+    return;
+
+  using namespace text_art;
+
+  style_manager sm;
+  style tree_style (get_style_from_color_cap_name ("note"));
+
+  style::id_t tree_style_id (sm.get_or_create_id (tree_style));
+
+  dump_widget_info dwi  (sm, *theme, tree_style_id);
+  if (std::unique_ptr<widget> w = n.make_dump_widget (dwi))
+    {
+      text_art::canvas c (w->to_canvas (sm));
+      c.print_to_pp (pp);
+    }
+}
+
+void
+node::dump () const
+{
+  pretty_printer pp;
+  pp.set_output_stream (stderr);
+  text_art::ascii_theme theme;
+  dump_node_tree_to_pp (*this, &theme, &pp);
+  pp_flush (&pp);
+}
+
+// class text : public node
+
+void
+text::print_to_pp (pretty_printer *pp) const
+{
+  pp_string (pp, m_str.c_str ());
+}
+
+std::unique_ptr<text_art::tree_widget>
+text::make_dump_widget (text_art::dump_widget_info &dwi) const
+{
+  return text_art::tree_widget::from_fmt (dwi, nullptr,
+                                         "text: %qs", m_str.c_str ());
+}
+
+std::unique_ptr<node>
+text::lower (const lowering_options &opts) const
+{
+  return std::make_unique<text> (m_str);
+}
+
+// class compound_node : public node
+
+void
+compound_node::print_to_pp (pretty_printer *pp) const
+{
+  for (auto &iter : m_children)
+    iter->print_to_pp (pp);
+}
+
+std::unique_ptr<text_art::tree_widget>
+compound_node::make_dump_widget (text_art::dump_widget_info &dwi) const
+{
+  std::unique_ptr<text_art::tree_widget> w
+    (text_art::tree_widget::from_fmt (dwi, nullptr,
+                                     "%s",
+                                     get_class_name ()));
+
+  for (auto &iter : m_children)
+    w->add_child (iter->make_dump_widget (dwi));
+
+  return w;
+}
+
+std::unique_ptr<node>
+compound_node::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<compound_node> ();
+
+  for (size_t idx = 0; idx < get_num_children (); ++idx)
+    result->add_child (get_child (idx).lower (opts));
+
+  return result;
+}
+
+void
+compound_node::add_child (std::string str)
+{
+  add_child (std::make_unique<text> (std::move (str)));
+}
+
+// class unary_node : public node
+
+void
+unary_node::print_to_pp (pretty_printer *pp) const
+{
+  pp_string (pp, get_class_name ());
+  pp_string (pp, " (");
+  m_operand->print_to_pp (pp);
+  pp_string (pp, ")");
+}
+
+std::unique_ptr<text_art::tree_widget>
+unary_node::make_dump_widget (text_art::dump_widget_info &dwi) const
+{
+  std::unique_ptr<text_art::tree_widget> w
+    (text_art::tree_widget::from_fmt (dwi, nullptr,
+                                     "%qs",
+                                     get_class_name ()));
+
+  w->add_child (m_operand->make_dump_widget (dwi));
+
+  return w;
+}
+
+// class binary_node : public node
+
+void
+binary_node::print_to_pp (pretty_printer *pp) const
+{
+  pp_string (pp, get_class_name ());
+  pp_string (pp, " (");
+  m_op0->print_to_pp (pp);
+  pp_string (pp, ", ");
+  m_op1->print_to_pp (pp);
+  pp_string (pp, ")");
+}
+
+std::unique_ptr<text_art::tree_widget>
+binary_node::make_dump_widget (text_art::dump_widget_info &dwi) const
+{
+  std::unique_ptr<text_art::tree_widget> w
+    (text_art::tree_widget::from_fmt (dwi, nullptr,
+                                     "%qs",
+                                     get_class_name ()));
+
+  w->add_child (m_op0->make_dump_widget (dwi));
+  w->add_child (m_op1->make_dump_widget (dwi));
+
+  return w;
+}
+
+// class math::associative_op : public compound_node
+
+std::unique_ptr<node>
+math::associative_op::lower (const lowering_options &opts) const
+{
+  gcc_assert (get_num_children () >= 2);
+
+  auto result = std::make_unique<compound_node> ();
+
+  pretty_printer pp_infix_symbol;
+  pp_printf (&pp_infix_symbol, " %s ",
+            get_symbol (opts.m_use_unicode_symbols));
+
+  for (size_t idx = 0; idx < get_num_children (); ++idx)
+    {
+      if (idx > 0)
+       result->add_child (pp_formatted_text (&pp_infix_symbol));
+      lower_and_append_with_any_parens (*result,
+                                       *this,
+                                       get_child (idx),
+                                       opts);
+    }
+
+  return result;
+}
+
+// class math::sum : public associative_op
+
+const char *
+math::sum::get_symbol (bool /* use_unicode */) const
+{
+  return "+";
+}
+
+// class math::product : public associative_op
+
+const char *
+math::product::get_symbol (bool use_unicode) const
+{
+  if (use_unicode)
+    return MARKUP_UNICODE_SYMBOL_MULTIPLICATION;
+  else
+    return "*";
+}
+
+// class math::negation : public unary_node
+
+std::unique_ptr<node>
+math::negation::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<compound_node> ();
+  const char *symbol = (opts.m_use_unicode_symbols
+                       ? MARKUP_UNICODE_SYMBOL_UNARY_NEGATION
+                       : "-");
+  result->add_child (symbol);
+  lower_and_append_with_any_parens (*result, *this, get_operand (), opts);
+  return result;
+}
+
+// class math::subtraction : public binary_node
+
+std::unique_ptr<node>
+math::subtraction::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<compound_node> ();
+
+  lower_and_append_with_any_parens (*result, *this, get_operand_0 (), opts);
+
+  const char *padded_symbol
+    = (opts.m_use_unicode_symbols
+       ? (" " MARKUP_UNICODE_SYMBOL_SUBTRACTION " ")
+       : " - ");
+  result->add_child (padded_symbol);
+  lower_and_append_with_any_parens (*result, *this, get_operand_1 (), opts);
+
+  return result;
+}
+
+// class math::half_open_interval : public binary_node
+
+std::unique_ptr<node>
+math::half_open_interval::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<compound_node> ();
+
+  result->add_child ("[");
+  lower_and_append_with_any_parens (*result, *this, get_operand_0 (), opts);
+  result->add_child (", ");
+  lower_and_append_with_any_parens (*result, *this, get_operand_1 (), opts);
+  result->add_child (")");
+  return result;
+}
+
+// class code::element_expr : public binary_node
+
+std::unique_ptr<node>
+code::element_expr::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<compound_node> ();
+  result->add_child (get_operand_0 ().lower (opts));
+  result->add_child ("[");
+  result->add_child (get_operand_1 ().lower (opts));
+  result->add_child ("]");
+  return result;
+}
+
+// class code::field_expr : public binary_node
+
+std::unique_ptr<node>
+code::field_expr::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<compound_node> ();
+  result->add_child (get_operand_0 ().lower (opts));
+  result->add_child (".");
+  result->add_child (get_operand_1 ().lower (opts));
+  return result;
+}
+
+// class code::infix_binary_op : public binary_node
+
+std::unique_ptr<node>
+code::infix_binary_op::lower (const lowering_options &opts) const
+{
+  pretty_printer pp_infix_symbol;
+  pp_printf (&pp_infix_symbol, " %s ", m_symbol);
+
+  auto result = std::make_unique<compound_node> ();
+  result->add_child (get_operand_0 ().lower (opts));
+  result->add_child (pp_formatted_text (&pp_infix_symbol));
+  result->add_child (get_operand_1 ().lower (opts));
+  return result;
+}
+
+// class inline_code : public compound_node
+
+void
+inline_code::print_to_pp (pretty_printer *pp) const
+{
+  pp_begin_quote (pp, pp_show_color (pp));
+  compound_node::print_to_pp (pp);
+  pp_end_quote (pp, pp_show_color (pp));
+}
+
+std::unique_ptr<node>
+inline_code::lower (const lowering_options &opts) const
+{
+  auto result = std::make_unique<inline_code> ();
+
+  for (size_t idx = 0; idx < get_num_children (); ++idx)
+    result->add_child (get_child (idx).lower (opts));
+
+  return result;
+}
+
+} // namespace markup
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Implementation detail of ASSERT_SYMBOL_MARKUP_TEXT_EQ.  */
+
+void
+assert_markup_text_eq (const ::selftest::location &loc,
+                      const markup::node &node,
+                      const markup::lowering_options &opts,
+                      const char *expected_str)
+{
+  auto lowered = node.lower (opts);
+  pretty_printer pp;
+  lowered->print_to_pp (&pp);
+  ASSERT_STREQ_AT (loc, pp_formatted_text (&pp), expected_str);
+}
+
+/* Verify that dumping markup::node hierarchies works as expected.  */
+
+static void
+test_dumping ()
+{
+  using namespace diagnostics::markup;
+
+  /* "a + b + (c * d)".  */
+  auto sum = std::make_unique<math::sum> ();
+  sum->add_child ("a");
+  sum->add_child ("b");
+  {
+    auto nested = std::make_unique<math::product> ();
+    nested->add_child ("c");
+    nested->add_child ("d");
+    sum->add_child (std::move (nested));
+  }
+
+  pretty_printer pp;
+  text_art::ascii_theme theme;
+  dump_node_tree_to_pp (*sum, &theme, &pp);
+  ASSERT_STREQ
+    (pp_formatted_text (&pp),
+     ("math::sum\n"
+      "+- text: 'a'\n"
+      "+- text: 'b'\n"
+      "`- math::product\n"
+      "   +- text: 'c'\n"
+      "   `- text: 'd'\n"));
+}
+
+static void
+test_math_nodes ()
+{
+  using namespace diagnostics::markup;
+
+  {
+    auto negation = std::make_unique<math::negation> (make_text_node ("a"));
+    ASSERT_MARKUP_TEXT_EQ
+      (*negation,
+       get_ascii_markup_opts (),
+       "-a");
+    ASSERT_MARKUP_TEXT_EQ
+      (*negation,
+       get_unicode_markup_opts (),
+       MARKUP_UNICODE_SYMBOL_UNARY_NEGATION "a");
+  }
+
+  {
+    auto subtraction
+      = std::make_unique<math::subtraction> (make_text_node ("a"),
+                                            make_text_node ("b"));
+    ASSERT_MARKUP_TEXT_EQ
+      (*subtraction,
+       get_ascii_markup_opts (),
+       "a - b");
+    ASSERT_MARKUP_TEXT_EQ
+      (*subtraction,
+       get_unicode_markup_opts (),
+       "a " MARKUP_UNICODE_SYMBOL_SUBTRACTION " b");
+  }
+
+  {
+    auto sum = std::make_unique<math::sum> ();
+    sum->add_child ("a");
+    sum->add_child ("b");
+    sum->add_child ("c");
+    ASSERT_MARKUP_TEXT_EQ
+      (*sum,
+       get_ascii_markup_opts (),
+       "a + b + c");
+  }
+
+  {
+    auto product = std::make_unique<math::product> ();
+    product->add_child ("a");
+    product->add_child ("b");
+    product->add_child ("c");
+    ASSERT_MARKUP_TEXT_EQ
+      (*product,
+       get_ascii_markup_opts (),
+       "a * b * c");
+    ASSERT_MARKUP_TEXT_EQ
+      (*product,
+       get_unicode_markup_opts (),
+       ("a "
+       MARKUP_UNICODE_SYMBOL_MULTIPLICATION
+       " b "
+       MARKUP_UNICODE_SYMBOL_MULTIPLICATION
+       " c"));
+  }
+
+  {
+    auto half_open_interval
+      = std::make_unique<math::half_open_interval> (make_text_node ("m"),
+                                                   make_text_node ("n"));
+    ASSERT_MARKUP_TEXT_EQ
+      (*half_open_interval,
+       get_ascii_markup_opts (),
+       "[m, n)");
+  }
+}
+
+static void
+test_code_nodes ()
+{
+  using namespace diagnostics::markup;
+
+  {
+    auto element_expr
+      = std::make_unique<code::element_expr> (make_text_node ("arr"),
+                                             make_text_node ("idx"));
+    ASSERT_MARKUP_TEXT_EQ
+      (*element_expr,
+       get_ascii_markup_opts (),
+       "arr[idx]");
+  }
+  {
+    auto field_expr
+      = std::make_unique<code::field_expr> (make_text_node ("foo"),
+                                           make_text_node ("m_bar"));
+    ASSERT_MARKUP_TEXT_EQ
+      (*field_expr,
+       get_ascii_markup_opts (),
+       "foo.m_bar");
+  }
+  {
+    auto subtraction
+      = std::make_unique<code::infix_binary_op> (make_text_node ("a"),
+                                                "-",
+                                                make_text_node ("b"));
+    ASSERT_MARKUP_TEXT_EQ
+      (*subtraction,
+       get_ascii_markup_opts (),
+       "a - b");
+    /* Verify that a code subtraction doesn't use the
+       unicode minus character.  */
+    ASSERT_MARKUP_TEXT_EQ
+      (*subtraction,
+       get_unicode_markup_opts (),
+       "a - b");
+  }
+}
+
+static void
+test_inline_code ()
+{
+  using namespace diagnostics::markup;
+
+  auto node = std::make_unique<inline_code> ();
+  node->add_child ("sizeof (foo)");
+  ASSERT_MARKUP_TEXT_EQ
+    (*node,
+     get_ascii_markup_opts (),
+     "'sizeof (foo)'");
+}
+
+/* Verify that we add parentheses correctly when lowering
+   math expressions to text.  */
+
+static void
+test_parens ()
+{
+  using namespace diagnostics::markup;
+
+  /* math::sum ("a", "b") --> "a + b".  */
+  {
+    auto sum = std::make_unique<math::sum> ();
+    sum->add_child ("a");
+    sum->add_child ("b");
+    ASSERT_MARKUP_TEXT_EQ
+      (*sum,
+       get_ascii_markup_opts (),
+       "a + b");
+  }
+
+  /* math::sum (math::product ("2", "a"), "b") --> "(2 * a) + b".  */
+  {
+    auto sum = std::make_unique<math::sum> ();
+    {
+      auto two_times_a = std::make_unique<math::product> ();
+      two_times_a->add_child ("2");
+      two_times_a->add_child ("a");
+      sum->add_child (std::move (two_times_a));
+    }
+    sum->add_child ("b");
+    ASSERT_MARKUP_TEXT_EQ
+      (*sum,
+       get_ascii_markup_opts (),
+       "(2 * a) + b");
+    ASSERT_MARKUP_TEXT_EQ
+      (*sum,
+       get_unicode_markup_opts (),
+       "(2 " MARKUP_UNICODE_SYMBOL_MULTIPLICATION " a) + b");
+  }
+
+  /* math::product ("2", math::sum ("a", "b")) --> "(2 * a) + b".  */
+  {
+    auto product = std::make_unique<math::product> ();
+    product->add_child ("2");
+    {
+      auto a_plus_b = std::make_unique<math::sum> ();
+      a_plus_b->add_child ("a");
+      a_plus_b->add_child ("b");
+      product->add_child (std::move (a_plus_b));
+    }
+    ASSERT_MARKUP_TEXT_EQ
+      (*product,
+       get_ascii_markup_opts (),
+       "2 * (a + b)");
+    ASSERT_MARKUP_TEXT_EQ
+      (*product,
+       get_unicode_markup_opts (),
+       "2 " MARKUP_UNICODE_SYMBOL_MULTIPLICATION " (a + b)");
+  }
+}
+
+/* Consider marking up this message:
+   | If 'x' * 'sizeof(foo)' (64 bytes) > 'SIZE_MAX' (2**64 - 1),
+   |    ^^^ ^ ^^^^^^^^^^^^^            ~  ^^^^^^^^   ^^^^^^^^^
+   |     |  |     |                    |     |         |
+   |     |  |     code                 math  code      math
+   |     |  math
+   |     code
+   | then 'x * sizeof(foo)' overflows.
+   |      ^^^^^^^^^^^^^^^^^
+   |       |
+   |       code
+*/
+static std::unique_ptr<markup::node>
+make_overflow_example ()
+{
+  using namespace diagnostics::markup;
+
+  auto make_condition = [] () -> auto {
+    auto make_lhs = [] () -> auto {
+      auto product = std::make_unique<math::product> ();
+      auto x = std::make_unique<inline_code> ();
+      x->add_child ("x");
+      product->add_child (std::move (x));
+      product->add_child ("'sizeof(foo)' (64 bytes)"); // TODO: partially code
+      return product;
+    };
+    auto make_rhs = [] () -> auto {
+      auto term = std::make_unique<compound_node> ();
+      term->add_child
+       (std::make_unique<inline_code> (make_text_node ("SIZE_MAX")));
+      term->add_child (" (2**64 - 1)"); // TODO: math
+      return term;
+    };
+    auto cond = std::make_unique<compound_node> ();
+    cond->add_child (make_lhs ());
+    cond->add_child (" > ");
+    cond->add_child (make_rhs ());
+    return cond;
+  };
+
+  auto make_overflowing_expr = [] () -> auto {
+    auto product = std::make_unique<code::infix_binary_op>
+      (make_text_node ("x"),
+       "*",
+       make_text_node ("sizeof(foo)"));
+    return std::make_unique<inline_code> (std::move (product));
+  };
+
+  auto msg = std::make_unique<compound_node> ();
+  msg->add_child ("If ");
+  msg->add_child (make_condition ());
+  msg->add_child (", then ");
+  msg->add_child (make_overflowing_expr ());
+  msg->add_child (" overflows.");
+  return msg;
+}
+
+static void
+test_overflow_example ()
+{
+  auto markup = make_overflow_example ();
+
+  ASSERT_MARKUP_TEXT_EQ
+    (*markup, get_ascii_markup_opts (),
+     ("If 'x' * 'sizeof(foo)' (64 bytes) > 'SIZE_MAX' (2**64 - 1),"
+      " then 'x * sizeof(foo)' overflows."));
+
+  ASSERT_MARKUP_TEXT_EQ
+    (*markup, get_unicode_markup_opts (),
+     ("If "
+      "'x' " MARKUP_UNICODE_SYMBOL_MULTIPLICATION " 'sizeof(foo)' (64 bytes)"
+      " > 'SIZE_MAX' (2**64 - 1),"
+      " then 'x * sizeof(foo)' overflows."));
+
+  // TODO: other formats
+}
+
+/* Run all of the selftests within this file.  */
+
+void
+markup_cc_tests ()
+{
+  test_dumping ();
+  test_math_nodes ();
+  test_code_nodes ();
+  test_inline_code ();
+  test_parens ();
+  test_overflow_example ();
+}
+
+markup::lowering_options
+get_ascii_markup_opts ()
+{
+  return markup::lowering_options ();
+}
+
+markup::lowering_options
+get_unicode_markup_opts ()
+{
+  markup::lowering_options opts;
+  opts.m_use_unicode_symbols = true;
+  return opts;
+}
+
+} // namespace selftest
+
+#endif /* CHECKING_P */
+
+} // namespace diagnostics
diff --git a/gcc/diagnostics/markup.h b/gcc/diagnostics/markup.h
new file mode 100644
index 00000000000..d1dbc62cd73
--- /dev/null
+++ b/gcc/diagnostics/markup.h
@@ -0,0 +1,596 @@
+/* A tree-like representation for use when building diagnostic messages.
+   Copyright (C) 2026 Free Software Foundation, Inc.
+   Contributed by David Malcolm <[email protected]>.
+
+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/>.  */
+
+#ifndef GCC_DIAGNOSTICS_MARKUP_H
+#define GCC_DIAGNOSTICS_MARKUP_H
+
+#include "selftest.h"
+#include "text-art/tree-widget.h"
+
+namespace diagnostics {
+namespace markup {
+
+struct lowering_options;
+
+/* Indentation shows inheritance hierarchy.  */
+class node;
+  class text;
+  class unary_node;
+    namespace math { class negation; }
+  class binary_node;
+    namespace math { class subtraction; }
+    namespace code { class element_expr; }
+    namespace code { class field_expr; }
+    namespace code { class infix_binary_op; }
+  class compound_node;
+    namespace math { class associative_op; }
+      namespace math { class sum; }
+      namespace math { class product; }
+    class inline_code;
+
+enum class kind {
+  text,
+  compound,
+
+  math_half_open_interval,
+  math_negation,
+  math_product,
+  math_subtraction,
+  math_sum,
+
+  code_element_expr,
+  code_field_expr,
+  code_infix_binary_op,
+
+  inline_code
+};
+
+/* Abstract base class for representing text we might want to put
+   in a diagnostic as an AST.
+
+   Ideally eventually we could lower this in sink-specific ways
+   e.g. text, markdown for SARIF, HTML, etc,
+   but that's overkill for now.
+
+   For now, all we need is enough for labels in the analyzer's access diagrams
+   to work.  */
+
+class node
+{
+public:
+  virtual ~node () = default;
+
+  virtual const char *
+  get_class_name () const = 0;
+
+  virtual kind
+  get_kind () const = 0;
+
+  virtual void
+  print_to_pp (pretty_printer *pp) const = 0;
+
+  void
+  dump () const;
+
+  virtual std::unique_ptr<text_art::tree_widget>
+  make_dump_widget (text_art::dump_widget_info &dwi) const = 0;
+
+  virtual const text *
+  dyn_cast_text () const { return nullptr; }
+
+  virtual const compound_node *
+  dyn_cast_compound_node () const { return nullptr; }
+
+  virtual const math::associative_op *
+  dyn_cast_math_associative_op () const { return nullptr; }
+
+  virtual math::negation *
+  dyn_cast_math_negation () { return nullptr; }
+
+  virtual std::unique_ptr<node>
+  lower (const lowering_options &opts) const = 0;
+};
+
+/* Concrete node subclass for a fully-lowered text string.  */
+
+class text : public node
+{
+public:
+  text (std::string str) : m_str (std::move (str))
+  {
+  }
+
+  const char *
+  get_class_name () const final override { return "text"; }
+
+  kind
+  get_kind () const final override { return kind::text; }
+
+  void
+  print_to_pp (pretty_printer *pp) const final override;
+
+  std::unique_ptr<text_art::tree_widget>
+  make_dump_widget (text_art::dump_widget_info &dwi) const final override;
+
+  const text *
+  dyn_cast_text () const final override { return this; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+
+  const std::string &
+  get_string () const { return m_str; }
+
+private:
+  std::string m_str;
+};
+
+
+/* Abstract subclass for nodes that have a single child.  */
+
+class unary_node : public node
+{
+public:
+  void
+  print_to_pp (pretty_printer *pp) const final override;
+
+  std::unique_ptr<text_art::tree_widget>
+  make_dump_widget (text_art::dump_widget_info &dwi) const final override;
+
+  const node &
+  get_operand () const
+  {
+    return *m_operand;
+  }
+
+  std::unique_ptr<node>
+  take_operand ()
+  {
+    return std::move (m_operand);
+  }
+
+protected:
+  unary_node (std::unique_ptr<node> operand)
+  : m_operand (std::move (operand))
+  {
+    gcc_assert (m_operand);
+  }
+
+private:
+  std::unique_ptr<node> m_operand;
+};
+
+/* Abstract subclass for nodes that have two children.  */
+
+class binary_node : public node
+{
+public:
+  void
+  print_to_pp (pretty_printer *pp) const final override;
+
+  std::unique_ptr<text_art::tree_widget>
+  make_dump_widget (text_art::dump_widget_info &dwi) const final override;
+
+  const node &
+  get_operand_0 () const
+  {
+    return *m_op0;
+  }
+
+  const node &
+  get_operand_1 () const
+  {
+    return *m_op1;
+  }
+
+protected:
+  binary_node (std::unique_ptr<node> op0,
+              std::unique_ptr<node> op1)
+  : m_op0 (std::move (op0)),
+    m_op1 (std::move (op1))
+  {
+  }
+
+private:
+  std::unique_ptr<node> m_op0;
+  std::unique_ptr<node> m_op1;
+};
+
+/* Concrete subclass for nodes that have an arbitrary number of children.  */
+
+class compound_node : public node
+{
+public:
+  compound_node () : m_children () {}
+
+  compound_node (std::vector<std::unique_ptr<node>> children)
+  : m_children (std::move (children))
+  {
+  }
+
+  const char *
+  get_class_name () const { return "compound_node"; }
+
+  kind
+  get_kind () const override { return kind::compound; }
+
+  void
+  print_to_pp (pretty_printer *pp) const override;
+
+  std::unique_ptr<text_art::tree_widget>
+  make_dump_widget (text_art::dump_widget_info &dwi) const final override;
+
+  const compound_node *
+  dyn_cast_compound_node () const final override { return this; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const override;
+
+  void
+  add_child (std::unique_ptr<node> child)
+  {
+    gcc_assert (child);
+    m_children.push_back (std::move (child));
+  }
+
+  void
+  add_child (std::string str);
+
+  size_t
+  get_num_children () const { return m_children.size (); }
+
+  const node &
+  get_child (size_t idx) const { return *m_children[idx]; }
+
+private:
+  std::vector<std::unique_ptr<node>> m_children;
+};
+
+namespace math {
+
+/* Abstract subclass for nodes that represent an associative sequence
+   of infix binary operations.  */
+
+class associative_op : public compound_node
+{
+public:
+  const associative_op *
+  dyn_cast_math_associative_op () const final override { return this; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+
+  virtual const char *
+  get_symbol (bool use_unicode) const = 0;
+
+protected:
+  associative_op ()
+  : compound_node ()
+  {
+  }
+
+  associative_op (std::vector<std::unique_ptr<node>> operands)
+  : compound_node (std::move (operands))
+  {
+  }
+};
+
+/* Concrete node subclass for a mathematical sum.
+   The children are the addends; there can be more than 2.  */
+
+class sum : public associative_op
+{
+public:
+  sum () : associative_op () {}
+
+  sum (std::vector<std::unique_ptr<node>> addends)
+  : associative_op (std::move (addends))
+  {
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "math::sum";
+  }
+
+  kind
+  get_kind () const final override { return kind::math_sum; }
+
+  const char *
+  get_symbol (bool use_unicode) const final override;
+};
+
+/* Concrete node subclass for a mathematical product.
+   The children are the multiplicands; there can be more than 2.  */
+
+class product : public associative_op
+{
+public:
+  product () : associative_op () {}
+
+  product (std::vector<std::unique_ptr<node>> multiplicands)
+  : associative_op (std::move (multiplicands))
+  {
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "math::product";
+  }
+
+  kind
+  get_kind () const final override { return kind::math_product; }
+
+  const char *
+  get_symbol (bool use_unicode) const final override;
+};
+
+/* Concrete node subclass for mathematical unary negation.  */
+
+class negation : public unary_node
+{
+public:
+  negation (std::unique_ptr<node> operand) : unary_node (std::move (operand)) 
{}
+
+  negation *
+  dyn_cast_math_negation () final override { return this; }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "math::negation";
+  }
+
+  kind
+  get_kind () const final override { return kind::math_negation; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+};
+
+/* Concrete node subclass for mathematical subtraction.  */
+
+class subtraction : public binary_node
+{
+public:
+  subtraction (std::unique_ptr<node> op0,
+              std::unique_ptr<node> op1)
+  : binary_node (std::move (op0),
+                std::move (op1))
+  {
+  }
+
+  const char *
+  get_class_name () const { return "math::subtraction"; }
+
+  kind
+  get_kind () const final override { return kind::math_subtraction; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+};
+
+/* Concrete node subclass for a half-open interval
+   of the form [start, end).  */
+
+class half_open_interval : public binary_node
+{
+public:
+  half_open_interval (std::unique_ptr<node> start,
+                     std::unique_ptr<node> next)
+  : binary_node (std::move (start),
+                std::move (next))
+  {
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "math::half_open_interval";
+  }
+
+  kind
+  get_kind () const final override { return kind::math_half_open_interval; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+};
+
+} // namespace diagnostics::markup::math
+
+namespace code {
+
+class element_expr : public binary_node
+{
+public:
+  element_expr (std::unique_ptr<node> array_node,
+               std::unique_ptr<node> index_node)
+  : binary_node (std::move (array_node), std::move (index_node))
+  {
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "code::element_expr";
+  }
+
+  kind
+  get_kind () const final override { return kind::code_element_expr; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+};
+
+class field_expr : public binary_node
+{
+public:
+  field_expr (std::unique_ptr<node> struct_or_union_node,
+             std::unique_ptr<node> field_node)
+  : binary_node (std::move (struct_or_union_node), std::move (field_node))
+  {
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "code::field_expr";
+  }
+
+  kind
+  get_kind () const final override { return kind::code_field_expr; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+};
+
+class infix_binary_op : public binary_node
+{
+public:
+  infix_binary_op (std::unique_ptr<node> op0,
+                  const char *symbol,
+                  std::unique_ptr<node> op1)
+  : binary_node (std::move (op0), std::move (op1)),
+    m_symbol (symbol)
+  {
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "code::infix_binary_op";
+  }
+
+  kind
+  get_kind () const final override { return kind::code_infix_binary_op; }
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+
+private:
+  const char *m_symbol;
+};
+
+} // namespace diagnostics::markup::code
+
+/* Concrete subclass for a run of inline code within text.
+   Adds quotes around its children when printed.  */
+
+class inline_code : public compound_node
+{
+public:
+  inline_code () {}
+
+  inline_code (std::unique_ptr<node> child)
+  : compound_node {}
+  {
+    add_child (std::move (child));
+  }
+
+  const char *
+  get_class_name () const final override
+  {
+    return "inline_code";
+  }
+
+  kind
+  get_kind () const final override { return kind::inline_code; }
+
+  void
+  print_to_pp (pretty_printer *pp) const override;
+
+  std::unique_ptr<node>
+  lower (const lowering_options &opts) const final override;
+};
+
+/* Helpers for making nodes.  */
+
+inline std::unique_ptr<text>
+make_text_node (std::string str)
+{
+  return std::make_unique<text> (std::move (str));
+}
+
+/* Lowering.  */
+
+struct lowering_options
+{
+  lowering_options ()
+  : m_use_unicode_symbols (false)
+  {
+  }
+
+  bool m_use_unicode_symbols;
+};
+
+} // namespace diagnostics::markup
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Implementation detail of ASSERT_SYMBOL_MARKUP_TEXT_EQ.  */
+
+void
+assert_markup_text_eq (const ::selftest::location &loc,
+                      const markup::node &,
+                      const markup::lowering_options &,
+                      const char *expected_str);
+
+/* Assert that MARKUP_NODE lowered to plain text is EXPECTED_STR.  */
+
+#define ASSERT_MARKUP_TEXT_EQ_AT(LOC, MARKUP_NODE, LOWERING_OPTS, 
EXPECTED_STR) \
+  SELFTEST_BEGIN_STMT                                                  \
+  ::diagnostics::selftest::assert_markup_text_eq                       \
+    (LOC, (MARKUP_NODE),                                               \
+     (LOWERING_OPTS), (EXPECTED_STR));                                 \
+  SELFTEST_END_STMT
+
+#define ASSERT_MARKUP_TEXT_EQ(MARKUP_NODE, LOWERING_OPTS, EXPECTED_STR) \
+  SELFTEST_BEGIN_STMT                                                  \
+  ::diagnostics::selftest::assert_markup_text_eq                       \
+    (SELFTEST_LOCATION, (MARKUP_NODE),                                 \
+     (LOWERING_OPTS), (EXPECTED_STR));                                 \
+  SELFTEST_END_STMT
+
+extern markup::lowering_options
+get_ascii_markup_opts ();
+
+extern markup::lowering_options
+get_unicode_markup_opts ();
+
+} // namespace diagnostics::selftest
+
+#endif /* #if CHECKING_P */
+
+} // namespace diagnostics
+
+/* Various string literals for unicode symbols.  */
+
+/* "−": U+2212: MINUS SIGN. */
+#define MARKUP_UNICODE_SYMBOL_UNARY_NEGATION "\u2212"
+#define MARKUP_UNICODE_SYMBOL_SUBTRACTION "\u2212"
+
+/* "×: U+00D7 MULTIPLICATION SIGN. */
+#define MARKUP_UNICODE_SYMBOL_MULTIPLICATION "\u00D7"
+
+#endif /* GCC_DIAGNOSTICS_MARKUP_H */
-- 
2.56.0

Reply via email to