public inbox for gcc-patches@gcc.gnu.org
 help / color / mirror / Atom feed
* [PATCH 0/3] Add diagram support to gcc diagnostics
@ 2023-05-31 18:06 David Malcolm
  2023-05-31 18:06 ` [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines David Malcolm
                   ` (2 more replies)
  0 siblings, 3 replies; 14+ messages in thread
From: David Malcolm @ 2023-05-31 18:06 UTC (permalink / raw)
  To: gcc-patches; +Cc: David Malcolm

Existing diagnostic text output in GCC has to be implemented by writing
sequentially to a pretty_printer instance.  This makes it hard to
implement some kinds of diagnostic output (see e.g.
diagnostic-show-locus.cc, which is reaching the limits of
maintainability).

I've posted various experimental patches over the years that add other
kinds of output to GCC, such as ASCII art:
- "rich vectorization hints":
  - https://gcc.gnu.org/legacy-ml/gcc-patches/2018-07/msg01576.html
- visualizations of -Wformat-overflow:
  - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77696 comment 9 onwards
  - https://gcc.gnu.org/legacy-ml/gcc-patches/2018-09/msg00771.html

This patch kit combines the above ideas.  It:
- adds more flexible ways to create diagnostic output:
  - a canvas class, which can be "painted" to via random-access (rather
    than sequentially), and then printed when the painting is complete.
    A formatted pretty_printer can be roundtripped to a canvas and back,
    preserving formatting data (colors and URLs)
  - a table class for 2D grid layout, supporting items that span multiple
    rows/columns
  - a widget class for organizing diagrams hierarchically and painting
  them to a canvas
- expands GCC's diagnostics subsystem so that diagnostics can have
  "text art" diagrams - think ASCII art, but potentially including some
  Unicode characters, such as box-drawing chars (by using the canvas
  class)
- uses this to implement visualizations of -Wanalyzer-out-of-bounds so
  that, where possible, it will emit a text art diagram visualizing the
  spatial relationship between (a) the memory region that the analyzer
  predicts would be accessed, versus (b) the range of memory that is
  valid to access - whether they overlap, are touching, are close or far
  apart; which one is before or after in memory, the relative sizes
  involved, the direction of the access (read vs write), and, in some
  cases, the values of data involved.

The new code is in a new "gcc/text-art" subdirectory and "text_art"
namespace.

Many examples of the visualizations can be seen in patch 3 of the kit;
here are two examples; given:

  int32_t arr[10];

  int32_t int_arr_read_element_before_start_far(void)
  {
    return arr[-100];
  }

it emits:

demo-1.c: In function ‘int_arr_read_element_before_start_far’:
demo-1.c:7:13: warning: buffer under-read [CWE-127] [-Wanalyzer-out-of-bounds]
    7 |   return arr[-100];
      |          ~~~^~~~~~
  ‘int_arr_read_element_before_start_far’: event 1
    |
    |    7 |   return arr[-100];
    |      |          ~~~^~~~~~
    |      |             |
    |      |             (1) out-of-bounds read from byte -400 till byte -397 but ‘arr’ starts at byte 0
    |
demo-1.c:7:13: note: valid subscripts for ‘arr’ are ‘[0]’ to ‘[9]’

  ┌───────────────────────────┐
  │read of ‘int32_t’ (4 bytes)│
  └───────────────────────────┘
                ^
                │
                │
  ┌───────────────────────────┐              ┌────────┬────────┬─────────┐
  │                           │              │  [0]   │  ...   │   [9]   │
  │    before valid range     │              ├────────┴────────┴─────────┤
  │                           │              │‘arr’ (type: ‘int32_t[10]’)│
  └───────────────────────────┘              └───────────────────────────┘
  ├─────────────┬─────────────┤├─────┬──────┤├─────────────┬─────────────┤
                │                    │                     │
   ╭────────────┴───────────╮   ╭────┴────╮        ╭───────┴──────╮
   │⚠️  under-read of 4 bytes│   │396 bytes│        │size: 40 bytes│
   ╰────────────────────────╯   ╰─────────╯        ╰──────────────╯

and given:

  #include <string.h>

  void
  test_non_ascii ()
  {
    char buf[5];
    strcpy (buf, "文字化け");
  }

it emits:

demo-2.c: In function ‘test_non_ascii’:
demo-2.c:7:3: warning: stack-based buffer overflow [CWE-121] [-Wanalyzer-out-of-bounds]
    7 |   strcpy (buf, "文字化け");
      |   ^~~~~~~~~~~~~~~~~~~~~~~~
  ‘test_non_ascii’: events 1-2
    |
    |    6 |   char buf[5];
    |      |        ^~~
    |      |        |
    |      |        (1) capacity: 5 bytes
    |    7 |   strcpy (buf, "文字化け");
    |      |   ~~~~~~~~~~~~~~~~~~~~~~~~
    |      |   |
    |      |   (2) out-of-bounds write from byte 5 till byte 12 but ‘buf’ ends at byte 5
    |
demo-2.c:7:3: note: write of 8 bytes to beyond the end of ‘buf’
    7 |   strcpy (buf, "文字化け");
      |   ^~~~~~~~~~~~~~~~~~~~~~~~
demo-2.c:7:3: note: valid subscripts for ‘buf’ are ‘[0]’ to ‘[4]’

  ┌─────┬─────┬─────┬────┬────┐┌────┬────┬────┬────┬────┬────┬────┬──────┐
  │ [0] │ [1] │ [2] │[3] │[4] ││[5] │[6] │[7] │[8] │[9] │[10]│[11]│ [12] │
  ├─────┼─────┼─────┼────┼────┤├────┼────┼────┼────┼────┼────┼────┼──────┤
  │0xe6 │0x96 │0x87 │0xe5│0xad││0x97│0xe5│0x8c│0x96│0xe3│0x81│0x91│ 0x00 │
  ├─────┴─────┴─────┼────┴────┴┴────┼────┴────┴────┼────┴────┴────┼──────┤
  │     U+6587      │    U+5b57     │    U+5316    │    U+3051    │U+0000│
  ├─────────────────┼───────────────┼──────────────┼──────────────┼──────┤
  │       文        │      字       │      化      │      け      │ NUL  │
  ├─────────────────┴───────────────┴──────────────┴──────────────┴──────┤
  │                  string literal (type: ‘char[13]’)                   │
  └──────────────────────────────────────────────────────────────────────┘
     │     │     │    │    │     │    │    │    │    │    │    │     │
     │     │     │    │    │     │    │    │    │    │    │    │     │
     v     v     v    v    v     v    v    v    v    v    v    v     v
  ┌─────┬────────────────┬────┐┌─────────────────────────────────────────┐
  │ [0] │      ...       │[4] ││                                         │
  ├─────┴────────────────┴────┤│            after valid range            │
  │  ‘buf’ (type: ‘char[5]’)  ││                                         │
  └───────────────────────────┘└─────────────────────────────────────────┘
  ├─────────────┬─────────────┤├────────────────────┬────────────────────┤
                │                                   │
       ╭────────┴────────╮              ╭───────────┴──────────╮
       │capacity: 5 bytes│              │⚠️  overflow of 8 bytes│
       ╰─────────────────╯              ╰──────────────────────╯

showing that the overflow occurs partway through the UTF-8 encoding of
the U+5b57 code point.

It doesn't show up in this email, but the above diagrams are colorized
to constrast the valid and invalid access ranges.

There are lots more examples in the test suites of patches 2 and 3,
including symbolic expressions.

I can self-approve most of this but:
- patch 1 touches the testsuite for handling newlines in multiline
  strings in DejaGnu tests
- patches 2 and 3 add string literals with non-ASCII, encoded in UTF-8,
  for use in selftests.  Is this OK?

Successfully bootstrapped & regrtested on x86_64-pc-linux-gnu, both with
gcc 4.8.5 and with gcc 10.3.1

Lightly tested with valgrind.

OK for trunk?


David Malcolm (3):
  testsuite: move handle-multiline-outputs to before check for blank
    lines
  diagnostics: add support for "text art" diagrams
  analyzer: add text-art visualizations of out-of-bounds accesses
    [PR106626]

 contrib/unicode/gen-box-drawing-chars.py      |   94 +
 contrib/unicode/gen-combining-chars.py        |   75 +
 contrib/unicode/gen-printable-chars.py        |   77 +
 gcc/Makefile.in                               |   12 +-
 gcc/analyzer/access-diagram.cc                | 2405 +++++++++++++++++
 gcc/analyzer/access-diagram.h                 |  165 ++
 gcc/analyzer/analyzer.h                       |   30 +
 gcc/analyzer/analyzer.opt                     |   20 +
 gcc/analyzer/bounds-checking.cc               |  270 +-
 gcc/analyzer/diagnostic-manager.cc            |    2 +-
 gcc/analyzer/engine.cc                        |    4 +-
 gcc/analyzer/infinite-recursion.cc            |    2 +-
 gcc/analyzer/kf-analyzer.cc                   |    2 +-
 gcc/analyzer/kf.cc                            |    6 +-
 gcc/analyzer/pending-diagnostic.h             |    2 +-
 gcc/analyzer/region-model-manager.cc          |   32 +-
 gcc/analyzer/region-model-manager.h           |    2 +-
 gcc/analyzer/region-model.cc                  |   52 +-
 gcc/analyzer/region-model.h                   |    4 +
 gcc/analyzer/region.cc                        |  369 ++-
 gcc/analyzer/region.h                         |    1 +
 gcc/analyzer/sm-fd.cc                         |   14 +-
 gcc/analyzer/sm-file.cc                       |    4 +-
 gcc/analyzer/sm-malloc.cc                     |   20 +-
 gcc/analyzer/sm-pattern-test.cc               |    2 +-
 gcc/analyzer/sm-sensitive.cc                  |    3 +-
 gcc/analyzer/sm-signal.cc                     |    2 +-
 gcc/analyzer/sm-taint.cc                      |   16 +-
 gcc/analyzer/store.cc                         |   11 +-
 gcc/analyzer/store.h                          |    9 +
 gcc/analyzer/varargs.cc                       |    8 +-
 gcc/color-macros.h                            |   16 +
 gcc/common.opt                                |   23 +
 gcc/configure                                 |    2 +-
 gcc/configure.ac                              |    2 +-
 gcc/diagnostic-diagram.h                      |   51 +
 gcc/diagnostic-format-json.cc                 |   10 +
 gcc/diagnostic-format-sarif.cc                |  106 +-
 gcc/diagnostic-text-art.h                     |   49 +
 gcc/diagnostic.cc                             |   72 +
 gcc/diagnostic.h                              |   21 +
 gcc/doc/invoke.texi                           |   40 +-
 gcc/gcc.cc                                    |    6 +
 gcc/opts-common.cc                            |    1 +
 gcc/opts.cc                                   |    6 +
 gcc/pretty-print.cc                           |   29 +
 gcc/pretty-print.h                            |    1 +
 gcc/selftest-run-tests.cc                     |    3 +
 .../c-c++-common/Wlogical-not-parentheses-2.c |    2 +
 gcc/testsuite/gcc.dg/analyzer/data-model-1.c  |    4 +-
 .../analyzer/malloc-macro-inline-events.c     |    5 -
 .../analyzer/out-of-bounds-diagram-1-ascii.c  |   55 +
 .../analyzer/out-of-bounds-diagram-1-debug.c  |   40 +
 .../analyzer/out-of-bounds-diagram-1-emoji.c  |   55 +
 .../analyzer/out-of-bounds-diagram-1-json.c   |   13 +
 .../analyzer/out-of-bounds-diagram-1-sarif.c  |   24 +
 .../out-of-bounds-diagram-1-unicode.c         |   55 +
 .../analyzer/out-of-bounds-diagram-10.c       |   29 +
 .../analyzer/out-of-bounds-diagram-11.c       |   82 +
 .../analyzer/out-of-bounds-diagram-12.c       |   54 +
 .../analyzer/out-of-bounds-diagram-13.c       |   43 +
 .../analyzer/out-of-bounds-diagram-14.c       |  110 +
 .../analyzer/out-of-bounds-diagram-15.c       |   42 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-2.c |   30 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-3.c |   45 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-4.c |   45 +
 .../analyzer/out-of-bounds-diagram-5-ascii.c  |   40 +
 .../out-of-bounds-diagram-5-unicode.c         |   42 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-6.c |  125 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-7.c |   36 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-8.c |   34 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-9.c |   42 +
 .../gcc.dg/analyzer/pattern-test-2.c          |    4 +-
 gcc/testsuite/gcc.dg/missing-header-fixit-5.c |   10 +-
 .../gcc.dg/plugin/analyzer_gil_plugin.c       |    6 +-
 .../diagnostic-test-text-art-ascii-bw.c       |   57 +
 .../diagnostic-test-text-art-ascii-color.c    |   58 +
 .../plugin/diagnostic-test-text-art-none.c    |    5 +
 .../diagnostic-test-text-art-unicode-bw.c     |   58 +
 .../diagnostic-test-text-art-unicode-color.c  |   59 +
 .../plugin/diagnostic_plugin_test_text_art.c  |  257 ++
 gcc/testsuite/gcc.dg/plugin/plugin.exp        |    6 +
 gcc/testsuite/lib/gcc-dg.exp                  |    5 +
 gcc/testsuite/lib/multiline.exp               |    7 +-
 gcc/testsuite/lib/prune.exp                   |    7 -
 gcc/text-art/box-drawing-chars.inc            |   18 +
 gcc/text-art/box-drawing.cc                   |   72 +
 gcc/text-art/box-drawing.h                    |   32 +
 gcc/text-art/canvas.cc                        |  437 +++
 gcc/text-art/canvas.h                         |   74 +
 gcc/text-art/ruler.cc                         |  723 +++++
 gcc/text-art/ruler.h                          |  125 +
 gcc/text-art/selftests.cc                     |   77 +
 gcc/text-art/selftests.h                      |   60 +
 gcc/text-art/style.cc                         |  632 +++++
 gcc/text-art/styled-string.cc                 | 1107 ++++++++
 gcc/text-art/table.cc                         | 1272 +++++++++
 gcc/text-art/table.h                          |  262 ++
 gcc/text-art/theme.cc                         |  183 ++
 gcc/text-art/theme.h                          |  123 +
 gcc/text-art/types.h                          |  504 ++++
 gcc/text-art/widget.cc                        |  275 ++
 gcc/text-art/widget.h                         |  246 ++
 libcpp/charset.cc                             |   89 +-
 libcpp/combining-chars.inc                    |   68 +
 libcpp/include/cpplib.h                       |    3 +
 libcpp/printable-chars.inc                    |  231 ++
 107 files changed, 12163 insertions(+), 194 deletions(-)
 create mode 100755 contrib/unicode/gen-box-drawing-chars.py
 create mode 100755 contrib/unicode/gen-combining-chars.py
 create mode 100755 contrib/unicode/gen-printable-chars.py
 create mode 100644 gcc/analyzer/access-diagram.cc
 create mode 100644 gcc/analyzer/access-diagram.h
 create mode 100644 gcc/diagnostic-diagram.h
 create mode 100644 gcc/diagnostic-text-art.h
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-ascii.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-debug.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-emoji.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-json.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-sarif.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-unicode.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-10.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-11.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-12.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-13.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-14.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-15.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-2.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-3.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-4.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-ascii.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-unicode.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-7.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-8.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-9.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
 create mode 100644 gcc/text-art/box-drawing-chars.inc
 create mode 100644 gcc/text-art/box-drawing.cc
 create mode 100644 gcc/text-art/box-drawing.h
 create mode 100644 gcc/text-art/canvas.cc
 create mode 100644 gcc/text-art/canvas.h
 create mode 100644 gcc/text-art/ruler.cc
 create mode 100644 gcc/text-art/ruler.h
 create mode 100644 gcc/text-art/selftests.cc
 create mode 100644 gcc/text-art/selftests.h
 create mode 100644 gcc/text-art/style.cc
 create mode 100644 gcc/text-art/styled-string.cc
 create mode 100644 gcc/text-art/table.cc
 create mode 100644 gcc/text-art/table.h
 create mode 100644 gcc/text-art/theme.cc
 create mode 100644 gcc/text-art/theme.h
 create mode 100644 gcc/text-art/types.h
 create mode 100644 gcc/text-art/widget.cc
 create mode 100644 gcc/text-art/widget.h
 create mode 100644 libcpp/combining-chars.inc
 create mode 100644 libcpp/printable-chars.inc

-- 
2.26.3


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines
  2023-05-31 18:06 [PATCH 0/3] Add diagram support to gcc diagnostics David Malcolm
@ 2023-05-31 18:06 ` David Malcolm
  2023-06-12 23:11   ` PING: " David Malcolm
  2023-05-31 18:06 ` [PATCH 2/3] diagnostics: add support for "text art" diagrams David Malcolm
  2023-05-31 18:06 ` [PATCH 3/3] analyzer: add text-art visualizations of out-of-bounds accesses [PR106626] David Malcolm
  2 siblings, 1 reply; 14+ messages in thread
From: David Malcolm @ 2023-05-31 18:06 UTC (permalink / raw)
  To: gcc-patches; +Cc: David Malcolm

I have followup patches that require checking for multiline patterns
that have blank lines within them, so this moves the handling of
multiline patterns before the check for blank lines, allowing for such
multiline patterns.

Doing so uncovers some issues with existing multiline directives, which
the patch fixes.

gcc/testsuite/ChangeLog:
	* c-c++-common/Wlogical-not-parentheses-2.c: Split up the
	multiline directive.
	* gcc.dg/analyzer/malloc-macro-inline-events.c: Remove redundant
	dg-regexp directives.
	* gcc.dg/missing-header-fixit-5.c: Split up the multiline
	directives.
	* lib/gcc-dg.exp (gcc-dg-prune): Move call to
	handle-multiline-outputs from prune_gcc_output to here.
	* lib/multiline.exp (dg-end-multiline-output): Move call to
	maybe-handle-nn-line-numbers from prune_gcc_output to here.
	* lib/prune.exp (prune_gcc_output): Move calls to
	maybe-handle-nn-line-numbers and handle-multiline-outputs from
	here to the above.
---
 .../c-c++-common/Wlogical-not-parentheses-2.c          |  2 ++
 .../gcc.dg/analyzer/malloc-macro-inline-events.c       |  5 -----
 gcc/testsuite/gcc.dg/missing-header-fixit-5.c          | 10 ++++++++--
 gcc/testsuite/lib/gcc-dg.exp                           |  5 +++++
 gcc/testsuite/lib/multiline.exp                        |  7 ++++++-
 gcc/testsuite/lib/prune.exp                            |  7 -------
 6 files changed, 21 insertions(+), 15 deletions(-)

diff --git a/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c b/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
index ba8dce84f5d..2d9382014c4 100644
--- a/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
+++ b/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
@@ -12,6 +12,8 @@ foo (int aaa, int bbb)
 /* { dg-begin-multiline-output "" }
    r += !aaa == bbb;
              ^~
+   { dg-end-multiline-output "" } */
+/* { dg-begin-multiline-output "" }
    r += !aaa == bbb;
         ^~~~
         (   )
diff --git a/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c b/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
index f08aee626a5..9134bb4781e 100644
--- a/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
+++ b/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
@@ -12,11 +12,6 @@ int test (void *ptr)
   WRAPPED_FREE (ptr); /* { dg-message "in expansion of macro 'WRAPPED_FREE'" } */
   WRAPPED_FREE (ptr); /* { dg-message "in expansion of macro 'WRAPPED_FREE'" } */
 
-  /* Erase the spans indicating the header file
-     (to avoid embedding path assumptions).  */
-  /* { dg-regexp "\[^|\]+/malloc-macro.h:\[0-9\]+:\[0-9\]+:" } */
-  /* { dg-regexp "\[^|\]+/malloc-macro.h:\[0-9\]+:\[0-9\]+:" } */
-
   /* { dg-begin-multiline-output "" }
    NN | #define WRAPPED_FREE(PTR) free(PTR)
       |                           ^~~~~~~~~
diff --git a/gcc/testsuite/gcc.dg/missing-header-fixit-5.c b/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
index 916033c689c..bf44feb24a9 100644
--- a/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
+++ b/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
@@ -12,14 +12,18 @@ foo (char *m, int i)
   /* { dg-begin-multiline-output "" }
    11 |   if (isdigit (m[0]))
       |       ^~~~~~~
+     { dg-end-multiline-output "" } */
+  /* { dg-begin-multiline-output "" }
   +++ |+#include <ctype.h>
     1 | 
      { dg-end-multiline-output "" } */
     {
       return abs (i); /* { dg-warning "implicit declaration of function" } */
   /* { dg-begin-multiline-output "" }
-   19 |       return abs (i);
+   21 |       return abs (i);
       |              ^~~
+     { dg-end-multiline-output "" } */
+  /* { dg-begin-multiline-output "" }
   +++ |+#include <stdlib.h>
     1 | 
      { dg-end-multiline-output "" } */
@@ -27,8 +31,10 @@ foo (char *m, int i)
   else
     putchar (m[0]); /* { dg-warning "implicit declaration of function" } */
   /* { dg-begin-multiline-output "" }
-   28 |     putchar (m[0]);
+   32 |     putchar (m[0]);
       |     ^~~~~~~
+     { dg-end-multiline-output "" } */
+  /* { dg-begin-multiline-output "" }
   +++ |+#include <stdio.h>
     1 | 
      { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/lib/gcc-dg.exp b/gcc/testsuite/lib/gcc-dg.exp
index 4ed4233efff..6475cab46de 100644
--- a/gcc/testsuite/lib/gcc-dg.exp
+++ b/gcc/testsuite/lib/gcc-dg.exp
@@ -364,6 +364,11 @@ proc gcc-dg-prune { system text } {
     # Always remember to clear it in .exp file after executed all tests.
     global dg_runtest_extra_prunes
 
+    # Call into multiline.exp to handle any multiline output directives.
+    # This is done before the check for blank lines so that multiline
+    # output directives can have blank lines within them.
+    set text [handle-multiline-outputs $text]
+
     # Complain about blank lines in the output (PR other/69006)
     global allow_blank_lines
     if { !$allow_blank_lines } {
diff --git a/gcc/testsuite/lib/multiline.exp b/gcc/testsuite/lib/multiline.exp
index 73621a0bdbd..4c25bb76f43 100644
--- a/gcc/testsuite/lib/multiline.exp
+++ b/gcc/testsuite/lib/multiline.exp
@@ -139,7 +139,7 @@ proc dg-end-multiline-output { args } {
     verbose "within dg-end-multiline-output: multiline_expected_outputs: $multiline_expected_outputs" 3
 }
 
-# Hook to be called by prune.exp's prune_gcc_output to
+# Hook to be called by gcc-dg.exp's gcc-dg-prune to
 # look for the expected multiline outputs, pruning them,
 # reporting PASS for those that are found, and FAIL for
 # those that weren't found.
@@ -149,6 +149,11 @@ proc dg-end-multiline-output { args } {
 proc handle-multiline-outputs { text } {
     global multiline_expected_outputs
     global testname_with_flags
+
+    # If dg-enable-nn-line-numbers was provided, then obscure source-margin
+    # line numbers by converting them to "NN" form.
+    set text [maybe-handle-nn-line-numbers $text]
+    
     set index 0
     foreach entry $multiline_expected_outputs {
 	verbose "  entry: $entry" 3
diff --git a/gcc/testsuite/lib/prune.exp b/gcc/testsuite/lib/prune.exp
index cfe427c99ac..8d37b24e59b 100644
--- a/gcc/testsuite/lib/prune.exp
+++ b/gcc/testsuite/lib/prune.exp
@@ -108,13 +108,6 @@ proc prune_gcc_output { text } {
     # Many tests that use visibility will still pass on platforms that don't support it.
     regsub -all "(^|\n)\[^\n\]*lto1: warning: visibility attribute not supported in this configuration; ignored\[^\n\]*" $text "" text
 
-    # If dg-enable-nn-line-numbers was provided, then obscure source-margin
-    # line numbers by converting them to "NN" form.
-    set text [maybe-handle-nn-line-numbers $text]
-    
-    # Call into multiline.exp to handle any multiline output directives.
-    set text [handle-multiline-outputs $text]
-
     #send_user "After:$text\n"
 
     return $text
-- 
2.26.3


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 2/3] diagnostics: add support for "text art" diagrams
  2023-05-31 18:06 [PATCH 0/3] Add diagram support to gcc diagnostics David Malcolm
  2023-05-31 18:06 ` [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines David Malcolm
@ 2023-05-31 18:06 ` David Malcolm
  2023-06-23 11:52   ` Alex Coplan
  2023-05-31 18:06 ` [PATCH 3/3] analyzer: add text-art visualizations of out-of-bounds accesses [PR106626] David Malcolm
  2 siblings, 1 reply; 14+ messages in thread
From: David Malcolm @ 2023-05-31 18:06 UTC (permalink / raw)
  To: gcc-patches; +Cc: David Malcolm

Existing text output in GCC has to be implemented by writing
sequentially to a pretty_printer instance.  This makes it
hard to implement some kinds of diagnostic output (see e.g.
diagnostic-show-locus.cc).

This patch adds more flexible ways of creating text output:
- a canvas class, which can be "painted" to via random-access (rather
that sequentially)
- a table class for 2D grid layout, supporting items that span
multiple rows/columns
- a widget class for organizing diagrams hierarchically.

The patch also expands GCC's diagnostics subsystem so that diagnostics
can have "text art" diagrams - think ASCII art, but potentially
including some Unicode characters, such as box-drawing chars.

The new code is in a new "gcc/text-art" subdirectory and "text_art"
namespace.

The patch adds a new "-fdiagnostics-text-art-charset=VAL" option, with
values:
- "none": don't emit diagrams (added to -fdiagnostics-plain-output)
- "ascii": use pure ASCII in diagrams
- "unicode": allow for conservative use of unicode drawing characters
(such as box-drawing characters).
- "emoji" (the default): as "unicode", but potentially allow for
conservative use of emoji in the output (such as U+26A0 WARNING SIGN).
I made it possible to disable emoji separately from unicode as I believe
there's a generation gap in acceptance of these characters (some older
programmers have a visceral reaction against them, whereas younger
programmers may have no problem with them).

Diagrams are emitted to stderr by default.  With SARIF output they are
captured as a location in "relatedLocations", with the diagram as a
code block in Markdown within a "markdown" property of a message.

This patch doesn't add any such diagram usage to GCC, saving that for
followups, apart from adding a plugin to the test suite to exercise the
functionality.

One issue is that in various places in the seltftests I've embedded
UTF-8 encoded characters in the source code.  Is that acceptable, or
do I need to e.g. move those tests to DejaGnu?

contrib/ChangeLog:
	* unicode/gen-box-drawing-chars.py: New file.
	* unicode/gen-combining-chars.py: New file.
	* unicode/gen-printable-chars.py: New file.

gcc/ChangeLog:
	* Makefile.in (OBJS-libcommon): Add text-art/box-drawing.o,
	text-art/canvas.o, text-art/ruler.o, text-art/selftests.o,
	text-art/style.o, text-art/styled-string.o, text-art/table.o,
	text-art/theme.o, and text-art/widget.o.
	* color-macros.h (COLOR_FG_BRIGHT_BLACK): New.
	(COLOR_FG_BRIGHT_RED): New.
	(COLOR_FG_BRIGHT_GREEN): New.
	(COLOR_FG_BRIGHT_YELLOW): New.
	(COLOR_FG_BRIGHT_BLUE): New.
	(COLOR_FG_BRIGHT_MAGENTA): New.
	(COLOR_FG_BRIGHT_CYAN): New.
	(COLOR_FG_BRIGHT_WHITE): New.
	(COLOR_BG_BRIGHT_BLACK): New.
	(COLOR_BG_BRIGHT_RED): New.
	(COLOR_BG_BRIGHT_GREEN): New.
	(COLOR_BG_BRIGHT_YELLOW): New.
	(COLOR_BG_BRIGHT_BLUE): New.
	(COLOR_BG_BRIGHT_MAGENTA): New.
	(COLOR_BG_BRIGHT_CYAN): New.
	(COLOR_BG_BRIGHT_WHITE): New.
	* common.opt (fdiagnostics-text-art-charset=): New option.
	(diagnostic-text-art.h): New SourceInclude.
	(diagnostic_text_art_charset) New Enum and EnumValues.
	* configure: Regenerate.
	* configure.ac (gccdepdir): Add text-art to loop.
	* diagnostic-diagram.h: New file.
	* diagnostic-format-json.cc (json_emit_diagram): New.
	(diagnostic_output_format_init_json): Wire it up to
	context->m_diagrams.m_emission_cb.
	* diagnostic-format-sarif.cc: Include "diagnostic-diagram.h" and
	"text-art/canvas.h".
	(sarif_result::on_nested_diagnostic): Move code to...
	(sarif_result::add_related_location): ...this new function.
	(sarif_result::on_diagram): New.
	(sarif_builder::emit_diagram): New.
	(sarif_builder::make_message_object_for_diagram): New.
	(sarif_emit_diagram): New.
	(diagnostic_output_format_init_sarif): Set
	context->m_diagrams.m_emission_cb to sarif_emit_diagram.
	* diagnostic-text-art.h: New file.
	* diagnostic.cc: Include "diagnostic-text-art.h",
	"diagnostic-diagram.h", and "text-art/theme.h".
	(diagnostic_initialize): Initialize context->m_diagrams and
	call diagnostics_text_art_charset_init.
	(diagnostic_finish): Clean up context->m_diagrams.m_theme.
	(diagnostic_emit_diagram): New.
	(diagnostics_text_art_charset_init): New.
	* diagnostic.h (text_art::theme): New forward decl.
	(class diagnostic_diagram): Likewise.
	(diagnostic_context::m_diagrams): New field.
	(diagnostic_emit_diagram): New decl.
	* doc/invoke.texi (Diagnostic Message Formatting Options): Add
	-fdiagnostics-text-art-charset=.
	(-fdiagnostics-plain-output): Add
	-fdiagnostics-text-art-charset=none.
	* gcc.cc: Include "diagnostic-text-art.h".
	(driver_handle_option): Handle OPT_fdiagnostics_text_art_charset_.
	* opts-common.cc (decode_cmdline_options_to_array): Add
	"-fdiagnostics-text-art-charset=none" to expanded_args for
	-fdiagnostics-plain-output.
	* opts.cc: Include "diagnostic-text-art.h".
	(common_handle_option): Handle OPT_fdiagnostics_text_art_charset_.
	* pretty-print.cc (pp_unicode_character): New.
	* pretty-print.h (pp_unicode_character): New decl.
	* selftest-run-tests.cc: Include "text-art/selftests.h".
	(selftest::run_tests): Call text_art_tests.
	* text-art/box-drawing-chars.inc: New file, generated by
	contrib/unicode/gen-box-drawing-chars.py.
	* text-art/box-drawing.cc: New file.
	* text-art/box-drawing.h: New file.
	* text-art/canvas.cc: New file.
	* text-art/canvas.h: New file.
	* text-art/ruler.cc: New file.
	* text-art/ruler.h: New file.
	* text-art/selftests.cc: New file.
	* text-art/selftests.h: New file.
	* text-art/style.cc: New file.
	* text-art/styled-string.cc: New file.
	* text-art/table.cc: New file.
	* text-art/table.h: New file.
	* text-art/theme.cc: New file.
	* text-art/theme.h: New file.
	* text-art/types.h: New file.
	* text-art/widget.cc: New file.
	* text-art/widget.h: New file.

gcc/testsuite/ChangeLog:
	* gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c: New test.
	* gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c: New test.
	* gcc.dg/plugin/diagnostic-test-text-art-none.c: New test.
	* gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c: New test.
	* gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c: New test.
	* gcc.dg/plugin/diagnostic_plugin_test_text_art.c: New test plugin.
	* gcc.dg/plugin/plugin.exp (plugin_test_list): Add them.

libcpp/ChangeLog:
	* charset.cc (get_cppchar_property): New function template, based
	on...
	(cpp_wcwidth): ...this function.  Rework to use the above.
	Include "combining-chars.inc".
	(cpp_is_combining_char): New function
	Include "printable-chars.inc".
	(cpp_is_printable_char): New function
	* combining-chars.inc: New file, generated by
	contrib/unicode/gen-combining-chars.py.
	* include/cpplib.h (cpp_is_combining_char): New function decl.
	(cpp_is_printable_char): New function decl.
	* printable-chars.inc: New file, generated by
	contrib/unicode/gen-printable-chars.py.
---
 contrib/unicode/gen-box-drawing-chars.py      |   94 ++
 contrib/unicode/gen-combining-chars.py        |   75 +
 contrib/unicode/gen-printable-chars.py        |   77 +
 gcc/Makefile.in                               |   11 +-
 gcc/color-macros.h                            |   16 +
 gcc/common.opt                                |   23 +
 gcc/configure                                 |    2 +-
 gcc/configure.ac                              |    2 +-
 gcc/diagnostic-diagram.h                      |   51 +
 gcc/diagnostic-format-json.cc                 |   10 +
 gcc/diagnostic-format-sarif.cc                |  106 +-
 gcc/diagnostic-text-art.h                     |   49 +
 gcc/diagnostic.cc                             |   72 +
 gcc/diagnostic.h                              |   21 +
 gcc/doc/invoke.texi                           |   25 +-
 gcc/gcc.cc                                    |    6 +
 gcc/opts-common.cc                            |    1 +
 gcc/opts.cc                                   |    6 +
 gcc/pretty-print.cc                           |   29 +
 gcc/pretty-print.h                            |    1 +
 gcc/selftest-run-tests.cc                     |    3 +
 .../diagnostic-test-text-art-ascii-bw.c       |   57 +
 .../diagnostic-test-text-art-ascii-color.c    |   58 +
 .../plugin/diagnostic-test-text-art-none.c    |    5 +
 .../diagnostic-test-text-art-unicode-bw.c     |   58 +
 .../diagnostic-test-text-art-unicode-color.c  |   59 +
 .../plugin/diagnostic_plugin_test_text_art.c  |  257 ++++
 gcc/testsuite/gcc.dg/plugin/plugin.exp        |    6 +
 gcc/text-art/box-drawing-chars.inc            |   18 +
 gcc/text-art/box-drawing.cc                   |   72 +
 gcc/text-art/box-drawing.h                    |   32 +
 gcc/text-art/canvas.cc                        |  437 ++++++
 gcc/text-art/canvas.h                         |   74 +
 gcc/text-art/ruler.cc                         |  723 ++++++++++
 gcc/text-art/ruler.h                          |  125 ++
 gcc/text-art/selftests.cc                     |   77 +
 gcc/text-art/selftests.h                      |   60 +
 gcc/text-art/style.cc                         |  632 ++++++++
 gcc/text-art/styled-string.cc                 | 1107 ++++++++++++++
 gcc/text-art/table.cc                         | 1272 +++++++++++++++++
 gcc/text-art/table.h                          |  262 ++++
 gcc/text-art/theme.cc                         |  183 +++
 gcc/text-art/theme.h                          |  123 ++
 gcc/text-art/types.h                          |  504 +++++++
 gcc/text-art/widget.cc                        |  275 ++++
 gcc/text-art/widget.h                         |  246 ++++
 libcpp/charset.cc                             |   89 +-
 libcpp/combining-chars.inc                    |   68 +
 libcpp/include/cpplib.h                       |    3 +
 libcpp/printable-chars.inc                    |  231 +++
 50 files changed, 7760 insertions(+), 33 deletions(-)
 create mode 100755 contrib/unicode/gen-box-drawing-chars.py
 create mode 100755 contrib/unicode/gen-combining-chars.py
 create mode 100755 contrib/unicode/gen-printable-chars.py
 create mode 100644 gcc/diagnostic-diagram.h
 create mode 100644 gcc/diagnostic-text-art.h
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
 create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
 create mode 100644 gcc/text-art/box-drawing-chars.inc
 create mode 100644 gcc/text-art/box-drawing.cc
 create mode 100644 gcc/text-art/box-drawing.h
 create mode 100644 gcc/text-art/canvas.cc
 create mode 100644 gcc/text-art/canvas.h
 create mode 100644 gcc/text-art/ruler.cc
 create mode 100644 gcc/text-art/ruler.h
 create mode 100644 gcc/text-art/selftests.cc
 create mode 100644 gcc/text-art/selftests.h
 create mode 100644 gcc/text-art/style.cc
 create mode 100644 gcc/text-art/styled-string.cc
 create mode 100644 gcc/text-art/table.cc
 create mode 100644 gcc/text-art/table.h
 create mode 100644 gcc/text-art/theme.cc
 create mode 100644 gcc/text-art/theme.h
 create mode 100644 gcc/text-art/types.h
 create mode 100644 gcc/text-art/widget.cc
 create mode 100644 gcc/text-art/widget.h
 create mode 100644 libcpp/combining-chars.inc
 create mode 100644 libcpp/printable-chars.inc

diff --git a/contrib/unicode/gen-box-drawing-chars.py b/contrib/unicode/gen-box-drawing-chars.py
new file mode 100755
index 00000000000..9a55266ab84
--- /dev/null
+++ b/contrib/unicode/gen-box-drawing-chars.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+#
+# Script to generate gcc/text-art/box-drawing-chars.inc
+#
+# 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/>.  */
+
+import unicodedata
+
+def get_box_drawing_char_name(up: bool,
+                              down: bool,
+                              left: bool,
+                              right: bool) -> str:
+    if 0:
+        print(f'{locals()=}')
+    if up and down:
+        vertical = True
+        up = False
+        down = False
+    else:
+        vertical = False
+
+    if left and right:
+        horizontal = True
+        left = False
+        right = False
+    else:
+        horizontal = False
+
+    weights = []
+    heavy = []
+    light = []
+    dirs = []
+    for dir_name in ('up', 'down', 'vertical', 'left', 'right', 'horizontal'):
+        val = locals()[dir_name]
+        if val:
+            dirs.append(dir_name.upper())
+
+    if not dirs:
+        return 'SPACE'
+
+    name = 'BOX DRAWINGS'
+    #print(f'{light=} {heavy=}')
+
+    if 0:
+        print(dirs)
+
+    def weights_frag(weight: str, dirs: list, prefix: bool):
+        """
+        Generate a fragment where all directions share the same weight, e.g.:
+        'HEAVY HORIZONTAL'
+        'DOWN LIGHT'
+        'LEFT DOWN HEAVY'
+        'HEAVY DOWN AND RIGHT'
+        """
+        assert len(dirs) >= 1
+        assert len(dirs) <= 2
+        if prefix:
+            return f' {weight} ' + (' AND '.join(dirs))
+        else:
+            return ' ' + (' '.join(dirs)) + f' {weight}'
+
+    assert(len(dirs) >= 1 and len(dirs) <= 2)
+    name += weights_frag('LIGHT', dirs, True)
+
+    return name
+
+print('/* Generated by contrib/unicode/gen-box-drawing-chars.py.  */')
+print()
+for i in range(16):
+    up = (i & 8)
+    down = (i & 4)
+    left = (i & 2)
+    right = (i & 1)
+    name = get_box_drawing_char_name(up, down, left, right)
+    if i < 15:
+        trailing_comma = ','
+    else:
+        trailing_comma = ' '
+    unichar = unicodedata.lookup(name)
+    print(f'0x{ord(unichar):04X}{trailing_comma} /* "{unichar}": U+{ord(unichar):04X}: {name} */')
diff --git a/contrib/unicode/gen-combining-chars.py b/contrib/unicode/gen-combining-chars.py
new file mode 100755
index 00000000000..fb5ef50ba4c
--- /dev/null
+++ b/contrib/unicode/gen-combining-chars.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+#
+# Script to generate libcpp/combining-chars.inc
+#
+# 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/>.  */
+
+from pprint import pprint
+import unicodedata
+
+def is_combining_char(code_point) -> bool:
+    return unicodedata.combining(chr(code_point)) != 0
+
+class Range:
+    def __init__(self, start, end, value):
+        self.start = start
+        self.end = end
+        self.value = value
+
+    def __repr__(self):
+        return f'Range({self.start:x}, {self.end:x}, {self.value})'
+
+def make_ranges(value_callback):
+    ranges = []
+    for code_point in range(0x10FFFF):
+        value = is_combining_char(code_point)
+        if 0:
+            print(f'{code_point=:x} {value=}')
+        if ranges and ranges[-1].value == value:
+            # Extend current range
+            ranges[-1].end = code_point
+        else:
+            # Start a new range
+            ranges.append(Range(code_point, code_point, value))
+    return ranges
+
+ranges = make_ranges(is_combining_char)
+if 0:
+    pprint(ranges)
+
+print(f"/* Generated by contrib/unicode/gen-combining-chars.py")
+print(f"   using version {unicodedata.unidata_version}"
+      " of the Unicode standard.  */")
+print("\nstatic const cppchar_t combining_range_ends[] = {", end="")
+for i, r in enumerate(ranges):
+    if i % 8:
+        print(" ", end="")
+    else:
+        print("\n  ", end="")
+    print("0x%x," % r.end, end="")
+print("\n};\n")
+print("static const bool is_combining[] = {", end="")
+for i, r in enumerate(ranges):
+    if i % 24:
+        print(" ", end="")
+    else:
+        print("\n  ", end="")
+    if r.value:
+        print("1,", end="")
+    else:
+        print("0,", end="")
+print("\n};")
diff --git a/contrib/unicode/gen-printable-chars.py b/contrib/unicode/gen-printable-chars.py
new file mode 100755
index 00000000000..7684c086638
--- /dev/null
+++ b/contrib/unicode/gen-printable-chars.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+#
+# Script to generate libcpp/printable-chars.inc
+#
+# 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/>.  */
+
+from pprint import pprint
+import unicodedata
+
+def is_printable_char(code_point) -> bool:
+    category = unicodedata.category(chr(code_point))
+    # "Cc" is "control" and "Cf" is "format"
+    return category[0] != 'C'
+
+class Range:
+    def __init__(self, start, end, value):
+        self.start = start
+        self.end = end
+        self.value = value
+
+    def __repr__(self):
+        return f'Range({self.start:x}, {self.end:x}, {self.value})'
+
+def make_ranges(value_callback):
+    ranges = []
+    for code_point in range(0x10FFFF):
+        value = is_printable_char(code_point)
+        if 0:
+            print(f'{code_point=:x} {value=}')
+        if ranges and ranges[-1].value == value:
+            # Extend current range
+            ranges[-1].end = code_point
+        else:
+            # Start a new range
+            ranges.append(Range(code_point, code_point, value))
+    return ranges
+
+ranges = make_ranges(is_printable_char)
+if 0:
+    pprint(ranges)
+
+print(f"/* Generated by contrib/unicode/gen-printable-chars.py")
+print(f"   using version {unicodedata.unidata_version}"
+      " of the Unicode standard.  */")
+print("\nstatic const cppchar_t printable_range_ends[] = {", end="")
+for i, r in enumerate(ranges):
+    if i % 8:
+        print(" ", end="")
+    else:
+        print("\n  ", end="")
+    print("0x%x," % r.end, end="")
+print("\n};\n")
+print("static const bool is_printable[] = {", end="")
+for i, r in enumerate(ranges):
+    if i % 24:
+        print(" ", end="")
+    else:
+        print("\n  ", end="")
+    if r.value:
+        print("1,", end="")
+    else:
+        print("0,", end="")
+print("\n};")
diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index 1d39e6dd3f8..c1e7257ed24 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1781,7 +1781,16 @@ OBJS-libcommon = diagnostic-spec.o diagnostic.o diagnostic-color.o \
 	json.o \
 	sbitmap.o \
 	vec.o input.o hash-table.o ggc-none.o memory-block.o \
-	selftest.o selftest-diagnostic.o sort.o
+	selftest.o selftest-diagnostic.o sort.o \
+	text-art/box-drawing.o \
+	text-art/canvas.o \
+	text-art/ruler.o \
+	text-art/selftests.o \
+	text-art/style.o \
+	text-art/styled-string.o \
+	text-art/table.o \
+	text-art/theme.o \
+	text-art/widget.o
 
 # Objects in libcommon-target.a, used by drivers and by the core
 # compiler and containing target-dependent code.
diff --git a/gcc/color-macros.h b/gcc/color-macros.h
index fcd79d09c01..9688f92110a 100644
--- a/gcc/color-macros.h
+++ b/gcc/color-macros.h
@@ -92,6 +92,14 @@ along with GCC; see the file COPYING3.  If not see
 #define COLOR_FG_MAGENTA	"35"
 #define COLOR_FG_CYAN		"36"
 #define COLOR_FG_WHITE		"37"
+#define COLOR_FG_BRIGHT_BLACK	"90"
+#define COLOR_FG_BRIGHT_RED	"91"
+#define COLOR_FG_BRIGHT_GREEN	"92"
+#define COLOR_FG_BRIGHT_YELLOW	"93"
+#define COLOR_FG_BRIGHT_BLUE	"94"
+#define COLOR_FG_BRIGHT_MAGENTA	"95"
+#define COLOR_FG_BRIGHT_CYAN	"96"
+#define COLOR_FG_BRIGHT_WHITE	"97"
 #define COLOR_BG_BLACK		"40"
 #define COLOR_BG_RED		"41"
 #define COLOR_BG_GREEN		"42"
@@ -100,6 +108,14 @@ along with GCC; see the file COPYING3.  If not see
 #define COLOR_BG_MAGENTA	"45"
 #define COLOR_BG_CYAN		"46"
 #define COLOR_BG_WHITE		"47"
+#define COLOR_BG_BRIGHT_BLACK	"100"
+#define COLOR_BG_BRIGHT_RED	"101"
+#define COLOR_BG_BRIGHT_GREEN	"102"
+#define COLOR_BG_BRIGHT_YELLOW	"103"
+#define COLOR_BG_BRIGHT_BLUE	"104"
+#define COLOR_BG_BRIGHT_MAGENTA	"105"
+#define COLOR_BG_BRIGHT_CYAN	"106"
+#define COLOR_BG_BRIGHT_WHITE	"107"
 #define SGR_START		"\33["
 #define SGR_END			"m\33[K"
 #define SGR_SEQ(str)		SGR_START str SGR_END
diff --git a/gcc/common.opt b/gcc/common.opt
index a28ca13385a..b3c82b8607c 100644
--- a/gcc/common.opt
+++ b/gcc/common.opt
@@ -1502,6 +1502,29 @@ fdiagnostics-show-path-depths
 Common Var(flag_diagnostics_show_path_depths) Init(0)
 Show stack depths of events in paths.
 
+fdiagnostics-text-art-charset=
+Driver Common Joined RejectNegative Var(flag_diagnostics_text_art_charset) Enum(diagnostic_text_art_charset) Init(DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI)
+-fdiagnostics-text-art-charset=[none|ascii|unicode|emoji]	Determine which characters to use in text arg diagrams.
+
+; Required for these enum values.
+SourceInclude
+diagnostic-text-art.h
+
+Enum
+Name(diagnostic_text_art_charset) Type(int)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(none) Value(DIAGNOSTICS_TEXT_ART_CHARSET_NONE)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(ascii) Value(DIAGNOSTICS_TEXT_ART_CHARSET_ASCII)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(unicode) Value(DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE)
+
+EnumValue
+Enum(diagnostic_text_art_charset) String(emoji) Value(DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI)
+
 fdiagnostics-minimum-margin-width=
 Common Joined UInteger Var(diagnostics_minimum_margin_width) Init(6)
 Set minimum width of left margin of source code when showing source.
diff --git a/gcc/configure b/gcc/configure
index 5f67808b774..e061d2b1949 100755
--- a/gcc/configure
+++ b/gcc/configure
@@ -33995,7 +33995,7 @@ $as_echo "$as_me: executing $ac_file commands" >&6;}
     "depdir":C) $SHELL $ac_aux_dir/mkinstalldirs $DEPDIR ;;
     "gccdepdir":C)
   ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
-  for lang in $subdirs c-family common analyzer rtl-ssa
+  for lang in $subdirs c-family common analyzer text-art rtl-ssa
   do
       ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
   done ;;
diff --git a/gcc/configure.ac b/gcc/configure.ac
index cc8dd9e20bf..350d245c89f 100644
--- a/gcc/configure.ac
+++ b/gcc/configure.ac
@@ -1384,7 +1384,7 @@ AC_CHECK_HEADERS(ext/hash_map)
 ZW_CREATE_DEPDIR
 AC_CONFIG_COMMANDS([gccdepdir],[
   ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
-  for lang in $subdirs c-family common analyzer rtl-ssa
+  for lang in $subdirs c-family common analyzer text-art rtl-ssa
   do
       ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
   done], [subdirs="$subdirs" ac_aux_dir=$ac_aux_dir DEPDIR=$DEPDIR])
diff --git a/gcc/diagnostic-diagram.h b/gcc/diagnostic-diagram.h
new file mode 100644
index 00000000000..fc923c512ed
--- /dev/null
+++ b/gcc/diagnostic-diagram.h
@@ -0,0 +1,51 @@
+/* Support for diagrams within diagnostics.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_DIAGNOSTIC_DIAGRAM_H
+#define GCC_DIAGNOSTIC_DIAGRAM_H
+
+namespace text_art
+{
+  class canvas;
+} // namespace text_art
+
+/* A text art diagram, along with an "alternative text" string
+   describing it.  */
+
+class diagnostic_diagram
+{
+ public:
+  diagnostic_diagram (const text_art::canvas &canvas,
+		      const char *alt_text)
+  : m_canvas (canvas),
+    m_alt_text (alt_text)
+  {
+    gcc_assert (alt_text);
+  }
+
+  const text_art::canvas &get_canvas () const { return m_canvas; }
+  const char *get_alt_text () const { return m_alt_text; }
+
+ private:
+  const text_art::canvas &m_canvas;
+  const char *const m_alt_text;
+};
+
+#endif /* ! GCC_DIAGNOSTIC_DIAGRAM_H */
diff --git a/gcc/diagnostic-format-json.cc b/gcc/diagnostic-format-json.cc
index 694dddca9e8..539b98b5e74 100644
--- a/gcc/diagnostic-format-json.cc
+++ b/gcc/diagnostic-format-json.cc
@@ -324,6 +324,15 @@ json_file_final_cb (diagnostic_context *)
   free (filename);
 }
 
+/* Callback for diagnostic_context::m_diagrams.m_emission_cb.  */
+
+static void
+json_emit_diagram (diagnostic_context *,
+		   const diagnostic_diagram &)
+{
+  /* No-op.  */
+}
+
 /* Populate CONTEXT in preparation for JSON output (either to stderr, or
    to a file).  */
 
@@ -340,6 +349,7 @@ diagnostic_output_format_init_json (diagnostic_context *context)
   context->begin_group_cb = json_begin_group;
   context->end_group_cb =  json_end_group;
   context->print_path = NULL; /* handled in json_end_diagnostic.  */
+  context->m_diagrams.m_emission_cb = json_emit_diagram;
 
   /* The metadata is handled in JSON format, rather than as text.  */
   context->show_cwe = false;
diff --git a/gcc/diagnostic-format-sarif.cc b/gcc/diagnostic-format-sarif.cc
index fd29ac2ca3b..ac2f5b844e3 100644
--- a/gcc/diagnostic-format-sarif.cc
+++ b/gcc/diagnostic-format-sarif.cc
@@ -29,6 +29,8 @@ along with GCC; see the file COPYING3.  If not see
 #include "cpplib.h"
 #include "logical-location.h"
 #include "diagnostic-client-data-hooks.h"
+#include "diagnostic-diagram.h"
+#include "text-art/canvas.h"
 
 class sarif_builder;
 
@@ -66,8 +68,13 @@ public:
 			diagnostic_info *diagnostic,
 			diagnostic_t orig_diag_kind,
 			sarif_builder *builder);
+  void on_diagram (diagnostic_context *context,
+		   const diagnostic_diagram &diagram,
+		   sarif_builder *builder);
 
 private:
+  void add_related_location (json::object *location_obj);
+
   json::array *m_related_locations_arr;
 };
 
@@ -135,7 +142,8 @@ public:
 
   void end_diagnostic (diagnostic_context *context, diagnostic_info *diagnostic,
 		       diagnostic_t orig_diag_kind);
-
+  void emit_diagram (diagnostic_context *context,
+		     const diagnostic_diagram &diagram);
   void end_group ();
 
   void flush_to_file (FILE *outf);
@@ -144,6 +152,9 @@ public:
   json::object *make_location_object (const rich_location &rich_loc,
 				      const logical_location *logical_loc);
   json::object *make_message_object (const char *msg) const;
+  json::object *
+  make_message_object_for_diagram (diagnostic_context *context,
+				   const diagnostic_diagram &diagram);
 
 private:
   sarif_result *make_result_object (diagnostic_context *context,
@@ -261,12 +272,6 @@ sarif_result::on_nested_diagnostic (diagnostic_context *context,
 				    diagnostic_t /*orig_diag_kind*/,
 				    sarif_builder *builder)
 {
-  if (!m_related_locations_arr)
-    {
-      m_related_locations_arr = new json::array ();
-      set ("relatedLocations", m_related_locations_arr);
-    }
-
   /* We don't yet generate meaningful logical locations for notes;
      sometimes these will related to current_function_decl, but
      often they won't.  */
@@ -277,6 +282,39 @@ sarif_result::on_nested_diagnostic (diagnostic_context *context,
   pp_clear_output_area (context->printer);
   location_obj->set ("message", message_obj);
 
+  add_related_location (location_obj);
+}
+
+/* Handle diagrams that occur within a diagnostic group.
+   The closest thing in SARIF seems to be to add a location to the
+   "releatedLocations" property  (SARIF v2.1.0 section 3.27.22),
+   and to put the diagram into the "message" property of that location
+   (SARIF v2.1.0 section 3.28.5).  */
+
+void
+sarif_result::on_diagram (diagnostic_context *context,
+			  const diagnostic_diagram &diagram,
+			  sarif_builder *builder)
+{
+  json::object *location_obj = new json::object ();
+  json::object *message_obj
+    = builder->make_message_object_for_diagram (context, diagram);
+  location_obj->set ("message", message_obj);
+
+  add_related_location (location_obj);
+}
+
+/* Add LOCATION_OBJ to this result's "relatedLocations" array,
+   creating it if it doesn't yet exist.  */
+
+void
+sarif_result::add_related_location (json::object *location_obj)
+{
+  if (!m_related_locations_arr)
+    {
+      m_related_locations_arr = new json::array ();
+      set ("relatedLocations", m_related_locations_arr);
+    }
   m_related_locations_arr->append (location_obj);
 }
 
@@ -348,6 +386,18 @@ sarif_builder::end_diagnostic (diagnostic_context *context,
     }
 }
 
+/* Implementation of diagnostic_context::m_diagrams.m_emission_cb
+   for SARIF output.  */
+
+void
+sarif_builder::emit_diagram (diagnostic_context *context,
+			     const diagnostic_diagram &diagram)
+{
+  /* We must be within the emission of a top-level diagnostic.  */
+  gcc_assert (m_cur_group_result);
+  m_cur_group_result->on_diagram (context, diagram, this);
+}
+
 /* Implementation of "end_group_cb" for SARIF output.  */
 
 void
@@ -1115,6 +1165,37 @@ sarif_builder::make_message_object (const char *msg) const
   return message_obj;
 }
 
+/* Make a message object (SARIF v2.1.0 section 3.11) for DIAGRAM.
+   We emit the diagram as a code block within the Markdown part
+   of the message.  */
+
+json::object *
+sarif_builder::make_message_object_for_diagram (diagnostic_context *context,
+						const diagnostic_diagram &diagram)
+{
+  json::object *message_obj = new json::object ();
+
+  /* "text" property (SARIF v2.1.0 section 3.11.8).  */
+  message_obj->set ("text", new json::string (diagram.get_alt_text ()));
+
+  char *saved_prefix = pp_take_prefix (context->printer);
+  pp_set_prefix (context->printer, NULL);
+
+  /* "To produce a code block in Markdown, simply indent every line of
+     the block by at least 4 spaces or 1 tab."
+     Here we use 4 spaces.  */
+  diagram.get_canvas ().print_to_pp (context->printer, "    ");
+  pp_set_prefix (context->printer, saved_prefix);
+
+  /* "markdown" property (SARIF v2.1.0 section 3.11.9).  */
+  message_obj->set ("markdown",
+		    new json::string (pp_formatted_text (context->printer)));
+
+  pp_clear_output_area (context->printer);
+
+  return message_obj;
+}
+
 /* Make a multiformatMessageString object (SARIF v2.1.0 section 3.12)
    for MSG.  */
 
@@ -1630,6 +1711,16 @@ sarif_ice_handler (diagnostic_context *context)
   fnotice (stderr, "Internal compiler error:\n");
 }
 
+/* Callback for diagnostic_context::m_diagrams.m_emission_cb.  */
+
+static void
+sarif_emit_diagram (diagnostic_context *context,
+		    const diagnostic_diagram &diagram)
+{
+  gcc_assert (the_builder);
+  the_builder->emit_diagram (context, diagram);
+}
+
 /* Populate CONTEXT in preparation for SARIF output (either to stderr, or
    to a file).  */
 
@@ -1645,6 +1736,7 @@ diagnostic_output_format_init_sarif (diagnostic_context *context)
   context->end_group_cb =  sarif_end_group;
   context->print_path = NULL; /* handled in sarif_end_diagnostic.  */
   context->ice_handler_cb = sarif_ice_handler;
+  context->m_diagrams.m_emission_cb = sarif_emit_diagram;
 
   /* The metadata is handled in SARIF format, rather than as text.  */
   context->show_cwe = false;
diff --git a/gcc/diagnostic-text-art.h b/gcc/diagnostic-text-art.h
new file mode 100644
index 00000000000..a0d8a78f52a
--- /dev/null
+++ b/gcc/diagnostic-text-art.h
@@ -0,0 +1,49 @@
+/* Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_DIAGNOSTIC_TEXT_ART_H
+#define GCC_DIAGNOSTIC_TEXT_ART_H
+
+/* Values for -fdiagnostics-text-art-charset=.  */
+
+enum diagnostic_text_art_charset
+{
+  /* No text art diagrams shall be emitted.  */
+  DIAGNOSTICS_TEXT_ART_CHARSET_NONE,
+
+  /* Use pure ASCII for text art diagrams.  */
+  DIAGNOSTICS_TEXT_ART_CHARSET_ASCII,
+
+  /* Use ASCII + conservative use of other unicode characters
+     in text art diagrams.  */
+  DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE,
+
+  /* Use Emoji.  */
+  DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI
+};
+
+const enum diagnostic_text_art_charset DIAGNOSTICS_TEXT_ART_CHARSET_DEFAULT
+  = DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI;
+
+extern void
+diagnostics_text_art_charset_init (diagnostic_context *context,
+				  enum diagnostic_text_art_charset charset);
+
+
+#endif /* ! GCC_DIAGNOSTIC_TEXT_ART_H */
diff --git a/gcc/diagnostic.cc b/gcc/diagnostic.cc
index 0f093081161..7c2289f0634 100644
--- a/gcc/diagnostic.cc
+++ b/gcc/diagnostic.cc
@@ -35,11 +35,14 @@ along with GCC; see the file COPYING3.  If not see
 #include "diagnostic-metadata.h"
 #include "diagnostic-path.h"
 #include "diagnostic-client-data-hooks.h"
+#include "diagnostic-text-art.h"
+#include "diagnostic-diagram.h"
 #include "edit-context.h"
 #include "selftest.h"
 #include "selftest-diagnostic.h"
 #include "opts.h"
 #include "cpplib.h"
+#include "text-art/theme.h"
 
 #ifdef HAVE_TERMIOS_H
 # include <termios.h>
@@ -244,6 +247,10 @@ diagnostic_initialize (diagnostic_context *context, int n_opts)
   context->ice_handler_cb = NULL;
   context->includes_seen = NULL;
   context->m_client_data_hooks = NULL;
+  context->m_diagrams.m_theme = NULL;
+  context->m_diagrams.m_emission_cb = NULL;
+  diagnostics_text_art_charset_init (context,
+				     DIAGNOSTICS_TEXT_ART_CHARSET_DEFAULT);
 }
 
 /* Maybe initialize the color support. We require clients to do this
@@ -320,6 +327,12 @@ diagnostic_finish (diagnostic_context *context)
   if (context->final_cb)
     context->final_cb (context);
 
+  if (context->m_diagrams.m_theme)
+    {
+      delete context->m_diagrams.m_theme;
+      context->m_diagrams.m_theme = NULL;
+    }
+
   diagnostic_file_cache_fini ();
 
   XDELETEVEC (context->classify_diagnostic);
@@ -2174,6 +2187,33 @@ internal_error_no_backtrace (const char *gmsgid, ...)
 
   gcc_unreachable ();
 }
+
+/* Emit DIAGRAM to CONTEXT, respecting the output format.  */
+
+void
+diagnostic_emit_diagram (diagnostic_context *context,
+			 const diagnostic_diagram &diagram)
+{
+  if (context->m_diagrams.m_theme == nullptr)
+    return;
+
+  if (context->m_diagrams.m_emission_cb)
+    {
+      context->m_diagrams.m_emission_cb (context, diagram);
+      return;
+    }
+
+  /* Default implementation.  */
+  char *saved_prefix = pp_take_prefix (context->printer);
+  pp_set_prefix (context->printer, NULL);
+  /* Use a newline before and after and a two-space indent
+     to make the diagram stand out a little from the wall of text.  */
+  pp_newline (context->printer);
+  diagram.get_canvas ().print_to_pp (context->printer, "  ");
+  pp_newline (context->printer);
+  pp_set_prefix (context->printer, saved_prefix);
+  pp_flush (context->printer);
+}
 \f
 /* Special case error functions.  Most are implemented in terms of the
    above, or should be.  */
@@ -2316,6 +2356,38 @@ diagnostic_output_format_init (diagnostic_context *context,
     }
 }
 
+/* Initialize CONTEXT->m_diagrams based on CHARSET.
+   Specifically, make a text_art::theme object for m_diagrams.m_theme,
+   (or NULL for "no diagrams").  */
+
+void
+diagnostics_text_art_charset_init (diagnostic_context *context,
+				   enum diagnostic_text_art_charset charset)
+{
+  delete context->m_diagrams.m_theme;
+  switch (charset)
+    {
+    default:
+      gcc_unreachable ();
+
+    case DIAGNOSTICS_TEXT_ART_CHARSET_NONE:
+      context->m_diagrams.m_theme = NULL;
+      break;
+
+    case DIAGNOSTICS_TEXT_ART_CHARSET_ASCII:
+      context->m_diagrams.m_theme = new text_art::ascii_theme ();
+      break;
+
+    case DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE:
+      context->m_diagrams.m_theme = new text_art::unicode_theme ();
+      break;
+
+    case DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI:
+      context->m_diagrams.m_theme = new text_art::emoji_theme ();
+      break;
+    }
+}
+
 /* Implementation of diagnostic_path::num_events vfunc for
    simple_diagnostic_path: simply get the number of events in the vec.  */
 
diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
index 9a51097f146..00b828f230d 100644
--- a/gcc/diagnostic.h
+++ b/gcc/diagnostic.h
@@ -24,6 +24,11 @@ along with GCC; see the file COPYING3.  If not see
 #include "pretty-print.h"
 #include "diagnostic-core.h"
 
+namespace text_art
+{
+  class theme;
+} // namespace text_art
+
 /* An enum for controlling what units to use for the column number
    when diagnostics are output, used by the -fdiagnostics-column-unit option.
    Tabs will be expanded or not according to the value of -ftabstop.  The origin
@@ -170,6 +175,7 @@ class edit_context;
 namespace json { class value; }
 class diagnostic_client_data_hooks;
 class logical_location;
+class diagnostic_diagram;
 
 /* This data structure bundles altogether any information relevant to
    the context of a diagnostic message.  */
@@ -417,6 +423,18 @@ struct diagnostic_context
      Used by SARIF output to give metadata about the client that's
      producing diagnostics.  */
   diagnostic_client_data_hooks *m_client_data_hooks;
+
+  /* Support for diagrams.  */
+  struct
+  {
+    /* Theme to use when generating diagrams.
+       Can be NULL (if text art is disabled).  */
+    text_art::theme *m_theme;
+
+    /* Callback for emitting diagrams.  */
+    void (*m_emission_cb) (diagnostic_context *context,
+			   const diagnostic_diagram &diagram);
+  } m_diagrams;
 };
 
 inline void
@@ -619,4 +637,7 @@ extern bool warning_enabled_at (location_t, int);
 
 extern char *get_cwe_url (int cwe);
 
+extern void diagnostic_emit_diagram (diagnostic_context *context,
+				     const diagnostic_diagram &diagram);
+
 #endif /* ! GCC_DIAGNOSTIC_H */
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index 898a88ce33e..023a56a647e 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -316,7 +316,8 @@ Objective-C and Objective-C++ Dialects}.
 -fno-show-column
 -fdiagnostics-column-unit=@r{[}display@r{|}byte@r{]}
 -fdiagnostics-column-origin=@var{origin}
--fdiagnostics-escape-format=@r{[}unicode@r{|}bytes@r{]}}
+-fdiagnostics-escape-format=@r{[}unicode@r{|}bytes@r{]}
+-fdiagnostics-text-art-charset=@r{[}none@r{|}ascii@r{|}unicode@r{|}emoji@r{]}}
 
 @item Warning Options
 @xref{Warning Options,,Options to Request or Suppress Warnings}.
@@ -5066,7 +5067,8 @@ options:
 -fno-diagnostics-show-line-numbers
 -fdiagnostics-color=never
 -fdiagnostics-urls=never
--fdiagnostics-path-format=separate-events}
+-fdiagnostics-path-format=separate-events
+-fdiagnostics-text-art-charset=none}
 In the future, if GCC changes the default appearance of its diagnostics, the
 corresponding option to disable the new behavior will be added to this list.
 
@@ -5592,6 +5594,25 @@ Unicode characters.  For the example above, the following will be printed:
  before<CF><80><BF>after
 @end smallexample
 
+@opindex fdiagnostics-text-art-charset
+@item -fdiagnostics-text-art-charset=@var{CHARSET}
+Some diagnostics can contain ``text art'' diagrams: visualizations created
+from text, intended to be viewed in a monospaced font.
+
+This option selects which characters should be used for printing such
+diagrams, if any.  @var{CHARSET} is @samp{none}, @samp{ascii}, @samp{unicode},
+or @samp{emoji}.
+
+The @samp{none} value suppresses the printing of such diagrams.
+The @samp{ascii} value will ensure that such diagrams are pure ASCII
+(``ASCII art'').  The @samp{unicode} value will allow for conservative use of
+unicode drawing characters (such as box-drawing characters).  The @samp{emoji}
+value further adds the possibility of emoji in the output (such as emitting
+U+26A0 WARNING SIGN followed by U+FE0F VARIATION SELECTOR-16 to select the
+emoji variant of the character).
+
+The default is @samp{emoji}.
+
 @opindex fdiagnostics-format
 @item -fdiagnostics-format=@var{FORMAT}
 Select a different format for printing diagnostics.
diff --git a/gcc/gcc.cc b/gcc/gcc.cc
index 2ccca00d603..f9f0a7eaad4 100644
--- a/gcc/gcc.cc
+++ b/gcc/gcc.cc
@@ -46,6 +46,7 @@ compilation is specified by a string called a "spec".  */
 #include "spellcheck.h"
 #include "opts-jobserver.h"
 #include "common/common-target.h"
+#include "diagnostic-text-art.h"
 
 \f
 
@@ -4299,6 +4300,11 @@ driver_handle_option (struct gcc_options *opts,
 	  break;
 	}
 
+    case OPT_fdiagnostics_text_art_charset_:
+      diagnostics_text_art_charset_init (dc,
+					 (enum diagnostic_text_art_charset)value);
+      break;
+
     case OPT_Wa_:
       {
 	int prev, j;
diff --git a/gcc/opts-common.cc b/gcc/opts-common.cc
index 23ddcaa3b55..f0c5f483665 100644
--- a/gcc/opts-common.cc
+++ b/gcc/opts-common.cc
@@ -1068,6 +1068,7 @@ decode_cmdline_options_to_array (unsigned int argc, const char **argv,
 	    "-fdiagnostics-color=never",
 	    "-fdiagnostics-urls=never",
 	    "-fdiagnostics-path-format=separate-events",
+	    "-fdiagnostics-text-art-charset=none"
 	  };
 	  const int num_expanded = ARRAY_SIZE (expanded_args);
 	  opt_array_len += num_expanded - 1;
diff --git a/gcc/opts.cc b/gcc/opts.cc
index 86b94d62b58..3087bdac2c6 100644
--- a/gcc/opts.cc
+++ b/gcc/opts.cc
@@ -35,6 +35,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "version.h"
 #include "selftest.h"
 #include "file-prefix-map.h"
+#include "diagnostic-text-art.h"
 
 /* In this file all option sets are explicit.  */
 #undef OPTION_SET_P
@@ -2887,6 +2888,11 @@ common_handle_option (struct gcc_options *opts,
 	  break;
 	}
 
+    case OPT_fdiagnostics_text_art_charset_:
+      diagnostics_text_art_charset_init (dc,
+					 (enum diagnostic_text_art_charset)value);
+      break;
+
     case OPT_fdiagnostics_parseable_fixits:
       dc->extra_output_kind = (value
 			       ? EXTRA_DIAGNOSTIC_OUTPUT_fixits_v1
diff --git a/gcc/pretty-print.cc b/gcc/pretty-print.cc
index 7d294717f50..3d789a23812 100644
--- a/gcc/pretty-print.cc
+++ b/gcc/pretty-print.cc
@@ -1828,6 +1828,35 @@ pp_string (pretty_printer *pp, const char *str)
   pp_maybe_wrap_text (pp, str, str + strlen (str));
 }
 
+/* Append code point C to the output area of PRETTY-PRINTER, encoding it
+   as UTF-8.  */
+
+void
+pp_unicode_character (pretty_printer *pp, unsigned c)
+{
+  static const uchar masks[6] =  { 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
+  static const uchar limits[6] = { 0x80, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE };
+  size_t nbytes;
+  uchar buf[6], *p = &buf[6];
+
+  nbytes = 1;
+  if (c < 0x80)
+    *--p = c;
+  else
+    {
+      do
+	{
+	  *--p = ((c & 0x3F) | 0x80);
+	  c >>= 6;
+	  nbytes++;
+	}
+      while (c >= 0x3F || (c & limits[nbytes-1]));
+      *--p = (c | masks[nbytes-1]);
+    }
+
+  pp_append_r (pp, (const char *)p, nbytes);
+}
+
 /* Append the leading N characters of STRING to the output area of
    PRETTY-PRINTER, quoting in hexadecimal non-printable characters.
    Setting N = -1 is as if N were set to strlen (STRING).  The STRING
diff --git a/gcc/pretty-print.h b/gcc/pretty-print.h
index 0230a289df5..369be6e7ba7 100644
--- a/gcc/pretty-print.h
+++ b/gcc/pretty-print.h
@@ -401,6 +401,7 @@ extern void pp_indent (pretty_printer *);
 extern void pp_newline (pretty_printer *);
 extern void pp_character (pretty_printer *, int);
 extern void pp_string (pretty_printer *, const char *);
+extern void pp_unicode_character (pretty_printer *, unsigned);
 
 extern void pp_write_text_to_stream (pretty_printer *);
 extern void pp_write_text_as_dot_label_to_stream (pretty_printer *, bool);
diff --git a/gcc/selftest-run-tests.cc b/gcc/selftest-run-tests.cc
index 915f2129702..e2fc8f84b1b 100644
--- a/gcc/selftest-run-tests.cc
+++ b/gcc/selftest-run-tests.cc
@@ -28,6 +28,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "stringpool.h"
 #include "attribs.h"
 #include "analyzer/analyzer-selftests.h"
+#include "text-art/selftests.h"
 
 /* This function needed to be split out from selftest.cc as it references
    tests from the whole source tree, and so is within
@@ -118,6 +119,8 @@ selftest::run_tests ()
   /* Run any lang-specific selftests.  */
   lang_hooks.run_lang_selftests ();
 
+  text_art_tests ();
+
   /* Run the analyzer selftests (if enabled).  */
   ana::selftest::run_analyzer_selftests ();
 
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
new file mode 100644
index 00000000000..e4239aab032
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
@@ -0,0 +1,57 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fdiagnostics-color=never" } */
+
+int non_empty;
+
+/* { dg-begin-multiline-output "" }
+
+  A
+   B
+    C
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
+  ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
+  
+  
+  
+  
+  ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
+  ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  +--+
+  |🙂|
+  +--+
+
+   { dg-end-multiline-output "" } */
+/* { dg-begin-multiline-output "" }
+
+  +-------+-----+---------------+---------------------+-----------------------+-----------------------+
+  |Offsets|Octet|       0       |          1          |           2           |           3           |
+  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+  | Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
+  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+  |   0   |  0  |Version|  IHL  |     DSCP      | ECN |                 Total Length                  |
+  +-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+
+  |   4   | 32  |           Identification            | Flags  |           Fragment Offset            |
+  +-------+-----+---------------+---------------------+--------+--------------------------------------+
+  |   8   | 64  | Time To Live  |      Protocol       |                Header Checksum                |
+  +-------+-----+---------------+---------------------+-----------------------------------------------+
+  |  12   | 96  |                                  Source IP Address                                  |
+  +-------+-----+-------------------------------------------------------------------------------------+
+  |  16   | 128 |                               Destination IP Address                                |
+  +-------+-----+-------------------------------------------------------------------------------------+
+  |  20   | 160 |                                                                                     |
+  +-------+-----+                                                                                     |
+  |  ...  | ... |                                       Options                                       |
+  +-------+-----+                                                                                     |
+  |  56   | 448 |                                                                                     |
+  +-------+-----+-------------------------------------------------------------------------------------+
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
new file mode 100644
index 00000000000..0650428b1ce
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
@@ -0,0 +1,58 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fdiagnostics-color=always" } */
+
+int non_empty;
+
+/* { dg-begin-multiline-output "" }
+
+  A
+   B
+    C
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ^[[38;2;0;0;0;48;2;240;217;181m^[[K♜ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♞ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♝ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♛ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♚ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♝ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♞ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♜ ^[[m^[[K
+  ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[m^[[K
+  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
+  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
+  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
+  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
+  ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[m^[[K
+  ^[[38;2;255;255;255;48;2;181;136;99m^[[K♖ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♘ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♗ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♕ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♔ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♗ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♘ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♖ ^[[m^[[K
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  +--+
+  |🙂|
+  +--+
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  +-------+-----+---------------+---------------------+-----------------------+-----------------------+
+  |Offsets|Octet|       0       |          1          |           2           |           3           |
+  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+  | Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
+  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+  |   0   |  0  |Version|  IHL  |     DSCP      | ECN |                 Total Length                  |
+  +-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+
+  |   4   | 32  |           Identification            | Flags  |           Fragment Offset            |
+  +-------+-----+---------------+---------------------+--------+--------------------------------------+
+  |   8   | 64  | Time To Live  |      Protocol       |                Header Checksum                |
+  +-------+-----+---------------+---------------------+-----------------------------------------------+
+  |  12   | 96  |                                  Source IP Address                                  |
+  +-------+-----+-------------------------------------------------------------------------------------+
+  |  16   | 128 |                               Destination IP Address                                |
+  +-------+-----+-------------------------------------------------------------------------------------+
+  |  20   | 160 |                                                                                     |
+  +-------+-----+                                                                                     |
+  |  ...  | ... |                                       Options                                       |
+  +-------+-----+                                                                                     |
+  |  56   | 448 |                                                                                     |
+  +-------+-----+-------------------------------------------------------------------------------------+
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
new file mode 100644
index 00000000000..c8118b46759
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
@@ -0,0 +1,5 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=none" } */
+
+int non_empty;
+
+/* We expect no output.  */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
new file mode 100644
index 00000000000..c9f5b36571a
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
@@ -0,0 +1,58 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -fdiagnostics-color=never" } */
+
+int non_empty;
+
+/* { dg-begin-multiline-output "" }
+
+  A
+   B
+    C
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
+  ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
+  
+  
+  
+  
+  ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
+  ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──┐
+  │🙂│
+  └──┘
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐
+  │Offsets│Octet│       0       │          1          │           2           │           3           │
+  ├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤
+  │ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│
+  ├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤
+  │   0   │  0  │Version│  IHL  │     DSCP      │ ECN │                 Total Length                  │
+  ├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤
+  │   4   │ 32  │           Identification            │ Flags  │           Fragment Offset            │
+  ├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤
+  │   8   │ 64  │ Time To Live  │      Protocol       │                Header Checksum                │
+  ├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤
+  │  12   │ 96  │                                  Source IP Address                                  │
+  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+  │  16   │ 128 │                               Destination IP Address                                │
+  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+  │  20   │ 160 │                                                                                     │
+  ├───────┼─────┤                                                                                     │
+  │  ...  │ ... │                                       Options                                       │
+  ├───────┼─────┤                                                                                     │
+  │  56   │ 448 │                                                                                     │
+  └───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
new file mode 100644
index 00000000000..f402836f889
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
@@ -0,0 +1,59 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -fdiagnostics-color=always" } */
+
+int non_empty;
+
+
+/* { dg-begin-multiline-output "" }
+
+  A
+   B
+    C
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ^[[38;2;0;0;0;48;2;240;217;181m^[[K♜ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♞ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♝ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♛ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♚ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♝ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♞ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♜ ^[[m^[[K
+  ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[m^[[K
+  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
+  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
+  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
+  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
+  ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[m^[[K
+  ^[[38;2;255;255;255;48;2;181;136;99m^[[K♖ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♘ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♗ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♕ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♔ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♗ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♘ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♖ ^[[m^[[K
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──┐
+  │🙂│
+  └──┘
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐
+  │Offsets│Octet│       0       │          1          │           2           │           3           │
+  ├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤
+  │ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│
+  ├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤
+  │   0   │  0  │Version│  IHL  │     DSCP      │ ECN │                 Total Length                  │
+  ├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤
+  │   4   │ 32  │           Identification            │ Flags  │           Fragment Offset            │
+  ├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤
+  │   8   │ 64  │ Time To Live  │      Protocol       │                Header Checksum                │
+  ├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤
+  │  12   │ 96  │                                  Source IP Address                                  │
+  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+  │  16   │ 128 │                               Destination IP Address                                │
+  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
+  │  20   │ 160 │                                                                                     │
+  ├───────┼─────┤                                                                                     │
+  │  ...  │ ... │                                       Options                                       │
+  ├───────┼─────┤                                                                                     │
+  │  56   │ 448 │                                                                                     │
+  └───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
new file mode 100644
index 00000000000..27c341b9f2f
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
@@ -0,0 +1,257 @@
+/* { dg-options "-O" } */
+
+/* This plugin exercises the text_art code.  */
+
+#include "gcc-plugin.h"
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "plugin-version.h"
+#include "diagnostic.h"
+#include "diagnostic-diagram.h"
+#include "text-art/canvas.h"
+#include "text-art/table.h"
+
+int plugin_is_GPL_compatible;
+
+using namespace text_art;
+
+/* Canvas tests.  */
+
+static void
+emit_canvas (const canvas &c, const char *alt_text)
+{
+  diagnostic_diagram diagram (c, alt_text);
+  diagnostic_emit_diagram (global_dc, diagram);
+}
+
+static void
+test_abc ()
+{
+  style_manager sm;
+  canvas c (canvas::size_t (3, 3), sm);
+  c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
+  c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
+  c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));
+  emit_canvas (c, "test_abc");
+}
+
+/* Test of procedural art using 24-bit color: chess starting position.  */
+
+static void
+test_chessboard ()
+{
+  /* With the exception of NONE, these are in order of the chess symbols
+     in the Unicode Miscellaneous Symbols block.  */
+  enum class piece { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN, NONE };
+  enum class color { BLACK, WHITE, NONE };
+
+  style_manager sm;
+
+  /* We assume double-column chars for the pieces, so allow two canvas
+     columns per square.  */
+  canvas canvas (canvas::size_t (16, 8), sm);
+
+  for (int x = 0; x < 8; x++)
+    for (int y = 0; y < 8; y++)
+      {
+	enum piece piece_kind;
+	enum color piece_color;
+	switch (y)
+	  {
+	  case 0:
+	  case 7:
+	    switch (x)
+	      {
+	      default:
+		gcc_unreachable ();
+	      case 0:
+		piece_kind = piece::ROOK;
+		break;
+	      case 1:
+		piece_kind = piece::KNIGHT;
+		break;
+	      case 2:
+		piece_kind = piece::BISHOP;
+		break;
+	      case 3:
+		piece_kind = piece::QUEEN;
+		break;
+	      case 4:
+		piece_kind = piece::KING;
+		break;
+	      case 5:
+		piece_kind = piece::BISHOP;
+		break;
+	      case 6:
+		piece_kind = piece::KNIGHT;
+		break;
+	      case 7:
+		piece_kind = piece::ROOK;
+	      break;
+	      }
+	    piece_color = (y == 0) ? color::BLACK : color::WHITE;
+	    break;
+	  case 1:
+	  case 6:
+	    piece_kind = piece::PAWN;
+	    piece_color = (y == 1) ? color::BLACK : color::WHITE;
+	    break;
+	  default:
+	    piece_kind = piece::NONE;
+	    piece_color = color::NONE;
+	    break;
+	  }
+
+	style s;
+	const bool white_square = (x + y) % 2 == 0;
+	if (white_square)
+	  s.m_bg_color = style::color (0xf0, 0xd9, 0xb5);
+	else
+	  s.m_bg_color = style::color (0xb5, 0x88, 0x63);
+	switch (piece_color)
+	  {
+	  default:
+	    gcc_unreachable ();
+	  case color::WHITE:
+	    s.m_fg_color = style::color (0xff, 0xff, 0xff);
+	    break;
+	  case color::BLACK:
+	    s.m_fg_color = style::color (0x00, 0x00, 0x00);
+	    break;
+	  case color::NONE:
+	    break;
+	  }
+	style::id_t style_id = sm.get_or_create_id (s);
+
+	cppchar_t ch;
+	if (piece_kind == piece::NONE)
+	  ch = ' ';
+	else
+	  {
+	    const cppchar_t WHITE_KING = 0x2654;
+	    const cppchar_t BLACK_KING = 0x265A;
+	    cppchar_t base ((piece_color == color::WHITE)
+			    ? WHITE_KING : BLACK_KING);
+	    ch = base + ((int)piece_kind - (int)piece::KING);
+	  }
+	canvas.paint (canvas::coord_t (x * 2, y),
+		      canvas::cell_t (ch, false, style_id));
+	canvas.paint (canvas::coord_t (x * 2 + 1, y),
+		      canvas::cell_t (' ', false, style_id));
+      }
+  emit_canvas (canvas, "test_chessboard");
+}
+
+/* Table tests.  */
+
+static void
+emit_table (const table &table, const style_manager &sm, const char *alt_text)
+{
+  const text_art::theme *theme = global_dc->m_diagrams.m_theme;
+  if (!theme)
+    return;
+  canvas c (table.to_canvas (*theme, sm));
+  emit_canvas (c, alt_text);
+}
+
+static void
+test_double_width_chars ()
+{
+  style_manager sm;
+  table table (table::size_t (1, 1));
+  table.set_cell (table::coord_t (0,0),
+		  styled_string ((cppchar_t)0x1f642));
+
+  emit_table (table, sm, "test_double_width_chars");
+}
+
+static void
+test_ipv4_header ()
+{
+  style_manager sm;
+  table table (table::size_t (34, 10));
+  table.set_cell (table::coord_t (0, 0), styled_string (sm, "Offsets"));
+  table.set_cell (table::coord_t (1, 0), styled_string (sm, "Octet"));
+  table.set_cell (table::coord_t (0, 1), styled_string (sm, "Octet"));
+  for (int octet = 0; octet < 4; octet++)
+    table.set_cell_span (table::rect_t (table::coord_t (2 + (octet * 8), 0),
+					table::size_t (8, 1)),
+			 styled_string::from_fmt (sm, nullptr, "%i", octet));
+  table.set_cell (table::coord_t (1, 1), styled_string (sm, "Bit"));
+  for (int bit = 0; bit < 32; bit++)
+    table.set_cell (table::coord_t (bit + 2, 1),
+		    styled_string::from_fmt (sm, nullptr, "%i", bit));
+  for (int word = 0; word < 6; word++)
+    {
+      table.set_cell (table::coord_t (0, word + 2),
+		      styled_string::from_fmt (sm, nullptr, "%i", word * 4));
+      table.set_cell (table::coord_t (1, word + 2),
+		      styled_string::from_fmt (sm, nullptr, "%i", word * 32));
+    }
+
+  table.set_cell (table::coord_t (0, 8), styled_string (sm, "..."));
+  table.set_cell (table::coord_t (1, 8), styled_string (sm, "..."));
+  table.set_cell (table::coord_t (0, 9), styled_string (sm, "56"));
+  table.set_cell (table::coord_t (1, 9), styled_string (sm, "448"));
+
+#define SET_BITS(FIRST, LAST, NAME)					\
+  do {									\
+    const int first = (FIRST);						\
+    const int last = (LAST);						\
+    const char *name = (NAME);						\
+    const int row = first / 32;						\
+    gcc_assert (last / 32 == row);					\
+    table::rect_t rect (table::coord_t ((first % 32) + 2, row + 2),	\
+			table::size_t (last + 1 - first , 1));		\
+    table.set_cell_span (rect, styled_string (sm, name));		\
+  } while (0)
+
+  SET_BITS (0, 3, "Version");
+  SET_BITS (4, 7, "IHL");
+  SET_BITS (8, 13, "DSCP");
+  SET_BITS (14, 15, "ECN");
+  SET_BITS (16, 31, "Total Length");
+
+  SET_BITS (32 +  0, 32 + 15, "Identification");
+  SET_BITS (32 + 16, 32 + 18, "Flags");
+  SET_BITS (32 + 19, 32 + 31, "Fragment Offset");
+
+  SET_BITS (64 +  0, 64 +  7, "Time To Live");
+  SET_BITS (64 +  8, 64 + 15, "Protocol");
+  SET_BITS (64 + 16, 64 + 31, "Header Checksum");
+
+  SET_BITS (96 +  0, 96 + 31, "Source IP Address");
+  SET_BITS (128 +  0, 128 + 31, "Destination IP Address");
+
+  table.set_cell_span(table::rect_t (table::coord_t (2, 7),
+				     table::size_t (32, 3)),
+		      styled_string (sm, "Options"));
+
+  emit_table (table, sm, "test_ipv4_header");
+}
+
+static void
+show_diagrams ()
+{
+  test_abc ();
+  test_chessboard ();
+  test_double_width_chars ();
+  test_ipv4_header ();
+}
+
+int
+plugin_init (struct plugin_name_args *plugin_info,
+	     struct plugin_gcc_version *version)
+{
+  const char *plugin_name = plugin_info->base_name;
+  int argc = plugin_info->argc;
+  struct plugin_argument *argv = plugin_info->argv;
+
+  if (!plugin_default_version_check (version, &gcc_version))
+    return 1;
+
+  show_diagrams ();
+
+  return 0;
+}
diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp b/gcc/testsuite/gcc.dg/plugin/plugin.exp
index 4d6304cd100..60723a20eda 100644
--- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
+++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
@@ -114,6 +114,12 @@ set plugin_test_list [list \
 	  diagnostic-path-format-inline-events-1.c \
 	  diagnostic-path-format-inline-events-2.c \
 	  diagnostic-path-format-inline-events-3.c } \
+    { diagnostic_plugin_test_text_art.c \
+	  diagnostic-test-text-art-none.c \
+	  diagnostic-test-text-art-ascii-bw.c \
+	  diagnostic-test-text-art-ascii-color.c \
+	  diagnostic-test-text-art-unicode-bw.c \
+	  diagnostic-test-text-art-unicode-color.c } \
     { location_overflow_plugin.c \
 	  location-overflow-test-1.c \
 	  location-overflow-test-2.c \
diff --git a/gcc/text-art/box-drawing-chars.inc b/gcc/text-art/box-drawing-chars.inc
new file mode 100644
index 00000000000..a370255d56d
--- /dev/null
+++ b/gcc/text-art/box-drawing-chars.inc
@@ -0,0 +1,18 @@
+/* Generated by contrib/unicode/gen-box-drawing-chars.py.  */
+
+0x0020, /* " ": U+0020: SPACE */
+0x2576, /* "╶": U+2576: BOX DRAWINGS LIGHT RIGHT */
+0x2574, /* "╴": U+2574: BOX DRAWINGS LIGHT LEFT */
+0x2500, /* "─": U+2500: BOX DRAWINGS LIGHT HORIZONTAL */
+0x2577, /* "╷": U+2577: BOX DRAWINGS LIGHT DOWN */
+0x250C, /* "┌": U+250C: BOX DRAWINGS LIGHT DOWN AND RIGHT */
+0x2510, /* "┐": U+2510: BOX DRAWINGS LIGHT DOWN AND LEFT */
+0x252C, /* "┬": U+252C: BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */
+0x2575, /* "╵": U+2575: BOX DRAWINGS LIGHT UP */
+0x2514, /* "└": U+2514: BOX DRAWINGS LIGHT UP AND RIGHT */
+0x2518, /* "┘": U+2518: BOX DRAWINGS LIGHT UP AND LEFT */
+0x2534, /* "┴": U+2534: BOX DRAWINGS LIGHT UP AND HORIZONTAL */
+0x2502, /* "│": U+2502: BOX DRAWINGS LIGHT VERTICAL */
+0x251C, /* "├": U+251C: BOX DRAWINGS LIGHT VERTICAL AND RIGHT */
+0x2524, /* "┤": U+2524: BOX DRAWINGS LIGHT VERTICAL AND LEFT */
+0x253C  /* "┼": U+253C: BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */
diff --git a/gcc/text-art/box-drawing.cc b/gcc/text-art/box-drawing.cc
new file mode 100644
index 00000000000..981d0b095cf
--- /dev/null
+++ b/gcc/text-art/box-drawing.cc
@@ -0,0 +1,72 @@
+/* Procedural lookup of box drawing characters.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "text-art/box-drawing.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+
+
+/* According to
+     https://en.wikipedia.org/wiki/Box-drawing_character#Character_code
+   "DOS line- and box-drawing characters are not ordered in any programmatic
+   manner, so calculating a particular character shape needs to use a look-up
+   table. "
+   Hence this array.  */
+static const cppchar_t box_drawing_chars[] = {
+#include "text-art/box-drawing-chars.inc"
+};
+
+cppchar_t
+text_art::get_box_drawing_char (directions line_dirs)
+{
+  const size_t idx = line_dirs.as_index ();
+  gcc_assert (idx < 16);
+  return box_drawing_chars[idx];
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Run all selftests in this file.  */
+
+void
+text_art_box_drawing_cc_tests ()
+{
+  ASSERT_EQ (text_art::get_box_drawing_char
+	      (text_art::directions (false, false, false, false)),
+	     ' ');
+  ASSERT_EQ (text_art::get_box_drawing_char
+	       (text_art::directions (false, false, true, true)),
+	     0x2500); /* BOX DRAWINGS LIGHT HORIZONTAL */
+  ASSERT_EQ (text_art::get_box_drawing_char
+	       (text_art::directions (true, true, false, false)),
+	     0x2502); /* BOX DRAWINGS LIGHT VERTICAL */
+  ASSERT_EQ (text_art::get_box_drawing_char
+	       (text_art::directions (true, false, true, false)),
+	     0x2518); /* BOX DRAWINGS LIGHT UP AND LEFT */
+}
+
+} // namespace selftest
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/box-drawing.h b/gcc/text-art/box-drawing.h
new file mode 100644
index 00000000000..29f4d9921b3
--- /dev/null
+++ b/gcc/text-art/box-drawing.h
@@ -0,0 +1,32 @@
+/* Procedural lookup of box drawing characters.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_BOX_DRAWING_H
+#define GCC_TEXT_ART_BOX_DRAWING_H
+
+#include "text-art/types.h"
+
+namespace text_art {
+
+extern cppchar_t get_box_drawing_char (directions line_dirs);
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_BOX_DRAWING_H */
diff --git a/gcc/text-art/canvas.cc b/gcc/text-art/canvas.cc
new file mode 100644
index 00000000000..f229612c919
--- /dev/null
+++ b/gcc/text-art/canvas.cc
@@ -0,0 +1,437 @@
+/* Canvas for random-access procedural text art.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/canvas.h"
+
+using namespace text_art;
+
+canvas::canvas (size_t size, const style_manager &style_mgr)
+: m_cells (size_t (size.w, size.h)),
+  m_style_mgr (style_mgr)
+{
+  m_cells.fill (cell_t (' '));
+}
+
+void
+canvas::paint (coord_t coord, styled_unichar ch)
+{
+  m_cells.set (coord, std::move (ch));
+}
+
+void
+canvas::paint_text (coord_t coord, const styled_string &text)
+{
+  for (auto ch : text)
+    {
+      paint (coord, ch);
+      if (ch.double_width_p ())
+	coord.x += 2;
+      else
+	coord.x++;
+    }
+}
+
+void
+canvas::fill (rect_t rect, cell_t c)
+{
+  for (int y = rect.get_min_y (); y < rect.get_next_y (); y++)
+    for (int x = rect.get_min_x (); x < rect.get_next_x (); x++)
+      paint(coord_t (x, y), c);
+}
+
+void
+canvas::debug_fill ()
+{
+  fill (rect_t (coord_t (0, 0), get_size ()), cell_t ('*'));
+}
+
+void
+canvas::print_to_pp (pretty_printer *pp,
+		     const char *per_line_prefix) const
+{
+  for (int y = 0; y < m_cells.get_size ().h; y++)
+    {
+      style::id_t curr_style_id = 0;
+      if (per_line_prefix)
+	pp_string (pp, per_line_prefix);
+
+      pretty_printer line_pp;
+      line_pp.show_color = pp->show_color;
+      line_pp.url_format = pp->url_format;
+      const int final_x_in_row = get_final_x_in_row (y);
+      for (int x = 0; x <= final_x_in_row; x++)
+	{
+	  if (x > 0)
+	    {
+	      const cell_t prev_cell = m_cells.get (coord_t (x - 1, y));
+	      if (prev_cell.double_width_p ())
+		 /* This cell is just a placeholder for the
+		    2nd column of a double width cell; skip it.  */
+		continue;
+	    }
+	  const cell_t cell = m_cells.get (coord_t (x, y));
+	  if (cell.get_style_id () != curr_style_id)
+	    {
+	      m_style_mgr.print_any_style_changes (&line_pp,
+						   curr_style_id,
+						   cell.get_style_id ());
+	      curr_style_id = cell.get_style_id ();
+	    }
+	  pp_unicode_character (&line_pp, cell.get_code ());
+	  if (cell.emoji_variant_p ())
+	    /* Append U+FE0F VARIATION SELECTOR-16 to select the emoji
+	       variation of the char.  */
+	    pp_unicode_character (&line_pp, 0xFE0F);
+	}
+      /* Reset the style at the end of each line.  */
+      m_style_mgr.print_any_style_changes (&line_pp, curr_style_id, 0);
+
+      /* Print from line_pp to pp, stripping trailing whitespace from
+	 the line.  */
+      const char *line_buf = pp_formatted_text (&line_pp);
+      ::size_t len = strlen (line_buf);
+      while (len > 0)
+	{
+	  if (line_buf[len - 1] == ' ')
+	    len--;
+	  else
+	    break;
+	}
+      pp_append_text (pp, line_buf, line_buf + len);
+      pp_newline (pp);
+    }
+}
+
+DEBUG_FUNCTION void
+canvas::debug (bool styled) const
+{
+  pretty_printer pp;
+  if (styled)
+    {
+      pp_show_color (&pp) = true;
+      pp.url_format = determine_url_format (DIAGNOSTICS_URL_AUTO);
+    }
+  print_to_pp (&pp);
+  fprintf (stderr, "%s\n", pp_formatted_text (&pp));
+}
+
+/* Find right-most non-default cell in this row,
+   or -1 if all are default.  */
+
+int
+canvas::get_final_x_in_row (int y) const
+{
+  for (int x = m_cells.get_size ().w - 1; x >= 0; x--)
+    {
+      cell_t cell = m_cells.get (coord_t (x, y));
+      if (cell.get_code () != ' '
+	  || cell.get_style_id () != style::id_plain)
+	return x;
+    }
+  return -1;
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+static void
+test_blank ()
+{
+  style_manager sm;
+  canvas c (canvas::size_t (5, 5), sm);
+  ASSERT_CANVAS_STREQ (c, false,
+		       ("\n"
+			"\n"
+			"\n"
+			"\n"
+			"\n"));
+}
+
+static void
+test_abc ()
+{
+  style_manager sm;
+  canvas c (canvas::size_t (3, 3), sm);
+  c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
+  c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
+  c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));
+
+  ASSERT_CANVAS_STREQ (c, false,
+		       "A\n B\n  C\n");
+}
+
+static void
+test_debug_fill ()
+{
+  style_manager sm;
+  canvas c (canvas::size_t (5, 3), sm);
+  c.debug_fill();
+  ASSERT_CANVAS_STREQ (c, false,
+		       ("*****\n"
+			"*****\n"
+			"*****\n"));
+}
+
+static void
+test_text ()
+{
+  style_manager sm;
+  canvas c (canvas::size_t (6, 1), sm);
+  c.paint_text (canvas::coord_t (0, 0), styled_string (sm, "012345"));
+  ASSERT_CANVAS_STREQ (c, false,
+		       ("012345\n"));
+
+  /* Paint an emoji character that should occupy two canvas columns when
+     printed.  */
+  c.paint_text (canvas::coord_t (2, 0), styled_string ((cppchar_t)0x1f642));
+  ASSERT_CANVAS_STREQ (c, false,
+		       ("01🙂45\n"));
+}
+
+static void
+test_circle ()
+{
+  canvas::size_t sz (30, 30);
+  style_manager sm;
+  canvas canvas (sz, sm);
+  canvas::coord_t center (sz.w / 2, sz.h / 2);
+  const int radius = 12;
+  const int radius_squared = radius * radius;
+  for (int x = 0; x < sz.w; x++)
+    for (int y = 0; y < sz.h; y++)
+      {
+	int dx = x - center.x;
+	int dy = y - center.y;
+	char ch = "AB"[(x + y) % 2];
+	if (dx * dx + dy * dy < radius_squared)
+	  canvas.paint (canvas::coord_t (x, y), styled_unichar (ch));
+      }
+  ASSERT_CANVAS_STREQ
+    (canvas, false,
+     ("\n"
+      "\n"
+      "\n"
+      "\n"
+      "           BABABABAB\n"
+      "         ABABABABABABA\n"
+      "        ABABABABABABABA\n"
+      "       ABABABABABABABABA\n"
+      "      ABABABABABABABABABA\n"
+      "     ABABABABABABABABABABA\n"
+      "     BABABABABABABABABABAB\n"
+      "    BABABABABABABABABABABAB\n"
+      "    ABABABABABABABABABABABA\n"
+      "    BABABABABABABABABABABAB\n"
+      "    ABABABABABABABABABABABA\n"
+      "    BABABABABABABABABABABAB\n"
+      "    ABABABABABABABABABABABA\n"
+      "    BABABABABABABABABABABAB\n"
+      "    ABABABABABABABABABABABA\n"
+      "    BABABABABABABABABABABAB\n"
+      "     BABABABABABABABABABAB\n"
+      "     ABABABABABABABABABABA\n"
+      "      ABABABABABABABABABA\n"
+      "       ABABABABABABABABA\n"
+      "        ABABABABABABABA\n"
+      "         ABABABABABABA\n"
+      "           BABABABAB\n"
+      "\n"
+      "\n"
+      "\n"));
+}
+
+static void
+test_color_circle ()
+{
+  const canvas::size_t sz (10, 10);
+  const canvas::coord_t center (sz.w / 2, sz.h / 2);
+  const int outer_r2 = 25;
+  const int inner_r2 = 10;
+  style_manager sm;
+  canvas c (sz, sm);
+  for (int x = 0; x < sz.w; x++)
+    for (int y = 0; y < sz.h; y++)
+      {
+	const int dist_from_center_squared
+	  = ((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y));
+	if (dist_from_center_squared < outer_r2)
+	  {
+	    style s;
+	    if (dist_from_center_squared < inner_r2)
+	      s.m_fg_color = style::named_color::RED;
+	    else
+	      s.m_fg_color = style::named_color::GREEN;
+	    c.paint (canvas::coord_t (x, y),
+		     styled_unichar ('*', false, sm.get_or_create_id (s)));
+	  }
+      }
+  ASSERT_EQ (sm.get_num_styles (), 3);
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("\n"
+      "   *****\n"
+      "  *******\n"
+      " *********\n"
+      " *********\n"
+      " *********\n"
+      " *********\n"
+      " *********\n"
+      "  *******\n"
+      "   *****\n"));
+  ASSERT_CANVAS_STREQ
+    (c, true,
+     ("\n"
+      "   ^[[32m^[[K*****^[[m^[[K\n"
+      "  ^[[32m^[[K***^[[31m^[[K*^[[32m^[[K***^[[m^[[K\n"
+      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
+      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
+      " ^[[32m^[[K*^[[31m^[[K*******^[[32m^[[K*^[[m^[[K\n"
+      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
+      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
+      "  ^[[32m^[[K***^[[31m^[[K*^[[32m^[[K***^[[m^[[K\n"
+      "   ^[[32m^[[K*****^[[m^[[K\n"));
+}
+
+static void
+test_bold ()
+{
+  auto_fix_quotes fix_quotes;
+  style_manager sm;
+  styled_string s (styled_string::from_fmt (sm, nullptr,
+					    "before %qs after", "foo"));
+  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
+  c.paint_text (canvas::coord_t (0, 0), s);
+  ASSERT_CANVAS_STREQ (c, false,
+		       "before `foo' after\n");
+  ASSERT_CANVAS_STREQ (c, true,
+		       "before `^[[00;01m^[[Kfoo^[[00m^[[K' after\n");
+}
+
+static void
+test_emoji ()
+{
+  style_manager sm;
+  styled_string s (0x26A0, /* U+26A0 WARNING SIGN.  */
+		   true);
+  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
+  c.paint_text (canvas::coord_t (0, 0), s);
+  ASSERT_CANVAS_STREQ (c, false, "⚠️\n");
+  ASSERT_CANVAS_STREQ (c, true, "⚠️\n");
+}
+
+static void
+test_emoji_2 ()
+{
+  style_manager sm;
+  styled_string s;
+  s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN.  */
+			   true));
+  s.append (styled_string (sm, "test"));
+  ASSERT_EQ (s.size (), 5);
+  ASSERT_EQ (s.calc_canvas_width (), 5);
+  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
+  c.paint_text (canvas::coord_t (0, 0), s);
+  ASSERT_CANVAS_STREQ (c, false,
+		       /* U+26A0 WARNING SIGN, as UTF-8: 0xE2 0x9A 0xA0.  */
+		       "\xE2\x9A\xA0"
+		       /* U+FE0F VARIATION SELECTOR-16, as UTF-8: 0xEF 0xB8 0x8F.  */
+		       "\xEF\xB8\x8F"
+		       "test\n");
+}
+
+static void
+test_canvas_urls ()
+{
+  style_manager sm;
+  canvas canvas (canvas::size_t (9, 3), sm);
+  styled_string foo_ss (sm, "foo");
+  foo_ss.set_url (sm, "https://www.example.com/foo");
+  styled_string bar_ss (sm, "bar");
+  bar_ss.set_url (sm, "https://www.example.com/bar");
+  canvas.paint_text(canvas::coord_t (1, 1), foo_ss);
+  canvas.paint_text(canvas::coord_t (5, 1), bar_ss);
+
+  ASSERT_CANVAS_STREQ (canvas, false,
+		       ("\n"
+			" foo bar\n"
+			"\n"));
+  {
+    pretty_printer pp;
+    pp_show_color (&pp) = true;
+    pp.url_format = URL_FORMAT_ST;
+    assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
+			 (/* Line 1.  */
+			  "\n"
+			  /* Line 2.  */
+			  " "
+			  "\33]8;;https://www.example.com/foo\33\\foo\33]8;;\33\\"
+			  " "
+			  "\33]8;;https://www.example.com/bar\33\\bar\33]8;;\33\\"
+			  "\n"
+			  /* Line 3.  */
+			  "\n"));
+  }
+
+  {
+    pretty_printer pp;
+    pp_show_color (&pp) = true;
+    pp.url_format = URL_FORMAT_BEL;
+    assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
+			 (/* Line 1.  */
+			  "\n"
+			  /* Line 2.  */
+			  " "
+			  "\33]8;;https://www.example.com/foo\afoo\33]8;;\a"
+			  " "
+			  "\33]8;;https://www.example.com/bar\abar\33]8;;\a"
+			  "\n"
+			  /* Line 3.  */
+			  "\n"));
+  }
+}
+
+/* Run all selftests in this file.  */
+
+void
+text_art_canvas_cc_tests ()
+{
+  test_blank ();
+  test_abc ();
+  test_debug_fill ();
+  test_text ();
+  test_circle ();
+  test_color_circle ();
+  test_bold ();
+  test_emoji ();
+  test_emoji_2 ();
+  test_canvas_urls ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/canvas.h b/gcc/text-art/canvas.h
new file mode 100644
index 00000000000..495497754f5
--- /dev/null
+++ b/gcc/text-art/canvas.h
@@ -0,0 +1,74 @@
+/* Canvas for random-access procedural text art.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_CANVAS_H
+#define GCC_TEXT_ART_CANVAS_H
+
+#include "text-art/types.h"
+
+namespace text_art {
+
+class canvas;
+
+/* A 2 dimensional grid of text cells (a "canvas"), which
+   can be written to ("painted") via random access, and then
+   written out to a pretty_printer once the picture is complete.
+
+   Each text cell can be styled independently (colorization,
+   URLs, etc).  */
+
+class canvas
+{
+ public:
+  typedef styled_unichar cell_t;
+  typedef size<class canvas> size_t;
+  typedef coord<class canvas> coord_t;
+  typedef range<class canvas> range_t;
+  typedef rect<class canvas> rect_t;
+
+  canvas (size_t size, const style_manager &style_mgr);
+
+  size_t get_size () const { return m_cells.get_size (); }
+
+  void paint (coord_t coord, cell_t c);
+  void paint_text (coord_t coord, const styled_string &text);
+
+  void fill (rect_t rect, cell_t c);
+  void debug_fill ();
+
+  void print_to_pp (pretty_printer *pp,
+		    const char *per_line_prefix = NULL) const;
+  void debug (bool styled) const;
+
+  const cell_t &get (coord_t coord) const
+  {
+    return m_cells.get (coord);
+  }
+
+ private:
+  int get_final_x_in_row (int y) const;
+
+  array2<cell_t, size_t, coord_t> m_cells;
+  const style_manager &m_style_mgr;
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_CANVAS_H */
diff --git a/gcc/text-art/ruler.cc b/gcc/text-art/ruler.cc
new file mode 100644
index 00000000000..80c623f77ba
--- /dev/null
+++ b/gcc/text-art/ruler.cc
@@ -0,0 +1,723 @@
+/* Classes for printing labelled rulers.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#define INCLUDE_ALGORITHM
+#include "system.h"
+#include "coretypes.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/ruler.h"
+#include "text-art/theme.h"
+
+using namespace text_art;
+
+void
+x_ruler::add_label (const canvas::range_t &r,
+		    styled_string text,
+		    style::id_t style_id,
+		    label_kind kind)
+{
+  m_labels.push_back (label (r, std::move (text), style_id, kind));
+  m_has_layout = false;
+}
+
+int
+x_ruler::get_canvas_y (int rel_y) const
+{
+  gcc_assert (rel_y >= 0);
+  gcc_assert (rel_y < m_size.h);
+  switch (m_label_dir)
+    {
+    default:
+      gcc_unreachable ();
+    case label_dir::ABOVE:
+      return m_size.h - (rel_y + 1);
+    case label_dir::BELOW:
+      return rel_y;
+    }
+}
+
+void
+x_ruler::paint_to_canvas (canvas &canvas,
+			  canvas::coord_t offset,
+			  const theme &theme)
+{
+  ensure_layout ();
+
+  if (0)
+    canvas.fill (canvas::rect_t (offset, m_size),
+		 canvas::cell_t ('*'));
+
+  for (size_t idx = 0; idx < m_labels.size (); idx++)
+    {
+      const label &iter_label = m_labels[idx];
+
+      /* Paint the ruler itself.  */
+      const int ruler_rel_y = get_canvas_y (0);
+      for (int rel_x = iter_label.m_range.start;
+	   rel_x < iter_label.m_range.next;
+	   rel_x++)
+	{
+	  enum theme::cell_kind kind = theme::cell_kind::X_RULER_MIDDLE;
+
+	  if (rel_x == iter_label.m_range.start)
+	    {
+	      kind = theme::cell_kind::X_RULER_LEFT_EDGE;
+	      if (idx > 0)
+		{
+		  const label &prev_label = m_labels[idx - 1];
+		  if (prev_label.m_range.get_max () == iter_label.m_range.start)
+		    kind = theme::cell_kind::X_RULER_INTERNAL_EDGE;
+		}
+	    }
+	  else if (rel_x == iter_label.m_range.get_max ())
+	    kind = theme::cell_kind::X_RULER_RIGHT_EDGE;
+	  else if (rel_x == iter_label.m_connector_x)
+	    {
+	      switch (m_label_dir)
+		{
+		default:
+		  gcc_unreachable ();
+		case label_dir::ABOVE:
+		  kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE;
+		  break;
+		case label_dir::BELOW:
+		  kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW;
+		  break;
+		}
+	    }
+	  canvas.paint (canvas::coord_t (rel_x, ruler_rel_y) + offset,
+			theme.get_cell (kind, iter_label.m_style_id));
+	}
+
+      /* Paint the connector to the text.  */
+      for (int connector_rel_y = 1;
+	   connector_rel_y < iter_label.m_text_rect.get_min_y ();
+	   connector_rel_y++)
+	{
+	  canvas.paint
+	    ((canvas::coord_t (iter_label.m_connector_x,
+			       get_canvas_y (connector_rel_y))
+	      + offset),
+	     theme.get_cell (theme::cell_kind::X_RULER_VERTICAL_CONNECTOR,
+			     iter_label.m_style_id));
+	}
+
+      /* Paint the text.  */
+      switch (iter_label.m_kind)
+	{
+	default:
+	  gcc_unreachable ();
+	case x_ruler::label_kind::TEXT:
+	  canvas.paint_text
+	    ((canvas::coord_t (iter_label.m_text_rect.get_min_x (),
+			       get_canvas_y (iter_label.m_text_rect.get_min_y ()))
+	      + offset),
+	     iter_label.m_text);
+	  break;
+
+	case x_ruler::label_kind::TEXT_WITH_BORDER:
+	  {
+	    const canvas::range_t rel_x_range
+	      (iter_label.m_text_rect.get_x_range ());
+
+	    enum theme::cell_kind inner_left_kind;
+	    enum theme::cell_kind inner_connector_kind;
+	    enum theme::cell_kind inner_right_kind;
+	    enum theme::cell_kind outer_left_kind;
+	    enum theme::cell_kind outer_right_kind;
+
+	      switch (m_label_dir)
+		{
+		default:
+		  gcc_unreachable ();
+		case label_dir::ABOVE:
+		  outer_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT;
+		  outer_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT;
+		  inner_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT;
+		  inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW;
+		  inner_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT;
+		  break;
+		case label_dir::BELOW:
+		  inner_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT;
+		  inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE;
+		  inner_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT;
+		  outer_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT;
+		  outer_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT;
+		  break;
+		}
+	    /* Inner border.  */
+	    {
+	      const int rel_canvas_y
+		= get_canvas_y (iter_label.m_text_rect.get_min_y ());
+	      /* Left corner.  */
+	      canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
+					      rel_canvas_y)
+			     + offset),
+			    theme.get_cell (inner_left_kind,
+					    iter_label.m_style_id));
+	      /* Edge.  */
+	      const canvas::cell_t edge_border_cell
+		= theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL,
+				  iter_label.m_style_id);
+	      const canvas::cell_t connector_border_cell
+		= theme.get_cell (inner_connector_kind,
+				  iter_label.m_style_id);
+	      for (int rel_x = rel_x_range.get_min () + 1;
+		   rel_x < rel_x_range.get_max ();
+		   rel_x++)
+		if (rel_x == iter_label.m_connector_x)
+		  canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
+				 + offset),
+				connector_border_cell);
+		else
+		  canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
+				 + offset),
+				edge_border_cell);
+
+	      /* Right corner.  */
+	      canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
+					      rel_canvas_y)
+			     + offset),
+			    theme.get_cell (inner_right_kind,
+					    iter_label.m_style_id));
+	    }
+
+	    {
+	      const int rel_canvas_y
+		= get_canvas_y (iter_label.m_text_rect.get_min_y () + 1);
+	      const canvas::cell_t border_cell
+		= theme.get_cell (theme::cell_kind::TEXT_BORDER_VERTICAL,
+				  iter_label.m_style_id);
+
+	      /* Left border.  */
+	      canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
+					      rel_canvas_y)
+			     + offset),
+			    border_cell);
+	      /* Text.  */
+	      canvas.paint_text ((canvas::coord_t (rel_x_range.get_min () + 1,
+						   rel_canvas_y)
+				  + offset),
+				 iter_label.m_text);
+	      /* Right border.  */
+	      canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
+					      rel_canvas_y)
+			     + offset),
+			    border_cell);
+	    }
+
+	    /* Outer border.  */
+	    {
+	      const int rel_canvas_y
+		= get_canvas_y (iter_label.m_text_rect.get_max_y ());
+	      /* Left corner.  */
+	      canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
+					      rel_canvas_y)
+			     + offset),
+			    theme.get_cell (outer_left_kind,
+					    iter_label.m_style_id));
+	      /* Edge.  */
+	      const canvas::cell_t border_cell
+		= theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL,
+				  iter_label.m_style_id);
+	      for (int rel_x = rel_x_range.get_min () + 1;
+		   rel_x < rel_x_range.get_max ();
+		   rel_x++)
+		canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
+			       + offset),
+			      border_cell);
+
+	      /* Right corner.  */
+	      canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
+					      rel_canvas_y)
+			     + offset),
+			    theme.get_cell (outer_right_kind,
+					    iter_label.m_style_id));
+	    }
+	  }
+	  break;
+	}
+    }
+}
+
+DEBUG_FUNCTION void
+x_ruler::debug (const style_manager &sm)
+{
+  canvas c (get_size (), sm);
+  paint_to_canvas (c, canvas::coord_t (0, 0), unicode_theme ());
+  c.debug (true);
+}
+
+x_ruler::label::label (const canvas::range_t &range,
+		       styled_string text,
+		       style::id_t style_id,
+		       label_kind kind)
+: m_range (range),
+  m_text (std::move (text)),
+  m_style_id (style_id),
+  m_kind (kind),
+  m_text_rect (canvas::coord_t (0, 0),
+	       canvas::size_t (m_text.calc_canvas_width (), 1)),
+  m_connector_x ((m_range.get_min () + m_range.get_max ()) / 2)
+{
+  if (kind == label_kind::TEXT_WITH_BORDER)
+    {
+      m_text_rect.m_size.w += 2;
+      m_text_rect.m_size.h += 2;
+    }
+}
+
+bool
+x_ruler::label::operator< (const label &other) const
+{
+  int cmp = m_range.start - other.m_range.start;
+  if (cmp)
+    return cmp < 0;
+  return m_range.next < other.m_range.next;
+}
+
+void
+x_ruler::ensure_layout ()
+{
+  if (m_has_layout)
+    return;
+  update_layout ();
+  m_has_layout = true;
+}
+
+void
+x_ruler::update_layout ()
+{
+  if (m_labels.empty ())
+    return;
+
+  std::sort (m_labels.begin (), m_labels.end ());
+
+  /* Place labels.  */
+  int ruler_width = m_labels.back ().m_range.get_next ();
+  int width_with_labels = ruler_width;
+
+  /* Get x coordinates of text parts of each label
+     (m_text_rect.m_top_left.x for each label).  */
+  for (size_t idx = 0; idx < m_labels.size (); idx++)
+    {
+      label &iter_label = m_labels[idx];
+      /* Attempt to center the text label.  */
+      int min_x;
+      if (idx > 0)
+	{
+	  /* ...but don't overlap with the connector to the left.  */
+	  int left_neighbor_connector_x = m_labels[idx - 1].m_connector_x;
+	  min_x = left_neighbor_connector_x + 1;
+	}
+      else
+	{
+	  /* ...or go beyond the leftmost column.  */
+	  min_x = 0;
+	}
+      int connector_x = iter_label.m_connector_x;
+      int centered_x
+	= connector_x - ((int)iter_label.m_text_rect.get_width () / 2);
+      int text_x = std::max (min_x, centered_x);
+      iter_label.m_text_rect.m_top_left.x = text_x;
+    }
+
+  /* Now walk backwards trying to place them vertically,
+     setting m_text_rect.m_top_left.y for each label,
+     consolidating the rows where possible.
+     The y cooordinates are stored with respect to label_dir::BELOW.  */
+  int label_y = 2;
+  for (int idx = m_labels.size () - 1; idx >= 0; idx--)
+    {
+      label &iter_label = m_labels[idx];
+      /* Does it fit on the same row as the text label to the right?  */
+      size_t text_len = iter_label.m_text_rect.get_width ();
+      /* Get the x-coord of immediately beyond iter_label's text.  */
+      int next_x = iter_label.m_text_rect.get_min_x () + text_len;
+      if (idx < (int)m_labels.size () - 1)
+	{
+	  if (next_x >= m_labels[idx + 1].m_text_rect.get_min_x ())
+	    {
+	      /* If not, start a new row.  */
+	      label_y += m_labels[idx + 1].m_text_rect.get_height ();
+	    }
+	}
+      iter_label.m_text_rect.m_top_left.y = label_y;
+      width_with_labels = std::max (width_with_labels, next_x);
+    }
+
+  m_size = canvas::size_t (width_with_labels,
+			   label_y + m_labels[0].m_text_rect.get_height ());
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+static void
+assert_x_ruler_streq (const location &loc,
+		      x_ruler &ruler,
+		      const theme &theme,
+		      const style_manager &sm,
+		      bool styled,
+		      const char *expected_str)
+{
+  canvas c (ruler.get_size (), sm);
+  ruler.paint_to_canvas (c, canvas::coord_t (0, 0), theme);
+  if (0)
+    c.debug (styled);
+  assert_canvas_streq (loc, c, styled, expected_str);
+}
+
+#define ASSERT_X_RULER_STREQ(RULER, THEME, SM, STYLED, EXPECTED_STR)	\
+  SELFTEST_BEGIN_STMT							\
+    assert_x_ruler_streq ((SELFTEST_LOCATION),				\
+			  (RULER),					\
+			  (THEME),					\
+			  (SM),						\
+			  (STYLED),					\
+			  (EXPECTED_STR));				\
+  SELFTEST_END_STMT
+
+static void
+test_single ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
+	       style::id_plain, x_ruler::label_kind::TEXT);
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     ("|~~~~+~~~~|\n"
+      "     |\n"
+      "    foo\n"));
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├────┬────┤\n"
+      "     │\n"
+      "    foo\n"));
+}
+
+static void
+test_single_above ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::ABOVE);
+  r.add_label (canvas::range_t (0, 11), styled_string (sm, "hello world"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     ("hello world\n"
+      "     |\n"
+      "|~~~~+~~~~|\n"));
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("hello world\n"
+      "     │\n"
+      "├────┴────┤\n"));
+}
+
+static void
+test_multiple_contiguous ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     ("|~~~~+~~~~|~+~~|\n"
+      "     |      |\n"
+      "    foo    bar\n"));
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├────┬────┼─┬──┤\n"
+      "     │      │\n"
+      "    foo    bar\n"));
+}
+
+static void
+test_multiple_contiguous_above ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::ABOVE);
+  r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     ("    foo    bar\n"
+      "     |      |\n"
+      "|~~~~+~~~~|~+~~|\n"));
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("    foo    bar\n"
+      "     │      │\n"
+      "├────┴────┼─┴──┤\n"));
+}
+
+static void
+test_multiple_contiguous_abutting_labels ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 11), styled_string (sm, "12345678"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 16), styled_string (sm, "1234678"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├────┬────┼─┬──┤\n"
+      "     │      │\n"
+      "     │   1234678\n"
+      " 12345678\n"));
+}
+
+static void
+test_multiple_contiguous_overlapping_labels ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 11), styled_string (sm, "123456789"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 16), styled_string (sm, "12346789"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├────┬────┼─┬──┤\n"
+      "     │      │\n"
+      "     │  12346789\n"
+      " 123456789\n"));
+}
+static void
+test_abutting_left_border ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 6),
+	       styled_string (sm, "this is a long label"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├─┬──┤\n"
+      "  │\n"
+      "this is a long label\n"));
+}
+
+static void
+test_too_long_to_consolidate_vertically ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 11),
+	       styled_string (sm, "long string A"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 16),
+	       styled_string (sm, "long string B"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├────┬────┼─┬──┤\n"
+      "     │      │\n"
+      "     │long string B\n"
+      "long string A\n"));
+}
+
+static void
+test_abutting_neighbor ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 11),
+	       styled_string (sm, "very long string A"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 16),
+	       styled_string (sm, "very long string B"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, unicode_theme (), sm, true,
+     ("├────┬────┼─┬──┤\n"
+      "     │      │\n"
+      "     │very long string B\n"
+      "very long string A\n"));
+}
+
+static void
+test_gaps ()
+{
+  style_manager sm;
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 5),
+	       styled_string (sm, "foo"),
+	       style::id_plain);
+  r.add_label (canvas::range_t (10, 15),
+	       styled_string (sm, "bar"),
+	       style::id_plain);
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     ("|~+~|     |~+~|\n"
+      "  |         |\n"
+      " foo       bar\n"));
+}
+
+static void
+test_styled ()
+{
+  style_manager sm;
+  style s1, s2;
+  s1.m_bold = true;
+  s1.m_fg_color = style::named_color::YELLOW;
+  s2.m_bold = true;
+  s2.m_fg_color = style::named_color::BLUE;
+  style::id_t sid1 = sm.get_or_create_id (s1);
+  style::id_t sid2 = sm.get_or_create_id (s2);
+
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 5), styled_string (sm, "foo"), sid1);
+  r.add_label (canvas::range_t (10, 15), styled_string (sm, "bar"), sid2);
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     ("^[[00;01;33m^[[K|~+~|^[[00m^[[K     ^[[00;01;34m^[[K|~+~|^[[00m^[[K\n"
+      "  ^[[00;01;33m^[[K|^[[00m^[[K         ^[[00;01;34m^[[K|^[[00m^[[K\n"
+      " foo       bar\n"));
+}
+
+static void
+test_borders ()
+{
+  style_manager sm;
+  {
+    x_ruler r (x_ruler::label_dir::BELOW);
+    r.add_label (canvas::range_t (0, 5),
+		 styled_string (sm, "label 1"),
+		 style::id_plain,
+		 x_ruler::label_kind::TEXT_WITH_BORDER);
+    r.add_label (canvas::range_t (10, 15),
+		 styled_string (sm, "label 2"),
+		 style::id_plain);
+    r.add_label (canvas::range_t (20, 25),
+		 styled_string (sm, "label 3"),
+		 style::id_plain,
+		 x_ruler::label_kind::TEXT_WITH_BORDER);
+    ASSERT_X_RULER_STREQ
+      (r, ascii_theme (), sm, true,
+       "|~+~|     |~+~|     |~+~|\n"
+       "  |         |         |\n"
+       "  |      label 2  +---+---+\n"
+       "+-+-----+         |label 3|\n"
+       "|label 1|         +-------+\n"
+       "+-------+\n");
+    ASSERT_X_RULER_STREQ
+      (r, unicode_theme (), sm, true,
+       "├─┬─┤     ├─┬─┤     ├─┬─┤\n"
+       "  │         │         │\n"
+       "  │      label 2  ╭───┴───╮\n"
+       "╭─┴─────╮         │label 3│\n"
+       "│label 1│         ╰───────╯\n"
+       "╰───────╯\n");
+  }
+  {
+    x_ruler r (x_ruler::label_dir::ABOVE);
+    r.add_label (canvas::range_t (0, 5),
+		 styled_string (sm, "label 1"),
+		 style::id_plain,
+		 x_ruler::label_kind::TEXT_WITH_BORDER);
+    r.add_label (canvas::range_t (10, 15),
+		 styled_string (sm, "label 2"),
+		 style::id_plain);
+    r.add_label (canvas::range_t (20, 25),
+		 styled_string (sm, "label 3"),
+		 style::id_plain,
+		 x_ruler::label_kind::TEXT_WITH_BORDER);
+    ASSERT_X_RULER_STREQ
+      (r, ascii_theme (), sm, true,
+       "+-------+\n"
+       "|label 1|         +-------+\n"
+       "+-+-----+         |label 3|\n"
+       "  |      label 2  +---+---+\n"
+       "  |         |         |\n"
+       "|~+~|     |~+~|     |~+~|\n");
+    ASSERT_X_RULER_STREQ
+      (r, unicode_theme (), sm, true,
+       "╭───────╮\n"
+       "│label 1│         ╭───────╮\n"
+       "╰─┬─────╯         │label 3│\n"
+       "  │      label 2  ╰───┬───╯\n"
+       "  │         │         │\n"
+       "├─┴─┤     ├─┴─┤     ├─┴─┤\n");
+  }
+}
+
+static void
+test_emoji ()
+{
+  style_manager sm;
+
+  styled_string s;
+  s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN.  */
+			   true));
+  s.append (styled_string (sm, "  "));
+  s.append (styled_string (sm, "this is a warning"));
+
+  x_ruler r (x_ruler::label_dir::BELOW);
+  r.add_label (canvas::range_t (0, 5),
+	       std::move (s),
+	       style::id_plain,
+	       x_ruler::label_kind::TEXT_WITH_BORDER);
+
+  ASSERT_X_RULER_STREQ
+    (r, ascii_theme (), sm, true,
+     "|~+~|\n"
+     "  |\n"
+     "+-+------------------+\n"
+     "|⚠️  this is a warning|\n"
+     "+--------------------+\n");
+}
+
+/* Run all selftests in this file.  */
+
+void
+text_art_ruler_cc_tests ()
+{
+  test_single ();
+  test_single_above ();
+  test_multiple_contiguous ();
+  test_multiple_contiguous_above ();
+  test_multiple_contiguous_abutting_labels ();
+  test_multiple_contiguous_overlapping_labels ();
+  test_abutting_left_border ();
+  test_too_long_to_consolidate_vertically ();
+  test_abutting_neighbor ();
+  test_gaps ();
+  test_styled ();
+  test_borders ();
+  test_emoji ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/ruler.h b/gcc/text-art/ruler.h
new file mode 100644
index 00000000000..31f53549836
--- /dev/null
+++ b/gcc/text-art/ruler.h
@@ -0,0 +1,125 @@
+/* Classes for printing labelled rulers.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_RULER_H
+#define GCC_TEXT_ART_RULER_H
+
+#include "text-art/canvas.h"
+
+namespace text_art {
+
+/* A way to annotate a series of ranges of canvas coordinates
+   with text labels either above or, in this example, below:
+     ├───────┬───────┼───────┬───────┼───────┬───────┤
+             │               │               │
+           label A         label B          label C
+   with logic to ensure that the text labels don't overlap
+   when printed.  */
+
+class x_ruler
+{
+ public:
+  enum class label_dir { ABOVE, BELOW };
+  enum class label_kind
+  {
+    TEXT,
+    TEXT_WITH_BORDER
+  };
+
+  x_ruler (label_dir dir)
+  : m_label_dir (dir),
+    m_size (canvas::size_t (0, 0)),
+    m_has_layout (false)
+  {}
+
+  void add_label (const canvas::range_t &r,
+		  styled_string text,
+		  style::id_t style_id,
+		  label_kind kind = label_kind::TEXT);
+
+  canvas::size_t get_size ()
+  {
+    ensure_layout ();
+    return m_size;
+  }
+
+  void paint_to_canvas (canvas &canvas,
+			canvas::coord_t offset,
+			const theme &theme);
+
+  void debug (const style_manager &sm);
+
+ private:
+  /* A particular label within an x_ruler.
+     Consider e.g.:
+
+     #   x:  01234567890123456789012345678901234567890123456789
+     # y: 0: ├───────┬───────┼───────┬───────┼───────┬───────┤
+     #    1:         │               │               │
+     #    2:       label A         label B          label C
+     #
+
+     Then "label A" is:
+
+     #               m_connector_x == 8
+     #               V
+     #   x:  0123456789012
+     # y: 0:         ┬
+     #    1:         │
+     #    2:       label A
+     #   x:  0123456789012
+     #             ^
+     #             m_text_coord.x == 6
+
+     and m_text_coord is (2, 6).
+     The y cooordinates are stored with respect to label_dir::BELOW;
+     for label_dir::ABOVE we flip them when painting the ruler.  */
+  class label
+  {
+    friend class x_ruler;
+  public:
+    label (const canvas::range_t &range, styled_string text, style::id_t style_id,
+	   label_kind kind);
+
+    bool operator< (const label &other) const;
+
+  private:
+    canvas::range_t m_range;
+    styled_string m_text;
+    style::id_t m_style_id;
+    label_kind m_kind;
+    canvas::rect_t m_text_rect; // includes any border
+    int m_connector_x;
+  };
+
+  void ensure_layout ();
+  void update_layout ();
+  int get_canvas_y (int rel_y) const;
+
+  label_dir m_label_dir;
+  std::vector<label> m_labels;
+  canvas::size_t m_size;
+  bool m_has_layout = false;
+
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_RULER_H */
diff --git a/gcc/text-art/selftests.cc b/gcc/text-art/selftests.cc
new file mode 100644
index 00000000000..60ad003b549
--- /dev/null
+++ b/gcc/text-art/selftests.cc
@@ -0,0 +1,77 @@
+/* Selftests for text art.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "selftest.h"
+#include "pretty-print.h"
+#include "text-art/selftests.h"
+#include "text-art/canvas.h"
+
+#if CHECKING_P
+
+/* Run all tests, aborting if any fail.  */
+
+void
+selftest::text_art_tests ()
+{
+  text_art_style_cc_tests ();
+  text_art_styled_string_cc_tests ();
+
+  text_art_box_drawing_cc_tests ();
+  text_art_canvas_cc_tests ();
+  text_art_ruler_cc_tests ();
+  text_art_table_cc_tests ();
+  text_art_widget_cc_tests ();
+}
+
+/* Implementation detail of ASSERT_CANVAS_STREQ.  */
+
+void
+selftest::assert_canvas_streq (const location &loc,
+			       const text_art::canvas &canvas,
+			       pretty_printer *pp,
+			       const char *expected_str)
+{
+  canvas.print_to_pp (pp);
+  if (0)
+    fprintf (stderr, "%s\n", pp_formatted_text (pp));
+  ASSERT_STREQ_AT (loc, pp_formatted_text (pp), expected_str);
+}
+
+/* Implementation detail of ASSERT_CANVAS_STREQ.  */
+
+void
+selftest::assert_canvas_streq (const location &loc,
+			       const text_art::canvas &canvas,
+			       bool styled,
+			       const char *expected_str)
+{
+  pretty_printer pp;
+  if (styled)
+    {
+      pp_show_color (&pp) = true;
+      pp.url_format = URL_FORMAT_DEFAULT;
+    }
+  assert_canvas_streq (loc, canvas, &pp, expected_str);
+}
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/selftests.h b/gcc/text-art/selftests.h
new file mode 100644
index 00000000000..706a1d8b5d6
--- /dev/null
+++ b/gcc/text-art/selftests.h
@@ -0,0 +1,60 @@
+/* Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_SELFTESTS_H
+#define GCC_TEXT_ART_SELFTESTS_H
+
+#if CHECKING_P
+
+#include "text-art/types.h"
+
+namespace selftest {
+
+extern void text_art_box_drawing_cc_tests ();
+extern void text_art_canvas_cc_tests ();
+extern void text_art_ruler_cc_tests ();
+extern void text_art_style_cc_tests ();
+extern void text_art_styled_string_cc_tests ();
+extern void text_art_table_cc_tests ();
+extern void text_art_widget_cc_tests ();
+
+extern void text_art_tests ();
+
+extern void assert_canvas_streq (const location &loc,
+				 const text_art::canvas &canvas,
+				 pretty_printer *pp,
+				 const char *expected_str);
+extern void assert_canvas_streq (const location &loc,
+				 const text_art::canvas &canvas,
+				 bool styled,
+				 const char *expected_str);
+
+#define ASSERT_CANVAS_STREQ(CANVAS, STYLED, EXPECTED_STR)		\
+  SELFTEST_BEGIN_STMT							\
+    assert_canvas_streq ((SELFTEST_LOCATION),				\
+			 (CANVAS),					\
+			 (STYLED), 					\
+			 (EXPECTED_STR));				\
+  SELFTEST_END_STMT
+
+} /* end of namespace selftest.  */
+
+#endif /* #if CHECKING_P */
+
+#endif /* GCC_TEXT_ART_SELFTESTS_H */
diff --git a/gcc/text-art/style.cc b/gcc/text-art/style.cc
new file mode 100644
index 00000000000..00b056336fc
--- /dev/null
+++ b/gcc/text-art/style.cc
@@ -0,0 +1,632 @@
+/* Classes for styling text cells (color, URLs).
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#define INCLUDE_ALGORITHM
+#define INCLUDE_MEMORY
+#include "system.h"
+#include "coretypes.h"
+#include "make-unique.h"
+#include "pretty-print.h"
+#include "intl.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/types.h"
+#include "color-macros.h"
+
+using namespace text_art;
+
+/* class text_art::style.  */
+
+style &
+style::set_style_url (const char *url)
+{
+  m_url.clear ();
+  while (*url)
+    m_url.push_back (*(url++));
+  return *this;
+}
+
+/* class text_art::style::color.  */
+
+bool
+style::color::operator== (const style::color &other) const
+{
+  if (m_kind != other.m_kind)
+    return false;
+  switch (m_kind)
+    {
+    default:
+      gcc_unreachable ();
+    case kind::NAMED:
+      return (u.m_named.m_name == other.u.m_named.m_name
+	      && u.m_named.m_bright == other.u.m_named.m_bright);
+    case kind::BITS_8:
+      return u.m_8bit == other.u.m_8bit;
+    case kind::BITS_24:
+      return (u.m_24bit.r == other.u.m_24bit.r
+	      && u.m_24bit.g == other.u.m_24bit.g
+	      && u.m_24bit.b == other.u.m_24bit.b);
+    }
+}
+
+static void
+ensure_separator (pretty_printer *pp, bool &need_separator)
+{
+  if (need_separator)
+    pp_string (pp, COLOR_SEPARATOR);
+  need_separator = true;
+}
+
+void
+style::color::print_sgr (pretty_printer *pp,
+			 bool fg,
+			 bool &need_separator) const
+{
+  switch (m_kind)
+    {
+    default:
+      gcc_unreachable ();
+    case kind::NAMED:
+      {
+	static const char * const fg_normal[] = {"", // reset, for DEFAULT
+						 COLOR_FG_BLACK,
+						 COLOR_FG_RED,
+						 COLOR_FG_GREEN,
+						 COLOR_FG_YELLOW,
+						 COLOR_FG_BLUE,
+						 COLOR_FG_MAGENTA,
+						 COLOR_FG_CYAN,
+						 COLOR_FG_WHITE};
+	static const char * const fg_bright[] = {"", // reset, for DEFAULT
+						 COLOR_FG_BRIGHT_BLACK,
+						 COLOR_FG_BRIGHT_RED,
+						 COLOR_FG_BRIGHT_GREEN,
+						 COLOR_FG_BRIGHT_YELLOW,
+						 COLOR_FG_BRIGHT_BLUE,
+						 COLOR_FG_BRIGHT_MAGENTA,
+						 COLOR_FG_BRIGHT_CYAN,
+						 COLOR_FG_BRIGHT_WHITE};
+	static const char * const bg_normal[] = {"", // reset, for DEFAULT
+						 COLOR_BG_BLACK,
+						 COLOR_BG_RED,
+						 COLOR_BG_GREEN,
+						 COLOR_BG_YELLOW,
+						 COLOR_BG_BLUE,
+						 COLOR_BG_MAGENTA,
+						 COLOR_BG_CYAN,
+						 COLOR_BG_WHITE};
+	static const char * const bg_bright[] = {"", // reset, for DEFAULT
+						 COLOR_BG_BRIGHT_BLACK,
+						 COLOR_BG_BRIGHT_RED,
+						 COLOR_BG_BRIGHT_GREEN,
+						 COLOR_BG_BRIGHT_YELLOW,
+						 COLOR_BG_BRIGHT_BLUE,
+						 COLOR_BG_BRIGHT_MAGENTA,
+						 COLOR_BG_BRIGHT_CYAN,
+						 COLOR_BG_BRIGHT_WHITE};
+	STATIC_ASSERT (ARRAY_SIZE (fg_normal) == ARRAY_SIZE (fg_bright));
+	STATIC_ASSERT (ARRAY_SIZE (fg_normal) == ARRAY_SIZE (bg_normal));
+	STATIC_ASSERT (ARRAY_SIZE (fg_normal) == ARRAY_SIZE (bg_bright));
+	gcc_assert ((size_t)u.m_named.m_name < ARRAY_SIZE (fg_normal));
+	const char *const *arr;
+	if (fg)
+	  arr = u.m_named.m_bright ? fg_bright : fg_normal;
+	else
+	  arr = u.m_named.m_bright ? bg_bright : bg_normal;
+	const char *str = arr[(size_t)u.m_named.m_name];
+	if (strlen (str) > 0)
+	  {
+	    ensure_separator (pp, need_separator);
+	    pp_string (pp, str);
+	  }
+      }
+      break;
+    case kind::BITS_8:
+      {
+	ensure_separator (pp, need_separator);
+	if (fg)
+	  pp_string (pp, "38");
+	else
+	  pp_string (pp, "48");
+	pp_printf (pp, ";5;%i", (int)u.m_8bit);
+      }
+      break;
+    case kind::BITS_24:
+      {
+	ensure_separator (pp, need_separator);
+	if (fg)
+	  pp_string (pp, "38");
+	else
+	  pp_string (pp, "48");
+	pp_printf (pp, ";2;%i;%i;%i",
+		   (int)u.m_24bit.r,
+		   (int)u.m_24bit.g,
+		   (int)u.m_24bit.b);
+      }
+      break;
+    }
+}
+
+/* class text_art::style.  */
+
+/* See https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf
+   GRCM - GRAPHIC RENDITION COMBINATION MODE can be "REPLACING" or
+   "CUMULATIVE", which affects whether we need to respecify all attributes
+   at each SGR, or can accumulate them.  Looks like we can't rely on the value
+   of this, so we have to emit a single SGR for all changes, with a "0" reset
+   at the front, forcing it to be effectively replacing.  */
+
+void
+style::print_changes (pretty_printer *pp,
+		      const style &old_style,
+		      const style &new_style)
+{
+  if (pp_show_color (pp))
+    {
+      bool needs_sgr = ((old_style.m_bold != new_style.m_bold)
+			|| (old_style.m_underscore != new_style.m_underscore)
+			|| (old_style.m_blink != new_style.m_blink)
+			|| (old_style.m_fg_color != new_style.m_fg_color)
+			|| (old_style.m_bg_color != new_style.m_bg_color));
+      if (needs_sgr)
+	{
+	  bool emit_reset = (old_style.m_bold
+			     || new_style.m_bold
+			     || old_style.m_underscore
+			     || new_style.m_underscore
+			     || old_style.m_blink
+			     || new_style.m_blink);
+	  bool need_separator = false;
+
+	  pp_string (pp, SGR_START);
+	  if (emit_reset)
+	    {
+	      pp_string (pp, COLOR_NONE);
+	      need_separator = true;
+	    }
+	  if (new_style.m_bold)
+	    {
+	      gcc_assert (emit_reset);
+	      ensure_separator (pp, need_separator);
+	      pp_string (pp, COLOR_BOLD);
+	    }
+	  if (new_style.m_underscore)
+	    {
+	      gcc_assert (emit_reset);
+	      ensure_separator (pp, need_separator);
+	      pp_string (pp, COLOR_UNDERSCORE);
+	    }
+	  if (new_style.m_blink)
+	    {
+	      gcc_assert (emit_reset);
+	      ensure_separator (pp, need_separator);
+	      pp_string (pp, COLOR_BLINK);
+	    }
+	  new_style.m_fg_color.print_sgr (pp, true, need_separator);
+	  new_style.m_bg_color.print_sgr (pp, false, need_separator);
+	  pp_string (pp, SGR_END);
+	}
+    }
+
+  if (old_style.m_url != new_style.m_url)
+    {
+      if (!old_style.m_url.empty ())
+	pp_end_url (pp);
+      if (pp->url_format != URL_FORMAT_NONE
+	  && !new_style.m_url.empty ())
+	{
+	  /* Adapted from pp_begin_url, but encoding the
+	     chars to UTF-8 on the fly, rather than converting
+	     to a buffer.  */
+	  pp_string (pp, "\33]8;;");
+	  for (auto ch : new_style.m_url)
+	    pp_unicode_character (pp, ch);
+	  switch (pp->url_format)
+	    {
+	    default:
+	    case URL_FORMAT_NONE:
+	      gcc_unreachable ();
+	    case URL_FORMAT_ST:
+	      pp_string (pp, "\33\\");
+	      break;
+	    case URL_FORMAT_BEL:
+	      pp_string (pp, "\a");
+	      break;
+	    }
+	}
+    }
+}
+
+/* class text_art::style_manager.  */
+
+style_manager::style_manager ()
+{
+  // index 0 will be the default style
+  m_styles.push_back (style ());
+}
+
+style::id_t
+style_manager::get_or_create_id (const style &s)
+{
+  // For now, linear search
+  std::vector<style>::iterator existing
+    (std::find (m_styles.begin (), m_styles.end (), s));
+
+  /* If found, return index of slot.  */
+  if (existing != m_styles.end ())
+    return std::distance (m_styles.begin (), existing);
+
+  /* Not found.  */
+
+  /* styled_str uses 7 bits for style information, so we can only support
+     up to 128 different style combinations.
+     Gracefully fail by turning off styling when this limit is reached.  */
+  if (m_styles.size () >= 127)
+    return 0;
+
+  m_styles.push_back (s);
+  return m_styles.size () - 1;
+}
+
+void
+style_manager::print_any_style_changes (pretty_printer *pp,
+					style::id_t old_id,
+					style::id_t new_id) const
+{
+  gcc_assert (pp);
+  if (old_id == new_id)
+    return;
+
+  const style &old_style = m_styles[old_id];
+  const style &new_style = m_styles[new_id];
+  gcc_assert (!(old_style == new_style));
+  style::print_changes (pp, old_style, new_style);
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+void
+assert_style_change_streq (const location &loc,
+			   const style &old_style,
+			   const style &new_style,
+			   const char *expected_str)
+{
+  pretty_printer pp;
+  pp_show_color (&pp) = true;
+  style::print_changes (&pp, old_style, new_style);
+  ASSERT_STREQ_AT (loc, pp_formatted_text (&pp), expected_str);
+}
+
+#define ASSERT_STYLE_CHANGE_STREQ(OLD_STYLE, NEW_STYLE, EXPECTED_STR) \
+  SELFTEST_BEGIN_STMT						      \
+    assert_style_change_streq ((SELFTEST_LOCATION),		      \
+			       (OLD_STYLE),			      \
+			       (NEW_STYLE),			      \
+			       (EXPECTED_STR));			      \
+  SELFTEST_END_STMT
+
+static void
+test_bold ()
+{
+  style_manager sm;
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style plain;
+  ASSERT_EQ (sm.get_or_create_id (plain), 0);
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style bold;
+  bold.m_bold = true;
+
+  ASSERT_EQ (sm.get_or_create_id (bold), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+  ASSERT_EQ (sm.get_or_create_id (bold), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+
+  ASSERT_STYLE_CHANGE_STREQ (plain, bold, "\33[00;01m\33[K");
+  ASSERT_STYLE_CHANGE_STREQ (bold, plain, "\33[00m\33[K");
+}
+
+static void
+test_underscore ()
+{
+  style_manager sm;
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style plain;
+  ASSERT_EQ (sm.get_or_create_id (plain), 0);
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style underscore;
+  underscore.m_underscore = true;
+
+  ASSERT_EQ (sm.get_or_create_id (underscore), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+  ASSERT_EQ (sm.get_or_create_id (underscore), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+
+  ASSERT_STYLE_CHANGE_STREQ (plain, underscore, "\33[00;04m\33[K");
+  ASSERT_STYLE_CHANGE_STREQ (underscore, plain, "\33[00m\33[K");
+}
+
+static void
+test_blink ()
+{
+  style_manager sm;
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style plain;
+  ASSERT_EQ (sm.get_or_create_id (plain), 0);
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style blink;
+  blink.m_blink = true;
+
+  ASSERT_EQ (sm.get_or_create_id (blink), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+  ASSERT_EQ (sm.get_or_create_id (blink), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+
+  ASSERT_STYLE_CHANGE_STREQ (plain, blink, "\33[00;05m\33[K");
+  ASSERT_STYLE_CHANGE_STREQ (blink, plain, "\33[00m\33[K");
+}
+
+#define ASSERT_NAMED_COL_STREQ(NAMED_COLOR, FG, BRIGHT, EXPECTED_STR) \
+  SELFTEST_BEGIN_STMT						      \
+  {								      \
+    style plain;						      \
+    style s;							      \
+    if (FG)							      \
+      s.m_fg_color = style::color ((NAMED_COLOR), (BRIGHT));	      \
+    else							      \
+      s.m_bg_color = style::color ((NAMED_COLOR), (BRIGHT));	      \
+    assert_style_change_streq ((SELFTEST_LOCATION),		      \
+			       plain,				      \
+			       s,				      \
+			       (EXPECTED_STR));			      \
+  }								      \
+  SELFTEST_END_STMT
+
+static void
+test_named_colors ()
+{
+  /* Foreground colors.  */
+  {
+    const bool fg = true;
+    {
+      const bool bright = false;
+      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright, "");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
+			      "^[[30m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
+			      "^[[31m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
+			      "^[[32m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
+			      "^[[33m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
+			      "^[[34m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
+			      "^[[35m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
+			      "^[[36m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
+			      "^[[37m^[[K");
+    }
+    {
+      const bool bright = true;
+      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright,
+			      "^[[m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
+			      "^[[90m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
+			      "^[[91m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
+			      "^[[92m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
+			      "^[[93m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
+			      "^[[94m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
+			      "^[[95m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
+			      "^[[96m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
+			      "^[[97m^[[K");
+    }
+  }
+
+  /* Background colors.  */
+  {
+    const bool fg = false;
+    {
+      const bool bright = false;
+      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright, "");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
+			      "^[[40m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
+			      "^[[41m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
+			      "^[[42m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
+			      "^[[43m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
+			      "^[[44m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
+			      "^[[45m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
+			      "^[[46m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
+			      "^[[47m^[[K");
+    }
+    {
+      const bool bright = true;
+      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright,
+			      "^[[m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
+			      "^[[100m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
+			      "^[[101m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
+			      "^[[102m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
+			      "^[[103m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
+			      "^[[104m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
+			      "^[[105m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
+			      "^[[106m^[[K");
+      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
+			      "^[[107m^[[K");
+    }
+  }
+}
+
+#define ASSERT_8_BIT_COL_STREQ(COL_VAL, FG, EXPECTED_STR) \
+  SELFTEST_BEGIN_STMT						      \
+  {								      \
+    style plain;						      \
+    style s;							      \
+    if (FG)							      \
+      s.m_fg_color = style::color (COL_VAL);			      \
+    else							      \
+      s.m_bg_color = style::color (COL_VAL);			      \
+    assert_style_change_streq ((SELFTEST_LOCATION),		      \
+			       plain,				      \
+			       s,				      \
+			       (EXPECTED_STR));			      \
+  }								      \
+  SELFTEST_END_STMT
+
+static void
+test_8_bit_colors ()
+{
+  /* Foreground colors.  */
+  {
+    const bool fg = true;
+    /* 0-15: standard and high-intensity standard colors.  */
+    ASSERT_8_BIT_COL_STREQ (0, fg, "^[[38;5;0m^[[K");
+    ASSERT_8_BIT_COL_STREQ (15, fg, "^[[38;5;15m^[[K");
+    /* 16-231: 6x6x6 color cube.  */
+    ASSERT_8_BIT_COL_STREQ (16, fg, "^[[38;5;16m^[[K");
+    ASSERT_8_BIT_COL_STREQ (231, fg, "^[[38;5;231m^[[K");
+    /* 232-255: grayscale.  */
+    ASSERT_8_BIT_COL_STREQ (232, fg, "^[[38;5;232m^[[K");
+    ASSERT_8_BIT_COL_STREQ (255, fg, "^[[38;5;255m^[[K");
+  }
+  /* Background colors.  */
+  {
+    const bool fg = false;
+    /* 0-15: standard and high-intensity standard colors.  */
+    ASSERT_8_BIT_COL_STREQ (0, fg, "^[[48;5;0m^[[K");
+    ASSERT_8_BIT_COL_STREQ (15, fg, "^[[48;5;15m^[[K");
+    /* 16-231: 6x6x6 color cube.  */
+    ASSERT_8_BIT_COL_STREQ (16, fg, "^[[48;5;16m^[[K");
+    ASSERT_8_BIT_COL_STREQ (231, fg, "^[[48;5;231m^[[K");
+    /* 232-255: grayscale.  */
+    ASSERT_8_BIT_COL_STREQ (232, fg, "^[[48;5;232m^[[K");
+    ASSERT_8_BIT_COL_STREQ (255, fg, "^[[48;5;255m^[[K");
+  }
+}
+
+#define ASSERT_24_BIT_COL_STREQ(R, G, B, FG, EXPECTED_STR)	      \
+  SELFTEST_BEGIN_STMT						      \
+  {								      \
+    style plain;						      \
+    style s;							      \
+    if (FG)							      \
+      s.m_fg_color = style::color ((R), (G), (B));		      \
+    else							      \
+      s.m_bg_color = style::color ((R), (G), (B));		      \
+    assert_style_change_streq ((SELFTEST_LOCATION),		      \
+			       plain,				      \
+			       s,				      \
+			       (EXPECTED_STR));			      \
+  }								      \
+  SELFTEST_END_STMT
+
+static void
+test_24_bit_colors ()
+{
+  /* Foreground colors.  */
+  {
+    const bool fg = true;
+    // #F3FAF2:
+    ASSERT_24_BIT_COL_STREQ (0xf3, 0xfa, 0xf2, fg,
+			     "^[[38;2;243;250;242m^[[K");
+  }
+  /* Background colors.  */
+  {
+    const bool fg = false;
+    // #FDF7E7
+    ASSERT_24_BIT_COL_STREQ (0xfd, 0xf7, 0xe7, fg,
+			     "^[[48;2;253;247;231m^[[K");
+  }
+}
+
+static void
+test_style_combinations ()
+{
+  style_manager sm;
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style plain;
+  ASSERT_EQ (sm.get_or_create_id (plain), 0);
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  style bold;
+  bold.m_bold = true;
+
+  ASSERT_EQ (sm.get_or_create_id (bold), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+  ASSERT_EQ (sm.get_or_create_id (bold), 1);
+  ASSERT_EQ (sm.get_num_styles (), 2);
+
+  style magenta_on_blue;
+  magenta_on_blue.m_fg_color = style::named_color::MAGENTA;
+  magenta_on_blue.m_bg_color = style::named_color::BLUE;
+  ASSERT_EQ (sm.get_or_create_id (magenta_on_blue), 2);
+  ASSERT_EQ (sm.get_num_styles (), 3);
+  ASSERT_EQ (sm.get_or_create_id (magenta_on_blue), 2);
+  ASSERT_EQ (sm.get_num_styles (), 3);
+}
+
+/* Run all selftests in this file.  */
+
+void
+text_art_style_cc_tests ()
+{
+  test_bold ();
+  test_underscore ();
+  test_blink ();
+  test_named_colors ();
+  test_8_bit_colors ();
+  test_24_bit_colors ();
+  test_style_combinations ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/styled-string.cc b/gcc/text-art/styled-string.cc
new file mode 100644
index 00000000000..cd176b2313f
--- /dev/null
+++ b/gcc/text-art/styled-string.cc
@@ -0,0 +1,1107 @@
+/* Implementation of text_art::styled_string.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#define INCLUDE_MEMORY
+#include "system.h"
+#include "coretypes.h"
+#include "make-unique.h"
+#include "pretty-print.h"
+#include "intl.h"
+#include "diagnostic.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/types.h"
+#include "color-macros.h"
+
+using namespace text_art;
+
+namespace {
+
+/* Support class for parsing text containing escape codes.
+   See e.g. https://en.wikipedia.org/wiki/ANSI_escape_code
+   We only support the codes that pretty-print.cc can generate.  */
+
+class escape_code_parser
+{
+public:
+  escape_code_parser (style_manager &sm,
+		      std::vector<styled_unichar> &out)
+  : m_sm (sm),
+    m_out (out),
+    m_cur_style_obj (),
+    m_cur_style_id (style::id_plain),
+    m_state (state::START)
+  {
+  }
+
+  void on_char (cppchar_t ch)
+  {
+    switch (m_state)
+      {
+      default:
+	gcc_unreachable ();
+      case state::START:
+	if (ch == '\033')
+	  {
+	    /* The start of an escape sequence.  */
+	    m_state = state::AFTER_ESC;
+	    return;
+	  }
+	break;
+      case state::AFTER_ESC:
+	if (ch == '[')
+	  {
+	    /* ESC [ is a Control Sequence Introducer.  */
+	    m_state = state::CS_PARAMETER_BYTES;
+	    return;
+	  }
+	else if (ch == ']')
+	  {
+	    /* ESC ] is an Operating System Command.  */
+	    m_state = state::WITHIN_OSC;
+	    return;
+	  }
+	break;
+      case state::CS_PARAMETER_BYTES:
+	if (parameter_byte_p (ch))
+	  {
+	    m_parameter_bytes.push_back ((char)ch);
+	    return;
+	  }
+	else if (intermediate_byte_p (ch))
+	  {
+	    m_intermediate_bytes.push_back ((char)ch);
+	    m_state = state::CS_INTERMEDIATE_BYTES;
+	    return;
+	  }
+	else if (final_byte_p (ch))
+	  {
+	    on_final_csi_char (ch);
+	    return;
+	  }
+	break;
+      case state::CS_INTERMEDIATE_BYTES:
+	/* Expect zero or more intermediate bytes.  */
+	if (intermediate_byte_p (ch))
+	  {
+	    m_intermediate_bytes.push_back ((char)ch);
+	    return;
+	  }
+	else if (final_byte_p (ch))
+	  {
+	    on_final_csi_char (ch);
+	    return;
+	  }
+	break;
+      case state::WITHIN_OSC:
+	/* Accumulate chars into m_osc_string, until we see an ST or a BEL.  */
+	{
+	  /* Check for ESC \, the String Terminator (aka "ST").  */
+	  if (ch == '\\'
+	      && m_osc_string.size () > 0
+	      && m_osc_string.back () == '\033')
+	    {
+	      m_osc_string.pop_back ();
+	      on_final_osc_char ();
+	      return;
+	    }
+	  else if (ch == '\a')
+	    {
+	      // BEL
+	      on_final_osc_char ();
+	      return;
+	    }
+	  m_osc_string.push_back (ch);
+	  return;
+	}
+	break;
+      }
+
+    /* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji
+       variation for the previous character.  */
+    if (ch == 0xFE0F)
+      {
+	if (m_out.size () > 0)
+	  m_out.back ().set_emoji_variant ();
+	return;
+      }
+
+    if (cpp_is_combining_char (ch))
+      {
+	if (m_out.size () > 0)
+	  {
+	    m_out.back ().add_combining_char (ch);
+	    return;
+	  }
+      }
+    /* By default, add the char.  */
+    m_out.push_back (styled_unichar (ch, false, m_cur_style_id));
+  }
+
+private:
+  void on_final_csi_char (cppchar_t ch)
+  {
+    switch (ch)
+      {
+      default:
+	/* Unrecognized.  */
+	break;
+      case 'm':
+	{
+	  /* SGR control sequence.  */
+	  if (m_parameter_bytes.empty ())
+	    reset_style ();
+	  std::vector<int> params (params_from_decimal ());
+	  for (auto iter = params.begin (); iter != params.end (); )
+	    {
+	      const int param = *iter;
+	      switch (param)
+		{
+		default:
+		  /* Unrecognized SGR parameter.  */
+		  break;
+		case 0:
+		  reset_style ();
+		  break;
+		case 1:
+		  set_style_bold ();
+		  break;
+		case 4:
+		  set_style_underscore ();
+		  break;
+		case 5:
+		  set_style_blink ();
+		  break;
+
+		/* Named foreground colors.  */
+		case 30:
+		  set_style_fg_color (style::named_color::BLACK);
+		  break;
+		case 31:
+		  set_style_fg_color (style::named_color::RED);
+		  break;
+		case 32:
+		  set_style_fg_color (style::named_color::GREEN);
+		  break;
+		case 33:
+		  set_style_fg_color (style::named_color::YELLOW);
+		  break;
+		case 34:
+		  set_style_fg_color (style::named_color::BLUE);
+		  break;
+		case 35:
+		  set_style_fg_color (style::named_color::MAGENTA);
+		  break;
+		case 36:
+		  set_style_fg_color (style::named_color::CYAN);
+		  break;
+		case 37:
+		  set_style_fg_color (style::named_color::WHITE);
+		  break;
+
+		  /* 8-bit and 24-bit color */
+		case 38:
+		case 48:
+		  {
+		    const bool fg = (param == 38);
+		    iter++;
+		    if (iter != params.end ())
+		      switch (*(iter++))
+			{
+			default:
+			  break;
+			case 5:
+			  /* 8-bit color.  */
+			  if (iter != params.end ())
+			    {
+			      const uint8_t col = *(iter++);
+			      if (fg)
+				set_style_fg_color (style::color (col));
+			      else
+				set_style_bg_color (style::color (col));
+			    }
+			  continue;
+			case 2:
+			  /* 24-bit color.  */
+			  if (iter != params.end ())
+			    {
+			      const uint8_t r = *(iter++);
+			      if (iter != params.end ())
+				{
+				  const uint8_t g = *(iter++);
+				  if (iter != params.end ())
+				    {
+				      const uint8_t b = *(iter++);
+				      if (fg)
+					set_style_fg_color (style::color (r,
+									  g,
+									  b));
+				      else
+					set_style_bg_color (style::color (r,
+									  g,
+									  b));
+				    }
+				}
+			    }
+			  continue;
+			}
+		    continue;
+		  }
+		  break;
+
+		/* Named background colors.  */
+		case 40:
+		  set_style_bg_color (style::named_color::BLACK);
+		  break;
+		case 41:
+		  set_style_bg_color (style::named_color::RED);
+		  break;
+		case 42:
+		  set_style_bg_color (style::named_color::GREEN);
+		  break;
+		case 43:
+		  set_style_bg_color (style::named_color::YELLOW);
+		  break;
+		case 44:
+		  set_style_bg_color (style::named_color::BLUE);
+		  break;
+		case 45:
+		  set_style_bg_color (style::named_color::MAGENTA);
+		  break;
+		case 46:
+		  set_style_bg_color (style::named_color::CYAN);
+		  break;
+		case 47:
+		  set_style_bg_color (style::named_color::WHITE);
+		  break;
+
+		/* Named foreground colors, bright.  */
+		case 90:
+		  set_style_fg_color (style::color (style::named_color::BLACK,
+						    true));
+		  break;
+		case 91:
+		  set_style_fg_color (style::color (style::named_color::RED,
+						    true));
+		  break;
+		case 92:
+		  set_style_fg_color (style::color (style::named_color::GREEN,
+						    true));
+		  break;
+		case 93:
+		  set_style_fg_color (style::color (style::named_color::YELLOW,
+						    true));
+		  break;
+		case 94:
+		  set_style_fg_color (style::color (style::named_color::BLUE,
+						    true));
+		  break;
+		case 95:
+		  set_style_fg_color (style::color (style::named_color::MAGENTA,
+						    true));
+		  break;
+		case 96:
+		  set_style_fg_color (style::color (style::named_color::CYAN,
+						    true));
+		  break;
+		case 97:
+		  set_style_fg_color (style::color (style::named_color::WHITE,
+						    true));
+		  break;
+
+		/* Named foreground colors, bright.  */
+		case 100:
+		  set_style_bg_color (style::color (style::named_color::BLACK,
+						    true));
+		  break;
+		case 101:
+		  set_style_bg_color (style::color (style::named_color::RED,
+						    true));
+		  break;
+		case 102:
+		  set_style_bg_color (style::color (style::named_color::GREEN,
+						    true));
+		  break;
+		case 103:
+		  set_style_bg_color (style::color (style::named_color::YELLOW,
+						    true));
+		  break;
+		case 104:
+		  set_style_bg_color (style::color (style::named_color::BLUE,
+						    true));
+		  break;
+		case 105:
+		  set_style_bg_color (style::color (style::named_color::MAGENTA,
+						    true));
+		  break;
+		case 106:
+		  set_style_bg_color (style::color (style::named_color::CYAN,
+						    true));
+		  break;
+		case 107:
+		  set_style_bg_color (style::color (style::named_color::WHITE,
+						    true));
+		  break;
+		}
+	      ++iter;
+	    }
+	}
+	break;
+      }
+    m_parameter_bytes.clear ();
+    m_intermediate_bytes.clear ();
+    m_state = state::START;
+  }
+
+  void on_final_osc_char ()
+  {
+    if (!m_osc_string.empty ())
+      {
+	switch (m_osc_string[0])
+	  {
+	  default:
+	    break;
+	  case '8':
+	    /* Hyperlink support; see:
+	       https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+	       We don't support params, so we expect either:
+	       (a) "8;;URL" to begin a url (see pp_begin_url), or
+	       (b) "8;;" to end a URL (see pp_end_url).  */
+	    if (m_osc_string.size () >= 3
+		&& m_osc_string[1] == ';'
+		&& m_osc_string[2] == ';')
+	      {
+		set_style_url (m_osc_string.begin () + 3,
+			       m_osc_string.end ());
+	      }
+	    break;
+	  }
+      }
+    m_osc_string.clear ();
+    m_state = state::START;
+  }
+
+  std::vector<int> params_from_decimal () const
+  {
+    std::vector<int> result;
+
+    int curr_int = -1;
+    for (auto param_ch : m_parameter_bytes)
+      {
+	if (param_ch >= '0' && param_ch <= '9')
+	  {
+	    if (curr_int == -1)
+	      curr_int = 0;
+	    else
+	      curr_int *= 10;
+	    curr_int += param_ch - '0';
+	  }
+	else
+	  {
+	    if (curr_int != -1)
+	      {
+		result.push_back (curr_int);
+		curr_int = -1;
+	      }
+	  }
+      }
+    if (curr_int != -1)
+      result.push_back (curr_int);
+    return result;
+  }
+
+  void refresh_style_id ()
+  {
+    m_cur_style_id = m_sm.get_or_create_id (m_cur_style_obj);
+  }
+  void reset_style ()
+  {
+    m_cur_style_obj = style ();
+    refresh_style_id ();
+  }
+  void set_style_bold ()
+  {
+    m_cur_style_obj.m_bold = true;
+    refresh_style_id ();
+  }
+  void set_style_underscore ()
+  {
+    m_cur_style_obj.m_underscore = true;
+    refresh_style_id ();
+  }
+  void set_style_blink ()
+  {
+    m_cur_style_obj.m_blink = true;
+    refresh_style_id ();
+  }
+  void set_style_fg_color (style::color color)
+  {
+    m_cur_style_obj.m_fg_color = color;
+    refresh_style_id ();
+  }
+  void set_style_bg_color (style::color color)
+  {
+    m_cur_style_obj.m_bg_color = color;
+    refresh_style_id ();
+  }
+  void set_style_url (std::vector<cppchar_t>::iterator begin,
+		      std::vector<cppchar_t>::iterator end)
+  {
+    // The empty string means "no URL"
+    m_cur_style_obj.m_url = std::vector<cppchar_t> (begin, end);
+    refresh_style_id ();
+  }
+
+  static bool parameter_byte_p (cppchar_t ch)
+  {
+    return ch >= 0x30 && ch <= 0x3F;
+  }
+
+  static bool intermediate_byte_p (cppchar_t ch)
+  {
+    return ch >= 0x20 && ch <= 0x2F;
+  }
+
+  static bool final_byte_p (cppchar_t ch)
+  {
+    return ch >= 0x40 && ch <= 0x7E;
+  }
+
+  style_manager &m_sm;
+  std::vector<styled_unichar> &m_out;
+
+  style m_cur_style_obj;
+  style::id_t m_cur_style_id;
+
+  /* Handling of control sequences.  */
+  enum class state
+  {
+   START,
+
+   /* After ESC, expecting '['.  */
+   AFTER_ESC,
+
+   /* Expecting zero or more parameter bytes, an
+      intermediate byte, or a final byte.  */
+   CS_PARAMETER_BYTES,
+
+   /* Expecting zero or more intermediate bytes, or a final byte.  */
+   CS_INTERMEDIATE_BYTES,
+
+   /* Within OSC.  */
+   WITHIN_OSC
+
+  } m_state;
+  std::vector<char> m_parameter_bytes;
+  std::vector<char> m_intermediate_bytes;
+  std::vector<cppchar_t> m_osc_string;
+};
+
+} // anon namespace
+
+/* class text_art::styled_string.  */
+
+/* Construct a styled_string from STR.
+   STR is assumed to be UTF-8 encoded and 0-terminated.
+
+   Parse SGR formatting chars from being in-band (within in the sequence
+   of chars) to being out-of-band, as style elements.
+   We only support parsing the subset of SGR chars that can be emitted
+   by pretty-print.cc   */
+
+styled_string::styled_string (style_manager &sm, const char *str)
+: m_chars ()
+{
+  escape_code_parser parser (sm, m_chars);
+
+  /* We don't actually want the display widths here, but
+     it's an easy way to decode UTF-8.  */
+  cpp_char_column_policy policy (8, cpp_wcwidth);
+  cpp_display_width_computation dw (str, strlen (str), policy);
+  while (!dw.done ())
+    {
+      cpp_decoded_char decoded_char;
+      dw.process_next_codepoint (&decoded_char);
+
+      if (!decoded_char.m_valid_ch)
+	/* Skip bytes that aren't valid UTF-8.  */
+	continue;
+
+      /* Decode SGR formatting.  */
+      cppchar_t ch = decoded_char.m_ch;
+      parser.on_char (ch);
+    }
+}
+
+styled_string::styled_string (cppchar_t cppchar, bool emoji)
+{
+  m_chars.push_back (styled_unichar (cppchar, emoji, style::id_plain));
+}
+
+styled_string
+styled_string::from_fmt_va (style_manager &sm,
+			    printer_fn format_decoder,
+			    const char *fmt,
+			    va_list *args)
+{
+  text_info text;
+  text.err_no = errno;
+  text.args_ptr = args;
+  text.format_spec = fmt;
+  pretty_printer pp;
+  pp_show_color (&pp) = true;
+  pp.url_format = URL_FORMAT_DEFAULT;
+  pp_format_decoder (&pp) = format_decoder;
+  pp_format (&pp, &text);
+  pp_output_formatted_text (&pp);
+  styled_string result (sm, pp_formatted_text (&pp));
+  return result;
+}
+
+styled_string
+styled_string::from_fmt (style_manager &sm,
+			 printer_fn format_decoder,
+			 const char *fmt, ...)
+{
+  va_list ap;
+  va_start (ap, fmt);
+  styled_string result = from_fmt_va (sm, format_decoder, fmt, &ap);
+  va_end (ap);
+  return result;
+}
+
+int
+styled_string::calc_canvas_width () const
+{
+  int result = 0;
+  for (auto ch : m_chars)
+    result += ch.get_canvas_width ();
+  return result;
+}
+
+void
+styled_string::append (const styled_string &suffix)
+{
+  m_chars.insert<std::vector<styled_unichar>::const_iterator> (m_chars.end (),
+							       suffix.begin (),
+							       suffix.end ());
+}
+
+void
+styled_string::set_url (style_manager &sm, const char *url)
+{
+  for (auto& ch : m_chars)
+    {
+      const style &existing_style = sm.get_style (ch.get_style_id ());
+      style with_url (existing_style);
+      with_url.set_style_url (url);
+      ch.m_style_id = sm.get_or_create_id (with_url);
+    }
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+static void
+test_combining_chars ()
+{
+  /* This really ought to be in libcpp, but we don't have
+     selftests there.  */
+  ASSERT_FALSE (cpp_is_combining_char (0));
+  ASSERT_FALSE (cpp_is_combining_char ('a'));
+
+  /* COMBINING BREVE (U+0306).  */
+  ASSERT_TRUE (cpp_is_combining_char (0x0306));
+
+  /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57.  */
+  ASSERT_FALSE (cpp_is_combining_char (0x5B57));
+
+  /* U+FE0F VARIATION SELECTOR-16.  */
+  ASSERT_FALSE (cpp_is_combining_char (0xFE0F));
+}
+
+static void
+test_empty ()
+{
+  style_manager sm;
+  styled_string s (sm, "");
+  ASSERT_EQ (s.size (), 0);
+  ASSERT_EQ (s.calc_canvas_width (), 0);
+}
+
+/* Test of a pure ASCII string with no escape codes.  */
+
+static void
+test_simple ()
+{
+  const char *c_str = "hello world!";
+  style_manager sm;
+  styled_string s (sm, c_str);
+  ASSERT_EQ (s.size (), strlen (c_str));
+  ASSERT_EQ (s.calc_canvas_width (), (int)strlen (c_str));
+  for (size_t i = 0; i < strlen (c_str); i++)
+    {
+      ASSERT_EQ (s[i].get_code (), (cppchar_t)c_str[i]);
+      ASSERT_EQ (s[i].get_style_id (), 0);
+    }
+}
+
+/* Test of decoding UTF-8.  */
+
+static void
+test_pi_from_utf8 ()
+{
+  /* U+03C0 "GREEK SMALL LETTER PI".  */
+  const char * const pi_utf8 = "\xCF\x80";
+
+  style_manager sm;
+  styled_string s (sm, pi_utf8);
+  ASSERT_EQ (s.size (), 1);
+  ASSERT_EQ (s.calc_canvas_width (), 1);
+  ASSERT_EQ (s[0].get_code (), 0x03c0);
+  ASSERT_EQ (s[0].emoji_variant_p (), false);
+  ASSERT_EQ (s[0].double_width_p (), false);
+  ASSERT_EQ (s[0].get_style_id (), 0);
+}
+
+/* Test of double-width character.  */
+
+static void
+test_emoji_from_utf8 ()
+{
+  /* U+1F642 "SLIGHTLY SMILING FACE".  */
+  const char * const emoji_utf8 = "\xF0\x9F\x99\x82";
+
+  style_manager sm;
+  styled_string s (sm, emoji_utf8);
+  ASSERT_EQ (s.size (), 1);
+  ASSERT_EQ (s.calc_canvas_width (), 2);
+  ASSERT_EQ (s[0].get_code (), 0x1f642);
+  ASSERT_EQ (s[0].double_width_p (), true);
+  ASSERT_EQ (s[0].get_style_id (), 0);
+}
+
+/* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji
+   variation for the previous character.  */
+
+static void
+test_emoji_variant_from_utf8 ()
+{
+  const char * const emoji_utf8
+    = (/* U+26A0 WARNING SIGN.  */
+       "\xE2\x9A\xA0"
+       /* U+FE0F VARIATION SELECTOR-16 (emoji variation selector).  */
+       "\xEF\xB8\x8F");
+
+  style_manager sm;
+  styled_string s (sm, emoji_utf8);
+  ASSERT_EQ (s.size (), 1);
+  ASSERT_EQ (s.calc_canvas_width (), 1);
+  ASSERT_EQ (s[0].get_code (), 0x26a0);
+  ASSERT_EQ (s[0].emoji_variant_p (), true);
+  ASSERT_EQ (s[0].double_width_p (), false);
+  ASSERT_EQ (s[0].get_style_id (), 0);
+}
+
+static void
+test_emoji_from_codepoint ()
+{
+  styled_string s ((cppchar_t)0x1f642);
+  ASSERT_EQ (s.size (), 1);
+  ASSERT_EQ (s.calc_canvas_width (), 2);
+  ASSERT_EQ (s[0].get_code (), 0x1f642);
+  ASSERT_EQ (s[0].double_width_p (), true);
+  ASSERT_EQ (s[0].get_style_id (), 0);
+}
+
+static void
+test_from_mixed_width_utf8 ()
+{
+  /* This UTF-8 string literal is of the form
+     before mojibake after
+   where the Japanese word "mojibake" is written as the following
+   four unicode code points:
+     U+6587 CJK UNIFIED IDEOGRAPH-6587
+     U+5B57 CJK UNIFIED IDEOGRAPH-5B57
+     U+5316 CJK UNIFIED IDEOGRAPH-5316
+     U+3051 HIRAGANA LETTER KE.
+   Each of these is 3 bytes wide when encoded in UTF-8, whereas the
+   "before" and "after" are 1 byte per unicode character.  */
+  const char * const mixed_width_utf8
+    = ("before "
+
+       /* U+6587 CJK UNIFIED IDEOGRAPH-6587
+	  UTF-8: 0xE6 0x96 0x87
+	  C octal escaped UTF-8: \346\226\207.  */
+       "\346\226\207"
+
+       /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57
+	  UTF-8: 0xE5 0xAD 0x97
+	  C octal escaped UTF-8: \345\255\227.  */
+       "\345\255\227"
+
+       /* U+5316 CJK UNIFIED IDEOGRAPH-5316
+	  UTF-8: 0xE5 0x8C 0x96
+	  C octal escaped UTF-8: \345\214\226.  */
+       "\345\214\226"
+
+       /* U+3051 HIRAGANA LETTER KE
+	  UTF-8: 0xE3 0x81 0x91
+	  C octal escaped UTF-8: \343\201\221.  */
+       "\343\201\221"
+
+       " after");
+
+  style_manager sm;
+  styled_string s (sm, mixed_width_utf8);
+  ASSERT_EQ (s.size (), 6 + 1 + 4 + 1 + 5);
+  ASSERT_EQ (sm.get_num_styles (), 1);
+
+  // We expect the Japanese characters to be double width.
+  ASSERT_EQ (s.calc_canvas_width (), 6 + 1 + (2 * 4) + 1 + 5);
+
+  ASSERT_EQ (s[0].get_code (), 'b');
+  ASSERT_EQ (s[0].double_width_p (), false);
+  ASSERT_EQ (s[1].get_code (), 'e');
+  ASSERT_EQ (s[2].get_code (), 'f');
+  ASSERT_EQ (s[3].get_code (), 'o');
+  ASSERT_EQ (s[4].get_code (), 'r');
+  ASSERT_EQ (s[5].get_code (), 'e');
+  ASSERT_EQ (s[6].get_code (), ' ');
+  ASSERT_EQ (s[7].get_code (), 0x6587);
+  ASSERT_EQ (s[7].double_width_p (), true);
+  ASSERT_EQ (s[8].get_code (), 0x5B57);
+  ASSERT_EQ (s[9].get_code (), 0x5316);
+  ASSERT_EQ (s[10].get_code (), 0x3051);
+  ASSERT_EQ (s[11].get_code (), ' ');
+  ASSERT_EQ (s[12].get_code (), 'a');
+  ASSERT_EQ (s[13].get_code (), 'f');
+  ASSERT_EQ (s[14].get_code (), 't');
+  ASSERT_EQ (s[15].get_code (), 'e');
+  ASSERT_EQ (s[16].get_code (), 'r');
+
+  ASSERT_EQ (s[0].get_style_id (), 0);
+}
+
+static void
+assert_style_urleq (const location &loc,
+		    const style &s,
+		    const char *expected_str)
+{
+  ASSERT_EQ_AT (loc, s.m_url.size (), strlen (expected_str));
+  for (size_t i = 0; i < s.m_url.size (); i++)
+    ASSERT_EQ_AT (loc, s.m_url[i], (cppchar_t)expected_str[i]);
+}
+
+#define ASSERT_STYLE_URLEQ(STYLE, EXPECTED_STR) \
+  assert_style_urleq ((SELFTEST_LOCATION), (STYLE), (EXPECTED_STR))
+
+static void
+test_url ()
+{
+  // URL_FORMAT_ST
+  {
+    style_manager sm;
+    styled_string s
+      (sm, "\33]8;;http://example.com\33\\This is a link\33]8;;\33\\");
+    const char *expected = "This is a link";
+    ASSERT_EQ (s.size (), strlen (expected));
+    ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected));
+    ASSERT_EQ (sm.get_num_styles (), 2);
+    for (size_t i = 0; i < strlen (expected); i++)
+      {
+	ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]);
+	ASSERT_EQ (s[i].get_style_id (), 1);
+      }
+    ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com");
+  }
+
+  // URL_FORMAT_BEL
+  {
+    style_manager sm;
+    styled_string s
+      (sm, "\33]8;;http://example.com\aThis is a link\33]8;;\a");
+    const char *expected = "This is a link";
+    ASSERT_EQ (s.size (), strlen (expected));
+    ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected));
+    ASSERT_EQ (sm.get_num_styles (), 2);
+    for (size_t i = 0; i < strlen (expected); i++)
+      {
+	ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]);
+	ASSERT_EQ (s[i].get_style_id (), 1);
+      }
+    ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com");
+  }
+}
+
+static void
+test_from_fmt ()
+{
+  style_manager sm;
+  styled_string s (styled_string::from_fmt (sm, NULL, "%%i: %i", 42));
+  ASSERT_EQ (s[0].get_code (), '%');
+  ASSERT_EQ (s[1].get_code (), 'i');
+  ASSERT_EQ (s[2].get_code (), ':');
+  ASSERT_EQ (s[3].get_code (), ' ');
+  ASSERT_EQ (s[4].get_code (), '4');
+  ASSERT_EQ (s[5].get_code (), '2');
+  ASSERT_EQ (s.size (), 6);
+  ASSERT_EQ (s.calc_canvas_width (), 6);
+}
+
+static void
+test_from_fmt_qs ()
+{
+  auto_fix_quotes fix_quotes;
+  open_quote = "\xe2\x80\x98";
+  close_quote = "\xe2\x80\x99";
+
+  style_manager sm;
+  styled_string s (styled_string::from_fmt (sm, NULL, "%qs", "msg"));
+  ASSERT_EQ (sm.get_num_styles (), 2);
+  ASSERT_EQ (s[0].get_code (), 0x2018);
+  ASSERT_EQ (s[0].get_style_id (), 0);
+  ASSERT_EQ (s[1].get_code (), 'm');
+  ASSERT_EQ (s[1].get_style_id (), 1);
+  ASSERT_EQ (s[2].get_code (), 's');
+  ASSERT_EQ (s[2].get_style_id (), 1);
+  ASSERT_EQ (s[3].get_code (), 'g');
+  ASSERT_EQ (s[3].get_style_id (), 1);
+  ASSERT_EQ (s[4].get_code (), 0x2019);
+  ASSERT_EQ (s[4].get_style_id (), 0);
+  ASSERT_EQ (s.size (), 5);
+}
+
+// Test of parsing SGR codes.
+
+static void
+test_from_str_with_bold ()
+{
+  style_manager sm;
+  /* This is the result of pp_printf (pp, "%qs", "foo")
+     with auto_fix_quotes.  */
+  styled_string s (sm, "`\33[01m\33[Kfoo\33[m\33[K'");
+  ASSERT_EQ (s[0].get_code (), '`');
+  ASSERT_EQ (s[0].get_style_id (), 0);
+  ASSERT_EQ (s[1].get_code (), 'f');
+  ASSERT_EQ (s[1].get_style_id (), 1);
+  ASSERT_EQ (s[2].get_code (), 'o');
+  ASSERT_EQ (s[2].get_style_id (), 1);
+  ASSERT_EQ (s[3].get_code (), 'o');
+  ASSERT_EQ (s[3].get_style_id (), 1);
+  ASSERT_EQ (s[4].get_code (), '\'');
+  ASSERT_EQ (s[4].get_style_id (), 0);
+  ASSERT_EQ (s.size (), 5);
+  ASSERT_TRUE (sm.get_style (1).m_bold);
+}
+
+static void
+test_from_str_with_underscore ()
+{
+  style_manager sm;
+  styled_string s (sm, "\33[04m\33[KA");
+  ASSERT_EQ (s[0].get_code (), 'A');
+  ASSERT_EQ (s[0].get_style_id (), 1);
+  ASSERT_TRUE (sm.get_style (1).m_underscore);
+}
+
+static void
+test_from_str_with_blink ()
+{
+  style_manager sm;
+  styled_string s (sm, "\33[05m\33[KA");
+  ASSERT_EQ (s[0].get_code (), 'A');
+  ASSERT_EQ (s[0].get_style_id (), 1);
+  ASSERT_TRUE (sm.get_style (1).m_blink);
+}
+
+// Test of parsing SGR codes.
+
+static void
+test_from_str_with_color ()
+{
+  style_manager sm;
+
+  styled_string s (sm,
+		   ("0"
+		    SGR_SEQ (COLOR_FG_RED)
+		    "R"
+		    SGR_RESET
+		    "2"
+		    SGR_SEQ (COLOR_FG_GREEN)
+		    "G"
+		    SGR_RESET
+		    "4"));
+  ASSERT_EQ (s.size (), 5);
+  ASSERT_EQ (sm.get_num_styles (), 3);
+  ASSERT_EQ (s[0].get_code (), '0');
+  ASSERT_EQ (s[0].get_style_id (), 0);
+  ASSERT_EQ (s[1].get_code (), 'R');
+  ASSERT_EQ (s[1].get_style_id (), 1);
+  ASSERT_EQ (s[2].get_code (), '2');
+  ASSERT_EQ (s[2].get_style_id (), 0);
+  ASSERT_EQ (s[3].get_code (), 'G');
+  ASSERT_EQ (s[3].get_style_id (), 2);
+  ASSERT_EQ (s[4].get_code (), '4');
+  ASSERT_EQ (s[4].get_style_id (), 0);
+  ASSERT_EQ (sm.get_style (1).m_fg_color, style::named_color::RED);
+  ASSERT_EQ (sm.get_style (2).m_fg_color, style::named_color::GREEN);
+}
+
+static void
+test_from_str_with_named_color ()
+{
+  style_manager sm;
+  styled_string s (sm,
+		   ("F"
+		    SGR_SEQ (COLOR_FG_BLACK) "F"
+		    SGR_SEQ (COLOR_FG_RED) "F"
+		    SGR_SEQ (COLOR_FG_GREEN) "F"
+		    SGR_SEQ (COLOR_FG_YELLOW) "F"
+		    SGR_SEQ (COLOR_FG_BLUE) "F"
+		    SGR_SEQ (COLOR_FG_MAGENTA) "F"
+		    SGR_SEQ (COLOR_FG_CYAN) "F"
+		    SGR_SEQ (COLOR_FG_WHITE) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_BLACK) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_RED) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_GREEN) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_YELLOW) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_BLUE) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_MAGENTA) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_CYAN) "F"
+		    SGR_SEQ (COLOR_FG_BRIGHT_WHITE) "F"
+		    SGR_SEQ (COLOR_BG_BLACK) "B"
+		    SGR_SEQ (COLOR_BG_RED) "B"
+		    SGR_SEQ (COLOR_BG_GREEN) "B"
+		    SGR_SEQ (COLOR_BG_YELLOW) "B"
+		    SGR_SEQ (COLOR_BG_BLUE) "B"
+		    SGR_SEQ (COLOR_BG_MAGENTA) "B"
+		    SGR_SEQ (COLOR_BG_CYAN) "B"
+		    SGR_SEQ (COLOR_BG_WHITE) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_BLACK) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_RED) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_GREEN) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_YELLOW) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_BLUE) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_MAGENTA) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_CYAN) "B"
+		    SGR_SEQ (COLOR_BG_BRIGHT_WHITE) "B"));
+  ASSERT_EQ (s.size (), 33);
+  for (size_t i = 0; i < s.size (); i++)
+    ASSERT_EQ (s[i].get_style_id (), i);
+  for (size_t i = 0; i < 17; i++)
+    ASSERT_EQ (s[i].get_code (), 'F');
+  for (size_t i = 17; i < 33; i++)
+    ASSERT_EQ (s[i].get_code (), 'B');
+}
+
+static void
+test_from_str_with_8_bit_color ()
+{
+  {
+    style_manager sm;
+    styled_string s (sm,
+		     ("^[[38;5;232m^[[KF"));
+    ASSERT_EQ (s.size (), 1);
+    ASSERT_EQ (s[0].get_code (), 'F');
+    ASSERT_EQ (s[0].get_style_id (), 1);
+    ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (232));
+  }
+  {
+    style_manager sm;
+    styled_string s (sm,
+		     ("^[[48;5;231m^[[KB"));
+    ASSERT_EQ (s.size (), 1);
+    ASSERT_EQ (s[0].get_code (), 'B');
+    ASSERT_EQ (s[0].get_style_id (), 1);
+    ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (231));
+  }
+}
+
+static void
+test_from_str_with_24_bit_color ()
+{
+  {
+    style_manager sm;
+    styled_string s (sm,
+		     ("^[[38;2;243;250;242m^[[KF"));
+    ASSERT_EQ (s.size (), 1);
+    ASSERT_EQ (s[0].get_code (), 'F');
+    ASSERT_EQ (s[0].get_style_id (), 1);
+    ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (243, 250, 242));
+  }
+  {
+    style_manager sm;
+    styled_string s (sm,
+		     ("^[[48;2;253;247;231m^[[KB"));
+    ASSERT_EQ (s.size (), 1);
+    ASSERT_EQ (s[0].get_code (), 'B');
+    ASSERT_EQ (s[0].get_style_id (), 1);
+    ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (253, 247, 231));
+  }
+}
+
+static void
+test_from_str_combining_characters ()
+{
+  style_manager sm;
+  styled_string s (sm,
+		   /* CYRILLIC CAPITAL LETTER U (U+0423).  */
+		   "\xD0\xA3"
+		   /* COMBINING BREVE (U+0306).  */
+		   "\xCC\x86");
+  ASSERT_EQ (s.size (), 1);
+  ASSERT_EQ (s[0].get_code (), 0x423);
+  ASSERT_EQ (s[0].get_combining_chars ().size (), 1);
+  ASSERT_EQ (s[0].get_combining_chars ()[0], 0x306);
+}
+
+/* Run all selftests in this file.  */
+
+void
+text_art_styled_string_cc_tests ()
+{
+  test_combining_chars ();
+  test_empty ();
+  test_simple ();
+  test_pi_from_utf8 ();
+  test_emoji_from_utf8 ();
+  test_emoji_variant_from_utf8 ();
+  test_emoji_from_codepoint ();
+  test_from_mixed_width_utf8 ();
+  test_url ();
+  test_from_fmt ();
+  test_from_fmt_qs ();
+  test_from_str_with_bold ();
+  test_from_str_with_underscore ();
+  test_from_str_with_blink ();
+  test_from_str_with_color ();
+  test_from_str_with_named_color ();
+  test_from_str_with_8_bit_color ();
+  test_from_str_with_24_bit_color ();
+  test_from_str_combining_characters ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/table.cc b/gcc/text-art/table.cc
new file mode 100644
index 00000000000..42cc4228ea6
--- /dev/null
+++ b/gcc/text-art/table.cc
@@ -0,0 +1,1272 @@
+/* Support for tabular/grid-based content.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#define INCLUDE_MEMORY
+#include "system.h"
+#include "coretypes.h"
+#include "make-unique.h"
+#include "pretty-print.h"
+#include "diagnostic.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/table.h"
+
+using namespace text_art;
+
+/* class text_art::table_cell_content.  */
+
+table_cell_content::table_cell_content (styled_string &&s)
+: m_str (std::move (s)),
+  /* We assume here that the content occupies a single canvas row.  */
+  m_size (m_str.calc_canvas_width (), 1)
+{
+}
+
+void
+table_cell_content::paint_to_canvas (canvas &canvas,
+				     canvas::coord_t top_left) const
+{
+  canvas.paint_text (top_left, m_str);
+}
+
+/* struct text_art::table_dimension_sizes.  */
+
+table_dimension_sizes::table_dimension_sizes (unsigned num)
+: m_requirements (num, 0)
+{
+}
+
+/* class text_art::table::cell_placement.  */
+
+void
+table::cell_placement::paint_cell_contents_to_canvas(canvas &canvas,
+						     canvas::coord_t offset,
+						     const table_geometry &tg) const
+{
+  const canvas::size_t req_canvas_size = get_min_canvas_size ();
+  const canvas::size_t alloc_canvas_size = tg.get_canvas_size (m_rect);
+  gcc_assert (req_canvas_size.w <= alloc_canvas_size.w);
+  gcc_assert (req_canvas_size.h <= alloc_canvas_size.h);
+  const int x_padding = alloc_canvas_size.w - req_canvas_size.w;
+  const int y_padding = alloc_canvas_size.h - req_canvas_size.h;
+  const table::coord_t table_top_left = m_rect.m_top_left;
+  const canvas::coord_t canvas_top_left = tg.table_to_canvas (table_top_left);
+
+  gcc_assert (x_padding >= 0);
+  int x_align_offset;
+  switch (m_x_align)
+    {
+    default:
+      gcc_unreachable ();
+    case x_align::LEFT:
+      x_align_offset = 0;
+      break;
+    case x_align::CENTER:
+      x_align_offset = x_padding / 2;
+      break;
+    case x_align::RIGHT:
+      x_align_offset = x_padding;
+      break;
+    }
+
+  gcc_assert (y_padding >= 0);
+  int y_align_offset;
+  switch (m_y_align)
+    {
+    default:
+      gcc_unreachable ();
+    case y_align::TOP:
+      y_align_offset = 0;
+      break;
+    case y_align::CENTER:
+      y_align_offset = y_padding / 2;
+      break;
+    case y_align::BOTTOM:
+      y_align_offset = y_padding;
+      break;
+    }
+  const canvas::coord_t content_rel_coord
+    (canvas_top_left.x + 1 + x_align_offset,
+     canvas_top_left.y + 1 + y_align_offset);
+  m_content.paint_to_canvas (canvas, offset + content_rel_coord);
+}
+
+/* class text_art::table.  */
+
+
+table::table (size_t size)
+: m_size (size),
+  m_placements (),
+  m_occupancy (size)
+{
+  m_occupancy.fill (-1);
+}
+
+void
+table::set_cell (coord_t coord,
+		 table_cell_content &&content,
+		 enum x_align x_align,
+		 enum y_align y_align)
+{
+  set_cell_span (rect_t (coord, table::size_t (1, 1)),
+		 std::move (content), x_align, y_align);
+}
+
+void
+table::set_cell_span (rect_t span,
+		      table_cell_content &&content,
+		      enum x_align x_align,
+		      enum y_align y_align)
+{
+  gcc_assert (span.m_size.w > 0);
+  gcc_assert (span.m_size.h > 0);
+  int placement_idx = m_placements.size ();
+  m_placements.emplace_back (cell_placement (span, std::move (content),
+					     x_align, y_align));
+  for (int y = span.get_min_y (); y < span.get_next_y (); y++)
+    for (int x = span.get_min_x (); x < span.get_next_x (); x++)
+      {
+	gcc_assert (m_occupancy.get (coord_t (x, y)) == -1);
+	m_occupancy.set (coord_t (x, y), placement_idx);
+      }
+}
+
+canvas
+table::to_canvas (const theme &theme, const style_manager &sm) const
+{
+  table_dimension_sizes col_widths (m_size.w);
+  table_dimension_sizes row_heights (m_size.h);
+  table_cell_sizes cell_sizes (col_widths, row_heights);
+  cell_sizes.pass_1 (*this);
+  cell_sizes.pass_2 (*this);
+  table_geometry tg (*this, cell_sizes);
+  canvas canvas (tg.get_canvas_size (), sm);
+  paint_to_canvas (canvas, canvas::coord_t (0, 0), tg, theme);
+  return canvas;
+}
+
+void
+table::paint_to_canvas (canvas &canvas,
+			canvas::coord_t offset,
+			const table_geometry &tg,
+			const theme &theme) const
+{
+  canvas.fill (canvas::rect_t (offset, tg.get_canvas_size ()),
+	       styled_unichar (' '));
+  paint_cell_borders_to_canvas (canvas, offset, tg, theme);
+  paint_cell_contents_to_canvas (canvas, offset, tg);
+}
+
+/* Print this table to stderr.  */
+
+DEBUG_FUNCTION void
+table::debug () const
+{
+  /* Use a temporary style manager.
+     Styles in the table will be meaningless, so
+     print the canvas with styling disabled.  */
+  style_manager sm;
+  canvas canvas (to_canvas (unicode_theme (), sm));
+  canvas.debug (false);
+}
+
+const table::cell_placement *
+table::get_placement_at (coord_t coord) const
+{
+  const int placement_idx = m_occupancy.get (coord);
+  if (placement_idx == -1)
+    return nullptr;
+  return &m_placements[placement_idx];
+}
+
+int
+table::get_occupancy_safe (coord_t coord) const
+{
+  if (coord.x < 0)
+    return -1;
+  if (coord.x >= m_size.w)
+    return -1;
+  if (coord.y < 0)
+    return -1;
+  if (coord.y >= m_size.h)
+    return -1;
+  return m_occupancy.get (coord);
+}
+
+/* Determine if the "?" edges need borders for table cell D
+   in the following, for the directions relative to "X", based
+   on whether each of table cell boundaries AB, CD, AC, and BD
+   are boundaries between cell spans:
+
+   #            up?
+   #      +-----+-----+
+   #      |           |
+   #      |     ?     |
+   #      |  A  ?  B  |
+   #      |     ?     |
+   #      |           |
+   # left?+ ??? X ??? + right?
+   #      |           |
+   #      |     ?     |
+   #      |  C  ?  D  |
+   #      |     ?     |
+   #      |           |
+   #      +-----+-----+
+   #          down?
+*/
+
+directions
+table::get_connections (int table_x, int table_y) const
+{
+  int cell_a = get_occupancy_safe (coord_t (table_x - 1, table_y - 1));
+  int cell_b = get_occupancy_safe (coord_t (table_x, table_y - 1));
+  int cell_c = get_occupancy_safe (coord_t (table_x - 1, table_y));
+  int cell_d = get_occupancy_safe (coord_t (table_x, table_y));
+  const bool up = (cell_a != cell_b);
+  const bool down = (cell_c != cell_d);
+  const bool left = (cell_a != cell_c);
+  const bool right = (cell_b != cell_d);
+  return directions (up, down, left, right);
+}
+
+/* Paint the grid lines.
+
+   Consider painting
+   - a grid of cells,
+   - plus a right-hand border
+   - and a bottom border
+
+   Then we need to paint to the canvas like this:
+
+   #         PER-TABLE-COLUMN     R BORDER
+   #      +-------------------+   +-----+
+   #
+   #             TABLE CELL WIDTH (in canvas units)
+   #            +-------------+
+   #      .     .     . .     .   .     .
+   #   ...+-----+-----+.+-----+...+-----+ +
+   #      |  U  |     |.|     |   |  U  | |
+   #      |  U  |     |.|     |   |  U  | |
+   #      |LL+RR|RRRRR|.|RRRRR|   |LL+  | |
+   #      |  D  |     |.|     |   |  D  | |
+   #      |  D  |     |.|     |   |  D  | |
+   #   ...+-----+-----+.+-----+...+-----+ |
+   #      .....................   ......  +-- PER-TABLE-ROW
+   #   ...+-----+-----+.+-----+...+-----+ | +
+   #      |  D  |     |.|     |   |  D  | | |
+   #      |  D  |     |.|     |   |  D  | | |
+   #      |  D  |     |.|     |   |  D  | | +---- TABLE CELL HEIGHT (in canvas units)
+   #      |  D  |     |.|     |   |  D  | | |
+   #      |  D  |     |.|     |   |  D  | | |
+   #   ...+-----+-----+.+-----+...+-----+ + +
+   #      .     .     .     .   .     .
+   #   ...+-----+-----+.+-----+...+-----+  +
+   #      |  D  |     |.|     |   |  U  |  |
+   #      |  D  |     |.|     |   |  U  |  |
+   #      |LL+RR|RRRRR|.|RRRRR|   |LL+  |  | BOTTOM BORDER
+   #      |     |     |.|     |   |     |  |
+   #      |     |     |.|     |   |     |  |
+   #   ...+-----+-----+.+-----+...+-----+  +
+
+   where each:
+
+   #    +-----+
+   #    |     |
+   #    |     |
+   #    |     |
+   #    |     |
+   #    |     |
+   #    +-----+
+
+   is a canvas cell, and the U, L, R, D express the connections
+   that are present with neighboring table cells.  These affect
+   the kinds of borders that we draw for a particular table cell.  */
+
+void
+table::paint_cell_borders_to_canvas (canvas &canvas,
+				     canvas::coord_t offset,
+				     const table_geometry &tg,
+				     const theme &theme) const
+{
+  /* The per-table-cell left and top borders are either drawn or not,
+     but if they are, they aren't affected by per-table-cell connections.  */
+  const canvas::cell_t left_border
+    = theme.get_line_art (directions (true, /* up */
+				      true, /* down */
+				      false, /* left */
+				      false /* right */));
+  const canvas::cell_t top_border
+    = theme.get_line_art (directions (false, /* up */
+				      false, /* down */
+				      true, /* left */
+				      true)); /* right */
+  for (int table_y = 0; table_y < m_size.h; table_y++)
+    {
+      const int canvas_y = tg.table_y_to_canvas_y (table_y);
+      for (int table_x = 0; table_x < m_size.w; table_x++)
+	{
+	  canvas::coord_t canvas_top_left
+	    = tg.table_to_canvas(table::coord_t (table_x, table_y));
+
+	  const directions c (get_connections (table_x, table_y));
+
+	  /* Paint top-left corner of border, if any.  */
+	  canvas.paint (offset + canvas_top_left,
+			theme.get_line_art (c));
+
+	  /* Paint remainder of left border of cell, if any.
+	     We assume here that the content occupies a single canvas row.  */
+	  if (c.m_down)
+	    canvas.paint (offset + canvas::coord_t (canvas_top_left.x,
+						    canvas_y + 1),
+			  left_border);
+
+	  /* Paint remainder of top border of cell, if any.  */
+	  if (c.m_right)
+	    {
+	      const int col_width = tg.get_col_width (table_x);
+	      for (int x_offset = 0; x_offset < col_width; x_offset++)
+		{
+		  const int canvas_x = canvas_top_left.x + 1 + x_offset;
+		  canvas.paint (offset + canvas::coord_t (canvas_x, canvas_y),
+				top_border);
+		}
+	    }
+	}
+
+      /* Paint right-hand border of row.  */
+      const int table_x = m_size.w;
+      const int canvas_x = tg.table_x_to_canvas_x (table_x);
+      const directions c (get_connections (m_size.w, table_y));
+      canvas.paint(offset + canvas::coord_t (canvas_x, canvas_y),
+		   theme.get_line_art (directions (c.m_up,
+						   c.m_down,
+						   c.m_left,
+						   false))); /* right */
+      /* We assume here that the content occupies a single canvas row.  */
+      canvas.paint(offset + canvas::coord_t (canvas_x, canvas_y + 1),
+		   theme.get_line_art (directions (c.m_down, /* up */
+						   c.m_down, /* down */
+						   false, /* left */
+						   false))); /* right */
+    }
+
+  /* Draw bottom border of table.  */
+  {
+    const int canvas_y = tg.get_canvas_size ().h - 1;
+    for (int table_x = 0; table_x < m_size.w; table_x++)
+      {
+	const directions c (get_connections (table_x, m_size.h));
+	const int left_canvas_x = tg.table_x_to_canvas_x (table_x);
+	canvas.paint (offset + canvas::coord_t (left_canvas_x, canvas_y),
+		      theme.get_line_art (directions (c.m_up,
+						      false, /* down */
+						      c.m_left,
+						      c.m_right)));
+	const int col_width = tg.get_col_width (table_x);
+	for (int x_offset = 0; x_offset < col_width; x_offset++)
+	  {
+	    const int canvas_x = left_canvas_x + 1 + x_offset;
+	    canvas.paint(offset + canvas::coord_t (canvas_x, canvas_y),
+			 theme.get_line_art (directions (false, // up
+							 false, // down
+							 c.m_right, // left
+							 c.m_right))); // right
+	  }
+      }
+
+    /* Bottom-right corner of table.  */
+    const int table_x = m_size.w;
+    const int canvas_x = tg.table_x_to_canvas_x (table_x);
+    const directions c (get_connections (m_size.w, m_size.h));
+    canvas.paint (offset + canvas::coord_t (canvas_x, canvas_y),
+		  theme.get_line_art (directions (c.m_up, // up
+						  false, // down
+						  c.m_left, // left
+						  false))); // right
+  }
+}
+
+void
+table::paint_cell_contents_to_canvas(canvas &canvas,
+				     canvas::coord_t offset,
+				     const table_geometry &tg) const
+{
+  for (auto &placement : m_placements)
+    placement.paint_cell_contents_to_canvas (canvas, offset, tg);
+}
+
+/* class table_cell_sizes.  */
+
+/* Consider 1x1 cells.  */
+
+void
+table_cell_sizes::pass_1 (const table &table)
+{
+  for (auto &placement : table.m_placements)
+    if (placement.one_by_one_p ())
+      {
+	canvas::size_t canvas_size (placement.get_min_canvas_size ());
+	table::coord_t table_coord (placement.m_rect.m_top_left);
+	m_col_widths.require (table_coord.x, canvas_size.w);
+	m_row_heights.require (table_coord.y, canvas_size.h);
+      }
+}
+
+/* Consider cells that span more than one row or column.  */
+
+void
+table_cell_sizes::pass_2 (const table &table)
+{
+  for (auto &placement : table.m_placements)
+    if (!placement.one_by_one_p ())
+      {
+	const canvas::size_t req_canvas_size (placement.get_min_canvas_size ());
+	const canvas::size_t current_canvas_size
+	  = get_canvas_size (placement.m_rect);
+	/* Grow columns as necessary.  */
+	if (req_canvas_size.w > current_canvas_size.w)
+	  {
+	    /* Spread the deficit amongst the columns.  */
+	    int deficit = req_canvas_size.w - current_canvas_size.w;
+	    const int per_col = deficit / placement.m_rect.m_size.w;
+	    for (int table_x = placement.m_rect.get_min_x ();
+		 table_x < placement.m_rect.get_next_x ();
+		 table_x++)
+	    {
+	      m_col_widths.m_requirements[table_x] += per_col;
+	      deficit -= per_col;
+	    }
+	    /* Make sure we allocate all of the deficit.  */
+	    if (deficit > 0)
+	      {
+		const int table_x = placement.m_rect.get_max_x ();
+		m_col_widths.m_requirements[table_x] += deficit;
+	      }
+	  }
+	/* Grow rows as necessary.  */
+	if (req_canvas_size.h > current_canvas_size.h)
+	  {
+	    /* Spread the deficit amongst the rows.  */
+	    int deficit = req_canvas_size.h - current_canvas_size.h;
+	    const int per_row = deficit / placement.m_rect.m_size.h;
+	    for (int table_y = placement.m_rect.get_min_y ();
+		 table_y < placement.m_rect.get_next_y ();
+		 table_y++)
+	    {
+	      m_row_heights.m_requirements[table_y] += per_row;
+	      deficit -= per_row;
+	    }
+	    /* Make sure we allocate all of the deficit.  */
+	    if (deficit > 0)
+	      {
+		const int table_y = placement.m_rect.get_max_y ();
+		m_row_heights.m_requirements[table_y] += deficit;
+	      }
+	  }
+      }
+}
+
+canvas::size_t
+table_cell_sizes::get_canvas_size (const table::rect_t &rect) const
+{
+  canvas::size_t result (0, 0);
+  for (int table_x = rect.get_min_x ();
+       table_x < rect.get_next_x ();
+       table_x ++)
+    result.w += m_col_widths.m_requirements[table_x];
+  for (int table_y = rect.get_min_y ();
+       table_y < rect.get_next_y ();
+       table_y ++)
+    result.h += m_row_heights.m_requirements[table_y];
+  /* Allow space for the borders.  */
+  result.w += rect.m_size.w - 1;
+  result.h += rect.m_size.h - 1;
+  return result;
+}
+
+/* class text_art::table_geometry.  */
+
+table_geometry::table_geometry (const table &table, table_cell_sizes &cell_sizes)
+: m_table (table),
+  m_cell_sizes (cell_sizes),
+  m_canvas_size (canvas::size_t (0, 0)),
+  m_col_start_x (table.get_size ().w),
+  m_row_start_y (table.get_size ().h)
+{
+  recalc_coords ();
+}
+
+void
+table_geometry::recalc_coords ()
+{
+  /* Start canvas column of table cell, including leading border.  */
+  m_col_start_x.clear ();
+  int iter_canvas_x = 0;
+  for (auto w : m_cell_sizes.m_col_widths.m_requirements)
+    {
+      m_col_start_x.push_back (iter_canvas_x);
+      iter_canvas_x += w + 1;
+    }
+
+  /* Start canvas row of table cell, including leading border.  */
+  m_row_start_y.clear ();
+  int iter_canvas_y = 0;
+  for (auto h : m_cell_sizes.m_row_heights.m_requirements)
+    {
+      m_row_start_y.push_back (iter_canvas_y);
+      iter_canvas_y += h + 1;
+    }
+
+  m_canvas_size = canvas::size_t (iter_canvas_x + 1,
+				  iter_canvas_y + 1);
+}
+
+/* Get the TL corner of the table cell at TABLE_COORD
+   in canvas coords (including the border).  */
+
+canvas::coord_t
+table_geometry::table_to_canvas (table::coord_t table_coord) const
+{
+  return canvas::coord_t (table_x_to_canvas_x (table_coord.x),
+			  table_y_to_canvas_y (table_coord.y));
+}
+
+/* Get the left border of the table cell at column TABLE_X
+   in canvas coords (including the border).  */
+
+int
+table_geometry::table_x_to_canvas_x (int table_x) const
+{
+  /* Allow one beyond the end, for the right-hand border of the table.  */
+  if (table_x == m_col_start_x.size ())
+    return m_canvas_size.w - 1;
+  return m_col_start_x[table_x];
+}
+
+/* Get the top border of the table cell at column TABLE_Y
+   in canvas coords (including the border).  */
+
+int
+table_geometry::table_y_to_canvas_y (int table_y) const
+{
+  /* Allow one beyond the end, for the right-hand border of the table.  */
+  if (table_y == m_row_start_y.size ())
+    return m_canvas_size.h - 1;
+  return m_row_start_y[table_y];
+}
+
+/* class text_art::simple_table_geometry.  */
+
+simple_table_geometry::simple_table_geometry (const table &table)
+: m_col_widths (table.get_size ().w),
+  m_row_heights (table.get_size ().h),
+  m_cell_sizes (m_col_widths, m_row_heights),
+  m_tg (table, m_cell_sizes)
+{
+  m_cell_sizes.pass_1 (table);
+  m_cell_sizes.pass_2 (table);
+  m_tg.recalc_coords ();
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+static void
+test_tic_tac_toe ()
+{
+  style_manager sm;
+  table t (table::size_t (3, 3));
+  t.set_cell (table::coord_t (0, 0), styled_string (sm, "X"));
+  t.set_cell (table::coord_t (1, 0), styled_string (sm, ""));
+  t.set_cell (table::coord_t (2, 0), styled_string (sm, ""));
+  t.set_cell (table::coord_t (0, 1), styled_string (sm, "O"));
+  t.set_cell (table::coord_t (1, 1), styled_string (sm, "O"));
+  t.set_cell (table::coord_t (2, 1), styled_string (sm, ""));
+  t.set_cell (table::coord_t (0, 2), styled_string (sm, "X"));
+  t.set_cell (table::coord_t (1, 2), styled_string (sm, ""));
+  t.set_cell (table::coord_t (2, 2), styled_string (sm, "O"));
+
+  {
+    canvas canvas (t.to_canvas (ascii_theme (), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("+-+-+-+\n"
+	"|X| | |\n"
+	"+-+-+-+\n"
+	"|O|O| |\n"
+	"+-+-+-+\n"
+	"|X| |O|\n"
+	"+-+-+-+\n"));
+  }
+
+  {
+    canvas canvas (t.to_canvas (unicode_theme (), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("┌─┬─┬─┐\n"
+	"│X│ │ │\n"
+	"├─┼─┼─┤\n"
+	"│O│O│ │\n"
+	"├─┼─┼─┤\n"
+	"│X│ │O│\n"
+	"└─┴─┴─┘\n"));
+  }
+}
+
+static table
+make_text_table ()
+{
+  style_manager sm;
+  table t (table::size_t (3, 3));
+  t.set_cell (table::coord_t (0, 0), styled_string (sm, "top left"));
+  t.set_cell (table::coord_t (1, 0), styled_string (sm, "top middle"));
+  t.set_cell (table::coord_t (2, 0), styled_string (sm, "top right"));
+  t.set_cell (table::coord_t (0, 1), styled_string (sm, "middle left"));
+  t.set_cell (table::coord_t (1, 1), styled_string (sm, "middle middle"));
+  t.set_cell (table::coord_t (2, 1), styled_string (sm, "middle right"));
+  t.set_cell (table::coord_t (0, 2), styled_string (sm, "bottom left"));
+  t.set_cell (table::coord_t (1, 2), styled_string (sm, "bottom middle"));
+  t.set_cell (table::coord_t (2, 2), styled_string (sm, "bottom right"));
+  return t;
+}
+
+static void
+test_text_table ()
+{
+  style_manager sm;
+  table table = make_text_table ();
+  {
+    canvas canvas (table.to_canvas (ascii_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("+-----------+-------------+------------+\n"
+	"| top left  | top middle  | top right  |\n"
+	"+-----------+-------------+------------+\n"
+	"|middle left|middle middle|middle right|\n"
+	"+-----------+-------------+------------+\n"
+	"|bottom left|bottom middle|bottom right|\n"
+	"+-----------+-------------+------------+\n"));
+  }
+  {
+    canvas canvas (table.to_canvas (unicode_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("┌───────────┬─────────────┬────────────┐\n"
+	"│ top left  │ top middle  │ top right  │\n"
+	"├───────────┼─────────────┼────────────┤\n"
+	"│middle left│middle middle│middle right│\n"
+	"├───────────┼─────────────┼────────────┤\n"
+	"│bottom left│bottom middle│bottom right│\n"
+	"└───────────┴─────────────┴────────────┘\n"));
+  }
+}
+
+static void
+test_offset_table ()
+{
+  style_manager sm;
+  table table = make_text_table ();
+  simple_table_geometry stg (table);
+  const canvas::size_t tcs = stg.m_tg.get_canvas_size();
+  {
+    canvas canvas (canvas::size_t (tcs.w + 5, tcs.h + 5), sm);
+    canvas.debug_fill ();
+    table.paint_to_canvas (canvas, canvas::coord_t (3, 3),
+			   stg.m_tg,
+			   ascii_theme());
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("*********************************************\n"
+	"*********************************************\n"
+	"*********************************************\n"
+	"***+-----------+-------------+------------+**\n"
+	"***| top left  | top middle  | top right  |**\n"
+	"***+-----------+-------------+------------+**\n"
+	"***|middle left|middle middle|middle right|**\n"
+	"***+-----------+-------------+------------+**\n"
+	"***|bottom left|bottom middle|bottom right|**\n"
+	"***+-----------+-------------+------------+**\n"
+	"*********************************************\n"
+	"*********************************************\n"));
+  }
+  {
+    canvas canvas (canvas::size_t (tcs.w + 5, tcs.h + 5), sm);
+    canvas.debug_fill ();
+    table.paint_to_canvas (canvas, canvas::coord_t (3, 3),
+			   stg.m_tg,
+			   unicode_theme());
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("*********************************************\n"
+	"*********************************************\n"
+	"*********************************************\n"
+	"***┌───────────┬─────────────┬────────────┐**\n"
+	"***│ top left  │ top middle  │ top right  │**\n"
+	"***├───────────┼─────────────┼────────────┤**\n"
+	"***│middle left│middle middle│middle right│**\n"
+	"***├───────────┼─────────────┼────────────┤**\n"
+	"***│bottom left│bottom middle│bottom right│**\n"
+	"***└───────────┴─────────────┴────────────┘**\n"
+	"*********************************************\n"
+	"*********************************************\n"));
+  }
+}
+
+#define ASSERT_TABLE_CELL_STREQ(TABLE, TABLE_X, TABLE_Y, EXPECTED_STR)	\
+  SELFTEST_BEGIN_STMT							\
+    table::coord_t coord ((TABLE_X), (TABLE_Y));			\
+    const table::cell_placement *cp = (TABLE).get_placement_at (coord);	\
+    ASSERT_NE (cp, nullptr);						\
+    ASSERT_EQ (cp->get_content (), styled_string (sm, EXPECTED_STR)); \
+  SELFTEST_END_STMT
+
+#define ASSERT_TABLE_NULL_CELL(TABLE, TABLE_X, TABLE_Y)			\
+  SELFTEST_BEGIN_STMT							\
+    table::coord_t coord ((TABLE_X), (TABLE_Y));			\
+    const table::cell_placement *cp = (TABLE).get_placement_at (coord);	\
+    ASSERT_EQ (cp, nullptr);						\
+  SELFTEST_END_STMT
+
+static void
+test_spans ()
+{
+  style_manager sm;
+  table table (table::size_t (3, 3));
+  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
+				      table::size_t (3, 1)),
+		       styled_string (sm, "ABC"));
+  table.set_cell_span (table::rect_t (table::coord_t (0, 1),
+				      table::size_t (2, 1)),
+		       styled_string (sm, "DE"));
+  table.set_cell_span (table::rect_t (table::coord_t (2, 1),
+				      table::size_t (1, 1)),
+		       styled_string (sm, "F"));
+  table.set_cell (table::coord_t (0, 2), styled_string (sm, "G"));
+  table.set_cell (table::coord_t (1, 2), styled_string (sm, "H"));
+  table.set_cell (table::coord_t (2, 2), styled_string (sm, "I"));
+  {
+    canvas canvas (table.to_canvas (ascii_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("+-----+\n"
+	"| ABC |\n"
+	"+---+-+\n"
+	"|DE |F|\n"
+	"+-+-+-+\n"
+	"|G|H|I|\n"
+	"+-+-+-+\n"));
+  }
+  {
+    canvas canvas (table.to_canvas (unicode_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("┌─────┐\n"
+	"│ ABC │\n"
+	"├───┬─┤\n"
+	"│DE │F│\n"
+	"├─┬─┼─┤\n"
+	"│G│H│I│\n"
+	"└─┴─┴─┘\n"));
+  }
+}
+
+/* Verify building this 5x5 table with spans:
+     |0|1|2|3|4|
+     +-+-+-+-+-+
+    0|A A A|B|C|0
+     +     +-+ +
+    1|A A A|D|C|1
+     +     +-+-+
+    2|A A A|E|F|2
+     +-+-+-+-+-+
+    3|G G|H|I I|3
+     |   | +-+-+
+    4|G G|H|J J|4
+     +-+-+-+-+-+
+     |0|1|2|3|4|
+*/
+
+static void
+test_spans_2 ()
+{
+  style_manager sm;
+  table table (table::size_t (5, 5));
+  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "A"));
+  table.set_cell_span (table::rect_t (table::coord_t (3, 0),
+				      table::size_t (1, 1)),
+		       styled_string (sm, "B"));
+  table.set_cell_span (table::rect_t (table::coord_t (4, 0),
+				      table::size_t (1, 2)),
+		       styled_string (sm, "C"));
+  table.set_cell_span (table::rect_t (table::coord_t (3, 1),
+				      table::size_t (1, 1)),
+		       styled_string (sm, "D"));
+  table.set_cell_span (table::rect_t (table::coord_t (3, 2),
+				      table::size_t (1, 1)),
+		       styled_string (sm, "E"));
+  table.set_cell_span (table::rect_t (table::coord_t (4, 2),
+				      table::size_t (1, 1)),
+		       styled_string (sm, "F"));
+  table.set_cell_span (table::rect_t (table::coord_t (0, 3),
+				      table::size_t (2, 2)),
+		       styled_string (sm, "G"));
+  table.set_cell_span (table::rect_t (table::coord_t (2, 3),
+				      table::size_t (1, 2)),
+		       styled_string (sm, "H"));
+  table.set_cell_span (table::rect_t (table::coord_t (3, 3),
+				      table::size_t (2, 1)),
+		       styled_string (sm, "I"));
+  table.set_cell_span (table::rect_t (table::coord_t (3, 4),
+				      table::size_t (2, 1)),
+		       styled_string (sm, "J"));
+
+  /* Check occupancy at each table coordinate.  */
+  ASSERT_TABLE_CELL_STREQ (table, 0, 0, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 1, 0, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 2, 0, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 3, 0, "B");
+  ASSERT_TABLE_CELL_STREQ (table, 4, 0, "C");
+
+  ASSERT_TABLE_CELL_STREQ (table, 0, 1, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 1, 1, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 2, 1, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 3, 1, "D");
+  ASSERT_TABLE_CELL_STREQ (table, 4, 1, "C");
+
+  ASSERT_TABLE_CELL_STREQ (table, 0, 2, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 1, 2, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 2, 2, "A");
+  ASSERT_TABLE_CELL_STREQ (table, 3, 2, "E");
+  ASSERT_TABLE_CELL_STREQ (table, 4, 2, "F");
+
+  ASSERT_TABLE_CELL_STREQ (table, 0, 3, "G");
+  ASSERT_TABLE_CELL_STREQ (table, 1, 3, "G");
+  ASSERT_TABLE_CELL_STREQ (table, 2, 3, "H");
+  ASSERT_TABLE_CELL_STREQ (table, 3, 3, "I");
+  ASSERT_TABLE_CELL_STREQ (table, 4, 3, "I");
+
+  ASSERT_TABLE_CELL_STREQ (table, 0, 4, "G");
+  ASSERT_TABLE_CELL_STREQ (table, 1, 4, "G");
+  ASSERT_TABLE_CELL_STREQ (table, 2, 4, "H");
+  ASSERT_TABLE_CELL_STREQ (table, 3, 4, "J");
+  ASSERT_TABLE_CELL_STREQ (table, 4, 4, "J");
+
+  {
+    canvas canvas (table.to_canvas (ascii_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("+---+-+-+\n"
+	"|   |B| |\n"
+	"|   +-+C|\n"
+	"| A |D| |\n"
+	"|   +-+-+\n"
+	"|   |E|F|\n"
+	"+-+-+-+-+\n"
+	"| | | I |\n"
+	"|G|H+---+\n"
+	"| | | J |\n"
+	"+-+-+---+\n"));
+  }
+  {
+    canvas canvas (table.to_canvas (unicode_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("┌───┬─┬─┐\n"
+	"│   │B│ │\n"
+	"│   ├─┤C│\n"
+	"│ A │D│ │\n"
+	"│   ├─┼─┤\n"
+	"│   │E│F│\n"
+	"├─┬─┼─┴─┤\n"
+	"│ │ │ I │\n"
+	"│G│H├───┤\n"
+	"│ │ │ J │\n"
+	"└─┴─┴───┘\n"));
+  }
+}
+
+/* Experiment with adding a 1-table-column gap at the boundary between
+   valid vs invalid for visualizing a buffer overflow.  */
+static void
+test_spans_3 ()
+{
+  const char * const str = "hello world!";
+  const size_t buf_size = 10;
+  const size_t str_size = strlen (str) + 1;
+
+  style_manager sm;
+  table table (table::size_t (str_size + 1, 3));
+
+  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
+				      table::size_t (str_size + 1, 1)),
+		       styled_string (sm, "String literal"));
+
+  for (size_t i = 0; i < str_size; i++)
+    {
+      table::coord_t c (i, 1);
+      if (i >= buf_size)
+	c.x++;
+      if (str[i] == '\0')
+	table.set_cell (c, styled_string (sm, "NUL"));
+      else
+	table.set_cell (c, styled_string ((cppchar_t)str[i]));
+    }
+
+  table.set_cell_span (table::rect_t (table::coord_t (0, 2),
+				      table::size_t (buf_size, 1)),
+		       styled_string::from_fmt (sm,
+						     nullptr,
+						     "'buf' (char[%i])",
+						     (int)buf_size));
+  table.set_cell_span (table::rect_t (table::coord_t (buf_size + 1, 2),
+				      table::size_t (str_size - buf_size, 1)),
+		       styled_string (sm, "overflow"));
+
+  {
+    canvas canvas (table.to_canvas (ascii_theme (), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       "+-----------------------------+\n"
+       "|       String literal        |\n"
+       "+-+-+-+-+-+-+-+-+-+-++-+-+----+\n"
+       "|h|e|l|l|o| |w|o|r|l||d|!|NUL |\n"
+       "+-+-+-+-+-+-+-+-+-+-++-+-+----+\n"
+       "| 'buf' (char[10])  ||overflow|\n"
+       "+-------------------++--------+\n");
+  }
+  {
+    canvas canvas (table.to_canvas (unicode_theme (), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("┌─────────────────────────────┐\n"
+	"│       String literal        │\n"
+	"├─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬┬─┬─┬────┤\n"
+	"│h│e│l│l│o│ │w│o│r│l││d│!│NUL │\n"
+	"├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┤├─┴─┴────┤\n"
+	"│ 'buf' (char[10])  ││overflow│\n"
+	"└───────────────────┘└────────┘\n"));
+  }
+}
+
+static void
+test_double_width_chars ()
+{
+  table_cell_content tcc (styled_string ((cppchar_t)0x1f642));
+  ASSERT_EQ (tcc.get_canvas_size ().w, 2);
+  ASSERT_EQ (tcc.get_canvas_size ().h, 1);
+
+  style_manager sm;
+  table table (table::size_t (1, 1));
+  table.set_cell (table::coord_t (0,0),
+		  styled_string ((cppchar_t)0x1f642));
+
+  canvas canvas (table.to_canvas (unicode_theme(), sm));
+  ASSERT_CANVAS_STREQ
+    (canvas, false,
+     // FIXME: are we allowed unicode chars in string literals in our source?
+     ("┌──┐\n"
+      "│🙂│\n"
+      "└──┘\n"));
+}
+
+static void
+test_ipv4_header ()
+{
+  style_manager sm;
+  table table (table::size_t (34, 10));
+  table.set_cell (table::coord_t (0, 0), styled_string (sm, "Offsets"));
+  table.set_cell (table::coord_t (1, 0), styled_string (sm, "Octet"));
+  table.set_cell (table::coord_t (0, 1), styled_string (sm, "Octet"));
+  for (int octet = 0; octet < 4; octet++)
+    table.set_cell_span (table::rect_t (table::coord_t (2 + (octet * 8), 0),
+					table::size_t (8, 1)),
+			 styled_string::from_fmt (sm, nullptr, "%i", octet));
+  table.set_cell (table::coord_t (1, 1), styled_string (sm, "Bit"));
+  for (int bit = 0; bit < 32; bit++)
+    table.set_cell (table::coord_t (bit + 2, 1),
+		    styled_string::from_fmt (sm, nullptr, "%i", bit));
+  for (int word = 0; word < 6; word++)
+    {
+      table.set_cell (table::coord_t (0, word + 2),
+		      styled_string::from_fmt (sm, nullptr, "%i", word * 4));
+      table.set_cell (table::coord_t (1, word + 2),
+		      styled_string::from_fmt (sm, nullptr, "%i", word * 32));
+    }
+
+  table.set_cell (table::coord_t (0, 8), styled_string (sm, "..."));
+  table.set_cell (table::coord_t (1, 8), styled_string (sm, "..."));
+  table.set_cell (table::coord_t (0, 9), styled_string (sm, "56"));
+  table.set_cell (table::coord_t (1, 9), styled_string (sm, "448"));
+
+#define SET_BITS(FIRST, LAST, NAME)					\
+  do {									\
+    const int first = (FIRST);						\
+    const int last = (LAST);						\
+    const char *name = (NAME);						\
+    const int row = first / 32;						\
+    gcc_assert (last / 32 == row);					\
+    table::rect_t rect (table::coord_t ((first % 32) + 2, row + 2),	\
+			table::size_t (last + 1 - first , 1));		\
+    table.set_cell_span (rect, styled_string (sm, name));		\
+  } while (0)
+
+  SET_BITS (0, 3, "Version");
+  SET_BITS (4, 7, "IHL");
+  SET_BITS (8, 13, "DSCP");
+  SET_BITS (14, 15, "ECN");
+  SET_BITS (16, 31, "Total Length");
+
+  SET_BITS (32 +  0, 32 + 15, "Identification");
+  SET_BITS (32 + 16, 32 + 18, "Flags");
+  SET_BITS (32 + 19, 32 + 31, "Fragment Offset");
+
+  SET_BITS (64 +  0, 64 +  7, "Time To Live");
+  SET_BITS (64 +  8, 64 + 15, "Protocol");
+  SET_BITS (64 + 16, 64 + 31, "Header Checksum");
+
+  SET_BITS (96 +  0, 96 + 31, "Source IP Address");
+  SET_BITS (128 +  0, 128 + 31, "Destination IP Address");
+
+  table.set_cell_span(table::rect_t (table::coord_t (2, 7),
+				     table::size_t (32, 3)),
+		      styled_string (sm, "Options"));
+  {
+    canvas canvas (table.to_canvas (ascii_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("+-------+-----+---------------+---------------------+-----------------------+-----------------------+\n"
+	"|Offsets|Octet|       0       |          1          |           2           |           3           |\n"
+	"+-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\n"
+	"| Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|\n"
+	"+-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\n"
+	"|   0   |  0  |Version|  IHL  |     DSCP      | ECN |                 Total Length                  |\n"
+	"+-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+\n"
+	"|   4   | 32  |           Identification            | Flags  |           Fragment Offset            |\n"
+	"+-------+-----+---------------+---------------------+--------+--------------------------------------+\n"
+	"|   8   | 64  | Time To Live  |      Protocol       |                Header Checksum                |\n"
+	"+-------+-----+---------------+---------------------+-----------------------------------------------+\n"
+	"|  12   | 96  |                                  Source IP Address                                  |\n"
+	"+-------+-----+-------------------------------------------------------------------------------------+\n"
+	"|  16   | 128 |                               Destination IP Address                                |\n"
+	"+-------+-----+-------------------------------------------------------------------------------------+\n"
+	"|  20   | 160 |                                                                                     |\n"
+	"+-------+-----+                                                                                     |\n"
+	"|  ...  | ... |                                       Options                                       |\n"
+	"+-------+-----+                                                                                     |\n"
+	"|  56   | 448 |                                                                                     |\n"
+	"+-------+-----+-------------------------------------------------------------------------------------+\n"));
+  }
+  {
+    canvas canvas (table.to_canvas (unicode_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       // FIXME: are we allowed unicode chars in string literals in our source?
+       ("┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐\n"
+	"│Offsets│Octet│       0       │          1          │           2           │           3           │\n"
+	"├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤\n"
+	"│ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│\n"
+	"├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤\n"
+	"│   0   │  0  │Version│  IHL  │     DSCP      │ ECN │                 Total Length                  │\n"
+	"├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤\n"
+	"│   4   │ 32  │           Identification            │ Flags  │           Fragment Offset            │\n"
+	"├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤\n"
+	"│   8   │ 64  │ Time To Live  │      Protocol       │                Header Checksum                │\n"
+	"├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤\n"
+	"│  12   │ 96  │                                  Source IP Address                                  │\n"
+	"├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤\n"
+	"│  16   │ 128 │                               Destination IP Address                                │\n"
+	"├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤\n"
+	"│  20   │ 160 │                                                                                     │\n"
+	"├───────┼─────┤                                                                                     │\n"
+	"│  ...  │ ... │                                       Options                                       │\n"
+	"├───────┼─────┤                                                                                     │\n"
+	"│  56   │ 448 │                                                                                     │\n"
+	"└───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘\n"));
+  }
+}
+
+static void
+test_missing_cells ()
+{
+  style_manager sm;
+  table table (table::size_t (3, 3));
+  table.set_cell (table::coord_t (1, 0), styled_string (sm, "A"));
+  table.set_cell (table::coord_t (0, 1), styled_string (sm, "B"));
+  table.set_cell (table::coord_t (1, 1), styled_string (sm, "C"));
+  table.set_cell (table::coord_t (2, 1), styled_string (sm, "D"));
+  table.set_cell (table::coord_t (1, 2), styled_string (sm, "E"));
+
+  ASSERT_TABLE_NULL_CELL (table, 0, 0);
+  ASSERT_TABLE_CELL_STREQ (table, 1, 0, "A");
+  ASSERT_TABLE_NULL_CELL (table, 2, 0);
+
+  ASSERT_TABLE_CELL_STREQ (table, 0, 1, "B");
+  ASSERT_TABLE_CELL_STREQ (table, 1, 1, "C");
+  ASSERT_TABLE_CELL_STREQ (table, 2, 1, "D");
+
+  ASSERT_TABLE_NULL_CELL (table, 0, 2);
+  ASSERT_TABLE_CELL_STREQ (table, 1, 2, "E");
+  ASSERT_TABLE_NULL_CELL (table, 2, 2);
+
+  {
+    canvas canvas (table.to_canvas (ascii_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("  +-+\n"
+	"  |A|\n"
+	"+-+-+-+\n"
+	"|B|C|D|\n"
+	"+-+-+-+\n"
+	"  |E|\n"
+	"  +-+\n"));
+  }
+  {
+    canvas canvas (table.to_canvas (unicode_theme(), sm));
+    ASSERT_CANVAS_STREQ
+      (canvas, false,
+       ("  ┌─┐\n"
+	"  │A│\n"
+	"┌─┼─┼─┐\n"
+	"│B│C│D│\n"
+	"└─┼─┼─┘\n"
+	"  │E│\n"
+	"  └─┘\n"));
+  }
+}
+
+static void
+test_add_row ()
+{
+  style_manager sm;
+  table table (table::size_t (3, 0));
+  for (int i = 0; i < 5; i++)
+    {
+      const int y = table.add_row ();
+      for (int x = 0; x < 3; x++)
+	table.set_cell (table::coord_t (x, y),
+			styled_string::from_fmt (sm, nullptr,
+						 "%i, %i", x, y));
+    }
+  canvas canvas (table.to_canvas (ascii_theme(), sm));
+  ASSERT_CANVAS_STREQ
+    (canvas, false,
+     ("+----+----+----+\n"
+      "|0, 0|1, 0|2, 0|\n"
+      "+----+----+----+\n"
+      "|0, 1|1, 1|2, 1|\n"
+      "+----+----+----+\n"
+      "|0, 2|1, 2|2, 2|\n"
+      "+----+----+----+\n"
+      "|0, 3|1, 3|2, 3|\n"
+      "+----+----+----+\n"
+      "|0, 4|1, 4|2, 4|\n"
+      "+----+----+----+\n"));
+}
+
+static void
+test_alignment ()
+{
+  style_manager sm;
+  table table (table::size_t (9, 9));
+  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "left top"),
+		      x_align::LEFT, y_align::TOP);
+  table.set_cell_span (table::rect_t (table::coord_t (3, 0),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "center top"),
+		       x_align::CENTER, y_align::TOP);
+  table.set_cell_span (table::rect_t (table::coord_t (6, 0),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "right top"),
+		       x_align::RIGHT, y_align::TOP);
+  table.set_cell_span (table::rect_t (table::coord_t (0, 3),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "left center"),
+		       x_align::LEFT, y_align::CENTER);
+  table.set_cell_span (table::rect_t (table::coord_t (3, 3),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "center center"),
+		       x_align::CENTER, y_align::CENTER);
+  table.set_cell_span (table::rect_t (table::coord_t (6, 3),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "right center"),
+		       x_align::RIGHT, y_align::CENTER);
+  table.set_cell_span (table::rect_t (table::coord_t (0, 6),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "left bottom"),
+		       x_align::LEFT, y_align::BOTTOM);
+  table.set_cell_span (table::rect_t (table::coord_t (3, 6),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "center bottom"),
+		       x_align::CENTER, y_align::BOTTOM);
+  table.set_cell_span (table::rect_t (table::coord_t (6, 6),
+				      table::size_t (3, 3)),
+		       styled_string (sm, "right bottom"),
+		       x_align::RIGHT, y_align::BOTTOM);
+
+  canvas canvas (table.to_canvas (ascii_theme(), sm));
+  ASSERT_CANVAS_STREQ
+    (canvas, false,
+     ("+-----------+-------------+------------+\n"
+      "|left top   | center top  |   right top|\n"
+      "|           |             |            |\n"
+      "+-----------+-------------+------------+\n"
+      "|left center|center center|right center|\n"
+      "|           |             |            |\n"
+      "+-----------+-------------+------------+\n"
+      "|           |             |            |\n"
+      "|left bottom|center bottom|right bottom|\n"
+      "+-----------+-------------+------------+\n"));
+}
+
+/* Run all selftests in this file.  */
+
+void
+text_art_table_cc_tests ()
+{
+  test_tic_tac_toe ();
+  test_text_table ();
+  test_offset_table ();
+  test_spans ();
+  test_spans_2 ();
+  test_spans_3 ();
+  test_double_width_chars ();
+  test_ipv4_header ();
+  test_missing_cells ();
+  test_add_row ();
+  test_alignment ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/table.h b/gcc/text-art/table.h
new file mode 100644
index 00000000000..5e6c8ffb836
--- /dev/null
+++ b/gcc/text-art/table.h
@@ -0,0 +1,262 @@
+/* Support for tabular/grid-based content.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_TABLE_H
+#define GCC_TEXT_ART_TABLE_H
+
+#include "text-art/canvas.h"
+#include "text-art/theme.h"
+#include <vector>
+
+namespace text_art {
+
+class table;
+class table_geometry;
+
+/* A class representing the content of a particular table cell,
+   or of a span of table cells.  */
+
+class table_cell_content
+{
+ public:
+  table_cell_content () : m_str (), m_size (0, 0) {}
+  table_cell_content (styled_string &&s);
+
+  bool operator== (const table_cell_content &other) const
+  {
+    return m_str == other.m_str;
+  }
+
+  canvas::size_t get_canvas_size () const { return m_size; }
+
+  void paint_to_canvas (canvas &canvas,
+			canvas::coord_t top_left) const;
+
+ private:
+  styled_string m_str;
+  canvas::size_t m_size;
+};
+
+/* A list of required sizes of table rows or columns
+   in canvas units (row heights or column widths).  */
+
+struct table_dimension_sizes
+{
+  table_dimension_sizes (unsigned num);
+
+  void require (unsigned idx, int amount)
+  {
+    m_requirements[idx] = std::max (m_requirements[idx], amount);
+  }
+
+  std::vector<int> m_requirements;
+};
+
+/* A 2D grid of cells.  Instances of table_cell_content can be assigned
+   to individual table cells, and to rectangular spans of cells.  Such
+   assignments do not have to fully cover the 2D grid, but they must not
+   overlap.  */
+
+class table
+{
+ public:
+  typedef size<class table> size_t;
+  typedef coord<class table> coord_t;
+  typedef range<class table> range_t;
+  typedef rect<class table> rect_t;
+
+  /* A record of how a table_cell_content was placed at a table::rect_t
+     with a certain alignment.  */
+  class cell_placement
+  {
+  public:
+    cell_placement (rect_t rect,
+		    table_cell_content &&content,
+		    x_align x_align,
+		    y_align y_align)
+    : m_rect (rect),
+      m_content (std::move (content)),
+      m_x_align (x_align),
+      m_y_align (y_align)
+    {
+    }
+
+    bool one_by_one_p () const
+    {
+      return m_rect.m_size.w == 1 && m_rect.m_size.h == 1;
+    }
+
+    canvas::size_t get_min_canvas_size () const
+    {
+      // Doesn't include border
+      return m_content.get_canvas_size ();
+    }
+
+    void paint_cell_contents_to_canvas(canvas &canvas,
+				       canvas::coord_t offset,
+				       const table_geometry &tg) const;
+
+    const table_cell_content &get_content () const { return m_content; }
+
+  private:
+    friend class table_cell_sizes;
+    rect_t m_rect;
+    table_cell_content m_content;
+    x_align m_x_align;
+    y_align m_y_align;
+  };
+
+  table (size_t size);
+  ~table () = default;
+  table (table &&) = default;
+  table (const table &) = delete;
+  table &operator= (const table &) = delete;
+
+  const size_t &get_size () const { return m_size; }
+
+  int add_row ()
+  {
+    m_size.h++;
+    m_occupancy.add_row (-1);
+    return m_size.h - 1; // return the table_y of the newly-added row
+  }
+
+  void set_cell (coord_t coord,
+		 table_cell_content &&content,
+		 enum x_align x_align = x_align::CENTER,
+		 enum y_align y_align = y_align::CENTER);
+
+  void set_cell_span (rect_t span,
+		      table_cell_content &&content,
+		      enum x_align x_align = x_align::CENTER,
+		      enum y_align y_align = y_align::CENTER);
+
+  canvas to_canvas (const theme &theme, const style_manager &sm) const;
+
+  void paint_to_canvas(canvas &canvas,
+		       canvas::coord_t offset,
+		       const table_geometry &tg,
+		       const theme &theme) const;
+
+  void debug () const;
+
+  /* Self-test support.  */
+  const cell_placement *get_placement_at (coord_t coord) const;
+
+ private:
+  int get_occupancy_safe (coord_t coord) const;
+  directions get_connections (int table_x, int table_y) const;
+  void paint_cell_borders_to_canvas(canvas &canvas,
+				    canvas::coord_t offset,
+				    const table_geometry &tg,
+				    const theme &theme) const;
+  void paint_cell_contents_to_canvas(canvas &canvas,
+				     canvas::coord_t offset,
+				     const table_geometry &tg) const;
+
+  friend class table_cell_sizes;
+
+  size_t m_size;
+  std::vector<cell_placement> m_placements;
+  array2<int, size_t, coord_t> m_occupancy; /* indices into the m_placements vec.  */
+};
+
+/* A workspace for computing the row heights and column widths
+   of a table (in canvas units).
+   The col_widths and row_heights could be shared between multiple
+   instances, for aligning multiple tables vertically or horizontally.  */
+
+class table_cell_sizes
+{
+ public:
+  table_cell_sizes (table_dimension_sizes &col_widths,
+		    table_dimension_sizes &row_heights)
+  : m_col_widths (col_widths),
+    m_row_heights (row_heights)
+  {
+  }
+
+  void pass_1 (const table &table);
+  void pass_2 (const table &table);
+
+  canvas::size_t get_canvas_size (const table::rect_t &rect) const;
+
+  table_dimension_sizes &m_col_widths;
+  table_dimension_sizes &m_row_heights;
+};
+
+/* A class responsible for mapping from table cell coords
+   to canvas coords, handling column widths.
+   It's the result of solving "how big are all the table cells and where
+   do they go?"
+   The cell_sizes are passed in, for handling aligning multiple tables,
+   sharing column widths or row heights.  */
+
+class table_geometry
+{
+ public:
+  table_geometry (const table &table, table_cell_sizes &cell_sizes);
+
+  void recalc_coords ();
+
+  const canvas::size_t get_canvas_size () const { return m_canvas_size; }
+
+  canvas::coord_t table_to_canvas (table::coord_t table_coord) const;
+  int table_x_to_canvas_x (int table_x) const;
+  int table_y_to_canvas_y (int table_y) const;
+
+  int get_col_width (int table_x) const
+  {
+    return m_cell_sizes.m_col_widths.m_requirements[table_x];
+  }
+
+  canvas::size_t get_canvas_size (const table::rect_t &rect) const
+  {
+    return m_cell_sizes.get_canvas_size (rect);
+  }
+
+ private:
+  const table &m_table;
+  table_cell_sizes &m_cell_sizes;
+  canvas::size_t m_canvas_size;
+
+  /* Start canvas column of table cell, including leading border.  */
+  std::vector<int> m_col_start_x;
+
+  /* Start canvas row of table cell, including leading border.  */
+  std::vector<int> m_row_start_y;
+};
+
+/* Helper class for handling the simple case of a single table
+   that doesn't need to be aligned with respect to anything else.  */
+
+struct simple_table_geometry
+{
+  simple_table_geometry (const table &table);
+
+  table_dimension_sizes m_col_widths;
+  table_dimension_sizes m_row_heights;
+  table_cell_sizes m_cell_sizes;
+  table_geometry m_tg;
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_TABLE_H */
diff --git a/gcc/text-art/theme.cc b/gcc/text-art/theme.cc
new file mode 100644
index 00000000000..54dfe7c9213
--- /dev/null
+++ b/gcc/text-art/theme.cc
@@ -0,0 +1,183 @@
+/* Classes for abstracting ascii vs unicode output.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#include "system.h"
+#include "coretypes.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "text-art/selftests.h"
+#include "text-art/ruler.h"
+#include "text-art/theme.h"
+
+using namespace text_art;
+
+/* class theme.  */
+
+void
+theme::paint_y_arrow (canvas &canvas,
+		      int canvas_x,
+		      canvas::range_t y_range,
+		      y_arrow_dir dir,
+		      style::id_t style_id) const
+{
+  int canvas_y;
+  int delta_y;
+  const canvas::cell_t head (get_cppchar (dir == y_arrow_dir::UP
+					  ? cell_kind::Y_ARROW_UP_HEAD
+					  : cell_kind::Y_ARROW_DOWN_HEAD),
+			     false, style_id);
+  const canvas::cell_t tail (get_cppchar (dir == y_arrow_dir::UP
+					  ? cell_kind::Y_ARROW_UP_TAIL
+					  : cell_kind::Y_ARROW_DOWN_TAIL),
+			     false, style_id);
+  if (dir == y_arrow_dir::UP)
+    {
+      canvas_y = y_range.get_max ();
+      delta_y = -1;
+    }
+  else
+    {
+      canvas_y = y_range.get_min ();
+      delta_y = 1;
+    }
+  for (int len = y_range.get_size (); len; len--)
+    {
+      const canvas::cell_t cell = (len > 1) ? tail : head;
+      canvas.paint (canvas::coord_t (canvas_x, canvas_y), cell);
+      canvas_y += delta_y;
+    }
+}
+
+/* class ascii_theme : public theme.  */
+
+canvas::cell_t
+ascii_theme::get_line_art (directions line_dirs) const
+{
+  if (line_dirs.m_up
+      && line_dirs.m_down
+      && !(line_dirs.m_left || line_dirs.m_right))
+    return canvas::cell_t ('|');
+  if (line_dirs.m_left
+      && line_dirs.m_right
+      && !(line_dirs.m_up || line_dirs.m_down))
+    return canvas::cell_t ('-');
+  if (line_dirs.m_up
+      || line_dirs.m_down
+      || line_dirs.m_left
+      || line_dirs.m_right)
+    return canvas::cell_t ('+');
+  return canvas::cell_t (' ');
+}
+
+cppchar_t
+ascii_theme::get_cppchar (enum cell_kind kind) const
+{
+  switch (kind)
+    {
+    default:
+      gcc_unreachable ();
+    case cell_kind::X_RULER_LEFT_EDGE:
+      return '|';
+    case cell_kind::X_RULER_MIDDLE:
+      return '~';
+    case cell_kind::X_RULER_INTERNAL_EDGE:
+      return '|';
+    case cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW:
+    case cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE:
+      return '+';
+    case cell_kind::X_RULER_RIGHT_EDGE:
+      return '|';
+    case cell_kind::X_RULER_VERTICAL_CONNECTOR:
+      return '|';
+
+    case cell_kind::TEXT_BORDER_HORIZONTAL:
+      return '-';
+    case cell_kind::TEXT_BORDER_VERTICAL:
+      return '|';
+    case cell_kind::TEXT_BORDER_TOP_LEFT:
+    case cell_kind::TEXT_BORDER_TOP_RIGHT:
+    case cell_kind::TEXT_BORDER_BOTTOM_LEFT:
+    case cell_kind::TEXT_BORDER_BOTTOM_RIGHT:
+      return '+';
+
+    case cell_kind::Y_ARROW_UP_HEAD: return '^';
+    case cell_kind::Y_ARROW_DOWN_HEAD: return 'v';
+
+    case cell_kind::Y_ARROW_UP_TAIL:
+    case cell_kind::Y_ARROW_DOWN_TAIL:
+      return '|';
+    }
+}
+
+/* class unicode_theme : public theme.  */
+
+canvas::cell_t
+unicode_theme::get_line_art (directions line_dirs) const
+{
+  return canvas::cell_t (get_box_drawing_char (line_dirs));
+}
+
+cppchar_t
+unicode_theme::get_cppchar (enum cell_kind kind) const
+{
+  switch (kind)
+    {
+    default:
+      gcc_unreachable ();
+    case cell_kind::X_RULER_LEFT_EDGE:
+      return 0x251C; /* "├": U+251C: BOX DRAWINGS LIGHT VERTICAL AND RIGHT */
+    case cell_kind::X_RULER_MIDDLE:
+      return 0x2500; /* "─": U+2500: BOX DRAWINGS LIGHT HORIZONTAL */
+    case cell_kind::X_RULER_INTERNAL_EDGE:
+      return 0x253C; /* "┼": U+253C: BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */
+    case cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW:
+      return 0x252C; /* "┬": U+252C: BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */
+    case cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE:
+      return 0x2534; /* "┴": U+2534: BOX DRAWINGS LIGHT UP AND HORIZONTAL */
+    case cell_kind::X_RULER_RIGHT_EDGE:
+      return 0x2524; /* "┤": U+2524: BOX DRAWINGS LIGHT VERTICAL AND LEFT */
+    case cell_kind::X_RULER_VERTICAL_CONNECTOR:
+      return 0x2502; /* "│": U+2502: BOX DRAWINGS LIGHT VERTICAL */
+
+    case cell_kind::TEXT_BORDER_HORIZONTAL:
+      return 0x2500; /* "─": U+2500: BOX DRAWINGS LIGHT HORIZONTAL */
+    case cell_kind::TEXT_BORDER_VERTICAL:
+      return 0x2502; /* "│": U+2502: BOX DRAWINGS LIGHT VERTICAL */
+
+    /* Round corners.  */
+    case cell_kind::TEXT_BORDER_TOP_LEFT:
+      return 0x256D; /* "╭": U+256D BOX DRAWINGS LIGHT ARC DOWN AND RIGHT.  */
+    case cell_kind::TEXT_BORDER_TOP_RIGHT:
+      return 0x256E; /* "╮": U+256E BOX DRAWINGS LIGHT ARC DOWN AND LEFT.  */
+    case cell_kind::TEXT_BORDER_BOTTOM_LEFT:
+      return 0x2570; /* "╰": U+2570 BOX DRAWINGS LIGHT ARC UP AND RIGHT.  */
+    case cell_kind::TEXT_BORDER_BOTTOM_RIGHT:
+      return 0x256F; /* "╯": U+256F BOX DRAWINGS LIGHT ARC UP AND LEFT.  */
+
+    case cell_kind::Y_ARROW_UP_HEAD:
+      return '^';
+    case cell_kind::Y_ARROW_DOWN_HEAD:
+      return 'v';
+    case cell_kind::Y_ARROW_UP_TAIL:
+    case cell_kind::Y_ARROW_DOWN_TAIL:
+      return 0x2502; /* "│": U+2502: BOX DRAWINGS LIGHT VERTICAL */
+    }
+}
diff --git a/gcc/text-art/theme.h b/gcc/text-art/theme.h
new file mode 100644
index 00000000000..8edbc6efc76
--- /dev/null
+++ b/gcc/text-art/theme.h
@@ -0,0 +1,123 @@
+/* Classes for abstracting ascii vs unicode output.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_THEME_H
+#define GCC_TEXT_ART_THEME_H
+
+#include "text-art/canvas.h"
+#include "text-art/box-drawing.h"
+
+namespace text_art {
+
+class theme
+{
+ public:
+  enum class cell_kind
+  {
+    /* A left-hand edge of a range e.g. "├".  */
+    X_RULER_LEFT_EDGE,
+
+    /* Within a range e.g. "─".  */
+    X_RULER_MIDDLE,
+
+    /* A border between two neighboring ranges e.g. "┼".  */
+    X_RULER_INTERNAL_EDGE,
+
+    /* The connector with the text label within a range e.g. "┬".  */
+    X_RULER_CONNECTOR_TO_LABEL_BELOW,
+
+    /* As above, but when the text label is above the ruler.  */
+    X_RULER_CONNECTOR_TO_LABEL_ABOVE,
+
+    /* The vertical connection to a text label.  */
+    X_RULER_VERTICAL_CONNECTOR,
+
+    /* A right-hand edge of a range e.g. "┤".  */
+    X_RULER_RIGHT_EDGE,
+
+    TEXT_BORDER_HORIZONTAL,
+    TEXT_BORDER_VERTICAL,
+    TEXT_BORDER_TOP_LEFT,
+    TEXT_BORDER_TOP_RIGHT,
+    TEXT_BORDER_BOTTOM_LEFT,
+    TEXT_BORDER_BOTTOM_RIGHT,
+
+    Y_ARROW_UP_HEAD,
+    Y_ARROW_UP_TAIL,
+    Y_ARROW_DOWN_HEAD,
+    Y_ARROW_DOWN_TAIL,
+  };
+
+  virtual ~theme () = default;
+
+  virtual bool unicode_p () const = 0;
+  virtual bool emojis_p () const = 0;
+
+  virtual canvas::cell_t
+  get_line_art (directions line_dirs) const = 0;
+
+  canvas::cell_t get_cell (enum cell_kind kind, unsigned style_idx) const
+  {
+    return canvas::cell_t (get_cppchar (kind), false, style_idx);
+  }
+
+  virtual cppchar_t get_cppchar (enum cell_kind kind) const = 0;
+
+  enum class y_arrow_dir { UP, DOWN };
+  void paint_y_arrow (canvas &canvas,
+		      int x,
+		      canvas::range_t y_range,
+		      y_arrow_dir dir,
+		      style::id_t style_id) const;
+};
+
+class ascii_theme : public theme
+{
+ public:
+  bool unicode_p () const final override { return false; }
+  bool emojis_p () const final override { return false; }
+
+  canvas::cell_t
+  get_line_art (directions line_dirs) const final override;
+
+  cppchar_t get_cppchar (enum cell_kind kind) const final override;
+};
+
+class unicode_theme : public theme
+{
+ public:
+  bool unicode_p () const final override { return true; }
+  bool emojis_p () const override { return false; }
+
+  canvas::cell_t
+  get_line_art (directions line_dirs) const final override;
+
+  cppchar_t get_cppchar (enum cell_kind kind) const final override;
+};
+
+class emoji_theme : public unicode_theme
+{
+public:
+  bool emojis_p () const final override { return true; }
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_THEME_H */
diff --git a/gcc/text-art/types.h b/gcc/text-art/types.h
new file mode 100644
index 00000000000..b66188ae19c
--- /dev/null
+++ b/gcc/text-art/types.h
@@ -0,0 +1,504 @@
+/* Types for drawing 2d "text art".
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_TYPES_H
+#define GCC_TEXT_ART_TYPES_H
+
+#include "cpplib.h"
+#include "pretty-print.h"
+#include <vector>
+#include <string>
+
+namespace text_art {
+
+/* Forward decls.  */
+
+class canvas;
+class table;
+class theme;
+
+/* Classes for geometry.
+   We use templates to avoid mixing up e.g. canvas coordinates
+   with table coordinates.  */
+
+template <typename CoordinateSystem>
+struct size
+{
+  size (int w_, int h_) : w (w_), h (h_) {}
+  int w;
+  int h;
+};
+
+template <typename CoordinateSystem>
+struct coord
+{
+  coord (int x_, int y_) : x (x_), y (y_) {}
+  int x;
+  int y;
+};
+
+template <typename CoordinateSystem>
+coord<CoordinateSystem> operator+ (coord<CoordinateSystem> a,
+				   coord<CoordinateSystem> b)
+{
+  return coord<CoordinateSystem> (a.x + b.x, a.y + b.y);
+}
+
+/* A half-open range [start, next) of int.  */
+
+template <typename CoordinateSystem>
+struct range
+{
+  range (int start_, int next_)
+  : start (start_), next (next_)
+  {}
+
+  int get_min () const { return start; }
+  int get_max () const { return next - 1; }
+  int get_next () const { return next; }
+  int get_size () const { return next - start; }
+
+  int get_midpoint () const { return get_min () + get_size () / 2; }
+
+  int start;
+  int next;
+};
+
+/* A rectangle area within CoordinateSystem.  */
+
+template <typename CoordinateSystem>
+struct rect
+{
+  rect (coord<CoordinateSystem> top_left,
+	size<CoordinateSystem> size)
+  : m_top_left (top_left),
+    m_size (size)
+  {
+  }
+
+  rect (range<CoordinateSystem> x_range,
+	range<CoordinateSystem> y_range)
+  : m_top_left (x_range.get_min (), y_range.get_min ()),
+    m_size (x_range.get_size (), y_range.get_size ())
+  {
+  }
+
+  int get_min_x () const { return m_top_left.x; }
+  int get_min_y () const { return m_top_left.y; }
+  int get_max_x () const { return m_top_left.x + m_size.w - 1; }
+  int get_max_y () const { return m_top_left.y + m_size.h - 1; }
+  int get_next_x () const { return m_top_left.x + m_size.w; }
+  int get_next_y () const { return m_top_left.y + m_size.h; }
+
+  range<CoordinateSystem> get_x_range () const
+  {
+    return range<CoordinateSystem> (get_min_x (), get_next_x ());
+  }
+  range<CoordinateSystem> get_y_range () const
+  {
+    return range<CoordinateSystem> (get_min_y (), get_next_y ());
+  }
+
+  int get_width () const { return m_size.w; }
+  int get_height () const { return m_size.h; }
+
+  coord<CoordinateSystem> m_top_left;
+  size<CoordinateSystem> m_size;
+};
+
+template <typename ElementType, typename SizeType, typename CoordType>
+class array2
+{
+ public:
+  typedef ElementType element_t;
+  typedef SizeType size_t;
+  typedef CoordType coord_t;
+
+  array2 (size_t sz)
+  : m_size (sz),
+    m_elements (sz.w * sz.h)
+  {
+  }
+  array2 (array2 &&other)
+  : m_size (other.m_size),
+    m_elements (std::move (other.m_elements))
+  {
+  }
+
+  /* Move assignment not implemented or used.  */
+  array2 &operator== (array2 &&other) = delete;
+
+  /* No copy ctor or assignment op.  */
+  array2 (const array2 &other) = delete;
+  array2 &operator= (const array2 &other) = delete;
+
+
+  const size_t &get_size () const { return m_size; }
+
+  void add_row (const element_t &element)
+  {
+    m_size.h++;
+    m_elements.insert (m_elements.end (), m_size.w, element);
+  }
+
+  const element_t &get (const coord_t &coord) const
+  {
+    ::size_t idx = get_idx (coord);
+    return m_elements[idx];
+  }
+
+  void set (const coord_t &coord, const element_t &element)
+  {
+    ::size_t idx = get_idx (coord);
+    m_elements[idx] = element;
+  }
+
+  void fill (element_t element)
+  {
+    for (int y = 0; y < m_size.h; y++)
+      for (int x = 0; x < m_size.w; x++)
+	set (coord_t (x, y), element);
+  }
+
+ private:
+  ::size_t get_idx (const coord_t &coord) const
+  {
+    gcc_assert (coord.x >= 0);
+    gcc_assert (coord.x < m_size.w);
+    gcc_assert (coord.y >= 0);
+    gcc_assert (coord.y < m_size.h);
+    return (coord.y * m_size.w) + coord.x;
+  }
+
+  size_t m_size;
+  std::vector<element_t> m_elements;
+};
+
+/* A combination of attributes describing how to style a text cell.
+   We only support those attributes mentioned in invoke.texi:
+   - bold,
+   - underscore,
+   - blink,
+   - inverse,
+   - colors for foreground and background:
+     - default color
+     - named colors
+     - 16-color mode colors (the "bright" variants)
+     - 88-color mode
+     - 256-color mode
+   plus URLs. */
+
+struct style
+{
+  typedef unsigned char id_t;
+  static const id_t id_plain = 0;
+
+  /* Colors.  */
+  enum class named_color
+  {
+   DEFAULT,
+   // ANSI order
+   BLACK,
+   RED,
+   GREEN,
+   YELLOW,
+   BLUE,
+   MAGENTA,
+   CYAN,
+   WHITE
+  };
+
+
+  struct color
+  {
+    enum class kind
+    {
+      NAMED,
+      BITS_8,
+      BITS_24,
+    } m_kind;
+
+    union
+    {
+      struct {
+	enum named_color m_name;
+	bool m_bright;
+      } m_named;
+      uint8_t m_8bit;
+      struct {
+	uint8_t r;
+	uint8_t g;
+	uint8_t b;
+      } m_24bit;
+    } u;
+
+    /* Constructor for named colors.  */
+    color (enum named_color name = named_color::DEFAULT,
+	   bool bright = false)
+    : m_kind (kind::NAMED)
+    {
+      u.m_named.m_name = name;
+      u.m_named.m_bright = bright;
+    }
+
+    /* Constructor for 8-bit colors.  */
+    color (uint8_t col_val)
+    : m_kind (kind::BITS_8)
+    {
+      u.m_8bit = col_val;
+    }
+
+    /* Constructor for 24-bit colors.  */
+    color (uint8_t r, uint8_t g, uint8_t b)
+    : m_kind (kind::BITS_24)
+    {
+      u.m_24bit.r = r;
+      u.m_24bit.g = g;
+      u.m_24bit.b = b;
+    }
+
+    bool operator== (const color &other) const;
+    bool operator!= (const color &other) const
+    {
+      return !(*this == other);
+    }
+
+    void print_sgr (pretty_printer *pp, bool fg, bool &need_separator) const;
+  };
+
+  style ()
+  : m_bold (false),
+    m_underscore (false),
+    m_blink (false),
+    m_reverse (false),
+    m_fg_color (named_color::DEFAULT),
+    m_bg_color (named_color::DEFAULT),
+    m_url ()
+  {}
+
+  bool operator== (const style &other) const
+  {
+    return (m_bold == other.m_bold
+	    && m_underscore == other.m_underscore
+	    && m_blink == other.m_blink
+	    && m_reverse == other.m_reverse
+	    && m_fg_color == other.m_fg_color
+	    && m_bg_color == other.m_bg_color
+	    && m_url == other.m_url);
+  }
+
+  style &set_style_url (const char *url);
+
+  static void print_changes (pretty_printer *pp,
+			     const style &old_style,
+			     const style &new_style);
+
+  bool m_bold;
+  bool m_underscore;
+  bool m_blink;
+  bool m_reverse;
+  color m_fg_color;
+  color m_bg_color;
+  std::vector<cppchar_t> m_url; // empty = no URL
+};
+
+/* A class to keep track of all the styles in use in a drawing, so that
+   we can refer to them via the compact style::id_t type, rather than
+   via e.g. pointers.  */
+
+class style_manager
+{
+ public:
+  style_manager ();
+  style::id_t get_or_create_id (const style &style);
+  const style &get_style (style::id_t id) const
+  {
+    return m_styles[id];
+  }
+  void print_any_style_changes (pretty_printer *pp,
+				style::id_t old_id,
+				style::id_t new_id) const;
+  unsigned get_num_styles () const { return m_styles.size (); }
+
+private:
+  std::vector<style> m_styles;
+};
+
+class styled_unichar
+{
+ public:
+  friend class styled_string;
+
+  explicit styled_unichar ()
+  : m_code (0),
+    m_style_id (style::id_plain)
+  {}
+  explicit styled_unichar (cppchar_t ch)
+  : m_code (ch),
+    m_emoji_variant_p (false),
+    m_style_id (style::id_plain)
+  {}
+  explicit styled_unichar (cppchar_t ch, bool emoji, style::id_t style_id)
+  : m_code (ch),
+    m_emoji_variant_p (emoji),
+    m_style_id (style_id)
+  {
+    gcc_assert (style_id <= 0x7f);
+  }
+
+  cppchar_t get_code () const { return m_code; }
+  bool emoji_variant_p () const { return m_emoji_variant_p; }
+  style::id_t get_style_id () const { return m_style_id; }
+
+  bool double_width_p () const
+  {
+    int width = cpp_wcwidth (get_code ());
+    gcc_assert (width == 1 || width == 2);
+    return width == 2;
+  }
+
+  bool operator== (const styled_unichar &other) const
+  {
+    return (m_code == other.m_code
+	    && m_emoji_variant_p == other.m_emoji_variant_p
+	    && m_style_id == other.m_style_id);
+  }
+
+  void set_emoji_variant () { m_emoji_variant_p = true; }
+
+  int get_canvas_width () const
+  {
+      return cpp_wcwidth (m_code);
+  }
+
+  void add_combining_char (cppchar_t ch)
+  {
+    m_combining_chars.push_back (ch);
+  }
+
+  const std::vector<cppchar_t> get_combining_chars () const
+  {
+    return m_combining_chars;
+  }
+
+private:
+  cppchar_t m_code : 24;
+  bool m_emoji_variant_p : 1;
+  style::id_t m_style_id : 7;
+  std::vector<cppchar_t> m_combining_chars;
+};
+
+class styled_string
+{
+ public:
+  explicit styled_string () = default;
+  explicit styled_string (style_manager &sm, const char *str);
+  explicit styled_string (cppchar_t cppchar, bool emoji = false);
+
+  styled_string (styled_string &&) = default;
+  styled_string &operator= (styled_string &&) = default;
+
+  /* No copy ctor or assignment op.  */
+  styled_string (const styled_string &) = delete;
+  styled_string &operator= (const styled_string &) = delete;
+
+  /* For the few cases where copying is required, spell it out explicitly.  */
+  styled_string copy () const
+  {
+    styled_string result;
+    result.m_chars = m_chars;
+    return result;
+  }
+
+  bool operator== (const styled_string &other) const
+  {
+    return m_chars == other.m_chars;
+  }
+
+  static styled_string from_fmt (style_manager &sm,
+				 printer_fn format_decoder,
+				 const char *fmt, ...)
+    ATTRIBUTE_GCC_PPDIAG(3, 4);
+  static styled_string from_fmt_va (style_manager &sm,
+				    printer_fn format_decoder,
+				    const char *fmt,
+				    va_list *args)
+    ATTRIBUTE_GCC_PPDIAG(3, 0);
+
+  size_t size () const { return m_chars.size (); }
+  styled_unichar operator[] (size_t idx) const { return m_chars[idx]; }
+
+  std::vector<styled_unichar>::const_iterator begin () const
+  {
+    return m_chars.begin ();
+  }
+  std::vector<styled_unichar>::const_iterator end () const
+  {
+    return m_chars.end ();
+  }
+
+  int calc_canvas_width () const;
+
+  void append (const styled_string &suffix);
+
+  void set_url (style_manager &sm, const char *url);
+
+private:
+  std::vector<styled_unichar> m_chars;
+};
+
+enum class x_align
+{
+  LEFT,
+  CENTER,
+  RIGHT
+};
+
+enum class y_align
+{
+  TOP,
+  CENTER,
+  BOTTOM
+};
+
+/* A set of cardinal directions within a canvas or table.  */
+
+struct directions
+{
+public:
+  directions (bool up, bool down, bool left, bool right)
+  : m_up (up), m_down (down), m_left (left), m_right (right)
+  {
+  }
+
+  size_t as_index () const
+  {
+    return (m_up << 3) | (m_down << 2) | (m_left << 1) | m_right;
+  }
+
+  bool m_up: 1;
+  bool m_down: 1;
+  bool m_left: 1;
+  bool m_right: 1;
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_TYPES_H */
diff --git a/gcc/text-art/widget.cc b/gcc/text-art/widget.cc
new file mode 100644
index 00000000000..e6e544d5035
--- /dev/null
+++ b/gcc/text-art/widget.cc
@@ -0,0 +1,275 @@
+/* Hierarchical diagram elements.
+   Copyright (C) 2023 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/>.  */
+
+#include "config.h"
+#define INCLUDE_MEMORY
+#include "system.h"
+#include "coretypes.h"
+#include "pretty-print.h"
+#include "selftest.h"
+#include "make-unique.h"
+#include "text-art/selftests.h"
+#include "text-art/widget.h"
+
+using namespace text_art;
+
+/* class text_art::widget.  */
+
+canvas
+widget::to_canvas (const style_manager &style_mgr)
+{
+  const canvas::size_t req_size = get_req_size ();
+
+  /* For now we don't constrain the allocation; we give
+     the widget the full size it requested, and widgets
+     assume they got their full size request.  */
+  const canvas::size_t alloc_size = req_size;
+
+  set_alloc_rect (canvas::rect_t (canvas::coord_t (0, 0), alloc_size));
+  canvas c (alloc_size, style_mgr);
+  paint_to_canvas (c);
+  return c;
+}
+
+/* class text_art::vbox_widget : public text_art::container_widget.  */
+
+const char *
+vbox_widget::get_desc () const
+{
+  return "vbox_widget";
+}
+
+canvas::size_t
+vbox_widget::calc_req_size ()
+{
+  canvas::size_t result (0, 0);
+  for (auto &child : m_children)
+    {
+      canvas::size_t child_req_size = child->get_req_size();
+      result.h += child_req_size.h;
+      result.w = std::max (result.w, child_req_size.w);
+    }
+  return result;
+}
+
+void
+vbox_widget::update_child_alloc_rects ()
+{
+  const int x = get_min_x ();
+  int y = get_min_y ();
+  for (auto &child : m_children)
+    {
+      child->set_alloc_rect
+	(canvas::rect_t (canvas::coord_t (x, y),
+			 canvas::size_t (get_alloc_w (),
+					 child->get_req_h ())));
+      y += child->get_req_h ();
+    }
+}
+
+/* class text_art::text_widget : public text_art::leaf_widget.  */
+
+const char *
+text_widget::get_desc () const
+{
+  return "text_widget";
+}
+
+canvas::size_t
+text_widget::calc_req_size ()
+{
+  return canvas::size_t (m_str.size (), 1);
+}
+
+void
+text_widget::paint_to_canvas (canvas &canvas)
+{
+  canvas.paint_text (get_top_left (), m_str);
+}
+
+/* class text_art::canvas_widget : public text_art::leaf_widget.  */
+
+const char *
+canvas_widget::get_desc () const
+{
+  return "canvas_widget";
+}
+
+canvas::size_t
+canvas_widget::calc_req_size ()
+{
+  return m_canvas.get_size ();
+}
+
+void
+canvas_widget::paint_to_canvas (canvas &canvas)
+{
+  for (int y = 0; y < m_canvas.get_size ().h; y++)
+    for (int x = 0; x < m_canvas.get_size ().w; x++)
+      {
+	canvas::coord_t rel_xy (x, y);
+	canvas.paint (get_top_left () + rel_xy,
+		      m_canvas.get (rel_xy));
+      }
+}
+
+#if CHECKING_P
+
+namespace selftest {
+
+/* Concrete widget subclass for writing selftests.
+   Requests a hard-coded size, and fills its allocated rectangle
+   with a specific character.  */
+
+class test_widget : public leaf_widget
+{
+public:
+  test_widget (canvas::size_t size, char ch)
+  : m_test_size (size), m_ch (ch)
+  {}
+
+  const char *get_desc () const final override
+  {
+    return "test_widget";
+  }
+  canvas::size_t calc_req_size () final override
+  {
+    return m_test_size;
+  }
+  void paint_to_canvas (canvas &canvas) final override
+  {
+    canvas.fill (get_alloc_rect (), canvas::cell_t (m_ch));
+  }
+
+private:
+  canvas::size_t m_test_size;
+  char m_ch;
+};
+
+static void
+test_test_widget ()
+{
+  style_manager sm;
+  test_widget w (canvas::size_t (3, 3), 'A');
+  canvas c (w.to_canvas (sm));
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("AAA\n"
+      "AAA\n"
+      "AAA\n"));
+}
+
+static void
+test_text_widget ()
+{
+  style_manager sm;
+  text_widget w (styled_string (sm, "hello world"));
+  canvas c (w.to_canvas (sm));
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("hello world\n"));
+}
+
+static void
+test_wrapper_widget ()
+{
+  style_manager sm;
+  wrapper_widget w (::make_unique<test_widget> (canvas::size_t (3, 3), 'B'));
+  canvas c (w.to_canvas (sm));
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("BBB\n"
+      "BBB\n"
+      "BBB\n"));
+}
+
+static void
+test_vbox_1 ()
+{
+  style_manager sm;
+  vbox_widget w;
+  for (int i = 0; i < 5; i++)
+    w.add_child
+      (::make_unique <text_widget>
+       (styled_string::from_fmt (sm, nullptr,
+				 "this is line %i", i)));
+  canvas c (w.to_canvas (sm));
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("this is line 0\n"
+      "this is line 1\n"
+      "this is line 2\n"
+      "this is line 3\n"
+      "this is line 4\n"));
+}
+
+static void
+test_vbox_2 ()
+{
+  style_manager sm;
+  vbox_widget w;
+  w.add_child (::make_unique<test_widget> (canvas::size_t (1, 3), 'A'));
+  w.add_child (::make_unique<test_widget> (canvas::size_t (4, 1), 'B'));
+  w.add_child (::make_unique<test_widget> (canvas::size_t (1, 2), 'C'));
+  canvas c (w.to_canvas (sm));
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("AAAA\n"
+      "AAAA\n"
+      "AAAA\n"
+      "BBBB\n"
+      "CCCC\n"
+      "CCCC\n"));
+}
+
+static void
+test_canvas_widget ()
+{
+  style_manager sm;
+  canvas inner_canvas (canvas::size_t (5, 3), sm);
+  inner_canvas.fill (canvas::rect_t (canvas::coord_t (0, 0),
+				     canvas::size_t (5, 3)),
+		     canvas::cell_t ('a'));
+  canvas_widget cw (std::move (inner_canvas));
+  canvas c (cw.to_canvas (sm));
+  ASSERT_CANVAS_STREQ
+    (c, false,
+     ("aaaaa\n"
+      "aaaaa\n"
+      "aaaaa\n"));
+}
+
+/* Run all selftests in this file.  */
+
+void
+text_art_widget_cc_tests ()
+{
+  test_test_widget ();
+  test_text_widget ();
+  test_wrapper_widget ();
+  test_vbox_1 ();
+  test_vbox_2 ();
+  test_canvas_widget ();
+}
+
+} // namespace selftest
+
+
+#endif /* #if CHECKING_P */
diff --git a/gcc/text-art/widget.h b/gcc/text-art/widget.h
new file mode 100644
index 00000000000..91209444bf7
--- /dev/null
+++ b/gcc/text-art/widget.h
@@ -0,0 +1,246 @@
+/* Hierarchical diagram elements.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_TEXT_ART_WIDGET_H
+#define GCC_TEXT_ART_WIDGET_H
+
+#include <vector>
+#include "text-art/canvas.h"
+#include "text-art/table.h"
+
+namespace text_art {
+
+/* Abstract base class: something that knows how to size itself and
+   how to paint itself to a canvas, potentially with children, with
+   support for hierarchical sizing and positioning.
+
+   Widgets have a two-phase sizing/positioning algorithm.
+
+   Step 1: size requests: the root widget is asked for its size request i.e
+   how big it wants to be.  This is handled by recursively asking child
+   widgets for their requested sizes.  Each widget subclass can implement
+   their own logic for this in the "calc_req_size" vfunc, and the result
+   is cached in m_req_size.
+
+   Step 2: rect allocation: the root widget is set a canvas::rect_t as
+   its "allocated" rectangle.  Each widget subclass can then place its
+   children recursively using the "update_child_alloc_rects" vfunc.
+   For simplicity, all coordinates in the hierarchy are within the same
+   coordinate system (rather than attempting to store per-child offsets).
+
+   Widget subclasses are responsible for managing their own children.  */
+
+/* Subclasses in this header, with indentation indicating inheritance.  */
+
+class widget;  /* Abstract base class.  */
+  class wrapper_widget;  /* Concrete subclass: a widget with a single child.  */
+  class container_widget; /* Abstract subclass: widgets with an arbitrary
+			     number of children.  */
+    class vbox_widget; /* Concrete widget subclass: lay out children
+			  vertically.  */
+  class leaf_widget; /* Abstract subclass: a widget with no children.  */
+    class text_widget; /* Concrete subclass: a text string.  */
+    class canvas_widget; /* Concrete subclass: a pre-rendered canvas.  */
+
+class widget
+{
+ public:
+  /* This can be very useful for debugging when implementing new
+     widget subclasses.  */
+  static const bool DEBUG_GEOMETRY = false;
+
+  virtual ~widget () {}
+
+  canvas to_canvas (const style_manager &style_mgr);
+
+  canvas::size_t get_req_size ()
+  {
+    m_req_size = calc_req_size();
+    if (DEBUG_GEOMETRY)
+      fprintf (stderr, "calc_req_size (%s) -> (w:%i, h:%i)\n",
+	       get_desc (),
+	       m_req_size.w, m_req_size.h);
+    return m_req_size;
+  }
+
+  void set_alloc_rect (const canvas::rect_t &rect)
+  {
+    if (DEBUG_GEOMETRY)
+      fprintf (stderr, "set_alloc_rect (%s): ((x:%i, y:%i), (w:%i, h:%i))\n",
+	       get_desc (),
+	       rect.m_top_left.x, rect.m_top_left.y,
+	       rect.m_size.w, rect.m_size.h);
+    m_alloc_rect = rect;
+    update_child_alloc_rects ();
+  }
+
+  virtual const char *get_desc () const = 0;
+  virtual canvas::size_t calc_req_size () = 0;
+  virtual void update_child_alloc_rects () = 0;
+  virtual void paint_to_canvas (canvas &canvas) = 0;
+
+  /* Access to the cached size request of this widget.  */
+  const canvas::size_t get_req_size () const { return m_req_size; }
+  int get_req_w () const { return m_req_size.w; }
+  int get_req_h () const { return m_req_size.h; }
+
+  /* Access to the allocated canvas coordinates of this widget.  */
+  const canvas::rect_t &get_alloc_rect () const { return m_alloc_rect; }
+  int get_alloc_w () const { return m_alloc_rect.get_width (); }
+  int get_alloc_h () const { return m_alloc_rect.get_height (); }
+  int get_min_x () const { return m_alloc_rect.get_min_x (); }
+  int get_max_x () const { return m_alloc_rect.get_max_x (); }
+  int get_next_x () const { return m_alloc_rect.get_next_x (); }
+  int get_min_y () const { return m_alloc_rect.get_min_y (); }
+  int get_max_y () const { return m_alloc_rect.get_max_y (); }
+  int get_next_y () const { return m_alloc_rect.get_max_y (); }
+  canvas::range_t get_x_range () const { return m_alloc_rect.get_x_range (); }
+  canvas::range_t get_y_range () const { return m_alloc_rect.get_y_range (); }
+  const canvas::coord_t &get_top_left () const
+  {
+    return m_alloc_rect.m_top_left;
+  }
+
+ protected:
+  widget ()
+  : m_req_size (0, 0),
+    m_alloc_rect (canvas::coord_t (0, 0),
+		  canvas::size_t (0, 0))
+  {}
+
+private:
+  /* How much size this widget requested.  */
+  canvas::size_t m_req_size;
+  /* Where (and how big) this widget was allocated.  */
+  canvas::rect_t m_alloc_rect;
+};
+
+/* Concrete subclass for a widget with a single child.  */
+
+class wrapper_widget : public widget
+{
+ public:
+  wrapper_widget (std::unique_ptr<widget> child)
+  : m_child (std::move (child))
+  {}
+
+  const char *get_desc () const override
+  {
+    return "wrapper_widget";
+  }
+  canvas::size_t calc_req_size () override
+  {
+    return m_child->get_req_size ();
+  }
+  void update_child_alloc_rects ()
+  {
+    m_child->set_alloc_rect (get_alloc_rect ());
+  }
+  void paint_to_canvas (canvas &canvas) override
+  {
+    m_child->paint_to_canvas (canvas);
+  }
+ private:
+  std::unique_ptr<widget> m_child;
+};
+
+/* Abstract subclass for widgets with an arbitrary number of children.  */
+
+class container_widget : public widget
+{
+ public:
+  void add_child (std::unique_ptr<widget> child)
+  {
+    m_children.push_back (std::move (child));
+  }
+
+  void paint_to_canvas (canvas &canvas) final override
+  {
+    for (auto &child : m_children)
+      child->paint_to_canvas (canvas);
+  }
+
+ protected:
+  std::vector<std::unique_ptr<widget>> m_children;
+};
+
+/* Concrete widget subclass: lay out children vertically.  */
+
+class vbox_widget : public container_widget
+{
+ public:
+  const char *get_desc () const override;
+  canvas::size_t calc_req_size () override;
+  void update_child_alloc_rects () final override;
+};
+
+/* Abstract subclass for widgets with no children.  */
+
+class leaf_widget : public widget
+{
+ public:
+  void update_child_alloc_rects () final override
+  {
+    /* no-op.  */
+  }
+
+ protected:
+  leaf_widget () : widget () {}
+};
+
+/* Concrete widget subclass for a text string.  */
+
+class text_widget : public leaf_widget
+{
+ public:
+  text_widget (styled_string str)
+  : leaf_widget (), m_str (std::move (str))
+  {
+  }
+
+  const char *get_desc () const override;
+  canvas::size_t calc_req_size () final override;
+  void paint_to_canvas (canvas &canvas) final override;
+
+private:
+  styled_string m_str;
+};
+
+/* Concrete widget subclass for a pre-rendered canvas.  */
+
+class canvas_widget : public leaf_widget
+{
+ public:
+  canvas_widget (canvas &&c)
+  : leaf_widget (), m_canvas (std::move (c))
+  {
+  }
+
+  const char *get_desc () const override;
+  canvas::size_t calc_req_size () final override;
+  void paint_to_canvas (canvas &canvas) final override;
+
+private:
+  canvas m_canvas;
+};
+
+} // namespace text_art
+
+#endif /* GCC_TEXT_ART_WIDGET_H */
diff --git a/libcpp/charset.cc b/libcpp/charset.cc
index d7f323b2cd5..a0bd2ede11c 100644
--- a/libcpp/charset.cc
+++ b/libcpp/charset.cc
@@ -3147,34 +3147,26 @@ cpp_display_column_to_byte_column (const char *data, int data_length,
   return dw.bytes_processed () + MAX (0, display_col - avail_display);
 }
 
-/* Our own version of wcwidth().  We don't use the actual wcwidth() in glibc,
-   because that will inspect the user's locale, and in particular in an ASCII
-   locale, it will not return anything useful for extended characters.  But GCC
-   in other respects (see e.g. _cpp_default_encoding()) behaves as if
-   everything is UTF-8.  We also make some tweaks that are useful for the way
-   GCC needs to use this data, e.g. tabs and other control characters should be
-   treated as having width 1.  The lookup tables are generated from
-   contrib/unicode/gen_wcwidth.py and were made by simply calling glibc
-   wcwidth() on all codepoints, then applying the small tweaks.  These tables
-   are not highly optimized, but for the present purpose of outputting
-   diagnostics, they are sufficient.  */
-
-#include "generated_cpp_wcwidth.h"
-int cpp_wcwidth (cppchar_t c)
+template <typename PropertyType>
+PropertyType
+get_cppchar_property (cppchar_t c,
+		      const cppchar_t *range_ends,
+		      const PropertyType *range_values,
+		      size_t num_ranges,
+		      PropertyType default_value)
 {
-  if (__builtin_expect (c <= wcwidth_range_ends[0], true))
-    return wcwidth_widths[0];
+  if (__builtin_expect (c <= range_ends[0], true))
+    return range_values[0];
 
   /* Binary search the tables.  */
   int begin = 1;
-  static const int end
-      = sizeof wcwidth_range_ends / sizeof (*wcwidth_range_ends);
+  static const int end = num_ranges;
   int len = end - begin;
   do
     {
       int half = len/2;
       int middle = begin + half;
-      if (c > wcwidth_range_ends[middle])
+      if (c > range_ends[middle])
 	{
 	  begin = middle + 1;
 	  len -= half + 1;
@@ -3184,6 +3176,61 @@ int cpp_wcwidth (cppchar_t c)
     } while (len);
 
   if (__builtin_expect (begin != end, true))
-    return wcwidth_widths[begin];
-  return 1;
+    return range_values[begin];
+
+  return default_value;
+}
+
+/* Our own version of wcwidth().  We don't use the actual wcwidth() in glibc,
+   because that will inspect the user's locale, and in particular in an ASCII
+   locale, it will not return anything useful for extended characters.  But GCC
+   in other respects (see e.g. _cpp_default_encoding()) behaves as if
+   everything is UTF-8.  We also make some tweaks that are useful for the way
+   GCC needs to use this data, e.g. tabs and other control characters should be
+   treated as having width 1.  The lookup tables are generated from
+   contrib/unicode/gen_wcwidth.py and were made by simply calling glibc
+   wcwidth() on all codepoints, then applying the small tweaks.  These tables
+   are not highly optimized, but for the present purpose of outputting
+   diagnostics, they are sufficient.  */
+
+#include "generated_cpp_wcwidth.h"
+
+int
+cpp_wcwidth (cppchar_t c)
+{
+  const size_t num_ranges
+    = sizeof wcwidth_range_ends / sizeof (*wcwidth_range_ends);
+  return get_cppchar_property<unsigned char > (c,
+					       &wcwidth_range_ends[0],
+					       &wcwidth_widths[0],
+					       num_ranges,
+					       1);
+}
+
+#include "combining-chars.inc"
+
+bool
+cpp_is_combining_char (cppchar_t c)
+{
+  const size_t num_ranges
+    = sizeof combining_range_ends / sizeof (*combining_range_ends);
+  return get_cppchar_property<bool> (c,
+				     &combining_range_ends[0],
+				     &is_combining[0],
+				     num_ranges,
+				     false);
+}
+
+#include "printable-chars.inc"
+
+bool
+cpp_is_printable_char (cppchar_t c)
+{
+  const size_t num_ranges
+    = sizeof printable_range_ends / sizeof (*printable_range_ends);
+  return get_cppchar_property<bool> (c,
+				     &printable_range_ends[0],
+				     &is_printable[0],
+				     num_ranges,
+				     false);
 }
diff --git a/libcpp/combining-chars.inc b/libcpp/combining-chars.inc
new file mode 100644
index 00000000000..dfec966970a
--- /dev/null
+++ b/libcpp/combining-chars.inc
@@ -0,0 +1,68 @@
+/* Generated by contrib/unicode/gen-combining-chars.py
+   using version 12.1.0 of the Unicode standard.  */
+
+static const cppchar_t combining_range_ends[] = {
+  0x2ff, 0x34e, 0x34f, 0x36f, 0x482, 0x487, 0x590, 0x5bd,
+  0x5be, 0x5bf, 0x5c0, 0x5c2, 0x5c3, 0x5c5, 0x5c6, 0x5c7,
+  0x60f, 0x61a, 0x64a, 0x65f, 0x66f, 0x670, 0x6d5, 0x6dc,
+  0x6de, 0x6e4, 0x6e6, 0x6e8, 0x6e9, 0x6ed, 0x710, 0x711,
+  0x72f, 0x74a, 0x7ea, 0x7f3, 0x7fc, 0x7fd, 0x815, 0x819,
+  0x81a, 0x823, 0x824, 0x827, 0x828, 0x82d, 0x858, 0x85b,
+  0x8d2, 0x8e1, 0x8e2, 0x8ff, 0x93b, 0x93c, 0x94c, 0x94d,
+  0x950, 0x954, 0x9bb, 0x9bc, 0x9cc, 0x9cd, 0x9fd, 0x9fe,
+  0xa3b, 0xa3c, 0xa4c, 0xa4d, 0xabb, 0xabc, 0xacc, 0xacd,
+  0xb3b, 0xb3c, 0xb4c, 0xb4d, 0xbcc, 0xbcd, 0xc4c, 0xc4d,
+  0xc54, 0xc56, 0xcbb, 0xcbc, 0xccc, 0xccd, 0xd3a, 0xd3c,
+  0xd4c, 0xd4d, 0xdc9, 0xdca, 0xe37, 0xe3a, 0xe47, 0xe4b,
+  0xeb7, 0xeba, 0xec7, 0xecb, 0xf17, 0xf19, 0xf34, 0xf35,
+  0xf36, 0xf37, 0xf38, 0xf39, 0xf70, 0xf72, 0xf73, 0xf74,
+  0xf79, 0xf7d, 0xf7f, 0xf80, 0xf81, 0xf84, 0xf85, 0xf87,
+  0xfc5, 0xfc6, 0x1036, 0x1037, 0x1038, 0x103a, 0x108c, 0x108d,
+  0x135c, 0x135f, 0x1713, 0x1714, 0x1733, 0x1734, 0x17d1, 0x17d2,
+  0x17dc, 0x17dd, 0x18a8, 0x18a9, 0x1938, 0x193b, 0x1a16, 0x1a18,
+  0x1a5f, 0x1a60, 0x1a74, 0x1a7c, 0x1a7e, 0x1a7f, 0x1aaf, 0x1abd,
+  0x1b33, 0x1b34, 0x1b43, 0x1b44, 0x1b6a, 0x1b73, 0x1ba9, 0x1bab,
+  0x1be5, 0x1be6, 0x1bf1, 0x1bf3, 0x1c36, 0x1c37, 0x1ccf, 0x1cd2,
+  0x1cd3, 0x1ce0, 0x1ce1, 0x1ce8, 0x1cec, 0x1ced, 0x1cf3, 0x1cf4,
+  0x1cf7, 0x1cf9, 0x1dbf, 0x1df9, 0x1dfa, 0x1dff, 0x20cf, 0x20dc,
+  0x20e0, 0x20e1, 0x20e4, 0x20f0, 0x2cee, 0x2cf1, 0x2d7e, 0x2d7f,
+  0x2ddf, 0x2dff, 0x3029, 0x302f, 0x3098, 0x309a, 0xa66e, 0xa66f,
+  0xa673, 0xa67d, 0xa69d, 0xa69f, 0xa6ef, 0xa6f1, 0xa805, 0xa806,
+  0xa8c3, 0xa8c4, 0xa8df, 0xa8f1, 0xa92a, 0xa92d, 0xa952, 0xa953,
+  0xa9b2, 0xa9b3, 0xa9bf, 0xa9c0, 0xaaaf, 0xaab0, 0xaab1, 0xaab4,
+  0xaab6, 0xaab8, 0xaabd, 0xaabf, 0xaac0, 0xaac1, 0xaaf5, 0xaaf6,
+  0xabec, 0xabed, 0xfb1d, 0xfb1e, 0xfe1f, 0xfe2f, 0x101fc, 0x101fd,
+  0x102df, 0x102e0, 0x10375, 0x1037a, 0x10a0c, 0x10a0d, 0x10a0e, 0x10a0f,
+  0x10a37, 0x10a3a, 0x10a3e, 0x10a3f, 0x10ae4, 0x10ae6, 0x10d23, 0x10d27,
+  0x10f45, 0x10f50, 0x11045, 0x11046, 0x1107e, 0x1107f, 0x110b8, 0x110ba,
+  0x110ff, 0x11102, 0x11132, 0x11134, 0x11172, 0x11173, 0x111bf, 0x111c0,
+  0x111c9, 0x111ca, 0x11234, 0x11236, 0x112e8, 0x112ea, 0x1133a, 0x1133c,
+  0x1134c, 0x1134d, 0x11365, 0x1136c, 0x1136f, 0x11374, 0x11441, 0x11442,
+  0x11445, 0x11446, 0x1145d, 0x1145e, 0x114c1, 0x114c3, 0x115be, 0x115c0,
+  0x1163e, 0x1163f, 0x116b5, 0x116b7, 0x1172a, 0x1172b, 0x11838, 0x1183a,
+  0x119df, 0x119e0, 0x11a33, 0x11a34, 0x11a46, 0x11a47, 0x11a98, 0x11a99,
+  0x11c3e, 0x11c3f, 0x11d41, 0x11d42, 0x11d43, 0x11d45, 0x11d96, 0x11d97,
+  0x16aef, 0x16af4, 0x16b2f, 0x16b36, 0x1bc9d, 0x1bc9e, 0x1d164, 0x1d169,
+  0x1d16c, 0x1d172, 0x1d17a, 0x1d182, 0x1d184, 0x1d18b, 0x1d1a9, 0x1d1ad,
+  0x1d241, 0x1d244, 0x1dfff, 0x1e006, 0x1e007, 0x1e018, 0x1e01a, 0x1e021,
+  0x1e022, 0x1e024, 0x1e025, 0x1e02a, 0x1e12f, 0x1e136, 0x1e2eb, 0x1e2ef,
+  0x1e8cf, 0x1e8d6, 0x1e943, 0x1e94a, 0x10fffe,
+};
+
+static const bool is_combining[] = {
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
+};
diff --git a/libcpp/include/cpplib.h b/libcpp/include/cpplib.h
index a6f0abd894c..d326f5aa316 100644
--- a/libcpp/include/cpplib.h
+++ b/libcpp/include/cpplib.h
@@ -1602,4 +1602,7 @@ bool cpp_input_conversion_is_trivial (const char *input_charset);
 int cpp_check_utf8_bom (const char *data, size_t data_length);
 bool cpp_valid_utf8_p (const char *data, size_t num_bytes);
 
+bool cpp_is_combining_char (cppchar_t c);
+bool cpp_is_printable_char (cppchar_t c);
+
 #endif /* ! LIBCPP_CPPLIB_H */
diff --git a/libcpp/printable-chars.inc b/libcpp/printable-chars.inc
new file mode 100644
index 00000000000..470b1eef331
--- /dev/null
+++ b/libcpp/printable-chars.inc
@@ -0,0 +1,231 @@
+/* Generated by contrib/unicode/gen-printable-chars.py
+   using version 12.1.0 of the Unicode standard.  */
+
+static const cppchar_t printable_range_ends[] = {
+  0x1f, 0x7e, 0x9f, 0xac, 0xad, 0x377, 0x379, 0x37f,
+  0x383, 0x38a, 0x38b, 0x38c, 0x38d, 0x3a1, 0x3a2, 0x52f,
+  0x530, 0x556, 0x558, 0x58a, 0x58c, 0x58f, 0x590, 0x5c7,
+  0x5cf, 0x5ea, 0x5ee, 0x5f4, 0x605, 0x61b, 0x61d, 0x6dc,
+  0x6dd, 0x70d, 0x70f, 0x74a, 0x74c, 0x7b1, 0x7bf, 0x7fa,
+  0x7fc, 0x82d, 0x82f, 0x83e, 0x83f, 0x85b, 0x85d, 0x85e,
+  0x85f, 0x86a, 0x89f, 0x8b4, 0x8b5, 0x8bd, 0x8d2, 0x8e1,
+  0x8e2, 0x983, 0x984, 0x98c, 0x98e, 0x990, 0x992, 0x9a8,
+  0x9a9, 0x9b0, 0x9b1, 0x9b2, 0x9b5, 0x9b9, 0x9bb, 0x9c4,
+  0x9c6, 0x9c8, 0x9ca, 0x9ce, 0x9d6, 0x9d7, 0x9db, 0x9dd,
+  0x9de, 0x9e3, 0x9e5, 0x9fe, 0xa00, 0xa03, 0xa04, 0xa0a,
+  0xa0e, 0xa10, 0xa12, 0xa28, 0xa29, 0xa30, 0xa31, 0xa33,
+  0xa34, 0xa36, 0xa37, 0xa39, 0xa3b, 0xa3c, 0xa3d, 0xa42,
+  0xa46, 0xa48, 0xa4a, 0xa4d, 0xa50, 0xa51, 0xa58, 0xa5c,
+  0xa5d, 0xa5e, 0xa65, 0xa76, 0xa80, 0xa83, 0xa84, 0xa8d,
+  0xa8e, 0xa91, 0xa92, 0xaa8, 0xaa9, 0xab0, 0xab1, 0xab3,
+  0xab4, 0xab9, 0xabb, 0xac5, 0xac6, 0xac9, 0xaca, 0xacd,
+  0xacf, 0xad0, 0xadf, 0xae3, 0xae5, 0xaf1, 0xaf8, 0xaff,
+  0xb00, 0xb03, 0xb04, 0xb0c, 0xb0e, 0xb10, 0xb12, 0xb28,
+  0xb29, 0xb30, 0xb31, 0xb33, 0xb34, 0xb39, 0xb3b, 0xb44,
+  0xb46, 0xb48, 0xb4a, 0xb4d, 0xb55, 0xb57, 0xb5b, 0xb5d,
+  0xb5e, 0xb63, 0xb65, 0xb77, 0xb81, 0xb83, 0xb84, 0xb8a,
+  0xb8d, 0xb90, 0xb91, 0xb95, 0xb98, 0xb9a, 0xb9b, 0xb9c,
+  0xb9d, 0xb9f, 0xba2, 0xba4, 0xba7, 0xbaa, 0xbad, 0xbb9,
+  0xbbd, 0xbc2, 0xbc5, 0xbc8, 0xbc9, 0xbcd, 0xbcf, 0xbd0,
+  0xbd6, 0xbd7, 0xbe5, 0xbfa, 0xbff, 0xc0c, 0xc0d, 0xc10,
+  0xc11, 0xc28, 0xc29, 0xc39, 0xc3c, 0xc44, 0xc45, 0xc48,
+  0xc49, 0xc4d, 0xc54, 0xc56, 0xc57, 0xc5a, 0xc5f, 0xc63,
+  0xc65, 0xc6f, 0xc76, 0xc8c, 0xc8d, 0xc90, 0xc91, 0xca8,
+  0xca9, 0xcb3, 0xcb4, 0xcb9, 0xcbb, 0xcc4, 0xcc5, 0xcc8,
+  0xcc9, 0xccd, 0xcd4, 0xcd6, 0xcdd, 0xcde, 0xcdf, 0xce3,
+  0xce5, 0xcef, 0xcf0, 0xcf2, 0xcff, 0xd03, 0xd04, 0xd0c,
+  0xd0d, 0xd10, 0xd11, 0xd44, 0xd45, 0xd48, 0xd49, 0xd4f,
+  0xd53, 0xd63, 0xd65, 0xd7f, 0xd81, 0xd83, 0xd84, 0xd96,
+  0xd99, 0xdb1, 0xdb2, 0xdbb, 0xdbc, 0xdbd, 0xdbf, 0xdc6,
+  0xdc9, 0xdca, 0xdce, 0xdd4, 0xdd5, 0xdd6, 0xdd7, 0xddf,
+  0xde5, 0xdef, 0xdf1, 0xdf4, 0xe00, 0xe3a, 0xe3e, 0xe5b,
+  0xe80, 0xe82, 0xe83, 0xe84, 0xe85, 0xe8a, 0xe8b, 0xea3,
+  0xea4, 0xea5, 0xea6, 0xebd, 0xebf, 0xec4, 0xec5, 0xec6,
+  0xec7, 0xecd, 0xecf, 0xed9, 0xedb, 0xedf, 0xeff, 0xf47,
+  0xf48, 0xf6c, 0xf70, 0xf97, 0xf98, 0xfbc, 0xfbd, 0xfcc,
+  0xfcd, 0xfda, 0xfff, 0x10c5, 0x10c6, 0x10c7, 0x10cc, 0x10cd,
+  0x10cf, 0x1248, 0x1249, 0x124d, 0x124f, 0x1256, 0x1257, 0x1258,
+  0x1259, 0x125d, 0x125f, 0x1288, 0x1289, 0x128d, 0x128f, 0x12b0,
+  0x12b1, 0x12b5, 0x12b7, 0x12be, 0x12bf, 0x12c0, 0x12c1, 0x12c5,
+  0x12c7, 0x12d6, 0x12d7, 0x1310, 0x1311, 0x1315, 0x1317, 0x135a,
+  0x135c, 0x137c, 0x137f, 0x1399, 0x139f, 0x13f5, 0x13f7, 0x13fd,
+  0x13ff, 0x169c, 0x169f, 0x16f8, 0x16ff, 0x170c, 0x170d, 0x1714,
+  0x171f, 0x1736, 0x173f, 0x1753, 0x175f, 0x176c, 0x176d, 0x1770,
+  0x1771, 0x1773, 0x177f, 0x17dd, 0x17df, 0x17e9, 0x17ef, 0x17f9,
+  0x17ff, 0x180d, 0x180f, 0x1819, 0x181f, 0x1878, 0x187f, 0x18aa,
+  0x18af, 0x18f5, 0x18ff, 0x191e, 0x191f, 0x192b, 0x192f, 0x193b,
+  0x193f, 0x1940, 0x1943, 0x196d, 0x196f, 0x1974, 0x197f, 0x19ab,
+  0x19af, 0x19c9, 0x19cf, 0x19da, 0x19dd, 0x1a1b, 0x1a1d, 0x1a5e,
+  0x1a5f, 0x1a7c, 0x1a7e, 0x1a89, 0x1a8f, 0x1a99, 0x1a9f, 0x1aad,
+  0x1aaf, 0x1abe, 0x1aff, 0x1b4b, 0x1b4f, 0x1b7c, 0x1b7f, 0x1bf3,
+  0x1bfb, 0x1c37, 0x1c3a, 0x1c49, 0x1c4c, 0x1c88, 0x1c8f, 0x1cba,
+  0x1cbc, 0x1cc7, 0x1ccf, 0x1cfa, 0x1cff, 0x1df9, 0x1dfa, 0x1f15,
+  0x1f17, 0x1f1d, 0x1f1f, 0x1f45, 0x1f47, 0x1f4d, 0x1f4f, 0x1f57,
+  0x1f58, 0x1f59, 0x1f5a, 0x1f5b, 0x1f5c, 0x1f5d, 0x1f5e, 0x1f7d,
+  0x1f7f, 0x1fb4, 0x1fb5, 0x1fc4, 0x1fc5, 0x1fd3, 0x1fd5, 0x1fdb,
+  0x1fdc, 0x1fef, 0x1ff1, 0x1ff4, 0x1ff5, 0x1ffe, 0x1fff, 0x200a,
+  0x200f, 0x2029, 0x202e, 0x205f, 0x206f, 0x2071, 0x2073, 0x208e,
+  0x208f, 0x209c, 0x209f, 0x20bf, 0x20cf, 0x20f0, 0x20ff, 0x218b,
+  0x218f, 0x2426, 0x243f, 0x244a, 0x245f, 0x2b73, 0x2b75, 0x2b95,
+  0x2b97, 0x2c2e, 0x2c2f, 0x2c5e, 0x2c5f, 0x2cf3, 0x2cf8, 0x2d25,
+  0x2d26, 0x2d27, 0x2d2c, 0x2d2d, 0x2d2f, 0x2d67, 0x2d6e, 0x2d70,
+  0x2d7e, 0x2d96, 0x2d9f, 0x2da6, 0x2da7, 0x2dae, 0x2daf, 0x2db6,
+  0x2db7, 0x2dbe, 0x2dbf, 0x2dc6, 0x2dc7, 0x2dce, 0x2dcf, 0x2dd6,
+  0x2dd7, 0x2dde, 0x2ddf, 0x2e4f, 0x2e7f, 0x2e99, 0x2e9a, 0x2ef3,
+  0x2eff, 0x2fd5, 0x2fef, 0x2ffb, 0x2fff, 0x303f, 0x3040, 0x3096,
+  0x3098, 0x30ff, 0x3104, 0x312f, 0x3130, 0x318e, 0x318f, 0x31ba,
+  0x31bf, 0x31e3, 0x31ef, 0x321e, 0x321f, 0x4db5, 0x4dbf, 0x9fef,
+  0x9fff, 0xa48c, 0xa48f, 0xa4c6, 0xa4cf, 0xa62b, 0xa63f, 0xa6f7,
+  0xa6ff, 0xa7bf, 0xa7c1, 0xa7c6, 0xa7f6, 0xa82b, 0xa82f, 0xa839,
+  0xa83f, 0xa877, 0xa87f, 0xa8c5, 0xa8cd, 0xa8d9, 0xa8df, 0xa953,
+  0xa95e, 0xa97c, 0xa97f, 0xa9cd, 0xa9ce, 0xa9d9, 0xa9dd, 0xa9fe,
+  0xa9ff, 0xaa36, 0xaa3f, 0xaa4d, 0xaa4f, 0xaa59, 0xaa5b, 0xaac2,
+  0xaada, 0xaaf6, 0xab00, 0xab06, 0xab08, 0xab0e, 0xab10, 0xab16,
+  0xab1f, 0xab26, 0xab27, 0xab2e, 0xab2f, 0xab67, 0xab6f, 0xabed,
+  0xabef, 0xabf9, 0xabff, 0xd7a3, 0xd7af, 0xd7c6, 0xd7ca, 0xd7fb,
+  0xf8ff, 0xfa6d, 0xfa6f, 0xfad9, 0xfaff, 0xfb06, 0xfb12, 0xfb17,
+  0xfb1c, 0xfb36, 0xfb37, 0xfb3c, 0xfb3d, 0xfb3e, 0xfb3f, 0xfb41,
+  0xfb42, 0xfb44, 0xfb45, 0xfbc1, 0xfbd2, 0xfd3f, 0xfd4f, 0xfd8f,
+  0xfd91, 0xfdc7, 0xfdef, 0xfdfd, 0xfdff, 0xfe19, 0xfe1f, 0xfe52,
+  0xfe53, 0xfe66, 0xfe67, 0xfe6b, 0xfe6f, 0xfe74, 0xfe75, 0xfefc,
+  0xff00, 0xffbe, 0xffc1, 0xffc7, 0xffc9, 0xffcf, 0xffd1, 0xffd7,
+  0xffd9, 0xffdc, 0xffdf, 0xffe6, 0xffe7, 0xffee, 0xfffb, 0xfffd,
+  0xffff, 0x1000b, 0x1000c, 0x10026, 0x10027, 0x1003a, 0x1003b, 0x1003d,
+  0x1003e, 0x1004d, 0x1004f, 0x1005d, 0x1007f, 0x100fa, 0x100ff, 0x10102,
+  0x10106, 0x10133, 0x10136, 0x1018e, 0x1018f, 0x1019b, 0x1019f, 0x101a0,
+  0x101cf, 0x101fd, 0x1027f, 0x1029c, 0x1029f, 0x102d0, 0x102df, 0x102fb,
+  0x102ff, 0x10323, 0x1032c, 0x1034a, 0x1034f, 0x1037a, 0x1037f, 0x1039d,
+  0x1039e, 0x103c3, 0x103c7, 0x103d5, 0x103ff, 0x1049d, 0x1049f, 0x104a9,
+  0x104af, 0x104d3, 0x104d7, 0x104fb, 0x104ff, 0x10527, 0x1052f, 0x10563,
+  0x1056e, 0x1056f, 0x105ff, 0x10736, 0x1073f, 0x10755, 0x1075f, 0x10767,
+  0x107ff, 0x10805, 0x10807, 0x10808, 0x10809, 0x10835, 0x10836, 0x10838,
+  0x1083b, 0x1083c, 0x1083e, 0x10855, 0x10856, 0x1089e, 0x108a6, 0x108af,
+  0x108df, 0x108f2, 0x108f3, 0x108f5, 0x108fa, 0x1091b, 0x1091e, 0x10939,
+  0x1093e, 0x1093f, 0x1097f, 0x109b7, 0x109bb, 0x109cf, 0x109d1, 0x10a03,
+  0x10a04, 0x10a06, 0x10a0b, 0x10a13, 0x10a14, 0x10a17, 0x10a18, 0x10a35,
+  0x10a37, 0x10a3a, 0x10a3e, 0x10a48, 0x10a4f, 0x10a58, 0x10a5f, 0x10a9f,
+  0x10abf, 0x10ae6, 0x10aea, 0x10af6, 0x10aff, 0x10b35, 0x10b38, 0x10b55,
+  0x10b57, 0x10b72, 0x10b77, 0x10b91, 0x10b98, 0x10b9c, 0x10ba8, 0x10baf,
+  0x10bff, 0x10c48, 0x10c7f, 0x10cb2, 0x10cbf, 0x10cf2, 0x10cf9, 0x10d27,
+  0x10d2f, 0x10d39, 0x10e5f, 0x10e7e, 0x10eff, 0x10f27, 0x10f2f, 0x10f59,
+  0x10fdf, 0x10ff6, 0x10fff, 0x1104d, 0x11051, 0x1106f, 0x1107e, 0x110bc,
+  0x110bd, 0x110c1, 0x110cf, 0x110e8, 0x110ef, 0x110f9, 0x110ff, 0x11134,
+  0x11135, 0x11146, 0x1114f, 0x11176, 0x1117f, 0x111cd, 0x111cf, 0x111df,
+  0x111e0, 0x111f4, 0x111ff, 0x11211, 0x11212, 0x1123e, 0x1127f, 0x11286,
+  0x11287, 0x11288, 0x11289, 0x1128d, 0x1128e, 0x1129d, 0x1129e, 0x112a9,
+  0x112af, 0x112ea, 0x112ef, 0x112f9, 0x112ff, 0x11303, 0x11304, 0x1130c,
+  0x1130e, 0x11310, 0x11312, 0x11328, 0x11329, 0x11330, 0x11331, 0x11333,
+  0x11334, 0x11339, 0x1133a, 0x11344, 0x11346, 0x11348, 0x1134a, 0x1134d,
+  0x1134f, 0x11350, 0x11356, 0x11357, 0x1135c, 0x11363, 0x11365, 0x1136c,
+  0x1136f, 0x11374, 0x113ff, 0x11459, 0x1145a, 0x1145b, 0x1145c, 0x1145f,
+  0x1147f, 0x114c7, 0x114cf, 0x114d9, 0x1157f, 0x115b5, 0x115b7, 0x115dd,
+  0x115ff, 0x11644, 0x1164f, 0x11659, 0x1165f, 0x1166c, 0x1167f, 0x116b8,
+  0x116bf, 0x116c9, 0x116ff, 0x1171a, 0x1171c, 0x1172b, 0x1172f, 0x1173f,
+  0x117ff, 0x1183b, 0x1189f, 0x118f2, 0x118fe, 0x118ff, 0x1199f, 0x119a7,
+  0x119a9, 0x119d7, 0x119d9, 0x119e4, 0x119ff, 0x11a47, 0x11a4f, 0x11aa2,
+  0x11abf, 0x11af8, 0x11bff, 0x11c08, 0x11c09, 0x11c36, 0x11c37, 0x11c45,
+  0x11c4f, 0x11c6c, 0x11c6f, 0x11c8f, 0x11c91, 0x11ca7, 0x11ca8, 0x11cb6,
+  0x11cff, 0x11d06, 0x11d07, 0x11d09, 0x11d0a, 0x11d36, 0x11d39, 0x11d3a,
+  0x11d3b, 0x11d3d, 0x11d3e, 0x11d47, 0x11d4f, 0x11d59, 0x11d5f, 0x11d65,
+  0x11d66, 0x11d68, 0x11d69, 0x11d8e, 0x11d8f, 0x11d91, 0x11d92, 0x11d98,
+  0x11d9f, 0x11da9, 0x11edf, 0x11ef8, 0x11fbf, 0x11ff1, 0x11ffe, 0x12399,
+  0x123ff, 0x1246e, 0x1246f, 0x12474, 0x1247f, 0x12543, 0x12fff, 0x1342e,
+  0x143ff, 0x14646, 0x167ff, 0x16a38, 0x16a3f, 0x16a5e, 0x16a5f, 0x16a69,
+  0x16a6d, 0x16a6f, 0x16acf, 0x16aed, 0x16aef, 0x16af5, 0x16aff, 0x16b45,
+  0x16b4f, 0x16b59, 0x16b5a, 0x16b61, 0x16b62, 0x16b77, 0x16b7c, 0x16b8f,
+  0x16e3f, 0x16e9a, 0x16eff, 0x16f4a, 0x16f4e, 0x16f87, 0x16f8e, 0x16f9f,
+  0x16fdf, 0x16fe3, 0x16fff, 0x187f7, 0x187ff, 0x18af2, 0x1afff, 0x1b11e,
+  0x1b14f, 0x1b152, 0x1b163, 0x1b167, 0x1b16f, 0x1b2fb, 0x1bbff, 0x1bc6a,
+  0x1bc6f, 0x1bc7c, 0x1bc7f, 0x1bc88, 0x1bc8f, 0x1bc99, 0x1bc9b, 0x1bc9f,
+  0x1cfff, 0x1d0f5, 0x1d0ff, 0x1d126, 0x1d128, 0x1d172, 0x1d17a, 0x1d1e8,
+  0x1d1ff, 0x1d245, 0x1d2df, 0x1d2f3, 0x1d2ff, 0x1d356, 0x1d35f, 0x1d378,
+  0x1d3ff, 0x1d454, 0x1d455, 0x1d49c, 0x1d49d, 0x1d49f, 0x1d4a1, 0x1d4a2,
+  0x1d4a4, 0x1d4a6, 0x1d4a8, 0x1d4ac, 0x1d4ad, 0x1d4b9, 0x1d4ba, 0x1d4bb,
+  0x1d4bc, 0x1d4c3, 0x1d4c4, 0x1d505, 0x1d506, 0x1d50a, 0x1d50c, 0x1d514,
+  0x1d515, 0x1d51c, 0x1d51d, 0x1d539, 0x1d53a, 0x1d53e, 0x1d53f, 0x1d544,
+  0x1d545, 0x1d546, 0x1d549, 0x1d550, 0x1d551, 0x1d6a5, 0x1d6a7, 0x1d7cb,
+  0x1d7cd, 0x1da8b, 0x1da9a, 0x1da9f, 0x1daa0, 0x1daaf, 0x1dfff, 0x1e006,
+  0x1e007, 0x1e018, 0x1e01a, 0x1e021, 0x1e022, 0x1e024, 0x1e025, 0x1e02a,
+  0x1e0ff, 0x1e12c, 0x1e12f, 0x1e13d, 0x1e13f, 0x1e149, 0x1e14d, 0x1e14f,
+  0x1e2bf, 0x1e2f9, 0x1e2fe, 0x1e2ff, 0x1e7ff, 0x1e8c4, 0x1e8c6, 0x1e8d6,
+  0x1e8ff, 0x1e94b, 0x1e94f, 0x1e959, 0x1e95d, 0x1e95f, 0x1ec70, 0x1ecb4,
+  0x1ed00, 0x1ed3d, 0x1edff, 0x1ee03, 0x1ee04, 0x1ee1f, 0x1ee20, 0x1ee22,
+  0x1ee23, 0x1ee24, 0x1ee26, 0x1ee27, 0x1ee28, 0x1ee32, 0x1ee33, 0x1ee37,
+  0x1ee38, 0x1ee39, 0x1ee3a, 0x1ee3b, 0x1ee41, 0x1ee42, 0x1ee46, 0x1ee47,
+  0x1ee48, 0x1ee49, 0x1ee4a, 0x1ee4b, 0x1ee4c, 0x1ee4f, 0x1ee50, 0x1ee52,
+  0x1ee53, 0x1ee54, 0x1ee56, 0x1ee57, 0x1ee58, 0x1ee59, 0x1ee5a, 0x1ee5b,
+  0x1ee5c, 0x1ee5d, 0x1ee5e, 0x1ee5f, 0x1ee60, 0x1ee62, 0x1ee63, 0x1ee64,
+  0x1ee66, 0x1ee6a, 0x1ee6b, 0x1ee72, 0x1ee73, 0x1ee77, 0x1ee78, 0x1ee7c,
+  0x1ee7d, 0x1ee7e, 0x1ee7f, 0x1ee89, 0x1ee8a, 0x1ee9b, 0x1eea0, 0x1eea3,
+  0x1eea4, 0x1eea9, 0x1eeaa, 0x1eebb, 0x1eeef, 0x1eef1, 0x1efff, 0x1f02b,
+  0x1f02f, 0x1f093, 0x1f09f, 0x1f0ae, 0x1f0b0, 0x1f0bf, 0x1f0c0, 0x1f0cf,
+  0x1f0d0, 0x1f0f5, 0x1f0ff, 0x1f10c, 0x1f10f, 0x1f16c, 0x1f16f, 0x1f1ac,
+  0x1f1e5, 0x1f202, 0x1f20f, 0x1f23b, 0x1f23f, 0x1f248, 0x1f24f, 0x1f251,
+  0x1f25f, 0x1f265, 0x1f2ff, 0x1f6d5, 0x1f6df, 0x1f6ec, 0x1f6ef, 0x1f6fa,
+  0x1f6ff, 0x1f773, 0x1f77f, 0x1f7d8, 0x1f7df, 0x1f7eb, 0x1f7ff, 0x1f80b,
+  0x1f80f, 0x1f847, 0x1f84f, 0x1f859, 0x1f85f, 0x1f887, 0x1f88f, 0x1f8ad,
+  0x1f8ff, 0x1f90b, 0x1f90c, 0x1f971, 0x1f972, 0x1f976, 0x1f979, 0x1f9a2,
+  0x1f9a4, 0x1f9aa, 0x1f9ad, 0x1f9ca, 0x1f9cc, 0x1fa53, 0x1fa5f, 0x1fa6d,
+  0x1fa6f, 0x1fa73, 0x1fa77, 0x1fa7a, 0x1fa7f, 0x1fa82, 0x1fa8f, 0x1fa95,
+  0x1ffff, 0x2a6d6, 0x2a6ff, 0x2b734, 0x2b73f, 0x2b81d, 0x2b81f, 0x2cea1,
+  0x2ceaf, 0x2ebe0, 0x2f7ff, 0x2fa1d, 0xe00ff, 0xe01ef, 0x10fffe,
+};
+
+static const bool is_printable[] = {
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
+  0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
+};
-- 
2.26.3


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH 3/3] analyzer: add text-art visualizations of out-of-bounds accesses [PR106626]
  2023-05-31 18:06 [PATCH 0/3] Add diagram support to gcc diagnostics David Malcolm
  2023-05-31 18:06 ` [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines David Malcolm
  2023-05-31 18:06 ` [PATCH 2/3] diagnostics: add support for "text art" diagrams David Malcolm
@ 2023-05-31 18:06 ` David Malcolm
  2023-06-30 14:40   ` Martin Jambor
  2 siblings, 1 reply; 14+ messages in thread
From: David Malcolm @ 2023-05-31 18:06 UTC (permalink / raw)
  To: gcc-patches; +Cc: David Malcolm

This patch extends -Wanalyzer-out-of-bounds so that, where possible, it
will emit a text art diagram visualizing the spatial relationship between
(a) the memory region that the analyzer predicts would be accessed, versus
(b) the range of memory that is valid to access - whether they overlap,
are touching, are close or far apart; which one is before or after in
memory, the relative sizes involved, the direction of the access (read vs
write), and, in some cases, the values of data involved.  This diagram
can be suppressed using -fdiagnostics-text-art-charset=none.

For example, given:

int32_t arr[10];

int32_t int_arr_read_element_before_start_far(void)
{
  return arr[-100];
}

it emits:

demo-1.c: In function ‘int_arr_read_element_before_start_far’:
demo-1.c:7:13: warning: buffer under-read [CWE-127] [-Wanalyzer-out-of-bounds]
    7 |   return arr[-100];
      |          ~~~^~~~~~
  ‘int_arr_read_element_before_start_far’: event 1
    |
    |    7 |   return arr[-100];
    |      |          ~~~^~~~~~
    |      |             |
    |      |             (1) out-of-bounds read from byte -400 till byte -397 but ‘arr’ starts at byte 0
    |
demo-1.c:7:13: note: valid subscripts for ‘arr’ are ‘[0]’ to ‘[9]’

  ┌───────────────────────────┐
  │read of ‘int32_t’ (4 bytes)│
  └───────────────────────────┘
                ^
                │
                │
  ┌───────────────────────────┐              ┌────────┬────────┬─────────┐
  │                           │              │  [0]   │  ...   │   [9]   │
  │    before valid range     │              ├────────┴────────┴─────────┤
  │                           │              │‘arr’ (type: ‘int32_t[10]’)│
  └───────────────────────────┘              └───────────────────────────┘
  ├─────────────┬─────────────┤├─────┬──────┤├─────────────┬─────────────┤
                │                    │                     │
   ╭────────────┴───────────╮   ╭────┴────╮        ╭───────┴──────╮
   │⚠️  under-read of 4 bytes│   │396 bytes│        │size: 40 bytes│
   ╰────────────────────────╯   ╰─────────╯        ╰──────────────╯

and given:

  #include <string.h>

  void
  test_non_ascii ()
  {
    char buf[5];
    strcpy (buf, "文字化け");
  }

it emits:

demo-2.c: In function ‘test_non_ascii’:
demo-2.c:7:3: warning: stack-based buffer overflow [CWE-121] [-Wanalyzer-out-of-bounds]
    7 |   strcpy (buf, "文字化け");
      |   ^~~~~~~~~~~~~~~~~~~~~~~~
  ‘test_non_ascii’: events 1-2
    |
    |    6 |   char buf[5];
    |      |        ^~~
    |      |        |
    |      |        (1) capacity: 5 bytes
    |    7 |   strcpy (buf, "文字化け");
    |      |   ~~~~~~~~~~~~~~~~~~~~~~~~
    |      |   |
    |      |   (2) out-of-bounds write from byte 5 till byte 12 but ‘buf’ ends at byte 5
    |
demo-2.c:7:3: note: write of 8 bytes to beyond the end of ‘buf’
    7 |   strcpy (buf, "文字化け");
      |   ^~~~~~~~~~~~~~~~~~~~~~~~
demo-2.c:7:3: note: valid subscripts for ‘buf’ are ‘[0]’ to ‘[4]’

  ┌─────┬─────┬─────┬────┬────┐┌────┬────┬────┬────┬────┬────┬────┬──────┐
  │ [0] │ [1] │ [2] │[3] │[4] ││[5] │[6] │[7] │[8] │[9] │[10]│[11]│ [12] │
  ├─────┼─────┼─────┼────┼────┤├────┼────┼────┼────┼────┼────┼────┼──────┤
  │0xe6 │0x96 │0x87 │0xe5│0xad││0x97│0xe5│0x8c│0x96│0xe3│0x81│0x91│ 0x00 │
  ├─────┴─────┴─────┼────┴────┴┴────┼────┴────┴────┼────┴────┴────┼──────┤
  │     U+6587      │    U+5b57     │    U+5316    │    U+3051    │U+0000│
  ├─────────────────┼───────────────┼──────────────┼──────────────┼──────┤
  │       文        │      字       │      化      │      け      │ NUL  │
  ├─────────────────┴───────────────┴──────────────┴──────────────┴──────┤
  │                  string literal (type: ‘char[13]’)                   │
  └──────────────────────────────────────────────────────────────────────┘
     │     │     │    │    │     │    │    │    │    │    │    │     │
     │     │     │    │    │     │    │    │    │    │    │    │     │
     v     v     v    v    v     v    v    v    v    v    v    v     v
  ┌─────┬────────────────┬────┐┌─────────────────────────────────────────┐
  │ [0] │      ...       │[4] ││                                         │
  ├─────┴────────────────┴────┤│            after valid range            │
  │  ‘buf’ (type: ‘char[5]’)  ││                                         │
  └───────────────────────────┘└─────────────────────────────────────────┘
  ├─────────────┬─────────────┤├────────────────────┬────────────────────┤
                │                                   │
       ╭────────┴────────╮              ╭───────────┴──────────╮
       │capacity: 5 bytes│              │⚠️  overflow of 8 bytes│
       ╰─────────────────╯              ╰──────────────────────╯

showing that the overflow occurs partway through the UTF-8 encoding of
the U+5b57 code point.

There are lots more examples in the test suite.

It doesn't show up in this email, but the above diagrams are colorized
to constrast the valid and invalid access ranges.

gcc/ChangeLog:
	PR analyzer/106626
	* Makefile.in (ANALYZER_OBJS): Add analyzer/access-diagram.o.
	* doc/invoke.texi (Wanalyzer-out-of-bounds): Add description of
	text art.
	(fanalyzer-debug-text-art): New.

gcc/analyzer/ChangeLog:
	PR analyzer/106626
	* access-diagram.cc: New file.
	* access-diagram.h: New file.
	* analyzer.h (class region_offset): Add default ctor.
	(region_offset::make_byte_offset): New decl.
	(region_offset::concrete_p): New.
	(region_offset::get_concrete_byte_offset): New.
	(region_offset::calc_symbolic_bit_offset): New decl.
	(region_offset::calc_symbolic_byte_offset): New decl.
	(region_offset::dump_to_pp): New decl.
	(region_offset::dump): New decl.
	(operator<, operator<=, operator>, operator>=): New decls for
	region_offset.
	* analyzer.opt
	(-param=analyzer-text-art-string-ellipsis-threshold=): New.
	(-param=analyzer-text-art-string-ellipsis-head-len=): New.
	(-param=analyzer-text-art-string-ellipsis-tail-len=): New.
	(-param=analyzer-text-art-ideal-canvas-width=): New.
	(fanalyzer-debug-text-art): New.
	* bounds-checking.cc: Include "intl.h", "diagnostic-diagram.h",
	and "analyzer/access-diagram.h".
	(class out_of_bounds::oob_region_creation_event_capacity): New.
	(out_of_bounds::out_of_bounds): Add "model" and "sval_hint"
	params.
	(out_of_bounds::mark_interesting_stuff): Use the base region.
	(out_of_bounds::add_region_creation_events): Use
	oob_region_creation_event_capacity.
	(out_of_bounds::get_dir): New pure vfunc.
	(out_of_bounds::maybe_show_notes): New.
	(out_of_bounds::maybe_show_diagram): New.
	(out_of_bounds::make_access_diagram): New.
	(out_of_bounds::m_model): New field.
	(out_of_bounds::m_sval_hint): New field.
	(out_of_bounds::m_region_creation_event_id): New field.
	(concrete_out_of_bounds::concrete_out_of_bounds): Update for new
	fields.
	(concrete_past_the_end::concrete_past_the_end): Likewise.
	(concrete_past_the_end::add_region_creation_events): Use
	oob_region_creation_event_capacity.
	(concrete_buffer_overflow::concrete_buffer_overflow): Update for
	new fields.
	(concrete_buffer_overflow::emit): Replace call to
	maybe_describe_array_bounds with maybe_show_notes.
	(concrete_buffer_overflow::get_dir): New.
	(concrete_buffer_over_read::concrete_buffer_over_read): Update for
	new fields.
	(concrete_buffer_over_read::emit): Replace call to
	maybe_describe_array_bounds with maybe_show_notes.
	(concrete_buffer_overflow::get_dir): New.
	(concrete_buffer_underwrite::concrete_buffer_underwrite): Update
	for new fields.
	(concrete_buffer_underwrite::emit): Replace call to
	maybe_describe_array_bounds with maybe_show_notes.
	(concrete_buffer_underwrite::get_dir): New.
	(concrete_buffer_under_read::concrete_buffer_under_read): Update
	for new fields.
	(concrete_buffer_under_read::emit): Replace call to
	maybe_describe_array_bounds with maybe_show_notes.
	(concrete_buffer_under_read::get_dir): New.
	(symbolic_past_the_end::symbolic_past_the_end): Update for new
	fields.
	(symbolic_buffer_overflow::symbolic_buffer_overflow): Likewise.
	(symbolic_buffer_overflow::emit): Call maybe_show_notes.
	(symbolic_buffer_overflow::get_dir): New.
	(symbolic_buffer_over_read::symbolic_buffer_over_read): Update for
	new fields.
	(symbolic_buffer_over_read::emit): Call maybe_show_notes.
	(symbolic_buffer_over_read::get_dir): New.
	(region_model::check_symbolic_bounds): Add "sval_hint" param.  Pass
	it and sized_offset_reg to diagnostics.
	(region_model::check_region_bounds): Add "sval_hint" param, passing
	it to diagnostics.
	* diagnostic-manager.cc
	(diagnostic_manager::emit_saved_diagnostic): Pass logger to
	pending_diagnostic::emit.
	* engine.cc: Add logger param to pending_diagnostic::emit
	implementations.
	* infinite-recursion.cc: Likewise.
	* kf-analyzer.cc: Likewise.
	* kf.cc: Likewise.  Add nullptr for new param of
	check_region_for_write.
	* pending-diagnostic.h: Likewise in decl.
	* region-model-manager.cc
	(region_model_manager::get_or_create_int_cst): Convert param from
	poly_int64 to const poly_wide_int_ref &.
	(region_model_manager::maybe_fold_binop): Support type being NULL
	when checking for floating-point types.
	Check for (X + Y) - X => Y.  Be less strict about types when folding
	associative ops.  Check for (X + Y) * CST => (X * CST) + (Y * CST).
	* region-model-manager.h
	(region_model_manager::get_or_create_int_cst): Convert param from
	poly_int64 to const poly_wide_int_ref &.
	* region-model.cc: Add logger param to pending_diagnostic::emit
	implementations.
	(region_model::check_external_function_for_access_attr): Update
	for new param of check_region_for_write.
	(region_model::deref_rvalue): Use nullptr rather than NULL.
	(region_model::get_capacity): Handle RK_STRING.
	(region_model::check_region_access): Add "sval_hint" param; pass it to
	check_region_bounds.
	(region_model::check_region_for_write): Add "sval_hint" param;
	pass it to check_region_access.
	(region_model::check_region_for_read): Add NULL for new param to
	check_region_access.
	(region_model::set_value): Pass rhs_sval to
	check_region_for_write.
	(region_model::get_representative_path_var_1): Handle SK_CONSTANT
	in the check for infinite recursion.
	* region-model.h (region_model::check_region_for_write): Add
	"sval_hint" param.
	(region_model::check_region_access): Likewise.
	(region_model::check_symbolic_bounds): Likewise.
	(region_model::check_region_bounds): Likewise.
	* region.cc (region_offset::make_byte_offset): New.
	(region_offset::calc_symbolic_bit_offset): New.
	(region_offset::calc_symbolic_byte_offset): New.
	(region_offset::dump_to_pp): New.
	(region_offset::dump): New.
	(struct linear_op): New.
	(operator<, operator<=, operator>, operator>=): New, for
	region_offset.
	(region::get_next_offset): New.
	(region::get_relative_symbolic_offset): Use ptrdiff_type_node.
	(field_region::get_relative_symbolic_offset): Likewise.
	(element_region::get_relative_symbolic_offset): Likewise.
	(bit_range_region::get_relative_symbolic_offset): Likewise.
	* region.h (region::get_next_offset): New decl.
	* sm-fd.cc: Add logger param to pending_diagnostic::emit
	implementations.
	* sm-file.cc: Likewise.
	* sm-malloc.cc: Likewise.
	* sm-pattern-test.cc: Likewise.
	* sm-sensitive.cc: Likewise.
	* sm-signal.cc: Likewise.
	* sm-taint.cc: Likewise.
	* store.cc (bit_range::contains_p): Allow "out" to be null.
	* store.h (byte_range::get_start_bit_offset): New.
	(byte_range::get_next_bit_offset): New.
	* varargs.cc: Add logger param to pending_diagnostic::emit
	implementations.

gcc/testsuite/ChangeLog:
	PR analyzer/106626
	* gcc.dg/analyzer/data-model-1.c (test_16): Update for
	out-of-bounds working.
	* gcc.dg/analyzer/out-of-bounds-diagram-1-ascii.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-1-debug.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-1-emoji.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-1-json.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-1-sarif.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-1-unicode.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-10.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-11.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-12.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-13.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-14.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-15.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-2.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-3.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-4.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-5-ascii.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-5-unicode.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-6.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-7.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-8.c: New test.
	* gcc.dg/analyzer/out-of-bounds-diagram-9.c: New test.
	* gcc.dg/analyzer/pattern-test-2.c: Update expected results.
	* gcc.dg/plugin/analyzer_gil_plugin.c:  Add logger param to
	pending_diagnostic::emit implementations.
---
 gcc/Makefile.in                               |    1 +
 gcc/analyzer/access-diagram.cc                | 2405 +++++++++++++++++
 gcc/analyzer/access-diagram.h                 |  165 ++
 gcc/analyzer/analyzer.h                       |   30 +
 gcc/analyzer/analyzer.opt                     |   20 +
 gcc/analyzer/bounds-checking.cc               |  270 +-
 gcc/analyzer/diagnostic-manager.cc            |    2 +-
 gcc/analyzer/engine.cc                        |    4 +-
 gcc/analyzer/infinite-recursion.cc            |    2 +-
 gcc/analyzer/kf-analyzer.cc                   |    2 +-
 gcc/analyzer/kf.cc                            |    6 +-
 gcc/analyzer/pending-diagnostic.h             |    2 +-
 gcc/analyzer/region-model-manager.cc          |   32 +-
 gcc/analyzer/region-model-manager.h           |    2 +-
 gcc/analyzer/region-model.cc                  |   52 +-
 gcc/analyzer/region-model.h                   |    4 +
 gcc/analyzer/region.cc                        |  369 ++-
 gcc/analyzer/region.h                         |    1 +
 gcc/analyzer/sm-fd.cc                         |   14 +-
 gcc/analyzer/sm-file.cc                       |    4 +-
 gcc/analyzer/sm-malloc.cc                     |   20 +-
 gcc/analyzer/sm-pattern-test.cc               |    2 +-
 gcc/analyzer/sm-sensitive.cc                  |    3 +-
 gcc/analyzer/sm-signal.cc                     |    2 +-
 gcc/analyzer/sm-taint.cc                      |   16 +-
 gcc/analyzer/store.cc                         |   11 +-
 gcc/analyzer/store.h                          |    9 +
 gcc/analyzer/varargs.cc                       |    8 +-
 gcc/doc/invoke.texi                           |   15 +
 gcc/testsuite/gcc.dg/analyzer/data-model-1.c  |    4 +-
 .../analyzer/out-of-bounds-diagram-1-ascii.c  |   55 +
 .../analyzer/out-of-bounds-diagram-1-debug.c  |   40 +
 .../analyzer/out-of-bounds-diagram-1-emoji.c  |   55 +
 .../analyzer/out-of-bounds-diagram-1-json.c   |   13 +
 .../analyzer/out-of-bounds-diagram-1-sarif.c  |   24 +
 .../out-of-bounds-diagram-1-unicode.c         |   55 +
 .../analyzer/out-of-bounds-diagram-10.c       |   29 +
 .../analyzer/out-of-bounds-diagram-11.c       |   82 +
 .../analyzer/out-of-bounds-diagram-12.c       |   54 +
 .../analyzer/out-of-bounds-diagram-13.c       |   43 +
 .../analyzer/out-of-bounds-diagram-14.c       |  110 +
 .../analyzer/out-of-bounds-diagram-15.c       |   42 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-2.c |   30 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-3.c |   45 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-4.c |   45 +
 .../analyzer/out-of-bounds-diagram-5-ascii.c  |   40 +
 .../out-of-bounds-diagram-5-unicode.c         |   42 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-6.c |  125 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-7.c |   36 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-8.c |   34 +
 .../gcc.dg/analyzer/out-of-bounds-diagram-9.c |   42 +
 .../gcc.dg/analyzer/pattern-test-2.c          |    4 +-
 .../gcc.dg/plugin/analyzer_gil_plugin.c       |    6 +-
 53 files changed, 4382 insertions(+), 146 deletions(-)
 create mode 100644 gcc/analyzer/access-diagram.cc
 create mode 100644 gcc/analyzer/access-diagram.h
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-ascii.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-debug.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-emoji.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-json.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-sarif.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-unicode.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-10.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-11.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-12.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-13.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-14.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-15.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-2.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-3.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-4.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-ascii.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-unicode.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-7.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-8.c
 create mode 100644 gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-9.c

diff --git a/gcc/Makefile.in b/gcc/Makefile.in
index c1e7257ed24..1be7460b9d0 100644
--- a/gcc/Makefile.in
+++ b/gcc/Makefile.in
@@ -1275,6 +1275,7 @@ C_COMMON_OBJS = c-family/c-common.o c-family/c-cppbuiltin.o c-family/c-dump.o \
 
 # Analyzer object files
 ANALYZER_OBJS = \
+	analyzer/access-diagram.o \
 	analyzer/analysis-plan.o \
 	analyzer/analyzer.o \
 	analyzer/analyzer-language.o \
diff --git a/gcc/analyzer/access-diagram.cc b/gcc/analyzer/access-diagram.cc
new file mode 100644
index 00000000000..968ff50a0b7
--- /dev/null
+++ b/gcc/analyzer/access-diagram.cc
@@ -0,0 +1,2405 @@
+/* Text art visualizations within -fanalyzer.
+   Copyright (C) 2023 Free Software Foundation, Inc.
+
+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_ALGORITHM
+#define INCLUDE_MEMORY
+#define INCLUDE_MAP
+#define INCLUDE_SET
+#include "system.h"
+#include "coretypes.h"
+#include "coretypes.h"
+#include "tree.h"
+#include "function.h"
+#include "basic-block.h"
+#include "gimple.h"
+#include "diagnostic.h"
+#include "intl.h"
+#include "make-unique.h"
+#include "tree-diagnostic.h" /* for default_tree_printer.  */
+#include "analyzer/analyzer.h"
+#include "analyzer/region-model.h"
+#include "analyzer/access-diagram.h"
+#include "text-art/ruler.h"
+#include "fold-const.h"
+
+#if ENABLE_ANALYZER
+
+/* Consider this code:
+     int32_t arr[10];
+     arr[10] = x;
+   where we've emitted a buffer overflow diagnostic like this:
+     out-of-bounds write from byte 40 till byte 43 but 'arr' ends at byte 40
+
+   We want to emit a diagram that visualizes:
+   - the spatial relationship between the valid region to access, versus
+   the region that was actually accessed: does it overlap, was it touching,
+   close, or far away?  Was it before or after in memory?  What are the
+   relative sizes involved?
+   - the direction of the access (read vs write)
+
+   The following code supports emitting diagrams similar to the following:
+
+   #                                        +--------------------------------+
+   #                                        |write from ‘x’ (type: ‘int32_t’)|
+   #                                        +--------------------------------+
+   #                                                        |
+   #                                                        |
+   #                                                        v
+   #  +---------+-----------+-----------+   +--------------------------------+
+   #  |   [0]   |    ...    |    [9]    |   |       after valid range        |
+   #  +---------+-----------+-----------+   |                                |
+   #  |   ‘arr’ (type: ‘int32_t[10]’)   |   |                                |
+   #  +---------------------------------+   +--------------------------------+
+   #  |~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|   |~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|
+   #                   |                                    |
+   #         +---------+--------+                 +---------+---------+
+   #         |capacity: 40 bytes|                 |overflow of 4 bytes|
+   #         +------------------+                 +-------------------+
+
+  where the diagram is laid out via table columns where each table column
+  represents either a range of bits/bytes, or is a spacing column (to highlight
+  the boundary between valid vs invalid accesses).  The table columns can be
+  seen via -fanalyzer-debug-text-art.  For example, here there are 5 table
+  columns ("tc0" through "tc4"):
+
+   #  +---------+-----------+-----------+---+--------------------------------+
+   #  |   tc0   |    tc1    |    tc2    |tc3|              tc4               |
+   #  +---------+-----------+-----------+---+--------------------------------+
+   #  |bytes 0-3|bytes 4-35 |bytes 36-39|   |          bytes 40-43           |
+   #  +---------+-----------+-----------+   +--------------------------------+
+   #
+   #                                        +--------------------------------+
+   #                                        |write from ‘x’ (type: ‘int32_t’)|
+   #                                        +--------------------------------+
+   #                                                        |
+   #                                                        |
+   #                                                        v
+   #  +---------+-----------+-----------+   +--------------------------------+
+   #  |   [0]   |    ...    |    [9]    |   |       after valid range        |
+   #  +---------+-----------+-----------+   |                                |
+   #  |   ‘arr’ (type: ‘int32_t[10]’)   |   |                                |
+   #  +---------------------------------+   +--------------------------------+
+   #  |~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|   |~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|
+   #                   |                                    |
+   #         +---------+--------+                 +---------+---------+
+   #         |capacity: 40 bytes|                 |overflow of 4 bytes|
+   #         +------------------+                 +-------------------+
+
+  The diagram is built up from the following:
+
+   #                                        +--------------------------------+
+   #                                        | ITEM FOR SVALUE/ACCESSED REGION|
+   #                                        +--------------------------------+
+   #                                                        |
+   #                                                        | DIRECTION WIDGET
+   #                                                        v
+   #  +---------------------------------+   +--------------------------------+
+   #  |   VALID REGION                  |   | INVALID ACCESS                 |
+   #  +---------------------------------+   +--------------------------------+
+   #
+   #  |                       VALID-VS-INVALID RULER                         |
+
+  i.e. a vbox_widget containing 4 child widgets laid out vertically:
+  - ALIGNED CHILD WIDGET: ITEM FOR SVALUE/ACCESSED REGION
+  - DIRECTION WIDGET
+  - ALIGNED CHILD WIDGET: VALID AND INVALID ACCESSES
+  - VALID-VS-INVALID RULER.
+
+  A more complicated example, given this overflow:
+     char buf[100];
+     strcpy (buf, LOREM_IPSUM);
+
+   01| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   02| |[0]|[1]|[2]|[3]|[4]|[5]|   ...    |[440]|[441]|[442]|[443]|[444]|[445]|
+   03| +---+---+---+---+---+---+          +-----+-----+-----+-----+-----+-----+
+   04| |'L'|'o'|'r'|'e'|'m'|' '|          | 'o' | 'r' | 'u' | 'm' | '.' | NUL |
+   05| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   06| |                  string literal (type: 'char[446]')                  |
+   07| +----------------------------------------------------------------------+
+   08|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   09|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   10|   v   v   v   v   v   v  v  v    v    v     v     v     v     v     v
+   11| +---+---------------------+----++--------------------------------------+
+   12| |[0]|         ...         |[99]||          after valid range           |
+   13| +---+---------------------+----+|                                      |
+   14| |  'buf' (type: 'char[100]')   ||                                      |
+   15| +------------------------------++--------------------------------------+
+   16| |~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~||~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~|
+   17|                |                                   |
+   18|      +---------+---------+              +----------+----------+
+   19|      |capacity: 100 bytes|              |overflow of 346 bytes|
+   20|      +-------------------+              +---------------------+
+
+ which is:
+
+   01| ALIGNED CHILD WIDGET (lines 01-07): (string_region_spatial_item)-+-----+
+   02| |[0]|[1]|[2]|[3]|[4]|[5]|   ...    |[440]|[441]|[442]|[443]|[444]|[445]|
+   03| +---+---+---+---+---+---+          +-----+-----+-----+-----+-----+-----+
+   04| |'L'|'o'|'r'|'e'|'m'|' '|          | 'o' | 'r' | 'u' | 'm' | '.' | NUL |
+   05| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   06| |                  string literal (type: 'char[446]')                  |
+   07| +----------------------------------------------------------------------+
+   08| DIRECTION WIDGET (lines 08-10)   |    |     |     |     |     |     |
+   09|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   10|   v   v   v   v   v   v  v  v    v    v     v     v     v     v     v
+   11| ALIGNED CHILD WIDGET (lines 11-15)-------------------------------------+
+   12| VALID REGION  ...         |[99]|| INVALID ACCESS                       |
+   13| +---+---------------------+----+|                                      |
+   14| |  'buf' (type: 'char[100]')   ||                                      |
+   15| +------------------------------++--------------------------------------+
+   16| VALID-VS-INVALID RULER (lines 16-20): ~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~|
+   17|                |                                   |
+   18|      +---------+---------+              +----------+----------+
+   19|      |capacity: 100 bytes|              |overflow of 346 bytes|
+   20|      +-------------------+              +---------------------+
+
+   We build the diagram in several phases:
+   - (1) we construct an access_diagram_impl widget.  Within the ctor, we have
+   these subphases:
+   -   (1.1) find all of the boundaries of interest
+   -   (1.2) use the boundaries to build a bit_table_map, associating bit ranges
+   with table columns (e.g. "byte 0 is column 0, bytes 1-98 are column 2" etc)
+   -   (1.3) create child widgets that share this table-based geometry
+   - (2) ask the widget for its size request
+   -   (2.1) column widths and row heights for the table are computed by
+   access_diagram_impl::calc_req_size
+   -   (2.2) child widgets request sizes based on these widths/heights
+   - (3) create a canvas of the appropriate size
+   - (4) paint the widget hierarchy to the canvas.  */
+
+
+using namespace text_art;
+
+namespace ana {
+
+static styled_string
+fmt_styled_string (style_manager &sm,
+		   const char *fmt, ...)
+    ATTRIBUTE_GCC_DIAG(2, 3);
+
+static styled_string
+fmt_styled_string (style_manager &sm,
+		   const char *fmt, ...)
+{
+  va_list ap;
+  va_start (ap, fmt);
+  styled_string result
+    = styled_string::from_fmt_va (sm, default_tree_printer, fmt, &ap);
+  va_end (ap);
+  return result;
+}
+
+class access_diagram_impl;
+class bit_to_table_map;
+
+static void
+pp_bit_size_t (pretty_printer *pp, bit_size_t num_bits)
+{
+  if (num_bits % BITS_PER_UNIT == 0)
+    {
+      byte_size_t num_bytes = num_bits / BITS_PER_UNIT;
+      if (num_bytes == 1)
+	pp_printf (pp, _("%wi byte"), num_bytes.to_uhwi ());
+      else
+	pp_printf (pp, _("%wi bytes"), num_bytes.to_uhwi ());
+    }
+  else
+    {
+      if (num_bits == 1)
+	pp_printf (pp, _("%wi bit"), num_bits.to_uhwi ());
+      else
+	pp_printf (pp, _("%wi bits"), num_bits.to_uhwi ());
+    }
+}
+
+static styled_string
+get_access_size_str (style_manager &sm,
+		     const access_operation &op,
+		     access_range accessed_range,
+		     tree type)
+{
+  bit_size_expr num_bits;
+  if (accessed_range.get_size (op.m_model, &num_bits))
+    {
+      if (type)
+	{
+	  styled_string s;
+
+	  pretty_printer pp;
+	  num_bits.print (&pp);
+
+	  if (op.m_dir == DIR_READ)
+	    return fmt_styled_string (sm,
+				      _("read of %qT (%s)"),
+				      type,
+				      pp_formatted_text (&pp));
+	  else
+	    return fmt_styled_string (sm,
+				      _("write of %qT (%s)"),
+				      type,
+				      pp_formatted_text (&pp));
+	}
+      if (op.m_dir == DIR_READ)
+	return num_bits.get_formatted_str (sm,
+					   _("read of %wi bit"),
+					   _("read of %wi bits"),
+					   _("read of %wi byte"),
+					   _("read of %wi bytes"),
+					   _("read of %qE bits"),
+					   _("read of %qE bytes"));
+      else
+	return num_bits.get_formatted_str (sm,
+					   _("write of %wi bit"),
+					   _("write of %wi bits"),
+					   _("write of %wi byte"),
+					   _("write of %wi bytes"),
+					   _("write of %qE bits"),
+					   _("write of %qE bytes"));
+    }
+
+  if (type)
+    {
+      if (op.m_dir == DIR_READ)
+	return fmt_styled_string (sm, _("read of %qT"), type);
+      else
+	return fmt_styled_string (sm, _("write of %qT"), type);
+    }
+
+  if (op.m_dir == DIR_READ)
+    return styled_string (sm, _("read"));
+  else
+    return styled_string (sm, _("write"));
+}
+
+/* Subroutine of clean_up_for_diagram.  */
+
+static tree
+strip_any_cast (tree expr)
+{
+  if (TREE_CODE (expr) == NOP_EXPR
+      || TREE_CODE (expr) == NON_LVALUE_EXPR)
+    expr = TREE_OPERAND (expr, 0);
+  return expr;
+}
+
+/* Subroutine of clean_up_for_diagram.  */
+
+static tree
+remove_ssa_names (tree expr)
+{
+  if (TREE_CODE (expr) == SSA_NAME
+      && SSA_NAME_VAR (expr))
+    return SSA_NAME_VAR (expr);
+  tree t = copy_node (expr);
+  for (int i = 0; i < TREE_OPERAND_LENGTH (expr); i++)
+    TREE_OPERAND (t, i) = remove_ssa_names (TREE_OPERAND (expr, i));
+  return t;
+}
+
+/* We want to be able to print tree expressions from the analyzer,
+   which is in the middle end.
+
+   We could use the front-end pretty_printer's formatting routine,
+   but:
+   (a) some have additional state in a pretty_printer subclass, so we'd
+   need to clone global_dc->printer
+   (b) the "aka" type information added by the C and C++ frontends are
+   too verbose when building a diagram, and there isn't a good way to ask
+   for a less verbose version of them.
+
+   Hence we use default_tree_printer.
+   However, we want to avoid printing SSA names, and instead print the
+   underlying var name.
+   Ideally there would be a better tree printer for use by middle end
+   warnings, but as workaround, this function clones a tree, replacing
+   SSA names with the var names.  */
+
+tree
+clean_up_for_diagram (tree expr)
+{
+  tree without_ssa_names = remove_ssa_names (expr);
+  return strip_any_cast (without_ssa_names);
+}
+
+/* struct bit_size_expr.  */
+
+text_art::styled_string
+bit_size_expr::get_formatted_str (text_art::style_manager &sm,
+				  const char *concrete_single_bit_fmt,
+				  const char *concrete_plural_bits_fmt,
+				  const char *concrete_single_byte_fmt,
+				  const char *concrete_plural_bytes_fmt,
+				  const char *symbolic_bits_fmt,
+				  const char *symbolic_bytes_fmt) const
+{
+  if (TREE_CODE (m_num_bits) == INTEGER_CST)
+    {
+      bit_size_t concrete_num_bits = wi::to_offset (m_num_bits);
+      if (concrete_num_bits % BITS_PER_UNIT == 0)
+	{
+	  byte_size_t concrete_num_bytes = concrete_num_bits / BITS_PER_UNIT;
+	  if (concrete_num_bytes == 1)
+	    return fmt_styled_string (sm, concrete_single_byte_fmt,
+				      concrete_num_bytes.to_uhwi ());
+	  else
+	    return fmt_styled_string (sm, concrete_plural_bytes_fmt,
+				      concrete_num_bytes.to_uhwi ());
+	}
+      else
+	{
+	  if (concrete_num_bits == 1)
+	    return fmt_styled_string (sm, concrete_single_bit_fmt,
+				      concrete_num_bits.to_uhwi ());
+	  else
+	    return fmt_styled_string (sm, concrete_plural_bits_fmt,
+				      concrete_num_bits.to_uhwi ());
+	}
+    }
+  else
+    {
+      if (tree bytes_expr = maybe_get_as_bytes ())
+	return fmt_styled_string (sm,
+				  symbolic_bytes_fmt,
+				  clean_up_for_diagram (bytes_expr));
+      return fmt_styled_string (sm,
+				symbolic_bits_fmt,
+				clean_up_for_diagram (m_num_bits));
+    }
+}
+
+void
+bit_size_expr::print (pretty_printer *pp) const
+{
+  if (TREE_CODE (m_num_bits) == INTEGER_CST)
+    {
+      bit_size_t concrete_num_bits = wi::to_offset (m_num_bits);
+      pp_bit_size_t (pp, concrete_num_bits);
+    }
+  else
+    {
+      if (tree bytes_expr = maybe_get_as_bytes ())
+	pp_printf (pp, _("%qE bytes"), bytes_expr);
+      else
+	pp_printf (pp, _("%qE bits"), m_num_bits);
+    }
+}
+
+tree
+bit_size_expr::maybe_get_as_bytes () const
+{
+  switch (TREE_CODE (m_num_bits))
+    {
+    default:
+      break;
+    case INTEGER_CST:
+      {
+	const bit_size_t num_bits = wi::to_offset (m_num_bits);
+	if (num_bits % BITS_PER_UNIT != 0)
+	  return NULL_TREE;
+	const bit_size_t num_bytes = num_bits / BITS_PER_UNIT;
+	return wide_int_to_tree (size_type_node, num_bytes);
+      }
+      break;
+    case PLUS_EXPR:
+    case MINUS_EXPR:
+      {
+	bit_size_expr op0
+	  = bit_size_expr (TREE_OPERAND (m_num_bits, 0));
+	tree op0_as_bytes = op0.maybe_get_as_bytes ();
+	if (!op0_as_bytes)
+	  return NULL_TREE;
+	bit_size_expr op1
+	  = bit_size_expr (TREE_OPERAND (m_num_bits, 1));
+	tree op1_as_bytes = op1.maybe_get_as_bytes ();
+	if (!op1_as_bytes)
+	  return NULL_TREE;
+	return fold_build2 (TREE_CODE (m_num_bits), size_type_node,
+			    op0_as_bytes, op1_as_bytes);
+      }
+      break;
+    case MULT_EXPR:
+      {
+	bit_size_expr op1
+	  = bit_size_expr (TREE_OPERAND (m_num_bits, 1));
+	if (tree op1_as_bytes = op1.maybe_get_as_bytes ())
+	  return fold_build2 (MULT_EXPR, size_type_node,
+			      TREE_OPERAND (m_num_bits, 0),
+			      op1_as_bytes);
+      }
+      break;
+    }
+  return NULL_TREE;
+}
+
+/* struct access_range.  */
+
+access_range::access_range (const region *base_region, const bit_range &bits)
+: m_start (region_offset::make_concrete (base_region,
+					 bits.get_start_bit_offset ())),
+  m_next (region_offset::make_concrete (base_region,
+					bits.get_next_bit_offset ()))
+{
+}
+
+access_range::access_range (const region *base_region, const byte_range &bytes)
+: m_start (region_offset::make_concrete (base_region,
+					 bytes.get_start_bit_offset ())),
+  m_next (region_offset::make_concrete (base_region,
+					bytes.get_next_bit_offset ()))
+{
+}
+
+access_range::access_range (const region &reg, region_model_manager *mgr)
+: m_start (reg.get_offset (mgr)),
+  m_next (reg.get_next_offset (mgr))
+{
+}
+
+bool
+access_range::get_size (const region_model &model, bit_size_expr *out) const
+{
+  tree start_expr = m_start.calc_symbolic_bit_offset (model);
+  if (!start_expr)
+    return false;
+  tree next_expr = m_next.calc_symbolic_bit_offset (model);
+  if (!next_expr)
+    return false;
+  *out = bit_size_expr (fold_build2 (MINUS_EXPR, size_type_node,
+					 next_expr, start_expr));
+  return true;
+}
+
+bool
+access_range::contains_p (const access_range &other) const
+{
+  return (m_start <= other.m_start
+	  && other.m_next <= m_next);
+}
+
+bool
+access_range::empty_p () const
+{
+  bit_range concrete_bits (0, 0);
+  if (!as_concrete_bit_range (&concrete_bits))
+    return false;
+  return concrete_bits.empty_p ();
+}
+
+void
+access_range::dump_to_pp (pretty_printer *pp, bool simple) const
+{
+  if (m_start.concrete_p () && m_next.concrete_p ())
+    {
+      bit_range bits (m_start.get_bit_offset (),
+		      m_next.get_bit_offset () - m_start.get_bit_offset ());
+      bits.dump_to_pp (pp);
+      return;
+    }
+  pp_character (pp, '[');
+  m_start.dump_to_pp (pp, simple);
+  pp_string (pp, " to ");
+  m_next.dump_to_pp (pp, simple);
+  pp_character (pp, ')');
+}
+
+DEBUG_FUNCTION void
+access_range::dump (bool simple) const
+{
+  pretty_printer pp;
+  pp_format_decoder (&pp) = default_tree_printer;
+  pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = stderr;
+  dump_to_pp (&pp, simple);
+  pp_newline (&pp);
+  pp_flush (&pp);
+}
+
+void
+access_range::log (const char *title, logger &logger) const
+{
+  logger.start_log_line ();
+  logger.log_partial ("%s: ", title);
+  dump_to_pp (logger.get_printer (), true);
+  logger.end_log_line ();
+}
+
+/* struct access_operation.  */
+
+access_range
+access_operation::get_valid_bits () const
+{
+  const svalue *capacity_in_bytes_sval = m_model.get_capacity (m_base_region);
+  return access_range
+    (region_offset::make_concrete (m_base_region, 0),
+     region_offset::make_byte_offset (m_base_region, capacity_in_bytes_sval));
+}
+
+access_range
+access_operation::get_actual_bits () const
+{
+  return access_range (m_reg, get_manager ());
+}
+
+/* If there are any bits accessed invalidly before the valid range,
+   return true and write their range to *OUT.
+   Return false if there aren't, or if there's a problem
+   (e.g. symbolic ranges.  */
+
+bool
+access_operation::maybe_get_invalid_before_bits (access_range *out) const
+{
+  access_range valid_bits (get_valid_bits ());
+  access_range actual_bits (get_actual_bits ());
+
+  if (actual_bits.m_start >= valid_bits.m_start)
+    {
+      /* No part of accessed range is before the valid range.  */
+      return false;
+    }
+  else if (actual_bits.m_next > valid_bits.m_start)
+    {
+      /* Get part of accessed range that's before the valid range.  */
+      *out = access_range (actual_bits.m_start, valid_bits.m_start);
+      return true;
+    }
+  else
+    {
+      /* Accessed range is fully before valid range.  */
+      *out = actual_bits;
+      return true;
+    }
+}
+
+/* If there are any bits accessed invalidly after the valid range,
+   return true and write their range to *OUT.
+   Return false if there aren't, or if there's a problem.  */
+
+bool
+access_operation::maybe_get_invalid_after_bits (access_range *out) const
+{
+  access_range valid_bits (get_valid_bits ());
+  access_range actual_bits (get_actual_bits ());
+
+  if (actual_bits.m_next <= valid_bits.m_next)
+    {
+      /* No part of accessed range is after the valid range.  */
+      return false;
+    }
+  else if (actual_bits.m_start < valid_bits.m_next)
+    {
+      /* Get part of accessed range that's after the valid range.  */
+      *out = access_range (valid_bits.m_next, actual_bits.m_next);
+      return true;
+    }
+  else
+    {
+      /* Accessed range is fully after valid range.  */
+      *out = actual_bits;
+      return true;
+    }
+}
+
+/* A class for capturing all of the region offsets of interest (both concrete
+   and symbolic), to help align everything in the diagram.
+   Boundaries can be soft or hard; hard boundaries are emphasized visually
+   (e.g. the boundary between valid vs invalid accesses).
+
+   Offsets in the boundaries are all expressed relative to the base
+   region of the access_operation.  */
+
+class boundaries
+{
+public:
+  enum class kind { HARD, SOFT};
+
+  boundaries (const region &base_reg)
+  : m_base_reg (base_reg)
+  {
+  }
+
+  void add (region_offset offset, enum kind k)
+  {
+    m_all_offsets.insert (offset);
+    if (k == kind::HARD)
+      m_hard_offsets.insert (offset);
+  }
+
+  void add (const access_range &range, enum kind kind)
+  {
+    add (range.m_start, kind);
+    add (range.m_next, kind);
+  }
+
+  void add (const region &reg, region_model_manager *mgr, enum kind kind)
+  {
+    add (access_range (reg.get_offset (mgr),
+		       reg.get_next_offset (mgr)),
+	 kind);
+  }
+
+  void add (const byte_range bytes, enum kind kind)
+  {
+    add (access_range (&m_base_reg, bytes), kind);
+  }
+
+  void add_all_bytes_in_range (const byte_range &bytes)
+  {
+    for (byte_offset_t byte_idx = bytes.get_start_byte_offset ();
+	 byte_idx <= bytes.get_next_byte_offset ();
+	 byte_idx = byte_idx + 1)
+      add (region_offset::make_concrete (&m_base_reg, byte_idx * 8),
+	   kind::SOFT);
+  }
+
+  void add_all_bytes_in_range (const access_range &range)
+  {
+    byte_range bytes (0, 0);
+    bool valid = range.as_concrete_byte_range (&bytes);
+    gcc_assert (valid);
+    add_all_bytes_in_range (bytes);
+  }
+
+  void log (logger &logger) const
+  {
+    logger.log ("boundaries:");
+    logger.inc_indent ();
+    for (auto offset : m_all_offsets)
+      {
+	enum kind k = get_kind (offset);
+	logger.start_log_line ();
+	logger.log_partial ("%s: ", (k == kind::HARD) ? "HARD" : "soft");
+	offset.dump_to_pp (logger.get_printer (), true);
+	logger.end_log_line ();
+      }
+    logger.dec_indent ();
+  }
+
+  enum kind get_kind (region_offset offset) const
+  {
+    gcc_assert (m_all_offsets.find (offset) != m_all_offsets.end ());
+    if (m_hard_offsets.find (offset) != m_hard_offsets.end ())
+      return kind::HARD;
+    else
+      return kind::SOFT;
+  }
+
+  std::set<region_offset>::const_iterator begin () const
+  {
+    return m_all_offsets.begin ();
+  }
+  std::set<region_offset>::const_iterator end () const
+  {
+    return m_all_offsets.end ();
+  }
+  std::set<region_offset>::size_type size () const
+  {
+    return m_all_offsets.size ();
+  }
+
+private:
+  const region &m_base_reg;
+  std::set<region_offset> m_all_offsets;
+  std::set<region_offset> m_hard_offsets;
+};
+
+/* A widget that wraps a table but offloads column-width calculation
+   to a shared object, so that we can vertically line up multiple tables
+   and have them all align their columns.
+
+   For example, in:
+
+   01| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   02| |[0]|[1]|[2]|[3]|[4]|[5]|   ...    |[440]|[441]|[442]|[443]|[444]|[445]|
+   03| +---+---+---+---+---+---+          +-----+-----+-----+-----+-----+-----+
+   04| |'L'|'o'|'r'|'e'|'m'|' '|          | 'o' | 'r' | 'u' | 'm' | '.' | NUL |
+   05| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   06| |                  string literal (type: 'char[446]')                  |
+   07| +----------------------------------------------------------------------+
+   08|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   09|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   10|   v   v   v   v   v   v  v  v    v    v     v     v     v     v     v
+   11|+---+---------------------+----++--------------------------------------+
+   12||[0]|         ...         |[99]||          after valid range           |
+   13|+---+---------------------+----+|                                      |
+   14||  'buf' (type: 'char[100]')   ||                                      |
+   15|+------------------------------++--------------------------------------+
+   16||~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~||~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~|
+   17|               |                                   |
+   18|     +---------+---------+              +----------+----------+
+   19|     |capacity: 100 bytes|              |overflow of 346 bytes|
+   20|     +-------------------+              +---------------------+
+
+   rows 01-07 and rows 11-15 are x_aligned_table_widget instances.  */
+
+class x_aligned_table_widget : public leaf_widget
+{
+public:
+  x_aligned_table_widget (table t,
+			  const theme &theme,
+			  table_dimension_sizes &col_widths)
+  : m_table (std::move (t)),
+    m_theme (theme),
+    m_col_widths (col_widths),
+    m_row_heights (t.get_size ().h),
+    m_cell_sizes (m_col_widths, m_row_heights),
+    m_tg (m_table, m_cell_sizes)
+  {
+  }
+
+  const char *get_desc () const override
+  {
+    return "x_aligned_table_widget";
+  }
+
+  canvas::size_t calc_req_size () final override
+  {
+    /* We don't compute the size requirements;
+       the parent should have done this.  */
+    return m_tg.get_canvas_size ();
+  }
+
+  void paint_to_canvas (canvas &canvas) final override
+  {
+    m_table.paint_to_canvas (canvas,
+			     get_top_left (),
+			     m_tg,
+			     m_theme);
+  }
+
+  const table &get_table () const { return m_table; }
+  table_cell_sizes &get_cell_sizes () { return m_cell_sizes; }
+  void recalc_coords ()
+  {
+    m_tg.recalc_coords ();
+  }
+
+private:
+  table m_table;
+  const theme &m_theme;
+  table_dimension_sizes &m_col_widths; // Reference to shared column widths
+  table_dimension_sizes m_row_heights; // Unique row heights
+  table_cell_sizes m_cell_sizes;
+  table_geometry m_tg;
+};
+
+/* A widget for printing arrows between the accessed region
+   and the svalue, showing the direction of the access.
+
+   For example, in:
+
+   01| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   02| |[0]|[1]|[2]|[3]|[4]|[5]|   ...    |[440]|[441]|[442]|[443]|[444]|[445]|
+   03| +---+---+---+---+---+---+          +-----+-----+-----+-----+-----+-----+
+   04| |'L'|'o'|'r'|'e'|'m'|' '|          | 'o' | 'r' | 'u' | 'm' | '.' | NUL |
+   05| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   06| |                  string literal (type: 'char[446]')                  |
+   07| +----------------------------------------------------------------------+
+   08|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   09|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   10|   v   v   v   v   v   v  v  v    v    v     v     v     v     v     v
+   11|+---+---------------------+----++--------------------------------------+
+   12||[0]|         ...         |[99]||          after valid range           |
+   13|+---+---------------------+----+|                                      |
+   14||  'buf' (type: 'char[100]')   ||                                      |
+   15|+------------------------------++--------------------------------------+
+   16||~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~||~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~|
+   17|               |                                   |
+   18|     +---------+---------+              +----------+----------+
+   19|     |capacity: 100 bytes|              |overflow of 346 bytes|
+   20|     +-------------------+              +---------------------+
+
+   rows 8-10 are the direction widget.  */
+
+class direction_widget : public leaf_widget
+{
+public:
+  direction_widget (const access_diagram_impl &dia_impl,
+		    const bit_to_table_map &btm)
+  : leaf_widget (),
+    m_dia_impl (dia_impl),
+    m_btm (btm)
+  {
+  }
+  const char *get_desc () const override
+  {
+    return "direction_widget";
+  }
+  canvas::size_t calc_req_size () final override
+  {
+    /* Get our width from our siblings.  */
+    return canvas::size_t (0, 3);
+  }
+  void paint_to_canvas (canvas &canvas) final override;
+
+private:
+  const access_diagram_impl &m_dia_impl;
+  const bit_to_table_map &m_btm;
+};
+
+/* A widget for adding an x_ruler to a diagram based on table columns,
+   offloading column-width calculation to shared objects, so that the ruler
+   lines up with other tables in the diagram.
+
+   For example, in:
+
+   01| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   02| |[0]|[1]|[2]|[3]|[4]|[5]|   ...    |[440]|[441]|[442]|[443]|[444]|[445]|
+   03| +---+---+---+---+---+---+          +-----+-----+-----+-----+-----+-----+
+   04| |'L'|'o'|'r'|'e'|'m'|' '|          | 'o' | 'r' | 'u' | 'm' | '.' | NUL |
+   05| +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+   06| |                  string literal (type: 'char[446]')                  |
+   07| +----------------------------------------------------------------------+
+   08|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   09|   |   |   |   |   |   |  |  |    |    |     |     |     |     |     |
+   10|   v   v   v   v   v   v  v  v    v    v     v     v     v     v     v
+   11|+---+---------------------+----++--------------------------------------+
+   12||[0]|         ...         |[99]||          after valid range           |
+   13|+---+---------------------+----+|                                      |
+   14||  'buf' (type: 'char[100]')   ||                                      |
+   15|+------------------------------++--------------------------------------+
+   16||~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~||~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~|
+   17|               |                                   |
+   18|     +---------+---------+              +----------+----------+
+   19|     |capacity: 100 bytes|              |overflow of 346 bytes|
+   20|     +-------------------+              +---------------------+
+
+   rows 16-20 are the x_aligned_x_ruler_widget.  */
+
+class x_aligned_x_ruler_widget : public leaf_widget
+{
+public:
+  x_aligned_x_ruler_widget (const access_diagram_impl &dia_impl,
+			    const theme &theme,
+			    table_dimension_sizes &col_widths)
+  : m_dia_impl (dia_impl),
+    m_theme (theme),
+    m_col_widths (col_widths)
+  {
+  }
+
+  const char *get_desc () const override
+  {
+    return "x_aligned_ruler_widget";
+  }
+
+  void add_range (const table::range_t &x_range,
+		  styled_string text,
+		  style::id_t style_id)
+  {
+    m_labels.push_back (label (x_range, std::move (text), style_id));
+  }
+
+  canvas::size_t calc_req_size () final override
+  {
+    x_ruler r (make_x_ruler ());
+    return r.get_size ();
+  }
+
+  void paint_to_canvas (canvas &canvas) final override
+  {
+    x_ruler r (make_x_ruler ());
+    r.paint_to_canvas (canvas,
+		       get_top_left (),
+		       m_theme);
+  }
+
+private:
+  struct label
+  {
+    label (const table::range_t &table_x_range,
+	   styled_string text,
+	   style::id_t style_id)
+    : m_table_x_range (table_x_range),
+      m_text (std::move (text)),
+      m_style_id (style_id)
+    {
+    }
+    table::range_t m_table_x_range;
+    styled_string m_text;
+    style::id_t m_style_id;
+  };
+
+  x_ruler make_x_ruler () const;
+
+  const access_diagram_impl &m_dia_impl;
+  const theme &m_theme;
+  table_dimension_sizes &m_col_widths;
+  std::vector<label> m_labels;
+};
+
+/* A two-way mapping between access_ranges and table columns, for use by
+   spatial_item subclasses for creating tables.
+   For example when visualizing a bogus access of 'int arr[10];'
+   at 'arr[10]', we might have:
+   - table column 0 is "bytes 0-3" (for arr[0])
+   - table column 1 is "bytes 4-35" (for arr[1] through arr[8])
+   - table column 2 is "bytes 36-39 (for arr[9])
+   - table column 3 is blank to emphasize a hard boundary between
+     valid/invalid accesses.
+   - table column 4 is "bytes 40-44" (for arr[10])
+
+   We store this as a pair of maps from region_offset to table x; in
+   the abvove example:
+
+     region offset          table_x  prev_table_x
+     bit 0 (aka byte 0)     0        (none)
+     bit 32 (aka byte 4)    1        0
+     bit 288 (aka byte 36)  2        1
+     bit 320 (aka byte 40)  4        2
+     bit 352 (aka byte 44)  (none)   (none)
+
+     so that e.g given the half-open byte range [0, 40)
+     we can determine the closed range of table x [0, 2].  */
+
+class bit_to_table_map
+{
+public:
+  /* Populate m_table_x_for_bit and m_bit_for_table_x.  */
+  void populate (const boundaries &boundaries, logger *logger)
+  {
+    LOG_SCOPE (logger);
+
+    int table_x = 0;
+    std::vector <region_offset> vec_boundaries (boundaries.begin (),
+						boundaries.end ());
+
+    /* Sort into an order that makes sense.  */
+    std::sort (vec_boundaries.begin (),
+	       vec_boundaries.end ());
+
+    if (logger)
+      {
+	logger->log ("vec_boundaries");
+	logger->inc_indent ();
+	for (unsigned idx = 0; idx < vec_boundaries.size (); idx++)
+	  {
+	    logger->start_log_line ();
+	    logger->log_partial ("idx: %i: ", idx);
+	    vec_boundaries[idx].dump_to_pp (logger->get_printer (), true);
+	    logger->end_log_line ();
+	  }
+	logger->dec_indent ();
+      }
+
+    for (size_t idx = 0; idx < vec_boundaries.size (); idx++)
+      {
+	const region_offset &offset = vec_boundaries[idx];
+	if (idx > 0 && (idx + 1) < vec_boundaries.size ())
+	  {
+	    if (boundaries.get_kind (offset) == boundaries::kind::HARD)
+	      table_x += 1;
+	  }
+	m_table_x_for_offset[offset] = table_x;
+	if ((idx + 1) < vec_boundaries.size ())
+	  {
+	    const region_offset &next_offset = vec_boundaries[idx + 1];
+	    m_table_x_for_prev_offset[next_offset] = table_x;
+	    m_range_for_table_x[table_x] = access_range (offset, next_offset);
+	  }
+	table_x += 1;
+      }
+    m_num_columns = table_x - 1;
+
+    if (logger)
+      log (*logger);
+  }
+
+  unsigned get_num_columns () const
+  {
+    return m_num_columns;
+  }
+
+  table::range_t get_table_x_for_range (const access_range &range) const
+  {
+    return table::range_t (get_table_x_for_offset (range.m_start),
+			   get_table_x_for_prev_offset (range.m_next) + 1);
+  }
+
+  table::rect_t get_table_rect (const access_range &range,
+				const int table_y, const int table_h) const
+  {
+    const table::range_t x_range (get_table_x_for_range (range));
+    return table::rect_t (table::coord_t (x_range.start, table_y),
+			  table::size_t (x_range.get_size (), table_h));
+  }
+
+  table::rect_t get_table_rect (const region *base_reg,
+				const bit_range &bits,
+				const int table_y, const int table_h) const
+  {
+    const access_range range (base_reg, bits);
+    return get_table_rect (range, table_y, table_h);
+  }
+
+  table::rect_t get_table_rect (const region *base_reg,
+				const byte_range &bytes,
+				const int table_y, const int table_h) const
+  {
+    return get_table_rect (base_reg, bytes.as_bit_range (), table_y, table_h);
+  }
+
+  bool maybe_get_access_range_for_table_x (int table_x,
+					   access_range *out) const
+  {
+    auto slot = m_range_for_table_x.find (table_x);
+    if (slot == m_range_for_table_x.end ())
+      return false;
+    *out = slot->second;
+    return true;
+  }
+
+  void log (logger &logger) const
+  {
+    logger.log ("table columns");
+    logger.inc_indent ();
+    for (unsigned table_x = 0; table_x < get_num_columns (); table_x++)
+      {
+	logger.start_log_line ();
+	logger.log_partial ("table_x: %i", table_x);
+	access_range range_for_column (NULL, bit_range (0, 0));
+	if (maybe_get_access_range_for_table_x (table_x, &range_for_column))
+	  {
+	    logger.log_partial (": range: ");
+	    range_for_column.dump_to_pp (logger.get_printer (), true);
+	  }
+	logger.end_log_line ();
+      }
+    logger.dec_indent ();
+  }
+
+private:
+  int get_table_x_for_offset (region_offset offset) const
+  {
+    auto slot = m_table_x_for_offset.find (offset);
+
+    /* If this fails, then we probably failed to fully populate m_boundaries
+       in find_boundaries.  */
+    gcc_assert (slot != m_table_x_for_offset.end ());
+
+    return slot->second;
+  }
+
+  int get_table_x_for_prev_offset (region_offset offset) const
+  {
+    auto slot = m_table_x_for_prev_offset.find (offset);
+
+    /* If this fails, then we probably failed to fully populate m_boundaries
+       in find_boundaries.  */
+    gcc_assert (slot != m_table_x_for_prev_offset.end ());
+
+    return slot->second;
+  }
+
+  std::map<region_offset, int> m_table_x_for_offset;
+  std::map<region_offset, int> m_table_x_for_prev_offset;
+  std::map<int, access_range> m_range_for_table_x;
+  unsigned m_num_columns;
+};
+
+/* Base class for something in the diagram that participates
+   in two steps of diagram creation:
+   (a) populating a boundaries instance with the boundaries of interest
+   (b) creating a table instance for itself.
+
+   Offsets in the boundaries are all expressed relative to the base
+   region of the access_operation.  */
+
+class spatial_item
+{
+public:
+  virtual void add_boundaries (boundaries &out, logger *) const = 0;
+
+  virtual table make_table (const bit_to_table_map &btm,
+			    style_manager &sm) const = 0;
+};
+
+/* Subclass of spatial_item for visualizing the region of memory
+   that's valid to access relative to the base region of region accessed in
+   the operation.  */
+
+class valid_region_spatial_item : public spatial_item
+{
+public:
+  valid_region_spatial_item (const access_operation &op,
+			     diagnostic_event_id_t region_creation_event_id)
+  : m_op (op),
+    m_region_creation_event_id (region_creation_event_id)
+  {}
+
+  void add_boundaries (boundaries &out, logger *logger) const final override
+  {
+    LOG_SCOPE (logger);
+    access_range valid_bits = m_op.get_valid_bits ();
+    if (logger)
+      {
+	logger->start_log_line ();
+	logger->log_partial ("valid bits: ");
+	valid_bits.dump_to_pp (logger->get_printer (), true);
+	logger->end_log_line ();
+      }
+    out.add (valid_bits, boundaries::kind::HARD);
+
+    /* Support for showing first and final element in array types.  */
+    if (tree base_type = m_op.m_base_region->get_type ())
+      if (TREE_CODE (base_type) == ARRAY_TYPE)
+	{
+	  if (logger)
+	    logger->log ("showing first and final element in array type");
+	  region_model_manager *mgr = m_op.m_model.get_manager ();
+	  tree domain = TYPE_DOMAIN (base_type);
+	  if (TYPE_MIN_VALUE (domain) && TYPE_MAX_VALUE (domain))
+	    {
+	      const svalue *min_idx_sval
+		= mgr->get_or_create_constant_svalue (TYPE_MIN_VALUE (domain));
+	      const svalue *max_idx_sval
+		= mgr->get_or_create_constant_svalue (TYPE_MAX_VALUE (domain));
+	      const region *min_element =
+		mgr->get_element_region (m_op.m_base_region,
+					 TREE_TYPE (base_type),
+					 min_idx_sval);
+	      out.add (*min_element, mgr, boundaries::kind::SOFT);
+	      const region *max_element =
+		mgr->get_element_region (m_op.m_base_region,
+					 TREE_TYPE (base_type),
+					 max_idx_sval);
+	      out.add (*max_element, mgr, boundaries::kind::SOFT);
+	    }
+	}
+  }
+
+  /* Subroutine of make_table when base region has ARRAY_TYPE.  */
+  void add_array_elements_to_table (table &t,
+				    const bit_to_table_map &btm,
+				    style_manager &sm) const
+  {
+    tree base_type = m_op.m_base_region->get_type ();
+    gcc_assert (TREE_CODE (base_type) == ARRAY_TYPE);
+
+    tree domain = TYPE_DOMAIN (base_type);
+    if (!(TYPE_MIN_VALUE (domain) && TYPE_MAX_VALUE (domain)))
+      return;
+
+    region_model_manager * const mgr = m_op.get_manager ();
+    const int table_y = 0;
+    const int table_h = 1;
+    const table::range_t table_y_range (table_y, table_y + table_h);
+
+    t.add_row ();
+    const svalue *min_idx_sval
+      = mgr->get_or_create_constant_svalue (TYPE_MIN_VALUE (domain));
+    const region *min_element = mgr->get_element_region (m_op.m_base_region,
+							 TREE_TYPE (base_type),
+							 min_idx_sval);
+    const access_range min_element_range (*min_element, mgr);
+    const table::range_t min_element_x_range
+      = btm.get_table_x_for_range (min_element_range);
+
+    t.set_cell_span (table::rect_t (min_element_x_range,
+				    table_y_range),
+		     fmt_styled_string (sm, "[%E]",
+					TYPE_MIN_VALUE (domain)));
+
+    const svalue *max_idx_sval
+      = mgr->get_or_create_constant_svalue (TYPE_MAX_VALUE (domain));
+    const region *max_element = mgr->get_element_region (m_op.m_base_region,
+							 TREE_TYPE (base_type),
+							 max_idx_sval);
+    if (min_element == max_element)
+      return; // 1-element array
+
+    const access_range max_element_range (*max_element, mgr);
+    const table::range_t max_element_x_range
+      = btm.get_table_x_for_range (max_element_range);
+    t.set_cell_span (table::rect_t (max_element_x_range,
+				    table_y_range),
+		     fmt_styled_string (sm, "[%E]",
+					TYPE_MAX_VALUE (domain)));
+
+    const table::range_t other_elements_x_range (min_element_x_range.next,
+						 max_element_x_range.start);
+    if (other_elements_x_range.get_size () > 0)
+      t.set_cell_span (table::rect_t (other_elements_x_range, table_y_range),
+		       styled_string (sm, "..."));
+  }
+
+  table make_table (const bit_to_table_map &btm,
+		    style_manager &sm) const final override
+  {
+    table t (table::size_t (btm.get_num_columns (), 1));
+
+    if (tree base_type = m_op.m_base_region->get_type ())
+      if (TREE_CODE (base_type) == ARRAY_TYPE)
+	add_array_elements_to_table (t, btm, sm);
+
+    access_range valid_bits = m_op.get_valid_bits ();
+    const int table_y = t.get_size ().h - 1;
+    const int table_h = 1;
+    table::rect_t rect = btm.get_table_rect (valid_bits, table_y, table_h);
+    styled_string s;
+    switch (m_op.m_base_region->get_kind ())
+      {
+      default:
+	s = styled_string (sm, _("region"));
+	break;
+      case RK_DECL:
+	{
+	  const decl_region *decl_reg
+	    = as_a <const decl_region *> (m_op.m_base_region);
+	  tree decl = decl_reg->get_decl ();
+	  s = fmt_styled_string (sm, "%qE (type: %qT)",
+				 decl,
+				 TREE_TYPE (decl));
+	}
+	break;
+      case RK_HEAP_ALLOCATED:
+	{
+	  if (m_region_creation_event_id.known_p ())
+	    s = fmt_styled_string (sm, _("buffer allocated on heap at %@"),
+				   &m_region_creation_event_id);
+	  else
+	    s = styled_string (sm, _("heap-allocated buffer"));
+	}
+	break;
+      case RK_ALLOCA:
+	{
+	  if (m_region_creation_event_id.known_p ())
+	    s = fmt_styled_string (sm, _("buffer allocated on stack at %@"),
+				   &m_region_creation_event_id);
+	  else
+	    s = styled_string (sm, _("stack-allocated buffer"));
+	}
+	break;
+      case RK_STRING:
+	{
+	  const string_region *string_reg
+	    = as_a <const string_region *> (m_op.m_base_region);
+	  tree string_cst = string_reg->get_string_cst ();
+	  s = fmt_styled_string (sm, _("string literal (type: %qT)"),
+				 TREE_TYPE (string_cst));
+	}
+	break;
+      }
+    t.set_cell_span (rect, std::move (s));
+
+    return t;
+  }
+
+private:
+  const access_operation &m_op;
+  diagnostic_event_id_t m_region_creation_event_id;
+};
+
+/* Subclass of spatial_item for visualizing the region of memory
+   that's actually accessed by the read or write, for reads and
+   for write cases where we don't know the svalue written.  */
+
+class accessed_region_spatial_item : public spatial_item
+{
+public:
+  accessed_region_spatial_item (const access_operation &op) : m_op (op) {}
+
+  void add_boundaries (boundaries &out, logger *logger) const final override
+  {
+    LOG_SCOPE (logger);
+    access_range actual_bits = m_op.get_actual_bits ();
+    if (logger)
+      {
+	logger->start_log_line ();
+	logger->log_partial ("actual bits: ");
+	actual_bits.dump_to_pp (logger->get_printer (), true);
+	logger->end_log_line ();
+      }
+    out.add (actual_bits, boundaries::kind::HARD);
+  }
+
+  table make_table (const bit_to_table_map &btm,
+		    style_manager &sm) const final override
+  {
+    table t (table::size_t (btm.get_num_columns (), 1));
+
+    access_range actual_bits = m_op.get_actual_bits ();
+    const int table_y = 0;
+    const int table_h = 1;
+    table::rect_t rect = btm.get_table_rect (actual_bits, table_y, table_h);
+    t.set_cell_span (rect, styled_string (get_label_string (sm)));
+
+    return t;
+  }
+
+private:
+  styled_string get_label_string (style_manager &sm) const
+  {
+    const access_range accessed_bits (m_op.get_actual_bits ());
+    return get_access_size_str (sm,
+				m_op,
+				accessed_bits,
+				m_op.m_reg.get_type ());
+  }
+
+  const access_operation &m_op;
+};
+
+/* Subclass of spatial_item for when we know the svalue being written
+   to the accessed region.
+   Can be subclassed to give visualizations of specific kinds of svalue.  */
+
+class svalue_spatial_item : public spatial_item
+{
+public:
+  static std::unique_ptr<svalue_spatial_item> make (const access_operation &op,
+						    const svalue &sval,
+						    access_range actual_bits,
+						    const theme &theme);
+
+  svalue_spatial_item (const access_operation &op,
+		       const svalue &sval,
+		       access_range actual_bits)
+  : m_op (op), m_sval (sval), m_actual_bits (actual_bits)
+  {}
+
+  void add_boundaries (boundaries &out, logger *logger) const override
+  {
+    LOG_SCOPE (logger);
+    out.add (m_actual_bits, boundaries::kind::HARD);
+  }
+
+  table make_table (const bit_to_table_map &btm,
+		    style_manager &sm) const override
+  {
+    table t (table::size_t (btm.get_num_columns (), 0));
+
+    const int table_y = t.add_row ();
+    const int table_h = 1;
+    table::rect_t rect = btm.get_table_rect (m_actual_bits, table_y, table_h);
+    t.set_cell_span (rect, styled_string (get_label_string (sm)));
+    return t;
+  }
+
+protected:
+  styled_string get_label_string (style_manager &sm) const
+  {
+    tree rep_tree = m_op.m_model.get_representative_tree (&m_sval);
+    if (rep_tree)
+      {
+	if (TREE_CODE (rep_tree) == SSA_NAME)
+	  rep_tree = SSA_NAME_VAR (rep_tree);
+	switch (TREE_CODE (rep_tree))
+	  {
+	  default:
+	    break;
+	  case INTEGER_CST:
+	    return fmt_styled_string (sm, _("write of %<(%T) %E%>"),
+				      TREE_TYPE (rep_tree),
+				      rep_tree);
+
+	  case PARM_DECL:
+	  case VAR_DECL:
+	    return fmt_styled_string (sm, _("write from %qE (type: %qT)"),
+				      rep_tree,
+				      TREE_TYPE (rep_tree));
+	    break;
+	  }
+	}
+
+    const access_range accessed_bits (m_op.get_actual_bits ());
+    return get_access_size_str (sm,
+				m_op,
+				accessed_bits,
+				m_sval.get_type ());
+  }
+
+  const access_operation &m_op;
+  const svalue &m_sval;
+  access_range m_actual_bits;
+};
+
+/* Subclass of svalue_spatial_item for initial_svalue of a string_region
+   i.e. for string literals.
+
+   There are three cases:
+   (a) for long strings, show just the head and tail of the string,
+   with an ellipsis:
+     +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+     |[0]|[1]|[2]|[3]|[4]|[5]|          |[440]|[441]|[442]|[443]|[444]|[445]|
+     +---+---+---+---+---+---+   ...    +-----+-----+-----+-----+-----+-----+
+     |‘L’|‘o’|‘r’|‘e’|‘m’|‘ ’|          | ‘o’ | ‘r’ | ‘u’ | ‘m’ | ‘.’ | NUL |
+     +---+---+---+---+---+---+----------+-----+-----+-----+-----+-----+-----+
+     |                  string literal (type: ‘char[446]’)                  |
+     +----------------------------------------------------------------------+
+   (b) For sufficiently short strings, show the full string:
+     +----------+---------+---------+---------+---------+ +-----------------+
+     |   [0]    |   [1]   |   [2]   |   [3]   |   [4]   | |       [5]       |
+     +----------+---------+---------+---------+---------+ +-----------------+
+     |   ‘h’    |   ‘e’   |   ‘l’   |   ‘l’   |   ‘o’   | |       NUL       |
+     +----------+---------+---------+---------+---------+-+-----------------+
+     |                   string literal (type: ‘char[6]’)                   |
+     +----------------------------------------------------------------------+
+   (c) for non-ASCII strings that are short enough to show the full string,
+   show how unicode code points of the bytes decoded as UTF-8:
+     +-----+-----+-----+----+----++----+----+----+----+----+----+----+------+
+     | [0] | [1] | [2] |[3] |[4] ||[5] |[6] |[7] |[8] |[9] |[10]|[11]| [12] |
+     +-----+-----+-----+----+----++----+----+----+----+----+----+----+------+
+     |0xe6 |0x96 |0x87 |0xe5|0xad||0x97|0xe5|0x8c|0x96|0xe3|0x81|0x91| 0x00 |
+     +-----+-----+-----+----+----++----+----+----+----+----+----+----+------+
+     |     U+6587      |    U+5b57     |    U+5316    |    U+3051    |U+0000|
+     +-----------------+---------------+--------------+--------------+------+
+     |                  string literal (type: ‘char[13]’)                   |
+     +----------------------------------------------------------------------+
+   and show the characters themselves if unicode is supported and they are not
+   control characters:
+     ┌─────┬─────┬─────┬────┬────┐┌────┬────┬────┬────┬────┬────┬────┬──────┐
+     │ [0] │ [1] │ [2] │[3] │[4] ││[5] │[6] │[7] │[8] │[9] │[10]│[11]│ [12] │
+     ├─────┼─────┼─────┼────┼────┤├────┼────┼────┼────┼────┼────┼────┼──────┤
+     │0xe6 │0x96 │0x87 │0xe5│0xad││0x97│0xe5│0x8c│0x96│0xe3│0x81│0x91│ 0x00 │
+     ├─────┴─────┴─────┼────┴────┴┴────┼────┴────┴────┼────┴────┴────┼──────┤
+     │     U+6587      │    U+5b57     │    U+5316    │    U+3051    │U+0000│
+     ├─────────────────┼───────────────┼──────────────┼──────────────┼──────┤
+     │       文        │      字       │      化      │      け      │ NUL  │
+     ├─────────────────┴───────────────┴──────────────┴──────────────┴──────┤
+     │                  string literal (type: ‘char[13]’)                   │
+     └──────────────────────────────────────────────────────────────────────┘
+*/
+
+class string_region_spatial_item : public svalue_spatial_item
+{
+public:
+  string_region_spatial_item (const access_operation &op,
+			      const svalue &sval,
+			      access_range actual_bits,
+			      const string_region &string_reg,
+			      const theme &theme)
+  : svalue_spatial_item (op, sval, actual_bits),
+    m_string_reg (string_reg),
+    m_theme (theme),
+    m_ellipsis_threshold (param_analyzer_text_art_string_ellipsis_threshold),
+    m_ellipsis_head_len (param_analyzer_text_art_string_ellipsis_head_len),
+    m_ellipsis_tail_len (param_analyzer_text_art_string_ellipsis_tail_len),
+    m_show_full_string (calc_show_full_string ()),
+    m_show_utf8 (m_show_full_string && !pure_ascii_p ())
+  {
+  }
+
+  void add_boundaries (boundaries &out, logger *logger) const override
+  {
+    LOG_SCOPE (logger);
+    out.add (m_actual_bits, boundaries::kind::HARD);
+
+    tree string_cst = get_string_cst ();
+    /* TREE_STRING_LENGTH is sizeof, not strlen.  */
+    if (m_show_full_string)
+      out.add_all_bytes_in_range (m_actual_bits);
+    else
+      {
+	byte_range head_of_string (0, m_ellipsis_head_len);
+	out.add_all_bytes_in_range (head_of_string);
+	byte_range tail_of_string
+	  (TREE_STRING_LENGTH (string_cst) - m_ellipsis_tail_len,
+	   m_ellipsis_tail_len);
+	out.add_all_bytes_in_range (tail_of_string);
+	/* Adding the above pair of ranges will also effectively add
+	   the boundaries of the range of ellipsized chars, as they're
+	   exactly in between head_of_string and tail_of_string.  */
+      }
+  }
+
+  table make_table (const bit_to_table_map &btm,
+		    style_manager &sm) const override
+  {
+    table t (table::size_t (btm.get_num_columns (), 0));
+
+    const int byte_idx_table_y = t.add_row ();
+    const int byte_val_table_y = t.add_row ();
+
+    byte_range bytes (0, 0);
+    bool valid = m_actual_bits.as_concrete_byte_range (&bytes);
+    gcc_assert (valid);
+    tree string_cst = get_string_cst ();
+    if (m_show_full_string)
+      {
+       for (byte_offset_t byte_idx = bytes.get_start_byte_offset ();
+	    byte_idx < bytes.get_next_byte_offset ();
+	    byte_idx = byte_idx + 1)
+	 add_column_for_byte (t, btm, sm, byte_idx,
+			      byte_idx_table_y, byte_val_table_y);
+
+       if (m_show_utf8)
+	 {
+	   const bool show_unichars = m_theme.unicode_p ();
+	   const int utf8_code_point_table_y = t.add_row ();
+	   int utf8_character_table_y;
+	   if (show_unichars)
+	     utf8_character_table_y = t.add_row ();
+
+	   /* We don't actually want the display widths here, but
+	      it's an easy way to decode UTF-8.  */
+	   cpp_char_column_policy policy (8, cpp_wcwidth);
+	   cpp_display_width_computation dw (TREE_STRING_POINTER (string_cst),
+					     TREE_STRING_LENGTH (string_cst),
+					     policy);
+	   while (!dw.done ())
+	     {
+	       cpp_decoded_char decoded_char;
+	       dw.process_next_codepoint (&decoded_char);
+
+	       if (!decoded_char.m_valid_ch)
+		 continue;
+	       size_t start_byte_idx
+		 = decoded_char.m_start_byte - TREE_STRING_POINTER (string_cst);
+	       byte_size_t size_in_bytes
+		 = decoded_char.m_next_byte - decoded_char.m_start_byte;
+	       byte_range bytes (start_byte_idx, size_in_bytes);
+
+	       const table::rect_t code_point_table_rect
+		 = btm.get_table_rect (&m_string_reg, bytes,
+				       utf8_code_point_table_y, 1);
+	       char buf[100];
+	       sprintf (buf, "U+%04x", decoded_char.m_ch);
+	       t.set_cell_span (code_point_table_rect,
+				styled_string (sm, buf));
+
+	       if (show_unichars)
+		 {
+		   const table::rect_t character_table_rect
+		     = btm.get_table_rect (&m_string_reg, bytes,
+					   utf8_character_table_y, 1);
+		   if (cpp_is_printable_char (decoded_char.m_ch))
+		     t.set_cell_span (character_table_rect,
+				      styled_string (decoded_char.m_ch));
+		   else if (decoded_char.m_ch == 0)
+		     t.set_cell_span (character_table_rect,
+				      styled_string (sm, "NUL"));
+		   else
+		     t.set_cell_span (character_table_rect,
+				      styled_string (sm, ""));
+		 }
+	     }
+	 }
+      }
+    else
+      {
+	/* Head of string.  */
+	for (int byte_idx = 0; byte_idx < m_ellipsis_head_len; byte_idx++)
+	  add_column_for_byte (t, btm, sm, byte_idx,
+			       byte_idx_table_y, byte_val_table_y);
+
+	/* Ellipsis (two rows high).  */
+	const byte_range ellipsis_bytes
+	  (m_ellipsis_head_len,
+	   TREE_STRING_LENGTH (string_cst)
+	   - (m_ellipsis_head_len + m_ellipsis_tail_len));
+	const table::rect_t table_rect
+	  = btm.get_table_rect (&m_string_reg, ellipsis_bytes,
+				byte_idx_table_y, 2);
+	t.set_cell_span(table_rect, styled_string (sm, "..."));
+
+	/* Tail of string.  */
+	for (int byte_idx
+	       = (TREE_STRING_LENGTH (string_cst) - m_ellipsis_tail_len);
+	     byte_idx < TREE_STRING_LENGTH (string_cst);
+	     byte_idx++)
+	  add_column_for_byte (t, btm, sm, byte_idx,
+			       byte_idx_table_y, byte_val_table_y);
+      }
+
+    const int summary_table_y = t.add_row ();
+    t.set_cell_span (btm.get_table_rect (&m_string_reg, bytes,
+					 summary_table_y, 1),
+		     fmt_styled_string (sm,
+					_("string literal (type: %qT)"),
+					TREE_TYPE (string_cst)));
+
+    return t;
+  }
+
+  tree get_string_cst () const { return m_string_reg.get_string_cst (); }
+
+private:
+  bool calc_show_full_string () const
+  {
+    tree string_cst = get_string_cst ();
+    if (TREE_STRING_LENGTH (string_cst) < m_ellipsis_threshold)
+      return true;
+    if (TREE_STRING_LENGTH (string_cst) <
+	(m_ellipsis_head_len + m_ellipsis_tail_len))
+      return true;
+    return false;
+  }
+
+  bool pure_ascii_p () const
+  {
+    tree string_cst = get_string_cst ();
+    for (unsigned byte_idx = 0;
+	 byte_idx < (unsigned) TREE_STRING_LENGTH (string_cst);
+	 byte_idx++)
+      {
+	unsigned char ch = TREE_STRING_POINTER (string_cst)[byte_idx];
+	if (ch >= 0x80)
+	  return false;
+      }
+    return true;
+  }
+
+  void add_column_for_byte (table &t, const bit_to_table_map &btm,
+			    style_manager &sm,
+			    const byte_offset_t byte_idx,
+			    const int byte_idx_table_y,
+			    const int byte_val_table_y) const
+  {
+    tree string_cst = get_string_cst ();
+    gcc_assert (byte_idx >= 0);
+    gcc_assert (byte_idx < TREE_STRING_LENGTH (string_cst));
+
+    const byte_range bytes (byte_idx, 1);
+    if (1) // show_byte_indices
+      {
+	const table::rect_t idx_table_rect
+	  = btm.get_table_rect (&m_string_reg, bytes, byte_idx_table_y, 1);
+	t.set_cell_span (idx_table_rect,
+			 fmt_styled_string (sm, "[%li]",
+					    byte_idx.ulow ()));
+      }
+
+    char byte_val = TREE_STRING_POINTER (string_cst)[byte_idx.ulow ()];
+    const table::rect_t val_table_rect
+      = btm.get_table_rect (&m_string_reg, bytes, byte_val_table_y, 1);
+    table_cell_content content (make_cell_content_for_byte (sm, byte_val));
+    t.set_cell_span (val_table_rect, std::move (content));
+  }
+
+  table_cell_content make_cell_content_for_byte (style_manager &sm,
+						 unsigned char byte_val) const
+  {
+    if (!m_show_utf8)
+       {
+	if (byte_val == '\0')
+	  return styled_string (sm, "NUL");
+	else if (byte_val < 0x80)
+	  if (ISPRINT (byte_val))
+	    return fmt_styled_string (sm, "%qc", byte_val);
+       }
+    char buf[100];
+    sprintf (buf, "0x%02x", byte_val);
+    return styled_string (sm, buf);
+  }
+
+  const string_region &m_string_reg;
+  const theme &m_theme;
+  const int m_ellipsis_threshold;
+  const int m_ellipsis_head_len;
+  const int m_ellipsis_tail_len;
+  const bool m_show_full_string;
+  const bool m_show_utf8;
+};
+
+std::unique_ptr<svalue_spatial_item>
+svalue_spatial_item::make (const access_operation &op,
+			   const svalue &sval,
+			   access_range actual_bits,
+			   const theme &theme)
+{
+  if (const initial_svalue *initial_sval = sval.dyn_cast_initial_svalue ())
+    if (const string_region *string_reg
+	= initial_sval->get_region ()->dyn_cast_string_region ())
+      return make_unique <string_region_spatial_item> (op, sval, actual_bits,
+						       *string_reg, theme);
+  return make_unique <svalue_spatial_item> (op, sval, actual_bits);
+}
+
+/* Widget subclass implementing access diagrams.  */
+
+class access_diagram_impl : public vbox_widget
+{
+public:
+  access_diagram_impl (const access_operation &op,
+		       diagnostic_event_id_t region_creation_event_id,
+		       style_manager &sm,
+		       const theme &theme,
+		       logger *logger)
+  : m_op (op),
+    m_region_creation_event_id (region_creation_event_id),
+    m_sm (sm),
+    m_theme (theme),
+    m_logger (logger),
+    m_invalid (false),
+    m_valid_region_spatial_item (op, region_creation_event_id),
+    m_accessed_region_spatial_item (op),
+    m_btm (),
+    m_calc_req_size_called (false)
+  {
+    LOG_SCOPE (logger);
+
+    if (logger)
+      {
+	access_range invalid_before_bits;
+	if (op.maybe_get_invalid_before_bits (&invalid_before_bits))
+	  invalid_before_bits.log ("invalid before range", *logger);
+	access_range invalid_after_bits;
+	if (op.maybe_get_invalid_after_bits (&invalid_after_bits))
+	  invalid_after_bits.log ("invalid after range", *logger);
+
+	if (op.m_sval_hint)
+	  {
+	    logger->start_log_line ();
+	    logger->log_partial ("sval_hint: ");
+	    op.m_sval_hint->dump_to_pp (logger->get_printer (), true);
+	    logger->end_log_line ();
+	  }
+      }
+
+    /* Register painting styles.  */
+    {
+      style valid_style;
+      valid_style.m_fg_color = style::named_color::GREEN;
+      valid_style.m_bold = true;
+      m_valid_style_id = m_sm.get_or_create_id (valid_style);
+
+      style invalid_style;
+      invalid_style.m_fg_color = style::named_color::RED;
+      invalid_style.m_bold = true;
+      m_invalid_style_id = m_sm.get_or_create_id (invalid_style);
+    }
+
+    if (op.m_sval_hint)
+      {
+	access_range actual_bits = m_op.get_actual_bits ();
+	m_svalue_spatial_item = svalue_spatial_item::make (m_op,
+							   *op.m_sval_hint,
+							   actual_bits,
+							   m_theme);
+      }
+
+    /* Two passes:
+       First, figure out all of the boundaries of interest.
+       Then use that to build child widgets showing the regions of interest,
+       with a common tabular layout.  */
+
+    m_boundaries = find_boundaries ();
+    if (logger)
+      m_boundaries->log (*logger);
+
+    /* Populate m_table_x_for_bit and m_bit_for_table_x.
+       Each table column represents the range [offset, next_offset).
+       We don't create a column in the table for the final offset, but we
+       do populate it, so that looking at the table_x of one beyond the
+       final table column gives us the upper bound offset.  */
+    m_btm.populate (*m_boundaries, logger);
+
+    /* Gracefully reject cases where the boundary sorting has gone wrong
+       (due to awkward combinations of symbolic values).  */
+    {
+      table::range_t actual_bits_x_range
+	= m_btm.get_table_x_for_range (m_op.get_actual_bits ());
+      if (actual_bits_x_range.get_size () <= 0)
+	{
+	  if (logger)
+	    logger->log ("giving up: bad table columns for actual_bits");
+	  m_invalid = true;
+	  return;
+	}
+      table::range_t valid_bits_x_range
+	= m_btm.get_table_x_for_range (m_op.get_valid_bits ());
+      if (valid_bits_x_range.get_size () <= 0)
+	{
+	  if (logger)
+	    logger->log ("giving up: bad table columns for valid_bits");
+	  m_invalid = true;
+	  return;
+	}
+    }
+
+    m_col_widths
+      = make_unique <table_dimension_sizes> (m_btm.get_num_columns ());
+
+    /* Now create child widgets.  */
+
+    if (flag_analyzer_debug_text_art)
+      {
+	table t_headings (make_headings_table ());
+	add_aligned_child_table (std::move (t_headings));
+      }
+
+    if (m_svalue_spatial_item)
+      {
+	table t_sval (m_svalue_spatial_item->make_table (m_btm, m_sm));
+	add_aligned_child_table (std::move (t_sval));
+      }
+    else
+      {
+	table t_accessed
+	  (m_accessed_region_spatial_item.make_table (m_btm, m_sm));
+	add_aligned_child_table (std::move (t_accessed));
+      }
+
+    add_direction_widget ();
+
+    table t_valid (m_valid_region_spatial_item.make_table (m_btm, m_sm));
+    add_invalid_accesses_to_region_table (t_valid);
+    add_aligned_child_table (std::move (t_valid));
+
+    add_valid_vs_invalid_ruler ();
+  }
+
+  const char *get_desc () const override
+  {
+    return "access_diagram_impl";
+  }
+
+  canvas::size_t calc_req_size () final override
+  {
+    if (m_invalid)
+      return canvas::size_t (0, 0);
+
+    /* Now compute the size requirements for the tables.  */
+    for (auto iter : m_aligned_table_widgets)
+      iter->get_cell_sizes ().pass_1 (iter->get_table ());
+    for (auto iter : m_aligned_table_widgets)
+      iter->get_cell_sizes ().pass_2 (iter->get_table ());
+
+    adjust_to_scale();
+
+    /* ...and relayout the tables.  */
+    for (auto iter : m_aligned_table_widgets)
+      iter->recalc_coords ();
+
+    /* Populate the canvas_x per table_x.  */
+    m_col_start_x.clear ();
+    int iter_canvas_x = 0;
+    for (auto w : m_col_widths->m_requirements)
+      {
+	m_col_start_x.push_back (iter_canvas_x);
+	iter_canvas_x += w + 1;
+      }
+    m_col_start_x.push_back (iter_canvas_x);
+
+    m_calc_req_size_called = true;
+
+    return vbox_widget::calc_req_size ();
+  }
+
+  int get_canvas_x_for_table_x (int table_x) const
+  {
+    gcc_assert (m_calc_req_size_called);
+    return m_col_start_x[table_x];
+  }
+
+  canvas::range_t get_canvas_x_range (const table::range_t &table_x_range) const
+  {
+    gcc_assert (m_calc_req_size_called);
+    return canvas::range_t (get_canvas_x_for_table_x (table_x_range.start),
+			    get_canvas_x_for_table_x (table_x_range.next));
+  }
+
+  const access_operation &get_op () const { return m_op; }
+
+  style::id_t get_style_id_for_validity (bool is_valid) const
+  {
+    return is_valid ? m_valid_style_id : m_invalid_style_id;
+  }
+
+  const theme &get_theme () const { return m_theme; }
+
+private:
+  /* Figure out all of the boundaries of interest when visualizing ths op.  */
+  std::unique_ptr<boundaries>
+  find_boundaries () const
+  {
+    std::unique_ptr<boundaries> result
+      = make_unique<boundaries> (*m_op.m_base_region);
+
+    m_valid_region_spatial_item.add_boundaries (*result, m_logger);
+    m_accessed_region_spatial_item.add_boundaries (*result, m_logger);
+    if (m_svalue_spatial_item)
+      m_svalue_spatial_item->add_boundaries (*result, m_logger);
+
+    return result;
+  }
+
+  void add_aligned_child_table (table t)
+  {
+    x_aligned_table_widget *w
+      = new x_aligned_table_widget (std::move (t), m_theme, *m_col_widths);
+    m_aligned_table_widgets.push_back (w);
+    add_child (std::unique_ptr<widget> (w));
+  }
+
+  /* Create a table showing headings for use by -fanalyzer-debug-text-art, for
+     example:
+     +---------+-----------+-----------+---+--------------------------------+
+     |   tc0   |    tc1    |    tc2    |tc3|              tc4               |
+     +---------+-----------+-----------+---+--------------------------------+
+     |bytes 0-3|bytes 4-35 |bytes 36-39|   |          bytes 40-43           |
+     +---------+-----------+-----------+   +--------------------------------+
+     which has:
+     - a row showing the table column numbers, labelled "tc0", "tc1", etc
+     - a row showing the memory range of each table column that has one.  */
+
+  table make_headings_table () const
+  {
+    table t (table::size_t (m_btm.get_num_columns (), 2));
+
+    for (int table_x = 0; table_x < t.get_size ().w; table_x++)
+      {
+	const int table_y = 0;
+	t.set_cell (table::coord_t (table_x, table_y),
+		    fmt_styled_string (m_sm, "tc%i", table_x));
+      }
+    for (int table_x = 0; table_x < t.get_size ().w; table_x++)
+      {
+	const int table_y = 1;
+	access_range range_for_column (NULL, bit_range (0, 0));
+	if (m_btm.maybe_get_access_range_for_table_x (table_x,
+						      &range_for_column))
+	  {
+	    pretty_printer pp;
+	    pp_format_decoder (&pp) = default_tree_printer;
+	    range_for_column.dump_to_pp (&pp, true);
+	    t.set_cell (table::coord_t (table_x, table_y),
+			styled_string (m_sm, pp_formatted_text (&pp)));
+	  }
+      }
+
+    return t;
+  }
+
+  void add_direction_widget ()
+  {
+    add_child (::make_unique<direction_widget> (*this, m_btm));
+  }
+
+  void add_invalid_accesses_to_region_table (table &t_region)
+  {
+    gcc_assert (t_region.get_size ().w == (int)m_btm.get_num_columns ());
+
+    const int table_y = 0;
+    const int table_h = t_region.get_size ().h;
+
+    access_range invalid_before_bits;
+    if (m_op.maybe_get_invalid_before_bits (&invalid_before_bits))
+      {
+	t_region.set_cell_span (m_btm.get_table_rect (invalid_before_bits,
+						      table_y, table_h),
+				styled_string (m_sm,
+					       _("before valid range")));
+      }
+    access_range invalid_after_bits;
+    if (m_op.maybe_get_invalid_after_bits (&invalid_after_bits))
+      {
+	t_region.set_cell_span (m_btm.get_table_rect (invalid_after_bits,
+						      table_y, table_h),
+				styled_string (m_sm,
+					       _("after valid range")));
+      }
+  }
+
+  void maybe_add_gap (x_aligned_x_ruler_widget *w,
+		      const access_range &lower,
+		      const access_range &upper) const
+  {
+    LOG_SCOPE (m_logger);
+    if (m_logger)
+      {
+	lower.log ("lower", *m_logger);
+	upper.log ("upper", *m_logger);
+      }
+    tree lower_next = lower.m_next.calc_symbolic_bit_offset (m_op.m_model);
+    if (!lower_next)
+      {
+	if (m_logger)
+	  m_logger->log ("failed to get lower_next");
+	return;
+      }
+    tree upper_start = upper.m_start.calc_symbolic_bit_offset (m_op.m_model);
+    if (!upper_start)
+      {
+	if (m_logger)
+	  m_logger->log ("failed to get upper_start");
+	return;
+      }
+    tree num_bits_gap = fold_build2 (MINUS_EXPR,
+				     size_type_node,
+				     upper_start, lower_next);
+    if (m_logger)
+      m_logger->log ("num_bits_gap: %qE", num_bits_gap);
+    tree zero = build_int_cst (size_type_node, 0);
+    tristate ts_gt_zero = m_op.m_model.eval_condition (num_bits_gap,
+						       GT_EXPR,
+						       zero,
+						       NULL);
+    if (ts_gt_zero.is_false ())
+      {
+	if (m_logger)
+	  m_logger->log ("rejecting as not > 0");
+	return;
+      }
+
+    bit_size_expr num_bits (num_bits_gap);
+    styled_string label = num_bits.get_formatted_str (m_sm,
+						      _("%wi bit"),
+						      _("%wi bits"),
+						      _("%wi byte"),
+						      _("%wi bytes"),
+						      _("%qE bits"),
+						      _("%qE bytes"));
+    w->add_range (m_btm.get_table_x_for_range (access_range (lower.m_next,
+							     upper.m_start)),
+		  std::move (label),
+		  style::id_plain);
+  }
+
+  styled_string
+  make_warning_string (styled_string &&text)
+  {
+    styled_string result;
+    if (!m_theme.emojis_p ())
+      return std::move (text);
+
+    result.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN.  */
+				  true));
+    /* U+26A0 WARNING SIGN has East_Asian_Width == Neutral, but in its
+       emoji variant is printed (by vte at least) with a 2nd half
+       overlapping the next char.  Hence we add two spaces here: a space
+       to be covered by this overlap, plus another space of padding.  */
+    result.append (styled_string (m_sm, "  "));
+    result.append (std::move (text));
+    return result;
+  }
+
+  /* Add a ruler child widet showing valid, invalid, and gaps.  */
+  void add_valid_vs_invalid_ruler ()
+  {
+    LOG_SCOPE (m_logger);
+
+    x_aligned_x_ruler_widget *w
+      = new x_aligned_x_ruler_widget (*this, m_theme, *m_col_widths);
+
+    access_range invalid_before_bits;
+    if (m_op.maybe_get_invalid_before_bits (&invalid_before_bits))
+      {
+	if (m_logger)
+	  invalid_before_bits.log ("invalid_before_bits", *m_logger);
+	bit_size_expr num_before_bits;
+	if (invalid_before_bits.get_size (m_op.m_model, &num_before_bits))
+	  {
+	    styled_string label;
+	    if (m_op.m_dir == DIR_READ)
+	      label = num_before_bits.get_formatted_str
+		(m_sm,
+		 _("under-read of %wi bit"),
+		 _("under-read of %wi bits"),
+		 _("under-read of %wi byte"),
+		 _("under-read of %wi bytes"),
+		 _("under-read of %qE bits"),
+		 _("under-read of %qE bytes"));
+	    else
+	      label = num_before_bits.get_formatted_str
+		(m_sm,
+		 _("underwrite of %wi bit"),
+		 _("underwrite of %wi bits"),
+		 _("underwrite of %wi byte"),
+		 _("underwrite of %wi bytes"),
+		 _("underwrite of %qE bits"),
+		 _("underwrite of %qE bytes"));
+	    w->add_range (m_btm.get_table_x_for_range (invalid_before_bits),
+			  make_warning_string (std::move (label)),
+			  m_invalid_style_id);
+	  }
+      }
+    else
+      {
+	if (m_logger)
+	  m_logger->log ("no invalid_before_bits");
+      }
+
+    /* It would be nice to be able to use std::optional<access_range> here,
+       but std::optional is C++17.  */
+    bool got_valid_bits = false;
+    access_range valid_bits (m_op.get_valid_bits ());
+    bit_size_expr num_valid_bits;
+    if (valid_bits.get_size (m_op.m_model, &num_valid_bits))
+      {
+	if (m_logger)
+	  valid_bits.log ("valid_bits", *m_logger);
+
+	got_valid_bits = true;
+	maybe_add_gap (w, invalid_before_bits, valid_bits);
+
+	styled_string label;
+	if (m_op.m_dir == DIR_READ)
+	  label = num_valid_bits.get_formatted_str (m_sm,
+						    _("size: %wi bit"),
+						    _("size: %wi bits"),
+						    _("size: %wi byte"),
+						    _("size: %wi bytes"),
+						    _("size: %qE bits"),
+						    _("size: %qE bytes"));
+	else
+	  label = num_valid_bits.get_formatted_str (m_sm,
+						    _("capacity: %wi bit"),
+						    _("capacity: %wi bits"),
+						    _("capacity: %wi byte"),
+						    _("capacity: %wi bytes"),
+						    _("capacity: %qE bits"),
+						    _("capacity: %qE bytes"));
+	w->add_range (m_btm.get_table_x_for_range (m_op.get_valid_bits ()),
+		      std::move (label),
+		      m_valid_style_id);
+      }
+
+    access_range invalid_after_bits;
+    if (m_op.maybe_get_invalid_after_bits (&invalid_after_bits))
+      {
+	if (got_valid_bits)
+	  maybe_add_gap (w, valid_bits, invalid_after_bits);
+
+	if (m_logger)
+	  invalid_before_bits.log ("invalid_after_bits", *m_logger);
+
+	bit_size_expr num_after_bits;
+	if (invalid_after_bits.get_size (m_op.m_model, &num_after_bits))
+	  {
+	    styled_string label;
+	    if (m_op.m_dir == DIR_READ)
+	      label = num_after_bits.get_formatted_str
+		(m_sm,
+		 _("over-read of %wi bit"),
+		 _("over-read of %wi bits"),
+		 _("over-read of %wi byte"),
+		 _("over-read of %wi bytes"),
+		 _("over-read of %qE bits"),
+		 _("over-read of %qE bytes"));
+	    else
+	      label = num_after_bits.get_formatted_str
+		(m_sm,
+		 _("overflow of %wi bit"),
+		 _("overflow of %wi bits"),
+		 _("overflow of %wi byte"),
+		 _("overflow of %wi bytes"),
+		 _("over-read of %qE bits"),
+		 _("overflow of %qE bytes"));
+	    w->add_range (m_btm.get_table_x_for_range (invalid_after_bits),
+			  make_warning_string (std::move (label)),
+			  m_invalid_style_id);
+	  }
+      }
+    else
+      {
+	if (m_logger)
+	  m_logger->log ("no invalid_after_bits");
+      }
+
+    add_child (std::unique_ptr<widget> (w));
+  }
+
+  /* Subroutine of calc_req_size.
+     Try to allocate surplus canvas width to table columns to make the
+     per table-column canvas widths closer to being to scale.
+     See e.g.:
+       https://en.wikipedia.org/wiki/Fair_item_allocation
+       https://en.wikipedia.org/wiki/Mathematics_of_apportionment
+  */
+  void adjust_to_scale ()
+  {
+    LOG_SCOPE (m_logger);
+    const unsigned num_columns = m_btm.get_num_columns ();
+    std::vector<bit_offset_t> bit_sizes (num_columns);
+    for (unsigned table_x = 0; table_x < num_columns; table_x++)
+      {
+	access_range range_for_column (NULL, bit_range (0, 0));
+	if (m_btm.maybe_get_access_range_for_table_x (table_x,
+						      &range_for_column))
+	  {
+	    bit_size_t size_in_bits;
+	    if (!range_for_column.get_size_in_bits (&size_in_bits))
+	      size_in_bits = BITS_PER_UNIT; // arbitrary non-zero value
+	    gcc_assert (size_in_bits > 0);
+	    bit_sizes[table_x] = size_in_bits;
+	  }
+	else
+	  bit_sizes[table_x] = 0;
+      }
+
+    while (adjust_to_scale_once (bit_sizes))
+      {
+      }
+  }
+  bool adjust_to_scale_once (const std::vector<bit_offset_t> &bit_sizes)
+  {
+    LOG_SCOPE (m_logger);
+
+    const unsigned num_columns = m_btm.get_num_columns ();
+
+    /* Find the total canvas width currently required.
+       Require one extra canvas column for the right-hand border
+       of the table.  */
+    int total_width = 1;
+    for (unsigned table_x = 0; table_x < num_columns; table_x++)
+      {
+	int canvas_w = m_col_widths->m_requirements[table_x];
+	gcc_assert (canvas_w >= 0);
+	total_width += canvas_w + 1;
+      }
+
+    const int max_width = param_analyzer_text_art_ideal_canvas_width;
+    if (total_width >= max_width)
+      {
+	if (m_logger)
+	  m_logger->log ("bailing out: total_width=%i ,>= max_width (%i)\n",
+			 total_width, max_width);
+	return false;
+      }
+
+    const int fixed_point = 1024;
+    std::vector<bit_offset_t> canvas_w_per_bit (num_columns);
+    for (unsigned table_x = 0; table_x < num_columns; table_x++)
+      {
+	bit_offset_t bit_size = bit_sizes[table_x];
+	if (bit_size > 0)
+	  canvas_w_per_bit[table_x]
+	    = (m_col_widths->m_requirements[table_x] * fixed_point) / bit_size;
+	else
+	  canvas_w_per_bit[table_x] = INT_MAX;
+      }
+
+    /* Find the min canvas per bit, and give an extra canvas column to
+       the table column that has least.  */
+    size_t min_idx = std::distance (canvas_w_per_bit.begin (),
+				    std::min_element (canvas_w_per_bit.begin (),
+						      canvas_w_per_bit.end ()));
+    m_col_widths->m_requirements[min_idx] += 1;
+    if (m_logger)
+      m_logger->log ("adding 1 canvas_w to column %i\n", (int)min_idx);
+
+    return true; // keep going
+  }
+
+  const access_operation &m_op;
+  diagnostic_event_id_t m_region_creation_event_id;
+  style_manager &m_sm;
+  const theme &m_theme;
+  logger *m_logger;
+  /* In lieu of being able to throw exceptions, a flag to mark this object
+     as "invalid".  */
+  bool m_invalid;
+
+  style::id_t m_valid_style_id;
+  style::id_t m_invalid_style_id;
+
+  valid_region_spatial_item m_valid_region_spatial_item;
+  accessed_region_spatial_item m_accessed_region_spatial_item;
+  std::unique_ptr<svalue_spatial_item> m_svalue_spatial_item;
+
+  std::unique_ptr<boundaries> m_boundaries;
+
+  bit_to_table_map m_btm;
+
+  bool m_calc_req_size_called;
+
+  /* Column widths shared by all x_aligned_table_widget,
+     created once we know how many columns we need.  */
+  std::unique_ptr<table_dimension_sizes> m_col_widths;
+
+  /* All of the child x_aligned_table_widget that share
+     column widths.  */
+  std::vector<x_aligned_table_widget *> m_aligned_table_widgets;
+
+/* Mapping from table_x to canvas_x.  */
+  std::vector<int> m_col_start_x;
+};
+
+x_ruler
+x_aligned_x_ruler_widget::make_x_ruler () const
+{
+  x_ruler r (x_ruler::label_dir::BELOW);
+  for (auto& iter : m_labels)
+    {
+      canvas::range_t canvas_x_range
+	= m_dia_impl.get_canvas_x_range (iter.m_table_x_range);
+      /* Include the end-point.  */
+      canvas_x_range.next++;
+      r.add_label (canvas_x_range, iter.m_text.copy (), iter.m_style_id,
+		   x_ruler::label_kind::TEXT_WITH_BORDER);
+    }
+  return r;
+}
+
+/* class direction_widget : public leaf_widget.  */
+
+/* Paint arrows indicating the direction of the access (read vs write),
+   but only in the X-extent corresponding to the region that's actually
+   accessed.  */
+
+void
+direction_widget::paint_to_canvas (canvas &canvas)
+{
+  const access_range accessed_bits (m_dia_impl.get_op ().get_actual_bits ());
+
+  const access_range valid_bits (m_dia_impl.get_op ().get_valid_bits ());
+
+  for (unsigned table_x = 0; table_x < m_btm.get_num_columns (); table_x++)
+    {
+      access_range column_access_range;
+      if (m_btm.maybe_get_access_range_for_table_x (table_x,
+						    &column_access_range))
+	{
+	  /* Only paint arrows in the accessed region.  */
+	  if (!accessed_bits.contains_p (column_access_range))
+	    continue;
+
+	  /* Are we within the valid region?  */
+	  const bool is_valid (valid_bits.contains_p (column_access_range));
+	  const style::id_t style_id
+	    = m_dia_impl.get_style_id_for_validity (is_valid);
+	  const canvas::range_t x_canvas_range
+	    = m_dia_impl.get_canvas_x_range (table::range_t (table_x,
+							     table_x + 1));
+	  const int canvas_x = x_canvas_range.get_midpoint ();
+	  m_dia_impl.get_theme ().paint_y_arrow
+	    (canvas,
+	     canvas_x,
+	     canvas::range_t (get_y_range ()),
+	     (m_dia_impl.get_op ().m_dir == DIR_READ
+	      ? theme::y_arrow_dir::UP
+	      : theme::y_arrow_dir::DOWN),
+	     style_id);
+	}
+    }
+}
+
+/* class access_diagram : public text_art::wrapper_widget.  */
+
+/* To hide the implementation details, this is merely a wrapper around
+   an access_diagram_impl.  */
+
+access_diagram::access_diagram (const access_operation &op,
+				diagnostic_event_id_t region_creation_event_id,
+				style_manager &sm,
+				const theme &theme,
+				logger *logger)
+: wrapper_widget (make_unique <access_diagram_impl> (op,
+						     region_creation_event_id,
+						     sm,
+						     theme,
+						     logger))
+{
+}
+
+} // namespace ana
+
+#endif /* #if ENABLE_ANALYZER */
diff --git a/gcc/analyzer/access-diagram.h b/gcc/analyzer/access-diagram.h
new file mode 100644
index 00000000000..c124e80ac2e
--- /dev/null
+++ b/gcc/analyzer/access-diagram.h
@@ -0,0 +1,165 @@
+/* Text art visualizations within -fanalyzer.
+   Copyright (C) 2023 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/>.  */
+
+#ifndef GCC_ANALYZER_ACCESS_DIAGRAM_H
+#define GCC_ANALYZER_ACCESS_DIAGRAM_H
+
+#include "text-art/canvas.h"
+#include "text-art/theme.h"
+#include "text-art/widget.h"
+#include "analyzer/analyzer.h"
+#include "analyzer/store.h"
+
+namespace ana {
+
+class bit_size_expr
+{
+public:
+  bit_size_expr () : m_num_bits (NULL) {}
+  bit_size_expr (tree num_bits) : m_num_bits (num_bits) {}
+
+  text_art::styled_string
+  get_formatted_str (text_art::style_manager &sm,
+		     const char *concrete_single_bit_fmt,
+		     const char *concrete_plural_bits_fmt,
+		     const char *concrete_single_byte_fmt,
+		     const char *concrete_plural_bytes_fmt,
+		     const char *symbolic_bits_fmt,
+		     const char *symbolic_bytes_fmt) const;
+  void print (pretty_printer *pp) const;
+
+  tree maybe_get_as_bytes () const;
+
+private:
+  tree m_num_bits;
+};
+
+/* A range of bits within a base region, where each endpoint
+   could be concrete or symbolic (not necessarily the same).  */
+
+struct access_range
+{
+  access_range ()
+  : m_start (), m_next ()
+  {
+  }
+  access_range (region_offset start, region_offset next)
+  : m_start (start), m_next (next)
+  {}
+  access_range (const region *base_region, const bit_range &bits);
+  access_range (const region *base_region, const byte_range &bytes);
+  access_range (const region &reg, region_model_manager *);
+
+  bool concrete_p () const
+  {
+    return m_start.concrete_p () && m_next.concrete_p ();
+  }
+
+  bool empty_p () const;
+
+  bool get_size (const region_model &model, bit_size_expr *out) const;
+
+  bool get_size_in_bits (bit_size_t *out) const
+  {
+    if (concrete_p ())
+      {
+	*out = m_next.get_bit_offset () - m_start.get_bit_offset ();
+	return true;
+      }
+    return false;
+  }
+
+  bool as_concrete_bit_range (bit_range *out) const
+  {
+    if (!concrete_p ())
+      return false;
+    bit_size_t size = m_next.get_bit_offset () - m_start.get_bit_offset ();
+    *out = bit_range (m_start.get_bit_offset (), size);
+    return true;
+  }
+  bool as_concrete_byte_range (byte_range *out) const
+  {
+    bit_range bits (0, 0);
+    if (!as_concrete_bit_range (&bits))
+      return false;
+    return bits.as_byte_range (out);
+  }
+
+  bool contains_p (const access_range &other) const;
+
+  void dump_to_pp (pretty_printer *pp, bool) const;
+  void dump (bool) const;
+  void log (const char *title, logger &) const;
+
+  region_offset m_start;
+  region_offset m_next;
+};
+
+struct access_operation
+{
+  access_operation (const region_model &model,
+		    enum access_direction dir,
+		    const region &reg,
+		    const svalue *sval_hint)
+  : m_model (model),
+    m_dir (dir),
+    m_reg (reg),
+    m_sval_hint (sval_hint),
+    m_base_region (reg.get_base_region ())
+  {}
+
+  region_model_manager *get_manager () const
+  {
+    return m_model.get_manager ();
+  }
+
+  /* Get the valid bits to access within the base region.  */
+  access_range get_valid_bits () const;
+
+  /* Get the actual bits accessed within the base region.  */
+  access_range get_actual_bits () const;
+
+  bool maybe_get_invalid_before_bits (access_range *out) const;
+  bool maybe_get_invalid_after_bits (access_range *out) const;
+
+  const region_model &m_model;
+  enum access_direction m_dir;
+  const region &m_reg;
+  const svalue *m_sval_hint;
+  const region *m_base_region;
+};
+
+class access_diagram : public text_art::wrapper_widget
+{
+public:
+  access_diagram (const access_operation &op,
+		  diagnostic_event_id_t region_creation_event_id,
+		  text_art::style_manager &sm,
+		  const text_art::theme &theme,
+		  logger *logger);
+  const char *get_desc () const override
+  {
+    return "access_diagram";
+  }
+};
+
+} // namespace ana
+
+#endif /* GCC_ANALYZER_ACCESS_DIAGRAM_H */
diff --git a/gcc/analyzer/analyzer.h b/gcc/analyzer/analyzer.h
index a1619525afa..579517c23e6 100644
--- a/gcc/analyzer/analyzer.h
+++ b/gcc/analyzer/analyzer.h
@@ -183,6 +183,11 @@ extern tree get_field_at_bit_offset (tree record_type, bit_offset_t bit_offset);
 class region_offset
 {
 public:
+  region_offset ()
+  : m_base_region (NULL), m_offset (0), m_sym_offset (NULL)
+  {
+  }
+
   static region_offset make_concrete (const region *base_region,
 				      bit_offset_t offset)
   {
@@ -193,9 +198,12 @@ public:
   {
     return region_offset (base_region, 0, sym_offset);
   }
+  static region_offset make_byte_offset (const region *base_region,
+					 const svalue *num_bytes_sval);
 
   const region *get_base_region () const { return m_base_region; }
 
+  bool concrete_p () const { return m_sym_offset == NULL; }
   bool symbolic_p () const { return m_sym_offset != NULL; }
 
   bit_offset_t get_bit_offset () const
@@ -204,12 +212,26 @@ public:
     return m_offset;
   }
 
+  bool get_concrete_byte_offset (byte_offset_t *out) const
+  {
+    gcc_assert (!symbolic_p ());
+    if (m_offset % BITS_PER_UNIT == 0)
+      {
+	*out = m_offset / BITS_PER_UNIT;
+	return true;
+      }
+    return false;
+  }
+
   const svalue *get_symbolic_byte_offset () const
   {
     gcc_assert (symbolic_p ());
     return m_sym_offset;
   }
 
+  tree calc_symbolic_bit_offset (const region_model &model) const;
+  const svalue *calc_symbolic_byte_offset (region_model_manager *mgr) const;
+
   bool operator== (const region_offset &other) const
   {
     return (m_base_region == other.m_base_region
@@ -217,6 +239,9 @@ public:
 	    && m_sym_offset == other.m_sym_offset);
   }
 
+  void dump_to_pp (pretty_printer *pp, bool) const;
+  void dump (bool) const;
+
 private:
   region_offset (const region *base_region, bit_offset_t offset,
 		 const svalue *sym_offset)
@@ -228,6 +253,11 @@ private:
   const svalue *m_sym_offset;
 };
 
+extern bool operator< (const region_offset &, const region_offset &);
+extern bool operator<= (const region_offset &, const region_offset &);
+extern bool operator> (const region_offset &, const region_offset &);
+extern bool operator>= (const region_offset &, const region_offset &);
+
 extern location_t get_stmt_location (const gimple *stmt, function *fun);
 
 extern bool compat_types_p (tree src_type, tree dst_type);
diff --git a/gcc/analyzer/analyzer.opt b/gcc/analyzer/analyzer.opt
index 9d1a937e403..2760aaa8151 100644
--- a/gcc/analyzer/analyzer.opt
+++ b/gcc/analyzer/analyzer.opt
@@ -54,6 +54,22 @@ The minimum number of supernodes within a function for the analyzer to consider
 Common Joined UInteger Var(param_analyzer_max_enodes_for_full_dump) Init(200) Param
 The maximum depth of exploded nodes that should appear in a dot dump before switching to a less verbose format.
 
+-param=analyzer-text-art-string-ellipsis-threshold=
+Common Joined UInteger Var(param_analyzer_text_art_string_ellipsis_threshold) Init(15) Param
+The number of bytes at which to ellipsize string literals in analyzer text art diagrams.
+
+-param=analyzer-text-art-string-ellipsis-head-len=
+Common Joined UInteger Var(param_analyzer_text_art_string_ellipsis_head_len) Init(6) Param
+The number of literal bytes to show at the head of a string literal in text art when ellipsizing it.
+
+-param=analyzer-text-art-string-ellipsis-tail-len=
+Common Joined UInteger Var(param_analyzer_text_art_string_ellipsis_tail_len) Init(6) Param
+The number of literal bytes to show at the tail of a string literal in text art when ellipsizing it.
+
+-param=analyzer-text-art-ideal-canvas-width=
+Common Joined UInteger Var(param_analyzer_text_art_ideal_canvas_width) Init(72) Param
+The ideal width in characters of text art diagrams generated by the analyzer.
+
 Wanalyzer-allocation-size
 Common Var(warn_analyzer_allocation_size) Init(1) Warning
 Warn about code paths in which a pointer to a buffer is assigned to an incompatible type.
@@ -242,6 +258,10 @@ fanalyzer-checker=
 Common Joined RejectNegative Var(flag_analyzer_checker)
 Restrict the analyzer to run just the named checker.
 
+fanalyzer-debug-text-art
+Common Var(flag_analyzer_debug_text_art) Init(0)
+Add extra annotations to diagrams.
+
 fanalyzer-fine-grained
 Common Var(flag_analyzer_fine_grained) Init(0)
 Avoid combining multiple statements into one exploded edge.
diff --git a/gcc/analyzer/bounds-checking.cc b/gcc/analyzer/bounds-checking.cc
index 3bf542a8eba..6b618b5d398 100644
--- a/gcc/analyzer/bounds-checking.cc
+++ b/gcc/analyzer/bounds-checking.cc
@@ -25,15 +25,18 @@ along with GCC; see the file COPYING3.  If not see
 #include "tree.h"
 #include "function.h"
 #include "basic-block.h"
+#include "intl.h"
 #include "gimple.h"
 #include "gimple-iterator.h"
 #include "diagnostic-core.h"
 #include "diagnostic-metadata.h"
+#include "diagnostic-diagram.h"
 #include "analyzer/analyzer.h"
 #include "analyzer/analyzer-logging.h"
 #include "analyzer/region-model.h"
 #include "analyzer/checker-event.h"
 #include "analyzer/checker-path.h"
+#include "analyzer/access-diagram.h"
 
 #if ENABLE_ANALYZER
 
@@ -44,8 +47,35 @@ namespace ana {
 class out_of_bounds : public pending_diagnostic
 {
 public:
-  out_of_bounds (const region *reg, tree diag_arg)
-  : m_reg (reg), m_diag_arg (diag_arg)
+  class oob_region_creation_event_capacity : public region_creation_event_capacity
+  {
+  public:
+    oob_region_creation_event_capacity (tree capacity,
+					const event_loc_info &loc_info,
+					out_of_bounds &oob)
+      : region_creation_event_capacity (capacity,
+					loc_info),
+	m_oob (oob)
+    {
+    }
+    void prepare_for_emission (checker_path *path,
+			       pending_diagnostic *pd,
+			       diagnostic_event_id_t emission_id) override
+    {
+      region_creation_event_capacity::prepare_for_emission (path,
+							    pd,
+							    emission_id);
+      m_oob.m_region_creation_event_id = emission_id;
+    }
+    private:
+    out_of_bounds &m_oob;
+  };
+
+  out_of_bounds (const region_model &model,
+		 const region *reg,
+		 tree diag_arg,
+		 const svalue *sval_hint)
+  : m_model (model), m_reg (reg), m_diag_arg (diag_arg), m_sval_hint (sval_hint)
   {}
 
   bool subclass_equal_p (const pending_diagnostic &base_other) const override
@@ -63,7 +93,7 @@ public:
 
   void mark_interesting_stuff (interesting_t *interest) final override
   {
-    interest->add_region_creation (m_reg);
+    interest->add_region_creation (m_reg->get_base_region ());
   }
 
   void add_region_creation_events (const region *,
@@ -75,15 +105,25 @@ public:
        so we don't need an event for that.  */
     if (capacity)
       emission_path.add_event
-	(make_unique<region_creation_event_capacity> (capacity, loc_info));
+	(make_unique<oob_region_creation_event_capacity> (capacity, loc_info,
+							  *this));
   }
 
+  virtual enum access_direction get_dir () const = 0;
+
 protected:
   enum memory_space get_memory_space () const
   {
     return m_reg->get_memory_space ();
   }
 
+  void
+  maybe_show_notes (location_t loc, logger *logger) const
+  {
+    maybe_describe_array_bounds (loc);
+    maybe_show_diagram (logger);
+  }
+
   /* Potentially add a note about valid ways to index this array, such
      as (given "int arr[10];"):
        note: valid subscripts for 'arr' are '[0]' to '[9]'
@@ -112,8 +152,49 @@ protected:
 	    m_diag_arg, min_idx, max_idx);
   }
 
+  void
+  maybe_show_diagram (logger *logger) const
+  {
+    access_operation op (m_model, get_dir (), *m_reg, m_sval_hint);
+
+    /* Don't attempt to make a diagram if there's no valid way of
+       accessing the base region (e.g. a 0-element array).  */
+    if (op.get_valid_bits ().empty_p ())
+      return;
+
+    if (const text_art::theme *theme = global_dc->m_diagrams.m_theme)
+      {
+	text_art::style_manager sm;
+	text_art::canvas canvas (make_access_diagram (op, sm, *theme, logger));
+	if (canvas.get_size ().w == 0 && canvas.get_size ().h == 0)
+	  {
+	    /* In lieu of exceptions, return a zero-sized diagram if there's
+	       a problem.  Give up if that's happened.  */
+	    return;
+	  }
+	diagnostic_diagram diagram
+	  (canvas,
+	   /* Alt text.  */
+	   _("Diagram visualizing the predicted out-of-bounds access"));
+	diagnostic_emit_diagram (global_dc, diagram);
+      }
+  }
+
+  text_art::canvas
+  make_access_diagram (const access_operation &op,
+		       text_art::style_manager &sm,
+		       const text_art::theme &theme,
+		       logger *logger) const
+  {
+    access_diagram d (op, m_region_creation_event_id, sm, theme, logger);
+    return d.to_canvas (sm);
+  }
+
+  region_model m_model;
   const region *m_reg;
   tree m_diag_arg;
+  const svalue *m_sval_hint;
+  diagnostic_event_id_t m_region_creation_event_id;
 };
 
 /* Abstract base class for all out-of-bounds warnings where the
@@ -122,9 +203,11 @@ protected:
 class concrete_out_of_bounds : public out_of_bounds
 {
 public:
-  concrete_out_of_bounds (const region *reg, tree diag_arg,
-			  byte_range out_of_bounds_range)
-  : out_of_bounds (reg, diag_arg),
+  concrete_out_of_bounds (const region_model &model,
+			  const region *reg, tree diag_arg,
+			  byte_range out_of_bounds_range,
+			  const svalue *sval_hint)
+  : out_of_bounds (model, reg, diag_arg, sval_hint),
     m_out_of_bounds_range (out_of_bounds_range)
   {}
 
@@ -146,9 +229,12 @@ protected:
 class concrete_past_the_end : public concrete_out_of_bounds
 {
 public:
-  concrete_past_the_end (const region *reg, tree diag_arg, byte_range range,
-			 tree byte_bound)
-  : concrete_out_of_bounds (reg, diag_arg, range), m_byte_bound (byte_bound)
+  concrete_past_the_end (const region_model &model,
+			 const region *reg, tree diag_arg, byte_range range,
+			 tree byte_bound,
+			 const svalue *sval_hint)
+  : concrete_out_of_bounds (model, reg, diag_arg, range, sval_hint),
+    m_byte_bound (byte_bound)
   {}
 
   bool
@@ -168,7 +254,9 @@ public:
   {
     if (m_byte_bound && TREE_CODE (m_byte_bound) == INTEGER_CST)
       emission_path.add_event
-	(make_unique<region_creation_event_capacity> (m_byte_bound, loc_info));
+	(make_unique<oob_region_creation_event_capacity> (m_byte_bound,
+							  loc_info,
+							  *this));
   }
 
 protected:
@@ -180,9 +268,11 @@ protected:
 class concrete_buffer_overflow : public concrete_past_the_end
 {
 public:
-  concrete_buffer_overflow (const region *reg, tree diag_arg,
-		   byte_range range, tree byte_bound)
-  : concrete_past_the_end (reg, diag_arg, range, byte_bound)
+  concrete_buffer_overflow (const region_model &model,
+			    const region *reg, tree diag_arg,
+			    byte_range range, tree byte_bound,
+			    const svalue *sval_hint)
+  : concrete_past_the_end (model, reg, diag_arg, range, byte_bound, sval_hint)
   {}
 
   const char *get_kind () const final override
@@ -190,7 +280,8 @@ public:
     return "concrete_buffer_overflow";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc,
+	     logger *logger) final override
   {
     diagnostic_metadata m;
     bool warned;
@@ -238,7 +329,7 @@ public:
 		  "write to beyond the end of %qE",
 		  m_diag_arg);
 
-	maybe_describe_array_bounds (rich_loc->get_loc ());
+	maybe_show_notes (rich_loc->get_loc (), logger);
       }
 
     return warned;
@@ -276,6 +367,8 @@ public:
 				   start_buf, end_buf, m_byte_bound);
       }
   }
+
+  enum access_direction get_dir () const final override { return DIR_WRITE; }
 };
 
 /* Concrete subclass to complain about buffer over-reads.  */
@@ -283,9 +376,10 @@ public:
 class concrete_buffer_over_read : public concrete_past_the_end
 {
 public:
-  concrete_buffer_over_read (const region *reg, tree diag_arg,
+  concrete_buffer_over_read (const region_model &model,
+			     const region *reg, tree diag_arg,
 			     byte_range range, tree byte_bound)
-  : concrete_past_the_end (reg, diag_arg, range, byte_bound)
+  : concrete_past_the_end (model, reg, diag_arg, range, byte_bound, NULL)
   {}
 
   const char *get_kind () const final override
@@ -293,7 +387,7 @@ public:
     return "concrete_buffer_over_read";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *logger) final override
   {
     diagnostic_metadata m;
     bool warned;
@@ -339,7 +433,7 @@ public:
 		  "read from after the end of %qE",
 		  m_diag_arg);
 
-	maybe_describe_array_bounds (rich_loc->get_loc ());
+	maybe_show_notes (rich_loc->get_loc (), logger);
       }
 
     return warned;
@@ -377,6 +471,8 @@ public:
 				   start_buf, end_buf, m_byte_bound);
       }
   }
+
+  enum access_direction get_dir () const final override { return DIR_READ; }
 };
 
 /* Concrete subclass to complain about buffer underwrites.  */
@@ -384,9 +480,11 @@ public:
 class concrete_buffer_underwrite : public concrete_out_of_bounds
 {
 public:
-  concrete_buffer_underwrite (const region *reg, tree diag_arg,
-			      byte_range range)
-  : concrete_out_of_bounds (reg, diag_arg, range)
+  concrete_buffer_underwrite (const region_model &model,
+			      const region *reg, tree diag_arg,
+			      byte_range range,
+			      const svalue *sval_hint)
+  : concrete_out_of_bounds (model, reg, diag_arg, range, sval_hint)
   {}
 
   const char *get_kind () const final override
@@ -394,7 +492,7 @@ public:
     return "concrete_buffer_underwrite";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *logger) final override
   {
     diagnostic_metadata m;
     bool warned;
@@ -415,7 +513,7 @@ public:
 	break;
       }
     if (warned)
-      maybe_describe_array_bounds (rich_loc->get_loc ());
+      maybe_show_notes (rich_loc->get_loc (), logger);
     return warned;
   }
 
@@ -449,6 +547,8 @@ public:
 				   start_buf, end_buf);;
       }
   }
+
+  enum access_direction get_dir () const final override { return DIR_WRITE; }
 };
 
 /* Concrete subclass to complain about buffer under-reads.  */
@@ -456,9 +556,10 @@ public:
 class concrete_buffer_under_read : public concrete_out_of_bounds
 {
 public:
-  concrete_buffer_under_read (const region *reg, tree diag_arg,
+  concrete_buffer_under_read (const region_model &model,
+			      const region *reg, tree diag_arg,
 			      byte_range range)
-  : concrete_out_of_bounds (reg, diag_arg, range)
+  : concrete_out_of_bounds (model, reg, diag_arg, range, NULL)
   {}
 
   const char *get_kind () const final override
@@ -466,7 +567,7 @@ public:
     return "concrete_buffer_under_read";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *logger) final override
   {
     diagnostic_metadata m;
     bool warned;
@@ -487,7 +588,7 @@ public:
 	break;
       }
     if (warned)
-      maybe_describe_array_bounds (rich_loc->get_loc ());
+      maybe_show_notes (rich_loc->get_loc (), logger);
     return warned;
   }
 
@@ -521,6 +622,8 @@ public:
 				   start_buf, end_buf);;
       }
   }
+
+  enum access_direction get_dir () const final override { return DIR_READ; }
 };
 
 /* Abstract class to complain about out-of-bounds read/writes where
@@ -529,9 +632,11 @@ public:
 class symbolic_past_the_end : public out_of_bounds
 {
 public:
-  symbolic_past_the_end (const region *reg, tree diag_arg, tree offset,
-			 tree num_bytes, tree capacity)
-  : out_of_bounds (reg, diag_arg),
+  symbolic_past_the_end (const region_model &model,
+			 const region *reg, tree diag_arg, tree offset,
+			 tree num_bytes, tree capacity,
+			 const svalue *sval_hint)
+  : out_of_bounds (model, reg, diag_arg, sval_hint),
     m_offset (offset),
     m_num_bytes (num_bytes),
     m_capacity (capacity)
@@ -559,9 +664,12 @@ protected:
 class symbolic_buffer_overflow : public symbolic_past_the_end
 {
 public:
-  symbolic_buffer_overflow (const region *reg, tree diag_arg, tree offset,
-			    tree num_bytes, tree capacity)
-  : symbolic_past_the_end (reg, diag_arg, offset, num_bytes, capacity)
+  symbolic_buffer_overflow (const region_model &model,
+			    const region *reg, tree diag_arg, tree offset,
+			    tree num_bytes, tree capacity,
+			    const svalue *sval_hint)
+  : symbolic_past_the_end (model, reg, diag_arg, offset, num_bytes, capacity,
+			   sval_hint)
   {
   }
 
@@ -570,24 +678,31 @@ public:
     return "symbolic_buffer_overflow";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *logger) final override
   {
     diagnostic_metadata m;
+    bool warned;
     switch (get_memory_space ())
       {
       default:
 	m.add_cwe (787);
-	return warning_meta (rich_loc, m, get_controlling_option (),
-			     "buffer overflow");
+	warned = warning_meta (rich_loc, m, get_controlling_option (),
+			       "buffer overflow");
+	break;
       case MEMSPACE_STACK:
 	m.add_cwe (121);
-	return warning_meta (rich_loc, m, get_controlling_option (),
-			     "stack-based buffer overflow");
+	warned = warning_meta (rich_loc, m, get_controlling_option (),
+			       "stack-based buffer overflow");
+	break;
       case MEMSPACE_HEAP:
 	m.add_cwe (122);
-	return warning_meta (rich_loc, m, get_controlling_option (),
-			     "heap-based buffer overflow");
+	warned =  warning_meta (rich_loc, m, get_controlling_option (),
+				"heap-based buffer overflow");
+	break;
       }
+    if (warned)
+      maybe_show_notes (rich_loc->get_loc (), logger);
+    return warned;
   }
 
   label_text
@@ -658,6 +773,8 @@ public:
 				 m_diag_arg);
     return ev.formatted_print ("out-of-bounds write");
   }
+
+  enum access_direction get_dir () const final override { return DIR_WRITE; }
 };
 
 /* Concrete subclass to complain about over-reads with symbolic values.  */
@@ -665,9 +782,11 @@ public:
 class symbolic_buffer_over_read : public symbolic_past_the_end
 {
 public:
-  symbolic_buffer_over_read (const region *reg, tree diag_arg, tree offset,
+  symbolic_buffer_over_read (const region_model &model,
+			     const region *reg, tree diag_arg, tree offset,
 			     tree num_bytes, tree capacity)
-  : symbolic_past_the_end (reg, diag_arg, offset, num_bytes, capacity)
+  : symbolic_past_the_end (model, reg, diag_arg, offset, num_bytes, capacity,
+			   NULL)
   {
   }
 
@@ -676,25 +795,32 @@ public:
     return "symbolic_buffer_over_read";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *logger) final override
   {
     diagnostic_metadata m;
     m.add_cwe (126);
+    bool warned;
     switch (get_memory_space ())
       {
       default:
 	m.add_cwe (787);
-	return warning_meta (rich_loc, m, get_controlling_option (),
-			     "buffer over-read");
+	warned = warning_meta (rich_loc, m, get_controlling_option (),
+			       "buffer over-read");
+	break;
       case MEMSPACE_STACK:
 	m.add_cwe (121);
-	return warning_meta (rich_loc, m, get_controlling_option (),
-			     "stack-based buffer over-read");
+	warned = warning_meta (rich_loc, m, get_controlling_option (),
+			       "stack-based buffer over-read");
+	break;
       case MEMSPACE_HEAP:
 	m.add_cwe (122);
-	return warning_meta (rich_loc, m, get_controlling_option (),
-			     "heap-based buffer over-read");
+	warned = warning_meta (rich_loc, m, get_controlling_option (),
+			       "heap-based buffer over-read");
+	break;
       }
+    if (warned)
+      maybe_show_notes (rich_loc->get_loc (), logger);
+    return warned;
   }
 
   label_text
@@ -765,6 +891,8 @@ public:
 				 m_diag_arg);
     return ev.formatted_print ("out-of-bounds read");
   }
+
+  enum access_direction get_dir () const final override { return DIR_READ; }
 };
 
 /* Check whether an access is past the end of the BASE_REG.  */
@@ -775,6 +903,7 @@ region_model::check_symbolic_bounds (const region *base_reg,
 				     const svalue *num_bytes_sval,
 				     const svalue *capacity,
 				     enum access_direction dir,
+				     const svalue *sval_hint,
 				     region_model_context *ctxt) const
 {
   gcc_assert (ctxt);
@@ -789,24 +918,34 @@ region_model::check_symbolic_bounds (const region *base_reg,
       tree offset_tree = get_representative_tree (sym_byte_offset);
       tree num_bytes_tree = get_representative_tree (num_bytes_sval);
       tree capacity_tree = get_representative_tree (capacity);
+      const region *offset_reg = m_mgr->get_offset_region (base_reg,
+							   NULL_TREE,
+							   sym_byte_offset);
+      const region *sized_offset_reg = m_mgr->get_sized_region (offset_reg,
+								NULL_TREE,
+								num_bytes_sval);
       switch (dir)
 	{
 	default:
 	  gcc_unreachable ();
 	  break;
 	case DIR_READ:
-	  ctxt->warn (make_unique<symbolic_buffer_over_read> (base_reg,
+	  gcc_assert (sval_hint == nullptr);
+	  ctxt->warn (make_unique<symbolic_buffer_over_read> (*this,
+							      sized_offset_reg,
 							      diag_arg,
 							      offset_tree,
 							      num_bytes_tree,
 							      capacity_tree));
 	  break;
 	case DIR_WRITE:
-	  ctxt->warn (make_unique<symbolic_buffer_overflow> (base_reg,
+	  ctxt->warn (make_unique<symbolic_buffer_overflow> (*this,
+							     sized_offset_reg,
 							     diag_arg,
 							     offset_tree,
 							     num_bytes_tree,
-							     capacity_tree));
+							     capacity_tree,
+							     sval_hint));
 	  break;
 	}
     }
@@ -827,6 +966,7 @@ maybe_get_integer_cst_tree (const svalue *sval)
 void
 region_model::check_region_bounds (const region *reg,
 				   enum access_direction dir,
+				   const svalue *sval_hint,
 				   region_model_context *ctxt) const
 {
   gcc_assert (ctxt);
@@ -878,7 +1018,7 @@ region_model::check_region_bounds (const region *reg,
       else
 	byte_offset_sval = reg_offset.get_symbolic_byte_offset ();
       check_symbolic_bounds (base_reg, byte_offset_sval, num_bytes_sval,
-			     capacity, dir, ctxt);
+			     capacity, dir, sval_hint, ctxt);
       return;
     }
 
@@ -897,12 +1037,16 @@ region_model::check_region_bounds (const region *reg,
 	  gcc_unreachable ();
 	  break;
 	case DIR_READ:
-	  ctxt->warn (make_unique<concrete_buffer_under_read> (reg, diag_arg,
+	  gcc_assert (sval_hint == nullptr);
+	  ctxt->warn (make_unique<concrete_buffer_under_read> (*this, reg,
+							       diag_arg,
 							       out));
 	  break;
 	case DIR_WRITE:
-	  ctxt->warn (make_unique<concrete_buffer_underwrite> (reg, diag_arg,
-							       out));
+	  ctxt->warn (make_unique<concrete_buffer_underwrite> (*this,
+							       reg, diag_arg,
+							       out,
+							       sval_hint));
 	  break;
 	}
     }
@@ -927,12 +1071,16 @@ region_model::check_region_bounds (const region *reg,
 	  gcc_unreachable ();
 	  break;
 	case DIR_READ:
-	  ctxt->warn (make_unique<concrete_buffer_over_read> (reg, diag_arg,
+	  gcc_assert (sval_hint == nullptr);
+	  ctxt->warn (make_unique<concrete_buffer_over_read> (*this,
+							      reg, diag_arg,
 							      out, byte_bound));
 	  break;
 	case DIR_WRITE:
-	  ctxt->warn (make_unique<concrete_buffer_overflow> (reg, diag_arg,
-							     out, byte_bound));
+	  ctxt->warn (make_unique<concrete_buffer_overflow> (*this,
+							     reg, diag_arg,
+							     out, byte_bound,
+							     sval_hint));
 	  break;
 	}
     }
diff --git a/gcc/analyzer/diagnostic-manager.cc b/gcc/analyzer/diagnostic-manager.cc
index 0a447f7ba26..cfca305d552 100644
--- a/gcc/analyzer/diagnostic-manager.cc
+++ b/gcc/analyzer/diagnostic-manager.cc
@@ -1421,7 +1421,7 @@ diagnostic_manager::emit_saved_diagnostic (const exploded_graph &eg,
 
   auto_diagnostic_group d;
   auto_cfun sentinel (sd.m_snode->m_fun);
-  if (sd.m_d->emit (&rich_loc))
+  if (sd.m_d->emit (&rich_loc, get_logger ()))
     {
       sd.emit_any_notes ();
 
diff --git a/gcc/analyzer/engine.cc b/gcc/analyzer/engine.cc
index a5965c2b8ff..61685f43fba 100644
--- a/gcc/analyzer/engine.cc
+++ b/gcc/analyzer/engine.cc
@@ -1771,7 +1771,7 @@ public:
     return OPT_Wanalyzer_stale_setjmp_buffer;
   }
 
-  bool emit (rich_location *richloc) final override
+  bool emit (rich_location *richloc, logger *) final override
   {
     return warning_at
       (richloc, get_controlling_option (),
@@ -3925,7 +3925,7 @@ public:
     return OPT_Wanalyzer_jump_through_null;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     return warning_at (rich_loc, get_controlling_option (),
 		       "jump through null pointer");
diff --git a/gcc/analyzer/infinite-recursion.cc b/gcc/analyzer/infinite-recursion.cc
index c262e391953..3ba316ee964 100644
--- a/gcc/analyzer/infinite-recursion.cc
+++ b/gcc/analyzer/infinite-recursion.cc
@@ -95,7 +95,7 @@ public:
     return OPT_Wanalyzer_infinite_recursion;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* "CWE-674: Uncontrolled Recursion".  */
     diagnostic_metadata m;
diff --git a/gcc/analyzer/kf-analyzer.cc b/gcc/analyzer/kf-analyzer.cc
index 32aa87bfbdc..5aed007d6ea 100644
--- a/gcc/analyzer/kf-analyzer.cc
+++ b/gcc/analyzer/kf-analyzer.cc
@@ -255,7 +255,7 @@ public:
     return 0;
   }
 
-  bool emit (rich_location *richloc) final override
+  bool emit (rich_location *richloc, logger *) final override
   {
     inform (richloc, "path");
     return true;
diff --git a/gcc/analyzer/kf.cc b/gcc/analyzer/kf.cc
index 93c46630f36..34ff1cb8496 100644
--- a/gcc/analyzer/kf.cc
+++ b/gcc/analyzer/kf.cc
@@ -324,7 +324,9 @@ kf_memset::impl_call_pre (const call_details &cd) const
   const region *sized_dest_reg = mgr->get_sized_region (dest_reg,
 							NULL_TREE,
 							num_bytes_sval);
-  model->check_region_for_write (sized_dest_reg, cd.get_ctxt ());
+  model->check_region_for_write (sized_dest_reg,
+				 nullptr,
+				 cd.get_ctxt ());
   model->fill_region (sized_dest_reg, fill_value_u8);
 }
 
@@ -358,7 +360,7 @@ public:
     return OPT_Wanalyzer_putenv_of_auto_var;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
diff --git a/gcc/analyzer/pending-diagnostic.h b/gcc/analyzer/pending-diagnostic.h
index 6423c8be4ea..7582b37efe7 100644
--- a/gcc/analyzer/pending-diagnostic.h
+++ b/gcc/analyzer/pending-diagnostic.h
@@ -180,7 +180,7 @@ class pending_diagnostic
   /* Vfunc for emitting the diagnostic.  The rich_location will have been
      populated with a diagnostic_path.
      Return true if a diagnostic is actually emitted.  */
-  virtual bool emit (rich_location *) = 0;
+  virtual bool emit (rich_location *, logger *) = 0;
 
   /* Hand-coded RTTI: get an ID for the subclass.  */
   virtual const char *get_kind () const = 0;
diff --git a/gcc/analyzer/region-model-manager.cc b/gcc/analyzer/region-model-manager.cc
index 3b95e432aba..1453acf7bc9 100644
--- a/gcc/analyzer/region-model-manager.cc
+++ b/gcc/analyzer/region-model-manager.cc
@@ -230,10 +230,11 @@ region_model_manager::get_or_create_constant_svalue (tree cst_expr)
    for VAL of type TYPE, creating it if necessary.  */
 
 const svalue *
-region_model_manager::get_or_create_int_cst (tree type, poly_int64 val)
+region_model_manager::get_or_create_int_cst (tree type,
+					     const poly_wide_int_ref &cst)
 {
   gcc_assert (type);
-  tree tree_cst = build_int_cst (type, val);
+  tree tree_cst = wide_int_to_tree (type, cst);
   return get_or_create_constant_svalue (tree_cst);
 }
 
@@ -612,7 +613,7 @@ region_model_manager::maybe_fold_binop (tree type, enum tree_code op,
 	  return get_or_create_constant_svalue (result);
     }
 
-  if (FLOAT_TYPE_P (type)
+  if ((type && FLOAT_TYPE_P (type))
       || (arg0->get_type () && FLOAT_TYPE_P (arg0->get_type ()))
       || (arg1->get_type () && FLOAT_TYPE_P (arg1->get_type ())))
     return NULL;
@@ -634,6 +635,11 @@ region_model_manager::maybe_fold_binop (tree type, enum tree_code op,
       /* (0 - VAL) -> -VAL.  */
       if (cst0 && zerop (cst0))
 	return get_or_create_unaryop (type, NEGATE_EXPR, arg1);
+      /* (X + Y) - X -> Y.  */
+      if (const binop_svalue *binop = arg0->dyn_cast_binop_svalue ())
+	if (binop->get_op () == PLUS_EXPR)
+	  if (binop->get_arg0 () == arg1)
+	    return get_or_create_cast (type, binop->get_arg1 ());
       break;
     case MULT_EXPR:
       /* (VAL * 0).  */
@@ -726,10 +732,7 @@ region_model_manager::maybe_fold_binop (tree type, enum tree_code op,
   if (cst1 && associative_tree_code (op))
     if (const binop_svalue *binop = arg0->dyn_cast_binop_svalue ())
       if (binop->get_op () == op
-	  && binop->get_arg1 ()->maybe_get_constant ()
-	  && type == binop->get_type ()
-	  && type == binop->get_arg0 ()->get_type ()
-	  && type == binop->get_arg1 ()->get_type ())
+	  && binop->get_arg1 ()->maybe_get_constant ())
 	return get_or_create_binop
 	  (type, op, binop->get_arg0 (),
 	   get_or_create_binop (type, op,
@@ -748,6 +751,21 @@ region_model_manager::maybe_fold_binop (tree type, enum tree_code op,
 	     get_or_create_binop (size_type_node, op,
 				  binop->get_arg1 (), arg1));
 
+  /* Distribute multiplication by a constant through addition/subtraction:
+     (X + Y) * CST => (X * CST) + (Y * CST).  */
+  if (cst1 && op == MULT_EXPR)
+    if (const binop_svalue *binop = arg0->dyn_cast_binop_svalue ())
+      if (binop->get_op () == PLUS_EXPR
+	  || binop->get_op () == MINUS_EXPR)
+	{
+	  return get_or_create_binop
+	    (type, binop->get_op (),
+	     get_or_create_binop (type, op,
+				  binop->get_arg0 (), arg1),
+	     get_or_create_binop (type, op,
+				  binop->get_arg1 (), arg1));
+	}
+
   /* etc.  */
 
   return NULL;
diff --git a/gcc/analyzer/region-model-manager.h b/gcc/analyzer/region-model-manager.h
index 273fe7b32b6..3340c3ebd1e 100644
--- a/gcc/analyzer/region-model-manager.h
+++ b/gcc/analyzer/region-model-manager.h
@@ -42,7 +42,7 @@ public:
 
   /* svalue consolidation.  */
   const svalue *get_or_create_constant_svalue (tree cst_expr);
-  const svalue *get_or_create_int_cst (tree type, poly_int64);
+  const svalue *get_or_create_int_cst (tree type, const poly_wide_int_ref &cst);
   const svalue *get_or_create_null_ptr (tree pointer_type);
   const svalue *get_or_create_unknown_svalue (tree type);
   const svalue *get_or_create_setjmp_svalue (const setjmp_record &r,
diff --git a/gcc/analyzer/region-model.cc b/gcc/analyzer/region-model.cc
index 3bb3df2f063..d983822708f 100644
--- a/gcc/analyzer/region-model.cc
+++ b/gcc/analyzer/region-model.cc
@@ -507,7 +507,7 @@ public:
 
   bool terminate_path_p () const final override { return true; }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     switch (m_pkind)
       {
@@ -638,7 +638,7 @@ public:
     return OPT_Wanalyzer_shift_count_negative;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     return warning_at (rich_loc, get_controlling_option (),
 		       "shift by negative count (%qE)", m_count_cst);
@@ -685,7 +685,7 @@ public:
     return OPT_Wanalyzer_shift_count_overflow;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     return warning_at (rich_loc, get_controlling_option (),
 		       "shift by count (%qE) >= precision of type (%qi)",
@@ -1736,7 +1736,7 @@ check_external_function_for_access_attr (const gcall *call,
 	  tree ptr_tree = gimple_call_arg (call, access->ptrarg);
 	  const svalue *ptr_sval = get_rvalue (ptr_tree, &my_ctxt);
 	  const region *reg = deref_rvalue (ptr_sval, ptr_tree, &my_ctxt);
-	  check_region_for_write (reg, &my_ctxt);
+	  check_region_for_write (reg, nullptr, &my_ctxt);
 	  /* We don't use the size arg for now.  */
 	}
     }
@@ -2544,8 +2544,8 @@ region_model::deref_rvalue (const svalue *ptr_sval, tree ptr_tree,
 		const poisoned_svalue *poisoned_sval
 		  = as_a <const poisoned_svalue *> (ptr_sval);
 		enum poison_kind pkind = poisoned_sval->get_poison_kind ();
-		ctxt->warn (make_unique<poisoned_value_diagnostic>
-			      (ptr, pkind, NULL, NULL));
+		ctxt->warn (::make_unique<poisoned_value_diagnostic>
+			      (ptr, pkind, nullptr, nullptr));
 	      }
 	  }
       }
@@ -2598,7 +2598,7 @@ public:
     return OPT_Wanalyzer_write_to_const;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     bool warned;
@@ -2666,7 +2666,7 @@ public:
     return OPT_Wanalyzer_write_to_string_literal;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     return warning_at (rich_loc, get_controlling_option (),
 		       "write to string literal");
@@ -2764,6 +2764,15 @@ region_model::get_capacity (const region *reg) const
       /* Look through sized regions to get at the capacity
 	 of the underlying regions.  */
       return get_capacity (reg->get_parent_region ());
+    case RK_STRING:
+      {
+	/* "Capacity" here means "size".  */
+	const string_region *string_reg = as_a <const string_region *> (reg);
+	tree string_cst = string_reg->get_string_cst ();
+	return m_mgr->get_or_create_int_cst (size_type_node,
+					     TREE_STRING_LENGTH (string_cst));
+      }
+      break;
     }
 
   if (const svalue *recorded = get_dynamic_extents (reg))
@@ -2802,11 +2811,14 @@ region_model::get_string_size (const region *reg) const
 }
 
 /* If CTXT is non-NULL, use it to warn about any problems accessing REG,
-   using DIR to determine if this access is a read or write.  */
+   using DIR to determine if this access is a read or write.
+   If SVAL_HINT is non-NULL, use it as a hint in diagnostics
+   about the value that would be written to REG.  */
 
 void
 region_model::check_region_access (const region *reg,
 				   enum access_direction dir,
+				   const svalue *sval_hint,
 				   region_model_context *ctxt) const
 {
   /* Fail gracefully if CTXT is NULL.  */
@@ -2814,7 +2826,7 @@ region_model::check_region_access (const region *reg,
     return;
 
   check_region_for_taint (reg, dir, ctxt);
-  check_region_bounds (reg, dir, ctxt);
+  check_region_bounds (reg, dir, sval_hint, ctxt);
 
   switch (dir)
     {
@@ -2833,9 +2845,10 @@ region_model::check_region_access (const region *reg,
 
 void
 region_model::check_region_for_write (const region *dest_reg,
+				      const svalue *sval_hint,
 				      region_model_context *ctxt) const
 {
-  check_region_access (dest_reg, DIR_WRITE, ctxt);
+  check_region_access (dest_reg, DIR_WRITE, sval_hint, ctxt);
 }
 
 /* If CTXT is non-NULL, use it to warn about any problems reading from REG.  */
@@ -2844,7 +2857,7 @@ void
 region_model::check_region_for_read (const region *src_reg,
 				     region_model_context *ctxt) const
 {
-  check_region_access (src_reg, DIR_READ, ctxt);
+  check_region_access (src_reg, DIR_READ, NULL, ctxt);
 }
 
 /* Concrete subclass for casts of pointers that lead to trailing bytes.  */
@@ -2880,7 +2893,7 @@ public:
     return OPT_Wanalyzer_allocation_size;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     m.add_cwe (131);
@@ -3240,7 +3253,7 @@ region_model::set_value (const region *lhs_reg, const svalue *rhs_sval,
 
   check_region_size (lhs_reg, rhs_sval, ctxt);
 
-  check_region_for_write (lhs_reg, ctxt);
+  check_region_for_write (lhs_reg, rhs_sval, ctxt);
 
   m_store.set_value (m_mgr->get_store_manager(), lhs_reg, rhs_sval,
 		     ctxt ? ctxt->get_uncertainty () : NULL);
@@ -3873,7 +3886,12 @@ region_model::get_representative_path_var_1 (const svalue *sval,
 
   /* Prevent infinite recursion.  */
   if (visited->contains (sval))
-    return path_var (NULL_TREE, 0);
+    {
+      if (sval->get_kind () == SK_CONSTANT)
+	return path_var (sval->maybe_get_constant (), 0);
+      else
+	return path_var (NULL_TREE, 0);
+    }
   visited->add (sval);
 
   /* Handle casts by recursion into get_representative_path_var.  */
@@ -4978,7 +4996,7 @@ public:
     return same_tree_p (m_arg, ((const float_as_size_arg &) other).m_arg);
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     bool warned = warning_meta (rich_loc, m, get_controlling_option (),
@@ -5340,7 +5358,7 @@ public:
     return OPT_Wanalyzer_exposure_through_uninit_copy;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-200: Exposure of Sensitive Information to an Unauthorized Actor.  */
diff --git a/gcc/analyzer/region-model.h b/gcc/analyzer/region-model.h
index fe3db0b0c98..67bdb2a4dd1 100644
--- a/gcc/analyzer/region-model.h
+++ b/gcc/analyzer/region-model.h
@@ -490,6 +490,7 @@ class region_model
 				  region_model_context *ctxt) const;
 
   void check_region_for_write (const region *dest_reg,
+			       const svalue *sval_hint,
 			       region_model_context *ctxt) const;
 
 private:
@@ -555,6 +556,7 @@ private:
 				  region_model_context *ctxt) const;
   void check_region_access (const region *reg,
 			    enum access_direction dir,
+			    const svalue *sval_hint,
 			    region_model_context *ctxt) const;
   void check_region_for_read (const region *src_reg,
 			      region_model_context *ctxt) const;
@@ -567,8 +569,10 @@ private:
 			      const svalue *num_bytes_sval,
 			      const svalue *capacity,
 			      enum access_direction dir,
+			      const svalue *sval_hint,
 			      region_model_context *ctxt) const;
   void check_region_bounds (const region *reg, enum access_direction dir,
+			    const svalue *sval_hint,
 			    region_model_context *ctxt) const;
 
   void check_call_args (const call_details &cd) const;
diff --git a/gcc/analyzer/region.cc b/gcc/analyzer/region.cc
index 8f0eb569b33..f83e286cade 100644
--- a/gcc/analyzer/region.cc
+++ b/gcc/analyzer/region.cc
@@ -63,6 +63,332 @@ along with GCC; see the file COPYING3.  If not see
 
 namespace ana {
 
+region_offset
+region_offset::make_byte_offset (const region *base_region,
+				 const svalue *num_bytes_sval)
+{
+  if (tree num_bytes_cst = num_bytes_sval->maybe_get_constant ())
+    {
+      gcc_assert (TREE_CODE (num_bytes_cst) == INTEGER_CST);
+      bit_offset_t num_bits = wi::to_offset (num_bytes_cst) * BITS_PER_UNIT;
+      return make_concrete (base_region, num_bits);
+    }
+  else
+    {
+      return make_symbolic (base_region, num_bytes_sval);
+    }
+}
+
+tree
+region_offset::calc_symbolic_bit_offset (const region_model &model) const
+{
+  if (symbolic_p ())
+    {
+      tree num_bytes_expr = model.get_representative_tree (m_sym_offset);
+      if (!num_bytes_expr)
+	return NULL_TREE;
+      tree bytes_to_bits_scale = build_int_cst (size_type_node, BITS_PER_UNIT);
+      return fold_build2 (MULT_EXPR, size_type_node,
+			  num_bytes_expr, bytes_to_bits_scale);
+    }
+  else
+    {
+      tree cst = wide_int_to_tree (size_type_node, m_offset);
+      return cst;
+    }
+}
+
+const svalue *
+region_offset::calc_symbolic_byte_offset (region_model_manager *mgr) const
+{
+  if (symbolic_p ())
+    return m_sym_offset;
+  else
+    {
+      byte_offset_t concrete_byte_offset;
+      if (get_concrete_byte_offset (&concrete_byte_offset))
+	return mgr->get_or_create_int_cst (size_type_node,
+					   concrete_byte_offset);
+      else
+	/* Can't handle bitfields; return UNKNOWN.  */
+	return mgr->get_or_create_unknown_svalue (size_type_node);
+    }
+}
+
+void
+region_offset::dump_to_pp (pretty_printer *pp, bool simple) const
+{
+  if (symbolic_p ())
+    {
+      /* We don't bother showing the base region.  */
+      pp_string (pp, "byte ");
+      m_sym_offset->dump_to_pp (pp, simple);
+    }
+  else
+    {
+      if (m_offset % BITS_PER_UNIT == 0)
+	{
+	  pp_string (pp, "byte ");
+	  pp_wide_int (pp, m_offset / BITS_PER_UNIT, SIGNED);
+	}
+      else
+	{
+	  pp_string (pp, "bit ");
+	  pp_wide_int (pp, m_offset, SIGNED);
+	}
+    }
+}
+
+DEBUG_FUNCTION void
+region_offset::dump (bool simple) const
+{
+  pretty_printer pp;
+  pp_format_decoder (&pp) = default_tree_printer;
+  pp_show_color (&pp) = pp_show_color (global_dc->printer);
+  pp.buffer->stream = stderr;
+  dump_to_pp (&pp, simple);
+  pp_newline (&pp);
+  pp_flush (&pp);
+}
+
+/* An svalue that matches the pattern (BASE * FACTOR) + OFFSET
+   where FACTOR or OFFSET could be the identity (represented as NULL).  */
+
+struct linear_op
+{
+  linear_op (const svalue *base,
+	     const svalue *factor,
+	     const svalue *offset)
+  : m_base (base), m_factor (factor), m_offset (offset)
+  {
+  }
+
+  bool maybe_get_cst_factor (bit_offset_t *out) const
+  {
+    if (m_factor == nullptr)
+      {
+	*out = 1;
+	return true;
+      }
+    if (tree cst_factor = m_factor->maybe_get_constant ())
+      {
+	*out = wi::to_offset (cst_factor);
+	return true;
+      }
+    return false;
+  }
+
+  bool maybe_get_cst_offset (bit_offset_t *out) const
+  {
+    if (m_offset == nullptr)
+      {
+	*out = 0;
+	return true;
+      }
+    if (tree cst_offset = m_offset->maybe_get_constant ())
+      {
+	*out = wi::to_offset (cst_offset);
+	return true;
+      }
+    return false;
+  }
+
+  static tristate
+  less (const linear_op &a, const linear_op &b)
+  {
+    /* Same base.  */
+    if (a.m_base == b.m_base)
+      {
+	bit_offset_t a_wi_factor;
+	bit_offset_t b_wi_factor;
+	if (a.maybe_get_cst_factor (&a_wi_factor)
+	    && b.maybe_get_cst_factor (&b_wi_factor))
+	  {
+	    if (a_wi_factor != b_wi_factor)
+	      return tristate (a_wi_factor < b_wi_factor);
+	    else
+	      {
+		bit_offset_t a_wi_offset;
+		bit_offset_t b_wi_offset;
+		if (a.maybe_get_cst_offset (&a_wi_offset)
+		    && b.maybe_get_cst_offset (&b_wi_offset))
+		  return tristate (a_wi_offset < b_wi_offset);
+	      }
+	  }
+      }
+    return tristate::unknown ();
+  }
+
+  static tristate
+  le (const linear_op &a, const linear_op &b)
+  {
+    /* Same base.  */
+    if (a.m_base == b.m_base)
+      {
+	bit_offset_t a_wi_factor;
+	bit_offset_t b_wi_factor;
+	if (a.maybe_get_cst_factor (&a_wi_factor)
+	    && b.maybe_get_cst_factor (&b_wi_factor))
+	  {
+	    if (a_wi_factor != b_wi_factor)
+	      return tristate (a_wi_factor <= b_wi_factor);
+	    else
+	      {
+		bit_offset_t a_wi_offset;
+		bit_offset_t b_wi_offset;
+		if (a.maybe_get_cst_offset (&a_wi_offset)
+		    && b.maybe_get_cst_offset (&b_wi_offset))
+		  return tristate (a_wi_offset <= b_wi_offset);
+	      }
+	  }
+      }
+    return tristate::unknown ();
+  }
+
+  static bool
+  from_svalue (const svalue &sval, linear_op *out)
+  {
+    switch (sval.get_kind ())
+      {
+      default:
+	break;
+      case SK_BINOP:
+	{
+	  const binop_svalue &binop_sval ((const binop_svalue &)sval);
+	  if (binop_sval.get_op () == MULT_EXPR)
+	    {
+	      *out = linear_op (binop_sval.get_arg0 (),
+				binop_sval.get_arg1 (),
+				NULL);
+	      return true;
+	    }
+	  else if (binop_sval.get_op () == PLUS_EXPR)
+	    {
+	      if (binop_sval.get_arg0 ()->get_kind () == SK_BINOP)
+		{
+		  const binop_svalue &inner_binop_sval
+		    ((const binop_svalue &)*binop_sval.get_arg0 ());
+		  if (inner_binop_sval.get_op () == MULT_EXPR)
+		    {
+		      *out = linear_op (inner_binop_sval.get_arg0 (),
+					inner_binop_sval.get_arg1 (),
+					binop_sval.get_arg1 ());
+		      return true;
+		    }
+		}
+
+	      *out = linear_op (binop_sval.get_arg0 (),
+				NULL,
+				binop_sval.get_arg1 ());
+	      return true;
+	    }
+	}
+	break;
+      }
+    return false;
+  }
+
+  const svalue *m_base;
+  const svalue *m_factor;
+  const svalue *m_offset;
+};
+
+bool
+operator< (const region_offset &a, const region_offset &b)
+{
+  if (a.symbolic_p ())
+    {
+      if (b.symbolic_p ())
+	{
+	  /* Symbolic vs symbolic.  */
+	  const svalue &a_sval = *a.get_symbolic_byte_offset ();
+	  const svalue &b_sval = *b.get_symbolic_byte_offset ();
+
+	  linear_op op_a (NULL, NULL, NULL);
+	  linear_op op_b (NULL, NULL, NULL);
+	  if (linear_op::from_svalue (a_sval, &op_a)
+	      && linear_op::from_svalue (b_sval, &op_b))
+	    {
+	      tristate ts = linear_op::less (op_a, op_b);
+	      if (ts.is_true ())
+		return true;
+	      else if (ts.is_false ())
+		return false;
+	    }
+	  /* Use svalue's deterministic order, for now.  */
+	  return (svalue::cmp_ptr (a.get_symbolic_byte_offset (),
+				   b.get_symbolic_byte_offset ())
+		  < 0);
+	}
+      else
+	/* Symbolic vs concrete: put all symbolic after all concrete.  */
+	return false;
+    }
+  else
+    {
+      if (b.symbolic_p ())
+	/* Concrete vs symbolic: put all concrete before all symbolic.  */
+	return true;
+      else
+	/* Concrete vs concrete.  */
+	return a.get_bit_offset () < b.get_bit_offset ();
+    }
+}
+
+bool
+operator<= (const region_offset &a, const region_offset &b)
+{
+  if (a.symbolic_p ())
+    {
+      if (b.symbolic_p ())
+	{
+	  /* Symbolic vs symbolic.  */
+	  const svalue &a_sval = *a.get_symbolic_byte_offset ();
+	  const svalue &b_sval = *b.get_symbolic_byte_offset ();
+
+	  linear_op op_a (NULL, NULL, NULL);
+	  linear_op op_b (NULL, NULL, NULL);
+	  if (linear_op::from_svalue (a_sval, &op_a)
+	      && linear_op::from_svalue (b_sval, &op_b))
+	    {
+	      tristate ts = linear_op::le (op_a, op_b);
+	      if (ts.is_true ())
+		return true;
+	      else if (ts.is_false ())
+		return false;
+	    }
+	  /* Use svalue's deterministic order, for now.  */
+	  return (svalue::cmp_ptr (a.get_symbolic_byte_offset (),
+				   b.get_symbolic_byte_offset ())
+		  <= 0);
+	}
+      else
+	/* Symbolic vs concrete: put all symbolic after all concrete.  */
+	return false;
+    }
+  else
+    {
+      if (b.symbolic_p ())
+	/* Concrete vs symbolic: put all concrete before all symbolic.  */
+	return true;
+      else
+	/* Concrete vs concrete.  */
+	return a.get_bit_offset () <= b.get_bit_offset ();
+    }
+}
+
+bool
+operator> (const region_offset &a, const region_offset &b)
+{
+  return b < a;
+}
+
+bool
+operator>= (const region_offset &a, const region_offset &b)
+{
+  return b <= a;
+}
+
 /* class region and its various subclasses.  */
 
 /* class region.  */
@@ -294,6 +620,35 @@ region::get_offset (region_model_manager *mgr) const
   return *m_cached_offset;
 }
 
+/* Get the region_offset for immediately beyond this region.  */
+
+region_offset
+region::get_next_offset (region_model_manager *mgr) const
+{
+  region_offset start = get_offset (mgr);
+
+  bit_size_t bit_size;
+  if (get_bit_size (&bit_size))
+    {
+      if (start.concrete_p ())
+	{
+	  bit_offset_t next_bit_offset = start.get_bit_offset () + bit_size;
+	  return region_offset::make_concrete (start.get_base_region (),
+					       next_bit_offset);
+	}
+    }
+
+  const svalue *start_byte_offset_sval = start.calc_symbolic_byte_offset (mgr);
+  const svalue *byte_size_sval = get_byte_size_sval (mgr);
+  const svalue *sum_sval
+    = mgr->get_or_create_binop (size_type_node,
+				PLUS_EXPR,
+				start_byte_offset_sval,
+				byte_size_sval);
+  return region_offset::make_symbolic (start.get_base_region (),
+				       sum_sval);
+}
+
 /* Base class implementation of region::get_byte_size vfunc.
    If the size of this region (in bytes) is known statically, write it to *OUT
    and return true.
@@ -572,7 +927,7 @@ region::get_relative_concrete_offset (bit_offset_t *) const
 const svalue *
 region::get_relative_symbolic_offset (region_model_manager *mgr) const
 {
-  return mgr->get_or_create_unknown_svalue (integer_type_node);
+  return mgr->get_or_create_unknown_svalue (ptrdiff_type_node);
 }
 
 /* Attempt to get the position and size of this region expressed as a
@@ -1389,10 +1744,10 @@ field_region::get_relative_symbolic_offset (region_model_manager *mgr) const
   if (get_relative_concrete_offset (&out))
     {
       tree cst_tree
-	= wide_int_to_tree (integer_type_node, out / BITS_PER_UNIT);
+	= wide_int_to_tree (ptrdiff_type_node, out / BITS_PER_UNIT);
       return mgr->get_or_create_constant_svalue (cst_tree);
     }
-  return mgr->get_or_create_unknown_svalue (integer_type_node);
+  return mgr->get_or_create_unknown_svalue (ptrdiff_type_node);
 }
 
 /* class element_region : public region.  */
@@ -1474,14 +1829,14 @@ element_region::get_relative_symbolic_offset (region_model_manager *mgr) const
   HOST_WIDE_INT hwi_byte_size = int_size_in_bytes (elem_type);
   if (hwi_byte_size > 0)
 	  {
-      tree byte_size_tree = wide_int_to_tree (integer_type_node,
+      tree byte_size_tree = wide_int_to_tree (ptrdiff_type_node,
 					      hwi_byte_size);
       const svalue *byte_size_sval
 	= mgr->get_or_create_constant_svalue (byte_size_tree);
-      return mgr->get_or_create_binop (integer_type_node, MULT_EXPR,
+      return mgr->get_or_create_binop (ptrdiff_type_node, MULT_EXPR,
 				       m_index, byte_size_sval);
     }
-  return mgr->get_or_create_unknown_svalue (integer_type_node);
+  return mgr->get_or_create_unknown_svalue (ptrdiff_type_node);
 }
 
 /* class offset_region : public region.  */
@@ -1805,7 +2160,7 @@ bit_range_region::get_relative_symbolic_offset (region_model_manager *mgr)
   const
 {
   byte_offset_t start_byte = m_bits.get_start_bit_offset () / BITS_PER_UNIT;
-  tree start_bit_tree = wide_int_to_tree (integer_type_node, start_byte);
+  tree start_bit_tree = wide_int_to_tree (ptrdiff_type_node, start_byte);
   return mgr->get_or_create_constant_svalue (start_bit_tree);
 }
 
diff --git a/gcc/analyzer/region.h b/gcc/analyzer/region.h
index 5df0ae7487b..590f9ee7757 100644
--- a/gcc/analyzer/region.h
+++ b/gcc/analyzer/region.h
@@ -182,6 +182,7 @@ public:
   bool involves_p (const svalue *sval) const;
 
   region_offset get_offset (region_model_manager *mgr) const;
+  region_offset get_next_offset (region_model_manager *mgr) const;
 
   /* Attempt to get the size of this region as a concrete number of bytes.
      If successful, return true and write the size to *OUT.
diff --git a/gcc/analyzer/sm-fd.cc b/gcc/analyzer/sm-fd.cc
index d107390bc00..03ad3598a3c 100644
--- a/gcc/analyzer/sm-fd.cc
+++ b/gcc/analyzer/sm-fd.cc
@@ -465,7 +465,7 @@ public:
   }
 
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     /*CWE-775: Missing Release of File Descriptor or Handle after Effective
       Lifetime
@@ -550,7 +550,7 @@ public:
   }
 
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     bool warned;
     switch (m_fd_dir)
@@ -612,7 +612,7 @@ public:
     return OPT_Wanalyzer_fd_double_close;
   }
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     // CWE-1341: Multiple Releases of Same Resource or Handle
@@ -677,7 +677,7 @@ public:
   }
 
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     bool warned;
     warned = warning_at (rich_loc, get_controlling_option (),
@@ -748,7 +748,7 @@ public:
   }
 
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     bool warned;
     warned = warning_at (rich_loc, get_controlling_option (),
@@ -859,7 +859,7 @@ public:
   }
 
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     /* CWE-666: Operation on Resource in Wrong Phase of Lifetime.  */
     diagnostic_metadata m;
@@ -1019,7 +1019,7 @@ public:
   }
 
   bool
-  emit (rich_location *rich_loc) final override
+  emit (rich_location *rich_loc, logger *) final override
   {
     switch (m_expected_type)
       {
diff --git a/gcc/analyzer/sm-file.cc b/gcc/analyzer/sm-file.cc
index d99a09b76c4..0cfe6821722 100644
--- a/gcc/analyzer/sm-file.cc
+++ b/gcc/analyzer/sm-file.cc
@@ -176,7 +176,7 @@ public:
     return OPT_Wanalyzer_double_fclose;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-1341: Multiple Releases of Same Resource or Handle.  */
@@ -224,7 +224,7 @@ public:
     return OPT_Wanalyzer_file_leak;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-775: "Missing Release of File Descriptor or Handle after
diff --git a/gcc/analyzer/sm-malloc.cc b/gcc/analyzer/sm-malloc.cc
index 74701375409..a8c63eb1ce8 100644
--- a/gcc/analyzer/sm-malloc.cc
+++ b/gcc/analyzer/sm-malloc.cc
@@ -835,7 +835,7 @@ public:
     return OPT_Wanalyzer_mismatching_deallocation;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
@@ -914,7 +914,7 @@ public:
     return OPT_Wanalyzer_double_free;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
@@ -1010,7 +1010,7 @@ public:
     return OPT_Wanalyzer_possible_null_dereference;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* CWE-690: Unchecked Return Value to NULL Pointer Dereference.  */
     diagnostic_metadata m;
@@ -1099,7 +1099,7 @@ public:
     return OPT_Wanalyzer_possible_null_argument;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* CWE-690: Unchecked Return Value to NULL Pointer Dereference.  */
     auto_diagnostic_group d;
@@ -1152,7 +1152,7 @@ public:
 
   bool terminate_path_p () const final override { return true; }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* CWE-476: NULL Pointer Dereference.  */
     diagnostic_metadata m;
@@ -1207,7 +1207,7 @@ public:
 
   bool terminate_path_p () const final override { return true; }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* CWE-476: NULL Pointer Dereference.  */
     auto_diagnostic_group d;
@@ -1264,7 +1264,7 @@ public:
     return OPT_Wanalyzer_use_after_free;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* CWE-416: Use After Free.  */
     diagnostic_metadata m;
@@ -1358,7 +1358,7 @@ public:
     return OPT_Wanalyzer_malloc_leak;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* "CWE-401: Missing Release of Memory after Effective Lifetime".  */
     diagnostic_metadata m;
@@ -1432,7 +1432,7 @@ public:
     return OPT_Wanalyzer_free_of_non_heap;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
@@ -1511,7 +1511,7 @@ public:
     return OPT_Wanalyzer_deref_before_check;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     /* Don't emit the warning if we can't show where the deref
        and the check occur.  */
diff --git a/gcc/analyzer/sm-pattern-test.cc b/gcc/analyzer/sm-pattern-test.cc
index 6c1c95067d8..4c88bcaeb2b 100644
--- a/gcc/analyzer/sm-pattern-test.cc
+++ b/gcc/analyzer/sm-pattern-test.cc
@@ -92,7 +92,7 @@ public:
     return 0;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     return warning_at (rich_loc, get_controlling_option (),
 		       "pattern match on %<%E %s %E%>",
diff --git a/gcc/analyzer/sm-sensitive.cc b/gcc/analyzer/sm-sensitive.cc
index d94d9e03ece..0597e390b3b 100644
--- a/gcc/analyzer/sm-sensitive.cc
+++ b/gcc/analyzer/sm-sensitive.cc
@@ -95,7 +95,8 @@ public:
     return OPT_Wanalyzer_exposure_through_output_file;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc,
+	     logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-532: Information Exposure Through Log Files */
diff --git a/gcc/analyzer/sm-signal.cc b/gcc/analyzer/sm-signal.cc
index ac01f6a4794..e3f90921df9 100644
--- a/gcc/analyzer/sm-signal.cc
+++ b/gcc/analyzer/sm-signal.cc
@@ -114,7 +114,7 @@ public:
     return OPT_Wanalyzer_unsafe_call_within_signal_handler;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
diff --git a/gcc/analyzer/sm-taint.cc b/gcc/analyzer/sm-taint.cc
index f72f194460c..6d28d1f6f65 100644
--- a/gcc/analyzer/sm-taint.cc
+++ b/gcc/analyzer/sm-taint.cc
@@ -211,7 +211,7 @@ public:
     return OPT_Wanalyzer_tainted_array_index;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-129: "Improper Validation of Array Index".  */
@@ -327,7 +327,7 @@ public:
     return OPT_Wanalyzer_tainted_offset;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-823: "Use of Out-of-range Pointer Offset".  */
@@ -437,7 +437,7 @@ public:
     return OPT_Wanalyzer_tainted_size;
   }
 
-  bool emit (rich_location *rich_loc) override
+  bool emit (rich_location *rich_loc, logger *) override
   {
     /* "CWE-129: Improper Validation of Array Index".  */
     diagnostic_metadata m;
@@ -547,9 +547,9 @@ public:
     return "tainted_access_attrib_size";
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *logger) final override
   {
-    bool warned = tainted_size::emit (rich_loc);
+    bool warned = tainted_size::emit (rich_loc, logger);
     if (warned)
       {
 	inform (DECL_SOURCE_LOCATION (m_callee_fndecl),
@@ -583,7 +583,7 @@ public:
     return OPT_Wanalyzer_tainted_divisor;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* CWE-369: "Divide By Zero".  */
@@ -645,7 +645,7 @@ public:
     return OPT_Wanalyzer_tainted_allocation_size;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* "CWE-789: Memory Allocation with Excessive Size Value".  */
@@ -800,7 +800,7 @@ public:
     return OPT_Wanalyzer_tainted_assertion;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     diagnostic_metadata m;
     /* "CWE-617: Reachable Assertion".  */
diff --git a/gcc/analyzer/store.cc b/gcc/analyzer/store.cc
index e8c927b9fe9..dcabce92771 100644
--- a/gcc/analyzer/store.cc
+++ b/gcc/analyzer/store.cc
@@ -236,8 +236,8 @@ bit_range::dump () const
   pp_flush (&pp);
 }
 
-/* If OTHER is a subset of this, return true and write
-   to *OUT the relative range of OTHER within this.
+/* If OTHER is a subset of this, return true and, if OUT is
+   non-null, write to *OUT the relative range of OTHER within this.
    Otherwise return false.  */
 
 bool
@@ -246,8 +246,11 @@ bit_range::contains_p (const bit_range &other, bit_range *out) const
   if (contains_p (other.get_start_bit_offset ())
       && contains_p (other.get_last_bit_offset ()))
     {
-      out->m_start_bit_offset = other.m_start_bit_offset - m_start_bit_offset;
-      out->m_size_in_bits = other.m_size_in_bits;
+      if (out)
+	{
+	  out->m_start_bit_offset = other.m_start_bit_offset - m_start_bit_offset;
+	  out->m_size_in_bits = other.m_size_in_bits;
+	}
       return true;
     }
   else
diff --git a/gcc/analyzer/store.h b/gcc/analyzer/store.h
index 7ded650b608..af6cc7ed03c 100644
--- a/gcc/analyzer/store.h
+++ b/gcc/analyzer/store.h
@@ -350,6 +350,15 @@ struct byte_range
 		      m_size_in_bytes * BITS_PER_UNIT);
   }
 
+  bit_offset_t get_start_bit_offset () const
+  {
+    return m_start_byte_offset * BITS_PER_UNIT;
+  }
+  bit_offset_t get_next_bit_offset () const
+  {
+    return get_next_byte_offset () * BITS_PER_UNIT;
+  }
+
   static int cmp (const byte_range &br1, const byte_range &br2);
 
   byte_offset_t m_start_byte_offset;
diff --git a/gcc/analyzer/varargs.cc b/gcc/analyzer/varargs.cc
index aeea73a3899..72e1b31601c 100644
--- a/gcc/analyzer/varargs.cc
+++ b/gcc/analyzer/varargs.cc
@@ -403,7 +403,7 @@ public:
 	    && 0 == strcmp (m_usage_fnname, other.m_usage_fnname));
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     return warning_at (rich_loc, get_controlling_option (),
@@ -478,7 +478,7 @@ public:
     return va_list_sm_diagnostic::subclass_equal_p (other);
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     return warning_at (rich_loc, get_controlling_option (),
@@ -892,7 +892,7 @@ public:
     return OPT_Wanalyzer_va_arg_type_mismatch;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
@@ -942,7 +942,7 @@ public:
     return OPT_Wanalyzer_va_list_exhausted;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     diagnostic_metadata m;
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index 023a56a647e..18013ebdf96 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -10520,6 +10520,15 @@ offset as well as the capacity is symbolic.
 
 See @uref{https://cwe.mitre.org/data/definitions/119.html, CWE-119: Improper Restriction of Operations within the Bounds of a Memory Buffer}.
 
+For cases where the analyzer is able, it will emit a text art diagram
+visualizing the spatial relationship between the memory region that the
+analyzer predicts would be accessed, versus the range of memory that is
+valid to access: whether they overlap, are touching, are close or far
+apart; which one is before or after in memory, the relative sizes
+involved, the direction of the access (read vs write), and, in some
+cases, the values of data involved.  This diagram can be suppressed
+using @option{-fdiagnostics-text-art-charset=none}.
+
 @opindex Wanalyzer-possible-null-argument
 @opindex Wno-analyzer-possible-null-argument
 @item -Wno-analyzer-possible-null-argument
@@ -11030,6 +11039,12 @@ following warnings from @option{-fanalyzer}:
 -Wanalyzer-va-list-use-after-va-end
 }
 
+@opindex fanalyzer-debug-text-art
+@opindex fno-analyzer-debug-text-art
+@item -fanalyzer-debug-text-art-headings
+This option is intended for analyzer developers.  If enabled,
+the analyzer will add extra annotations to any diagrams it generates.
+
 @opindex fanalyzer-feasibility
 @opindex fno-analyzer-feasibility
 @item -fno-analyzer-feasibility
diff --git a/gcc/testsuite/gcc.dg/analyzer/data-model-1.c b/gcc/testsuite/gcc.dg/analyzer/data-model-1.c
index 86d1ccf11a7..3c4a45f4a4a 100644
--- a/gcc/testsuite/gcc.dg/analyzer/data-model-1.c
+++ b/gcc/testsuite/gcc.dg/analyzer/data-model-1.c
@@ -240,8 +240,8 @@ void test_16 (void)
   __analyzer_eval (strlen (msg) == 11); /* { dg-warning "TRUE" } */
 
   /* Out-of-bounds.  */
-  __analyzer_eval (msg[100] == 'e'); /* { dg-warning "UNKNOWN" } */
-  // TODO: some kind of warning for the out-of-bounds access
+  __analyzer_eval (msg[100] == 'e'); /* { dg-warning "UNKNOWN" "eval result" } */
+  /* { dg-warning "buffer over-read" "out-of-bounds" { target *-*-* } .-1 } */
 }
 
 static const char *__attribute__((noinline))
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-ascii.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-ascii.c
new file mode 100644
index 00000000000..5e6eadcf27d
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-ascii.c
@@ -0,0 +1,55 @@
+/* { dg-additional-options "-fdiagnostics-show-path-depths" } */
+/* { dg-additional-options "-fdiagnostics-path-format=inline-events -fdiagnostics-show-caret" } */
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii" } */
+
+#include <stdint.h>
+
+int32_t arr[10];
+
+void int_arr_write_element_after_end_off_by_one(int32_t x)
+{
+  arr[10] = x;  /* { dg-line line } */
+}
+/* { dg-warning "buffer overflow" "warning" { target *-*-* } line } */
+/* { dg-message "valid subscripts for 'arr' are '\\\[0\\\]' to '\\\[9\\\]'" "valid subscript note" { target *-*-* } line } */
+
+
+/* { dg-begin-multiline-output "" }
+   arr[10] = x;
+   ~~~~~~~~^~~
+  event 1 (depth 0)
+    |
+    | int32_t arr[10];
+    |         ^~~
+    |         |
+    |         (1) capacity: 40 bytes
+    |
+    +--> 'int_arr_write_element_after_end_off_by_one': event 2 (depth 1)
+           |
+           |   arr[10] = x;
+           |   ~~~~~~~~^~~
+           |           |
+           |           (2) out-of-bounds write from byte 40 till byte 43 but 'arr' ends at byte 40
+           |
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+                                        +--------------------------------+
+                                        |write from 'x' (type: 'int32_t')|
+                                        +--------------------------------+
+                                                        |
+                                                        |
+                                                        v
+  +--------+-----------------+---------++--------------------------------+
+  |  [0]   |       ...       |   [9]   ||                                |
+  +--------+-----------------+---------+|       after valid range        |
+  |    'arr' (type: 'int32_t[10]')     ||                                |
+  +------------------------------------++--------------------------------+
+  |~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~||~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|
+                    |                                   |
+          +---------+--------+                +---------+---------+
+          |capacity: 40 bytes|                |overflow of 4 bytes|
+          +------------------+                +-------------------+
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-debug.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-debug.c
new file mode 100644
index 00000000000..4c4d9d1b867
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-debug.c
@@ -0,0 +1,40 @@
+/* Test of -fanalyzer-debug-text-art.  */
+
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fanalyzer-debug-text-art" } */
+
+#include <stdint.h>
+
+int32_t arr[10];
+
+void int_arr_write_element_after_end_off_by_one(int32_t x)
+{
+  arr[10] = x;  /* { dg-line line } */
+}
+/* { dg-warning "buffer overflow" "warning" { target *-*-* } line } */
+/* { dg-message "valid subscripts for 'arr' are '\\\[0\\\]' to '\\\[9\\\]'" "valid subscript note" { target *-*-* } line } */
+
+/* { dg-begin-multiline-output "" }
+
+  +---------+-----------+-----------+---+--------------------------------+
+  |   tc0   |    tc1    |    tc2    |tc3|              tc4               |
+  +---------+-----------+-----------+---+--------------------------------+
+  |bytes 0-3|bytes 4-35 |bytes 36-39|   |          bytes 40-43           |
+  +---------+-----------+-----------+   +--------------------------------+
+                                        +--------------------------------+
+                                        |write from 'x' (type: 'int32_t')|
+                                        +--------------------------------+
+                                                        |
+                                                        |
+                                                        v
+  +---------+-----------+-----------+   +--------------------------------+
+  |   [0]   |    ...    |    [9]    |   |                                |
+  +---------+-----------+-----------+   |       after valid range        |
+  |   'arr' (type: 'int32_t[10]')   |   |                                |
+  +---------------------------------+   +--------------------------------+
+  |~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|   |~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~|
+                   |                                    |
+         +---------+--------+                 +---------+---------+
+         |capacity: 40 bytes|                 |overflow of 4 bytes|
+         +------------------+                 +-------------------+
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-emoji.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-emoji.c
new file mode 100644
index 00000000000..1c6125225ff
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-emoji.c
@@ -0,0 +1,55 @@
+/* { dg-additional-options "-fdiagnostics-show-path-depths" } */
+/* { dg-additional-options "-fdiagnostics-path-format=inline-events -fdiagnostics-show-caret" } */
+/* { dg-additional-options "-fdiagnostics-text-art-charset=emoji" } */
+
+#include <stdint.h>
+
+int32_t arr[10];
+
+void int_arr_write_element_after_end_off_by_one(int32_t x)
+{
+  arr[10] = x;  /* { dg-line line } */
+}
+/* { dg-warning "buffer overflow" "warning" { target *-*-* } line } */
+/* { dg-message "valid subscripts for 'arr' are '\\\[0\\\]' to '\\\[9\\\]'" "valid subscript note" { target *-*-* } line } */
+
+
+/* { dg-begin-multiline-output "" }
+   arr[10] = x;
+   ~~~~~~~~^~~
+  event 1 (depth 0)
+    |
+    | int32_t arr[10];
+    |         ^~~
+    |         |
+    |         (1) capacity: 40 bytes
+    |
+    +--> 'int_arr_write_element_after_end_off_by_one': event 2 (depth 1)
+           |
+           |   arr[10] = x;
+           |   ~~~~~~~~^~~
+           |           |
+           |           (2) out-of-bounds write from byte 40 till byte 43 but 'arr' ends at byte 40
+           |
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+                                        ┌────────────────────────────────┐
+                                        │write from 'x' (type: 'int32_t')│
+                                        └────────────────────────────────┘
+                                                        │
+                                                        │
+                                                        v
+  ┌────────┬─────────────────┬─────────┐┌────────────────────────────────┐
+  │  [0]   │       ...       │   [9]   ││                                │
+  ├────────┴─────────────────┴─────────┤│       after valid range        │
+  │    'arr' (type: 'int32_t[10]')     ││                                │
+  └────────────────────────────────────┘└────────────────────────────────┘
+  ├─────────────────┬──────────────────┤├───────────────┬────────────────┤
+                    │                                   │
+          ╭─────────┴────────╮              ╭───────────┴──────────╮
+          │capacity: 40 bytes│              │⚠️  overflow of 4 bytes│
+          ╰──────────────────╯              ╰──────────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-json.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-json.c
new file mode 100644
index 00000000000..0a2cc3493ab
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-json.c
@@ -0,0 +1,13 @@
+/* { dg-additional-options "-fdiagnostics-format=json-file" } */
+
+/* The custom JSON format doesn't support text art, so this is just a simple
+   smoketext.  */
+
+#include <stdint.h>
+
+int32_t arr[10];
+
+void int_arr_write_element_after_end_off_by_one(int32_t x)
+{
+  arr[10] = x;
+}
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-sarif.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-sarif.c
new file mode 100644
index 00000000000..051a1ceb463
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-sarif.c
@@ -0,0 +1,24 @@
+/* We require -fdiagnostics-text-art-charset= to get any text art here
+   because of the test suite using -fdiagnostics-plain-output.  */
+
+/* { dg-additional-options "-fdiagnostics-format=sarif-file -fdiagnostics-text-art-charset=ascii" } */
+
+#include <stdint.h>
+
+int32_t arr[10];
+
+void int_arr_write_element_after_end_off_by_one(int32_t x)
+{
+  arr[10] = x;
+}
+
+/* Verify that some JSON was written to a file with the expected name.
+
+   { dg-final { verify-sarif-file } }
+
+   Expect the "alt-text" to be captured.
+     { dg-final { scan-sarif-file "\"text\": \"Diagram visualizing the predicted out-of-bounds access\"," } }
+
+   Expect the diagram to have 4 leading spaces (to indicate a code block),
+   and that at least part of the diagram was written out.
+     { dg-final { scan-sarif-file "\"markdown\": \"    .*capacity: 40 bytes.*\"" } } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-unicode.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-unicode.c
new file mode 100644
index 00000000000..71f66ff87c9
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-1-unicode.c
@@ -0,0 +1,55 @@
+/* { dg-additional-options "-fdiagnostics-show-path-depths" } */
+/* { dg-additional-options "-fdiagnostics-path-format=inline-events -fdiagnostics-show-caret" } */
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdint.h>
+
+int32_t arr[10];
+
+void int_arr_write_element_after_end_off_by_one(int32_t x)
+{
+  arr[10] = x;  /* { dg-line line } */
+}
+/* { dg-warning "buffer overflow" "warning" { target *-*-* } line } */
+/* { dg-message "valid subscripts for 'arr' are '\\\[0\\\]' to '\\\[9\\\]'" "valid subscript note" { target *-*-* } line } */
+
+
+/* { dg-begin-multiline-output "" }
+   arr[10] = x;
+   ~~~~~~~~^~~
+  event 1 (depth 0)
+    |
+    | int32_t arr[10];
+    |         ^~~
+    |         |
+    |         (1) capacity: 40 bytes
+    |
+    +--> 'int_arr_write_element_after_end_off_by_one': event 2 (depth 1)
+           |
+           |   arr[10] = x;
+           |   ~~~~~~~~^~~
+           |           |
+           |           (2) out-of-bounds write from byte 40 till byte 43 but 'arr' ends at byte 40
+           |
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+                                        ┌────────────────────────────────┐
+                                        │write from 'x' (type: 'int32_t')│
+                                        └────────────────────────────────┘
+                                                        │
+                                                        │
+                                                        v
+  ┌────────┬─────────────────┬─────────┐┌────────────────────────────────┐
+  │  [0]   │       ...       │   [9]   ││                                │
+  ├────────┴─────────────────┴─────────┤│       after valid range        │
+  │    'arr' (type: 'int32_t[10]')     ││                                │
+  └────────────────────────────────────┘└────────────────────────────────┘
+  ├─────────────────┬──────────────────┤├───────────────┬────────────────┤
+                    │                                   │
+          ╭─────────┴────────╮                ╭─────────┴─────────╮
+          │capacity: 40 bytes│                │overflow of 4 bytes│
+          ╰──────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-10.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-10.c
new file mode 100644
index 00000000000..4a7b8e306de
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-10.c
@@ -0,0 +1,29 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+int32_t int_vla_write_element_symbolic_before_start (int32_t x, size_t n)
+{
+  int32_t arr[n]; /* { dg-message "\\(1\\) capacity: 'n \\* 4' bytes" } */
+  arr[-2] = 42;  /* { dg-warning "stack-based buffer underwrite" } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+  ┌───────────────────┐
+  │write of '(int) 42'│
+  └───────────────────┘
+            │
+            │
+            v
+  ┌───────────────────┐                 ┌────────────────────────────────┐
+  │before valid range │                 │buffer allocated on stack at (1)│
+  └───────────────────┘                 └────────────────────────────────┘
+  ├─────────┬─────────┤├───────┬───────┤├───────────────┬────────────────┤
+            │                  │                        │
+  ╭─────────┴───────────╮  ╭───┴───╮        ╭───────────┴───────────╮
+  │underwrite of 4 bytes│  │4 bytes│        │capacity: 'n * 4' bytes│
+  ╰─────────────────────╯  ╰───────╯        ╰───────────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-11.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-11.c
new file mode 100644
index 00000000000..f8eb1580eb3
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-11.c
@@ -0,0 +1,82 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+/* { dg-require-effective-target alloca } */
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+
+void test6 (size_t size)
+{
+  int32_t *buf = __builtin_alloca (4 * size);
+  memset (buf, 0, 4 * size);
+  int32_t last = *(buf + 4 * size); /* { dg-warning "stack-based buffer over-read" } */
+}
+
+/* (size * 16) - (size * 4) leads to a gap of (size * 12).  */
+
+/* { dg-begin-multiline-output "" }
+
+                                                       ┌─────────────────┐
+                                                       │ read of 4 bytes │
+                                                       └─────────────────┘
+                                                                ^
+                                                                │
+                                                                │
+  ┌────────────────────────────────┐                   ┌─────────────────┐
+  │buffer allocated on stack at (1)│                   │after valid range│
+  └────────────────────────────────┘                   └─────────────────┘
+  ├───────────────┬────────────────┤├────────┬────────┤├────────┬────────┤
+                  │                          │                  │
+                  │                          │       ╭──────────┴─────────╮
+                  │                          │       │over-read of 4 bytes│
+                  │                          │       ╰────────────────────╯
+      ╭───────────┴──────────╮      ╭────────┴────────╮
+      │size: 'size * 4' bytes│      │'size * 12' bytes│
+      ╰──────────────────────╯      ╰─────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+void test7 (size_t size)
+{
+  int32_t *buf = __builtin_alloca (4 * size + 3); /* { dg-warning "allocated buffer size is not a multiple of the pointee's size" } */
+  buf[size] = 42; /* { dg-warning "stack-based buffer overflow" } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+                                 ┌───────────────────────────────────────┐
+                                 │          write of '(int) 42'          │
+                                 └───────────────────────────────────────┘
+                                           │                   │
+                                           │                   │
+                                           v                   v
+  ┌──────────────────────────────────────────────────┐┌──────────────────┐
+  │         buffer allocated on stack at (1)         ││after valid range │
+  └──────────────────────────────────────────────────┘└──────────────────┘
+  ├────────────────────────┬─────────────────────────┤├────────┬─────────┤
+                           │                                   │
+           ╭───────────────┴──────────────╮          ╭─────────┴────────╮
+           │capacity: 'size * 4 + 3' bytes│          │overflow of 1 byte│
+           ╰──────────────────────────────╯          ╰──────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+
+/* We're currently not able to generate a diagram for this case;
+   make sure we handle this case gracefully.  */
+
+char *test99 (const char *x, const char *y)
+{
+  size_t len_x = __builtin_strlen (x);
+  size_t len_y = __builtin_strlen (y);
+  /* BUG (root cause): forgot to add 1 for terminator.  */
+  size_t sz = len_x + len_y;
+  char *result = __builtin_malloc (sz);
+  if (!result)
+    return NULL;
+  __builtin_memcpy (result, x, len_x);
+  __builtin_memcpy (result + len_x, y, len_y);
+  /* BUG (symptom): off-by-one out-of-bounds write to heap.  */
+  result[len_x + len_y] = '\0'; /* { dg-warning "heap-based buffer overflow" } */
+  return result;
+}
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-12.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-12.c
new file mode 100644
index 00000000000..5eb81f9fd98
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-12.c
@@ -0,0 +1,54 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+/* { dg-require-effective-target alloca } */
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+void test8 (size_t size, size_t offset)
+{
+  char src[size];
+  char dst[size];
+  memcpy (dst, src, size + offset); /* { dg-line test8 } */
+  /* { dg-warning "over-read" "warning" { target *-*-* } test8 } */
+  /* { dg-warning "use of uninitialized value" "warning" { target *-*-* } test8 } */
+  /* { dg-warning "overflow" "warning" { target *-*-* } test8 } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────────────────────────────────────────────────────────────────────┐
+  │                    read of 'size + offset' bytes                     │
+  └──────────────────────────────────────────────────────────────────────┘
+                   ^                                   ^
+                   │                                   │
+                   │                                   │
+  ┌──────────────────────────────────┐┌──────────────────────────────────┐
+  │ buffer allocated on stack at (1) ││        after valid range         │
+  └──────────────────────────────────┘└──────────────────────────────────┘
+  ├────────────────┬─────────────────┤├────────────────┬─────────────────┤
+                   │                                   │
+         ╭─────────┴────────╮            ╭─────────────┴─────────────╮
+         │size: 'size' bytes│            │over-read of 'offset' bytes│
+         ╰──────────────────╯            ╰───────────────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────────────────────────────────────────────────────────────────────┐
+  │                    write of 'size + offset' bytes                    │
+  └──────────────────────────────────────────────────────────────────────┘
+                   │                                   │
+                   │                                   │
+                   v                                   v
+  ┌──────────────────────────────────┐┌──────────────────────────────────┐
+  │ buffer allocated on stack at (1) ││        after valid range         │
+  └──────────────────────────────────┘└──────────────────────────────────┘
+  ├────────────────┬─────────────────┤├────────────────┬─────────────────┤
+                   │                                   │
+       ╭───────────┴──────────╮          ╭─────────────┴────────────╮
+       │capacity: 'size' bytes│          │overflow of 'offset' bytes│
+       ╰──────────────────────╯          ╰──────────────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-13.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-13.c
new file mode 100644
index 00000000000..dcd1263dfbb
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-13.c
@@ -0,0 +1,43 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <string.h>
+
+void
+test_non_ascii ()
+{
+  char buf[9];
+  strcpy (buf, "Liberté\n"); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "'__builtin_memcpy' writing 10 bytes into a region of size 9 overflows the destination" "" { target *-*-* } .-1 } */
+}
+
+/* Example of non-ASCII UTF-8 that's short enough to fully quote, whilst
+   containing control characters.  */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────┬──────┬──────┬──────┬──────┬──────┬────┬────┬──────┐┌─────────────────┐
+  │ [0]  │ [1]  │ [2]  │ [3]  │ [4]  │ [5]  │[6] │[7] │ [8]  ││       [9]       │
+  ├──────┼──────┼──────┼──────┼──────┼──────┼────┼────┼──────┤├─────────────────┤
+  │ 0x4c │ 0x69 │ 0x62 │ 0x65 │ 0x72 │ 0x74 │0xc3│0xa9│ 0x0a ││      0x00       │
+  ├──────┼──────┼──────┼──────┼──────┼──────┼────┴────┼──────┤├─────────────────┤
+  │U+004c│U+0069│U+0062│U+0065│U+0072│U+0074│ U+00e9  │U+000a││     U+0000      │
+  ├──────┼──────┼──────┼──────┼──────┼──────┼─────────┼──────┤├─────────────────┤
+  │  L   │  i   │  b   │  e   │  r   │  t   │    é    │      ││       NUL       │
+  ├──────┴──────┴──────┴──────┴──────┴──────┴─────────┴──────┴┴─────────────────┤
+  │                      string literal (type: 'char[10]')                      │
+  └─────────────────────────────────────────────────────────────────────────────┘
+     │      │      │      │      │      │     │    │     │             │
+     │      │      │      │      │      │     │    │     │             │
+     v      v      v      v      v      v     v    v     v             v
+  ┌──────┬────────────────────────────────────────────┬──────┐┌─────────────────┐
+  │ [0]  │                    ...                     │ [8]  ││                 │
+  ├──────┴────────────────────────────────────────────┴──────┤│after valid range│
+  │                 'buf' (type: 'char[9]')                  ││                 │
+  └──────────────────────────────────────────────────────────┘└─────────────────┘
+  ├────────────────────────────┬─────────────────────────────┤├────────┬────────┤
+                               │                                       │
+                      ╭────────┴────────╮                    ╭─────────┴────────╮
+                      │capacity: 9 bytes│                    │overflow of 1 byte│
+                      ╰─────────────────╯                    ╰──────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-14.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-14.c
new file mode 100644
index 00000000000..3cedf06d258
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-14.c
@@ -0,0 +1,110 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdint.h>
+
+extern int32_t arr_0[0]; /* { dg-message "capacity: 0 bytes" } */
+extern int32_t arr_1[1]; /* { dg-message "capacity: 4 bytes" } */
+extern int32_t arr_2[2]; /* { dg-message "capacity: 8 bytes" } */
+extern int32_t arr_3[3]; /* { dg-message "capacity: 12 bytes" } */
+extern int32_t arr_4[4]; /* { dg-message "capacity: 16 bytes" } */
+
+void test_immediately_after (int x)
+{
+  arr_0[0] = x; /* { dg-warning "buffer overflow" } */
+  arr_1[1] = x; /* { dg-warning "buffer overflow" } */
+  arr_2[2] = x; /* { dg-warning "buffer overflow" } */
+  arr_3[3] = x; /* { dg-warning "buffer overflow" } */
+  arr_4[4] = x; /* { dg-warning "buffer overflow" } */
+}
+
+/* Expect no diagram for the arr_0 case: there's no valid region 
+to write to.  */
+
+/* The arr_1 case.  */
+/* { dg-begin-multiline-output "" }
+
+                                      ┌──────────────────────────────────┐
+                                      │   write from 'x' (type: 'int')   │
+                                      └──────────────────────────────────┘
+                                                       │
+                                                       │
+                                                       v
+  ┌──────────────────────────────────┐┌──────────────────────────────────┐
+  │               [0]                ││                                  │
+  ├──────────────────────────────────┤│        after valid range         │
+  │   'arr_1' (type: 'int32_t[1]')   ││                                  │
+  └──────────────────────────────────┘└──────────────────────────────────┘
+  ├────────────────┬─────────────────┤├────────────────┬─────────────────┤
+                   │                                   │
+          ╭────────┴────────╮                ╭─────────┴─────────╮
+          │capacity: 4 bytes│                │overflow of 4 bytes│
+          ╰─────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* The arr_2 case.  */
+/* { dg-begin-multiline-output "" }
+
+                                            ┌────────────────────────────┐
+                                            │write from 'x' (type: 'int')│
+                                            └────────────────────────────┘
+                                                          │
+                                                          │
+                                                          v
+  ┌────────────────────┬───────────────────┐┌────────────────────────────┐
+  │        [0]         │        [1]        ││                            │
+  ├────────────────────┴───────────────────┤│     after valid range      │
+  │      'arr_2' (type: 'int32_t[2]')      ││                            │
+  └────────────────────────────────────────┘└────────────────────────────┘
+  ├───────────────────┬────────────────────┤├─────────────┬──────────────┤
+                      │                                   │
+             ╭────────┴────────╮                ╭─────────┴─────────╮
+             │capacity: 8 bytes│                │overflow of 4 bytes│
+             ╰─────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* The arr_3 case.  */
+// Perhaps we should show [1] rather than ellipsize here.
+/* { dg-begin-multiline-output "" }
+
+                                            ┌────────────────────────────┐
+                                            │write from 'x' (type: 'int')│
+                                            └────────────────────────────┘
+                                                          │
+                                                          │
+                                                          v
+  ┌─────────────┬─────────────┬────────────┐┌────────────────────────────┐
+  │     [0]     │     ...     │    [2]     ││                            │
+  ├─────────────┴─────────────┴────────────┤│     after valid range      │
+  │      'arr_3' (type: 'int32_t[3]')      ││                            │
+  └────────────────────────────────────────┘└────────────────────────────┘
+  ├───────────────────┬────────────────────┤├─────────────┬──────────────┤
+                      │                                   │
+            ╭─────────┴────────╮                ╭─────────┴─────────╮
+            │capacity: 12 bytes│                │overflow of 4 bytes│
+            ╰──────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* The arr_4 case.  */
+/* { dg-begin-multiline-output "" }
+
+                                            ┌────────────────────────────┐
+                                            │write from 'x' (type: 'int')│
+                                            └────────────────────────────┘
+                                                          │
+                                                          │
+                                                          v
+  ┌──────────┬──────────────────┬──────────┐┌────────────────────────────┐
+  │   [0]    │       ...        │   [3]    ││                            │
+  ├──────────┴──────────────────┴──────────┤│     after valid range      │
+  │      'arr_4' (type: 'int32_t[4]')      ││                            │
+  └────────────────────────────────────────┘└────────────────────────────┘
+  ├───────────────────┬────────────────────┤├─────────────┬──────────────┤
+                      │                                   │
+            ╭─────────┴────────╮                ╭─────────┴─────────╮
+            │capacity: 16 bytes│                │overflow of 4 bytes│
+            ╰──────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-15.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-15.c
new file mode 100644
index 00000000000..e2a6381d37e
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-15.c
@@ -0,0 +1,42 @@
+/* Regression test for ICE with short values of
+   --param=analyzer-text-art-string-ellipsis-threshold=.  */
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode --param=analyzer-text-art-string-ellipsis-threshold=0" } */
+
+#include <string.h>
+
+void
+test_non_ascii ()
+{
+  char buf[9];
+  strcpy (buf, "Liberté\n"); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "'__builtin_memcpy' writing 10 bytes into a region of size 9 overflows the destination" "" { target *-*-* } .-1 } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────┬──────┬──────┬──────┬──────┬──────┬────┬────┬──────┐┌─────────────────┐
+  │ [0]  │ [1]  │ [2]  │ [3]  │ [4]  │ [5]  │[6] │[7] │ [8]  ││       [9]       │
+  ├──────┼──────┼──────┼──────┼──────┼──────┼────┼────┼──────┤├─────────────────┤
+  │ 0x4c │ 0x69 │ 0x62 │ 0x65 │ 0x72 │ 0x74 │0xc3│0xa9│ 0x0a ││      0x00       │
+  ├──────┼──────┼──────┼──────┼──────┼──────┼────┴────┼──────┤├─────────────────┤
+  │U+004c│U+0069│U+0062│U+0065│U+0072│U+0074│ U+00e9  │U+000a││     U+0000      │
+  ├──────┼──────┼──────┼──────┼──────┼──────┼─────────┼──────┤├─────────────────┤
+  │  L   │  i   │  b   │  e   │  r   │  t   │    é    │      ││       NUL       │
+  ├──────┴──────┴──────┴──────┴──────┴──────┴─────────┴──────┴┴─────────────────┤
+  │                      string literal (type: 'char[10]')                      │
+  └─────────────────────────────────────────────────────────────────────────────┘
+     │      │      │      │      │      │     │    │     │             │
+     │      │      │      │      │      │     │    │     │             │
+     v      v      v      v      v      v     v    v     v             v
+  ┌──────┬────────────────────────────────────────────┬──────┐┌─────────────────┐
+  │ [0]  │                    ...                     │ [8]  ││                 │
+  ├──────┴────────────────────────────────────────────┴──────┤│after valid range│
+  │                 'buf' (type: 'char[9]')                  ││                 │
+  └──────────────────────────────────────────────────────────┘└─────────────────┘
+  ├────────────────────────────┬─────────────────────────────┤├────────┬────────┤
+                               │                                       │
+                      ╭────────┴────────╮                    ╭─────────┴────────╮
+                      │capacity: 9 bytes│                    │overflow of 1 byte│
+                      ╰─────────────────╯                    ╰──────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-2.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-2.c
new file mode 100644
index 00000000000..535dab1eb9e
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-2.c
@@ -0,0 +1,30 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+void int_vla_write_element_after_end_off_by_one(int32_t x, size_t n)
+{
+  int32_t arr[n]; /* { dg-message "\\(1\\) capacity: 'n \\* 4' bytes" } */
+
+  arr[n] = x;  /* { dg-warning "stack-based buffer overflow" } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+                                      ┌──────────────────────────────────┐
+                                      │ write from 'x' (type: 'int32_t') │
+                                      └──────────────────────────────────┘
+                                                       │
+                                                       │
+                                                       v
+  ┌──────────────────────────────────┐┌──────────────────────────────────┐
+  │ buffer allocated on stack at (1) ││        after valid range         │
+  └──────────────────────────────────┘└──────────────────────────────────┘
+  ├────────────────┬─────────────────┤├────────────────┬─────────────────┤
+                   │                                   │
+       ╭───────────┴───────────╮             ╭─────────┴─────────╮
+       │capacity: 'n * 4' bytes│             │overflow of 4 bytes│
+       ╰───────────────────────╯             ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-3.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-3.c
new file mode 100644
index 00000000000..064f3fad47e
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-3.c
@@ -0,0 +1,45 @@
+/* The multiline output assumes sizeof(size_t) == 8.
+   { dg-require-effective-target lp64 } */
+
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+
+struct str {
+  size_t len;
+  char data[];
+};
+
+struct str *
+make_str_badly (const char *src)
+{
+  size_t len = strlen(src);
+  struct str *str = malloc(sizeof(str) + len); /* { dg-message "\\(1\\) capacity: 'len \\+ 8' bytes" } */
+  if (!str)
+    return NULL;
+  str->len = len;
+  memcpy(str->data, src, len);
+  str->data[len] = '\0'; /* { dg-warning "heap-based buffer overflow" } */
+  return str;
+}
+
+/* { dg-begin-multiline-output "" }
+
+                                      ┌──────────────────────────────────┐
+                                      │       write of '(char) 0'        │
+                                      └──────────────────────────────────┘
+                                                       │
+                                                       │
+                                                       v
+  ┌──────────────────────────────────┐┌──────────────────────────────────┐
+  │ buffer allocated on heap at (1)  ││        after valid range         │
+  └──────────────────────────────────┘└──────────────────────────────────┘
+  ├────────────────┬─────────────────┤├────────────────┬─────────────────┤
+                   │                                   │
+      ╭────────────┴────────────╮            ╭─────────┴────────╮
+      │capacity: 'len + 8' bytes│            │overflow of 1 byte│
+      ╰─────────────────────────╯            ╰──────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-4.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-4.c
new file mode 100644
index 00000000000..ec8e4abd337
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-4.c
@@ -0,0 +1,45 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <string.h>
+
+#define LOREM_IPSUM \
+  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" \
+  " tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim" \
+  " veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea" \
+  " commodo consequat. Duis aute irure dolor in reprehenderit in voluptate" \
+  " velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint" \
+  " occaecat cupidatat non proident, sunt in culpa qui officia deserunt" \
+  " mollit anim id est laborum."
+
+void
+test_long_string ()
+{
+  char buf[100];
+  strcpy (buf, LOREM_IPSUM); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "'__builtin_memcpy' writing 446 bytes into a region of size 100 overflows the destination" "" { target *-*-* } .-1 } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+  ┌───┬───┬───┬───┬───┬───┬──────────┬─────┬─────┬─────┬─────┬─────┬─────┐
+  │[0]│[1]│[2]│[3]│[4]│[5]│          │[440]│[441]│[442]│[443]│[444]│[445]│
+  ├───┼───┼───┼───┼───┼───┤   ...    ├─────┼─────┼─────┼─────┼─────┼─────┤
+  │'L'│'o'│'r'│'e'│'m'│' '│          │ 'o' │ 'r' │ 'u' │ 'm' │ '.' │ NUL │
+  ├───┴───┴───┴───┴───┴───┴──────────┴─────┴─────┴─────┴─────┴─────┴─────┤
+  │                  string literal (type: 'char[446]')                  │
+  └──────────────────────────────────────────────────────────────────────┘
+    │   │   │   │   │   │  │  │    │    │     │     │     │     │     │
+    │   │   │   │   │   │  │  │    │    │     │     │     │     │     │
+    v   v   v   v   v   v  v  v    v    v     v     v     v     v     v
+  ┌───┬─────────────────────┬────┐┌──────────────────────────────────────┐
+  │[0]│         ...         │[99]││                                      │
+  ├───┴─────────────────────┴────┤│          after valid range           │
+  │  'buf' (type: 'char[100]')   ││                                      │
+  └──────────────────────────────┘└──────────────────────────────────────┘
+  ├──────────────┬───────────────┤├──────────────────┬───────────────────┤
+                 │                                   │
+       ╭─────────┴─────────╮              ╭──────────┴──────────╮
+       │capacity: 100 bytes│              │overflow of 346 bytes│
+       ╰───────────────────╯              ╰─────────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-ascii.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-ascii.c
new file mode 100644
index 00000000000..e82bce98fd1
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-ascii.c
@@ -0,0 +1,40 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii" } */
+
+#include <string.h>
+
+void
+test_non_ascii ()
+{
+  char buf[5];
+  strcpy (buf, "文字化け"); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "'__builtin_memcpy' writing 13 bytes into a region of size 5 overflows the destination" "" { target *-*-* } .-1 } */
+}
+
+/* Without unicode support, we shouldn't show the printable unicode chars.  */
+
+/* { dg-begin-multiline-output "" }
+
+  +-----+-----+-----+----+----++----+----+----+----+----+----+----+------+
+  | [0] | [1] | [2] |[3] |[4] ||[5] |[6] |[7] |[8] |[9] |[10]|[11]| [12] |
+  +-----+-----+-----+----+----++----+----+----+----+----+----+----+------+
+  |0xe6 |0x96 |0x87 |0xe5|0xad||0x97|0xe5|0x8c|0x96|0xe3|0x81|0x91| 0x00 |
+  +-----+-----+-----+----+----++----+----+----+----+----+----+----+------+
+  |     U+6587      |    U+5b57     |    U+5316    |    U+3051    |U+0000|
+  +-----------------+---------------+--------------+--------------+------+
+  |                  string literal (type: 'char[13]')                   |
+  +----------------------------------------------------------------------+
+     |     |     |    |    |     |    |    |    |    |    |    |     |
+     |     |     |    |    |     |    |    |    |    |    |    |     |
+     v     v     v    v    v     v    v    v    v    v    v    v     v
+  +-----+----------------+----++-----------------------------------------+
+  | [0] |      ...       |[4] ||                                         |
+  +-----+----------------+----+|            after valid range            |
+  |  'buf' (type: 'char[5]')  ||                                         |
+  +---------------------------++-----------------------------------------+
+  |~~~~~~~~~~~~~+~~~~~~~~~~~~~||~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~|
+                |                                   |
+       +--------+--------+                +---------+---------+
+       |capacity: 5 bytes|                |overflow of 8 bytes|
+       +-----------------+                +-------------------+
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-unicode.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-unicode.c
new file mode 100644
index 00000000000..48fa12fd483
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-5-unicode.c
@@ -0,0 +1,42 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <string.h>
+
+void
+test_non_ascii ()
+{
+  char buf[5];
+  strcpy (buf, "文字化け"); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "'__builtin_memcpy' writing 13 bytes into a region of size 5 overflows the destination" "" { target *-*-* } .-1 } */
+}
+
+/* With unicode support, we should show the printable unicode chars.  */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌─────┬─────┬─────┬────┬────┐┌────┬────┬────┬────┬────┬────┬────┬──────┐
+  │ [0] │ [1] │ [2] │[3] │[4] ││[5] │[6] │[7] │[8] │[9] │[10]│[11]│ [12] │
+  ├─────┼─────┼─────┼────┼────┤├────┼────┼────┼────┼────┼────┼────┼──────┤
+  │0xe6 │0x96 │0x87 │0xe5│0xad││0x97│0xe5│0x8c│0x96│0xe3│0x81│0x91│ 0x00 │
+  ├─────┴─────┴─────┼────┴────┴┴────┼────┴────┴────┼────┴────┴────┼──────┤
+  │     U+6587      │    U+5b57     │    U+5316    │    U+3051    │U+0000│
+  ├─────────────────┼───────────────┼──────────────┼──────────────┼──────┤
+  │       文        │      字       │      化      │      け      │ NUL  │
+  ├─────────────────┴───────────────┴──────────────┴──────────────┴──────┤
+  │                  string literal (type: 'char[13]')                   │
+  └──────────────────────────────────────────────────────────────────────┘
+     │     │     │    │    │     │    │    │    │    │    │    │     │
+     │     │     │    │    │     │    │    │    │    │    │    │     │
+     v     v     v    v    v     v    v    v    v    v    v    v     v
+  ┌─────┬────────────────┬────┐┌─────────────────────────────────────────┐
+  │ [0] │      ...       │[4] ││                                         │
+  ├─────┴────────────────┴────┤│            after valid range            │
+  │  'buf' (type: 'char[5]')  ││                                         │
+  └───────────────────────────┘└─────────────────────────────────────────┘
+  ├─────────────┬─────────────┤├────────────────────┬────────────────────┤
+                │                                   │
+       ╭────────┴────────╮                ╭─────────┴─────────╮
+       │capacity: 5 bytes│                │overflow of 8 bytes│
+       ╰─────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
new file mode 100644
index 00000000000..25bf9d53b2b
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-6.c
@@ -0,0 +1,125 @@
+/* { dg-require-effective-target lp64 }
+   Misbehaves with -m32 due to optimization turning the pointer arithmetic into:
+      _2 = &buf + 4294967246;
+      memcpy (_2, _1, 4096);
+*/
+
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <string.h>
+
+#define LOREM_IPSUM \
+  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod" \
+  " tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim" \
+  " veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea" \
+  " commodo consequat. Duis aute irure dolor in reprehenderit in voluptate" \
+  " velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint" \
+  " occaecat cupidatat non proident, sunt in culpa qui officia deserunt" \
+  " mollit anim id est laborum."
+
+/* This memcpy reads from both before and after the bounds of the
+   string literal, and writes to both before and after the bounds of "buf".  */
+
+void
+test_bad_memcpy ()
+{
+  char buf[100];
+  memcpy (buf - 50, LOREM_IPSUM - 100, 4096); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "stack-based buffer underwrite" "" { target *-*-* } .-1 } */
+  /* { dg-warning "buffer under-read" "" { target *-*-* } .-2 } */
+  /* { dg-warning "buffer over-read" "" { target *-*-* } .-3 } */
+  /* { dg-warning "'memcpy' writing 4096 bytes into a region of size 0 overflows the destination" "" { target *-*-* } .-4 } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+  ┌─────────────────────────────────────────────────────────────────────────┐
+  │                           read of 4096 bytes                            │
+  └─────────────────────────────────────────────────────────────────────────┘
+           ^               ^          ^           ^                ^
+           │               │          │           │                │
+           │               │          │           │                │
+  ┌──────────────────┐┌──────────┬──────────┬────────────┐┌─────────────────┐
+  │                  ││   [0]    │   ...    │   [445]    ││                 │
+  │before valid range│├──────────┴──────────┴────────────┤│after valid range│
+  │                  ││string literal (type: 'char[446]')││                 │
+  └──────────────────┘└──────────────────────────────────┘└─────────────────┘
+  ├────────┬─────────┤├────────────────┬─────────────────┤├────────┬────────┤
+           │                           │                           │
+  ╭────────┴──────────────╮    ╭───────┴───────╮       ╭───────────┴───────────╮
+  │under-read of 100 bytes│    │size: 446 bytes│       │over-read of 3550 bytes│
+  ╰───────────────────────╯    ╰───────────────╯       ╰───────────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────────────────────────────────────────────────────────────────────┐
+  │                         write of 4096 bytes                          │
+  └──────────────────────────────────────────────────────────────────────┘
+           │              │       │        │                 │
+           │              │       │        │                 │
+           v              v       v        v                 v
+  ┌──────────────────┐┌───────┬───────┬─────────┐┌───────────────────────┐
+  │                  ││  [0]  │  ...  │  [99]   ││                       │
+  │before valid range│├───────┴───────┴─────────┤│   after valid range   │
+  │                  ││'buf' (type: 'char[100]')││                       │
+  └──────────────────┘└─────────────────────────┘└───────────────────────┘
+  ├────────┬─────────┤├────────────┬────────────┤├───────────┬───────────┤
+           │                       │                         │
+           │             ╭─────────┴─────────╮   ╭───────────┴──────────╮
+           │             │capacity: 100 bytes│   │overflow of 3946 bytes│
+           │             ╰───────────────────╯   ╰──────────────────────╯
+  ╭────────┴─────────────╮
+  │underwrite of 50 bytes│
+  ╰──────────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* The read and write diagrams are each emitted twice: once for the "before"
+   and once for the "after" diagnostic.  */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌─────────────────────────────────────────────────────────────────────────┐
+  │                           read of 4096 bytes                            │
+  └─────────────────────────────────────────────────────────────────────────┘
+           ^               ^          ^           ^                ^
+           │               │          │           │                │
+           │               │          │           │                │
+  ┌──────────────────┐┌──────────┬──────────┬────────────┐┌─────────────────┐
+  │                  ││   [0]    │   ...    │   [445]    ││                 │
+  │before valid range│├──────────┴──────────┴────────────┤│after valid range│
+  │                  ││string literal (type: 'char[446]')││                 │
+  └──────────────────┘└──────────────────────────────────┘└─────────────────┘
+  ├────────┬─────────┤├────────────────┬─────────────────┤├────────┬────────┤
+           │                           │                           │
+  ╭────────┴──────────────╮    ╭───────┴───────╮       ╭───────────┴───────────╮
+  │under-read of 100 bytes│    │size: 446 bytes│       │over-read of 3550 bytes│
+  ╰───────────────────────╯    ╰───────────────╯       ╰───────────────────────╯
+
+   { dg-end-multiline-output "" } */
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────────────────────────────────────────────────────────────────────┐
+  │                         write of 4096 bytes                          │
+  └──────────────────────────────────────────────────────────────────────┘
+           │              │       │        │                 │
+           │              │       │        │                 │
+           v              v       v        v                 v
+  ┌──────────────────┐┌───────┬───────┬─────────┐┌───────────────────────┐
+  │                  ││  [0]  │  ...  │  [99]   ││                       │
+  │before valid range│├───────┴───────┴─────────┤│   after valid range   │
+  │                  ││'buf' (type: 'char[100]')││                       │
+  └──────────────────┘└─────────────────────────┘└───────────────────────┘
+  ├────────┬─────────┤├────────────┬────────────┤├───────────┬───────────┤
+           │                       │                         │
+           │             ╭─────────┴─────────╮   ╭───────────┴──────────╮
+           │             │capacity: 100 bytes│   │overflow of 3946 bytes│
+           │             ╰───────────────────╯   ╰──────────────────────╯
+  ╭────────┴─────────────╮
+  │underwrite of 50 bytes│
+  ╰──────────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-7.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-7.c
new file mode 100644
index 00000000000..25a9acc2f96
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-7.c
@@ -0,0 +1,36 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <string.h>
+
+void
+test_string_with_control_chars ()
+{
+  char buf[8];
+  strcpy (buf, "\tone\n\ttwo\n"); /* { dg-warning "stack-based buffer overflow" } */
+  /* { dg-warning "'__builtin_memcpy' writing 11 bytes into a region of size 8 overflows the destination" "" { target *-*-* } .-1 } */
+}
+
+/* { dg-begin-multiline-output "" }
+
+  ┌──────┬──────┬──────┬─────┬─────┬─────┬─────┬─────┐┌─────┬─────┬──────┐
+  │ [0]  │ [1]  │ [2]  │ [3] │ [4] │ [5] │ [6] │ [7] ││ [8] │ [9] │ [10] │
+  ├──────┼──────┼──────┼─────┼─────┼─────┼─────┼─────┤├─────┼─────┼──────┤
+  │ 0x09 │ 'o'  │ 'n'  │ 'e' │0x0a │0x09 │ 't' │ 'w' ││ 'o' │0x0a │ NUL  │
+  ├──────┴──────┴──────┴─────┴─────┴─────┴─────┴─────┴┴─────┴─────┴──────┤
+  │                  string literal (type: 'char[11]')                   │
+  └──────────────────────────────────────────────────────────────────────┘
+     │      │      │      │     │     │     │     │      │     │     │
+     │      │      │      │     │     │     │     │      │     │     │
+     v      v      v      v     v     v     v     v      v     v     v
+  ┌──────┬─────────────────────────────────────┬─────┐┌──────────────────┐
+  │ [0]  │                 ...                 │ [7] ││                  │
+  ├──────┴─────────────────────────────────────┴─────┤│after valid range │
+  │             'buf' (type: 'char[8]')              ││                  │
+  └──────────────────────────────────────────────────┘└──────────────────┘
+  ├────────────────────────┬─────────────────────────┤├────────┬─────────┤
+                           │                                   │
+                  ╭────────┴────────╮                ╭─────────┴─────────╮
+                  │capacity: 8 bytes│                │overflow of 3 bytes│
+                  ╰─────────────────╯                ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-8.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-8.c
new file mode 100644
index 00000000000..24d87357e2d
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-8.c
@@ -0,0 +1,34 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdlib.h>
+#include <stdint.h>
+
+/* Gap of 4, then an overflow of 4.  */
+
+void test2 (size_t size)
+{
+  int32_t *buf = __builtin_malloc (size * sizeof(int32_t)); /* { dg-message "\\(1\\) capacity: 'size \\* 4' bytes" } */
+  if (!buf) return;
+
+  buf[size + 1] = 42; /* { dg-warning "heap-based buffer overflow" } */
+  __builtin_free (buf);
+}
+
+/* { dg-begin-multiline-output "" }
+
+                                                     ┌───────────────────┐
+                                                     │write of '(int) 42'│
+                                                     └───────────────────┘
+                                                               │
+                                                               │
+                                                               v
+  ┌───────────────────────────────┐                  ┌───────────────────┐
+  │buffer allocated on heap at (1)│                  │ after valid range │
+  └───────────────────────────────┘                  └───────────────────┘
+  ├───────────────┬───────────────┤├───────┬────────┤├─────────┬─────────┤
+                  │                        │                   │
+    ╭─────────────┴────────────╮       ╭───┴───╮     ╭─────────┴─────────╮
+    │capacity: 'size * 4' bytes│       │4 bytes│     │overflow of 4 bytes│
+    ╰──────────────────────────╯       ╰───────╯     ╰───────────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-9.c b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-9.c
new file mode 100644
index 00000000000..bb9ad660fae
--- /dev/null
+++ b/gcc/testsuite/gcc.dg/analyzer/out-of-bounds-diagram-9.c
@@ -0,0 +1,42 @@
+/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode" } */
+
+#include <stdint.h>
+
+struct st
+{
+  char buf[16];
+  int32_t x;
+  int32_t y;
+};
+
+struct st arr[10];
+
+int32_t struct_arr_read_x_element_before_start_far(void)
+{
+  return arr[-100].x; /* { dg-warning "buffer under-read" "warning" } */
+  /* { dg-message "out-of-bounds read from byte -2384 till byte -2381 but 'arr' starts at byte 0" "final event" { target *-*-* } .-1 } */
+  /* { dg-message "valid subscripts for 'arr' are '\\\[0\\\]' to '\\\[9\\\]'" "valid subscript note" { target *-*-* } .-2 } */
+}
+
+// TODO: show index of accessed element
+// TODO: show field of accessed element
+/* { dg-begin-multiline-output "" }
+
+  ┌───────────────────────────┐
+  │read of 'int32_t' (4 bytes)│
+  └───────────────────────────┘
+                ^
+                │
+                │
+  ┌───────────────────────────┐            ┌─────────┬─────────┬─────────┐
+  │                           │            │   [0]   │   ...   │   [9]   │
+  │    before valid range     │            ├─────────┴─────────┴─────────┤
+  │                           │            │'arr' (type: 'struct st[10]')│
+  └───────────────────────────┘            └─────────────────────────────┘
+  ├─────────────┬─────────────┤├────┬─────┤├──────────────┬──────────────┤
+                │                   │                     │
+     ╭──────────┴──────────╮  ╭─────┴────╮        ╭───────┴───────╮
+     │under-read of 4 bytes│  │2380 bytes│        │size: 240 bytes│
+     ╰─────────────────────╯  ╰──────────╯        ╰───────────────╯
+
+   { dg-end-multiline-output "" } */
diff --git a/gcc/testsuite/gcc.dg/analyzer/pattern-test-2.c b/gcc/testsuite/gcc.dg/analyzer/pattern-test-2.c
index 7c8d1b33fc9..5b8ff7bc2e8 100644
--- a/gcc/testsuite/gcc.dg/analyzer/pattern-test-2.c
+++ b/gcc/testsuite/gcc.dg/analyzer/pattern-test-2.c
@@ -26,7 +26,7 @@ void test_2 (void *p, void *q)
   foo(p);
 
   /* { dg-warning "pattern match on 'p != 0'" "p != 0" { target *-*-* } cond_2 } */
-  /* { dg-warning "pattern match on 'tmp1 | tmp2 != 0'" "tmp1 | tmp2 != 0" { target *-*-* } cond_2 } */
+  /* { dg-warning "pattern match on 'p == 0 | q == 0 != 0'" "tmp1 | tmp2 != 0" { target *-*-* } cond_2 } */
   /* { dg-warning "pattern match on 'q != 0'" "q != 0" { target *-*-* } cond_2 } */
 }
 
@@ -42,6 +42,6 @@ void test_3 (void *p, void *q)
   foo(p);
 
   /* { dg-warning "pattern match on 'p == 0'" "p == 0" { target *-*-* } cond_3 } */
-  /* { dg-warning "pattern match on 'tmp1 & tmp2 == 0'" "tmp1 & tmp2 == 0" { target *-*-* } cond_3 } */
+  /* { dg-warning "pattern match on 'p == 0 & q == 0 == 0'" "tmp1 & tmp2 == 0" { target *-*-* } cond_3 } */
   /* { dg-warning "pattern match on 'q == 0'" "q == 0" { target *-*-* } cond_3 } */
 }
diff --git a/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c b/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c
index e494315bb34..e0fc9cd3e47 100644
--- a/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c
+++ b/gcc/testsuite/gcc.dg/plugin/analyzer_gil_plugin.c
@@ -155,7 +155,7 @@ class double_save_thread : public gil_diagnostic
     return m_call == sub_other.m_call;
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     return warning_at (rich_loc, get_controlling_option (),
 		       "nested usage of %qs", "Py_BEGIN_ALLOW_THREADS");
@@ -194,7 +194,7 @@ class fncall_without_gil : public gil_diagnostic
 	    && m_arg_idx == sub_other.m_arg_idx);
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     if (m_callee_fndecl)
@@ -245,7 +245,7 @@ class pyobject_usage_without_gil : public gil_diagnostic
 			((const pyobject_usage_without_gil&)base_other).m_expr);
   }
 
-  bool emit (rich_location *rich_loc) final override
+  bool emit (rich_location *rich_loc, logger *) final override
   {
     auto_diagnostic_group d;
     return warning_at (rich_loc, get_controlling_option (),
-- 
2.26.3


^ permalink raw reply	[flat|nested] 14+ messages in thread

* PING: Re: [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines
  2023-05-31 18:06 ` [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines David Malcolm
@ 2023-06-12 23:11   ` David Malcolm
  2023-06-20 17:21     ` PING^2: " David Malcolm
  0 siblings, 1 reply; 14+ messages in thread
From: David Malcolm @ 2023-06-12 23:11 UTC (permalink / raw)
  To: gcc-patches

Please can someone review this testsuite patch:
  https://gcc.gnu.org/pipermail/gcc-patches/2023-May/620275.html

Thanks
Dave

On Wed, 2023-05-31 at 14:06 -0400, David Malcolm wrote:
> I have followup patches that require checking for multiline patterns
> that have blank lines within them, so this moves the handling of
> multiline patterns before the check for blank lines, allowing for
> such
> multiline patterns.
> 
> Doing so uncovers some issues with existing multiline directives,
> which
> the patch fixes.
> 
> gcc/testsuite/ChangeLog:
>         * c-c++-common/Wlogical-not-parentheses-2.c: Split up the
>         multiline directive.
>         * gcc.dg/analyzer/malloc-macro-inline-events.c: Remove
> redundant
>         dg-regexp directives.
>         * gcc.dg/missing-header-fixit-5.c: Split up the multiline
>         directives.
>         * lib/gcc-dg.exp (gcc-dg-prune): Move call to
>         handle-multiline-outputs from prune_gcc_output to here.
>         * lib/multiline.exp (dg-end-multiline-output): Move call to
>         maybe-handle-nn-line-numbers from prune_gcc_output to here.
>         * lib/prune.exp (prune_gcc_output): Move calls to
>         maybe-handle-nn-line-numbers and handle-multiline-outputs
> from
>         here to the above.
> ---
>  .../c-c++-common/Wlogical-not-parentheses-2.c          |  2 ++
>  .../gcc.dg/analyzer/malloc-macro-inline-events.c       |  5 -----
>  gcc/testsuite/gcc.dg/missing-header-fixit-5.c          | 10
> ++++++++--
>  gcc/testsuite/lib/gcc-dg.exp                           |  5 +++++
>  gcc/testsuite/lib/multiline.exp                        |  7 ++++++-
>  gcc/testsuite/lib/prune.exp                            |  7 -------
>  6 files changed, 21 insertions(+), 15 deletions(-)
> 
> diff --git a/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> b/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> index ba8dce84f5d..2d9382014c4 100644
> --- a/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> +++ b/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> @@ -12,6 +12,8 @@ foo (int aaa, int bbb)
>  /* { dg-begin-multiline-output "" }
>     r += !aaa == bbb;
>               ^~
> +   { dg-end-multiline-output "" } */
> +/* { dg-begin-multiline-output "" }
>     r += !aaa == bbb;
>          ^~~~
>          (   )
> diff --git a/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-
> events.c b/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
> index f08aee626a5..9134bb4781e 100644
> --- a/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
> +++ b/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
> @@ -12,11 +12,6 @@ int test (void *ptr)
>    WRAPPED_FREE (ptr); /* { dg-message "in expansion of macro
> 'WRAPPED_FREE'" } */
>    WRAPPED_FREE (ptr); /* { dg-message "in expansion of macro
> 'WRAPPED_FREE'" } */
>  
> -  /* Erase the spans indicating the header file
> -     (to avoid embedding path assumptions).  */
> -  /* { dg-regexp "\[^|\]+/malloc-macro.h:\[0-9\]+:\[0-9\]+:" } */
> -  /* { dg-regexp "\[^|\]+/malloc-macro.h:\[0-9\]+:\[0-9\]+:" } */
> -
>    /* { dg-begin-multiline-output "" }
>     NN | #define WRAPPED_FREE(PTR) free(PTR)
>        |                           ^~~~~~~~~
> diff --git a/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> b/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> index 916033c689c..bf44feb24a9 100644
> --- a/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> +++ b/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> @@ -12,14 +12,18 @@ foo (char *m, int i)
>    /* { dg-begin-multiline-output "" }
>     11 |   if (isdigit (m[0]))
>        |       ^~~~~~~
> +     { dg-end-multiline-output "" } */
> +  /* { dg-begin-multiline-output "" }
>    +++ |+#include <ctype.h>
>      1 | 
>       { dg-end-multiline-output "" } */
>      {
>        return abs (i); /* { dg-warning "implicit declaration of
> function" } */
>    /* { dg-begin-multiline-output "" }
> -   19 |       return abs (i);
> +   21 |       return abs (i);
>        |              ^~~
> +     { dg-end-multiline-output "" } */
> +  /* { dg-begin-multiline-output "" }
>    +++ |+#include <stdlib.h>
>      1 | 
>       { dg-end-multiline-output "" } */
> @@ -27,8 +31,10 @@ foo (char *m, int i)
>    else
>      putchar (m[0]); /* { dg-warning "implicit declaration of
> function" } */
>    /* { dg-begin-multiline-output "" }
> -   28 |     putchar (m[0]);
> +   32 |     putchar (m[0]);
>        |     ^~~~~~~
> +     { dg-end-multiline-output "" } */
> +  /* { dg-begin-multiline-output "" }
>    +++ |+#include <stdio.h>
>      1 | 
>       { dg-end-multiline-output "" } */
> diff --git a/gcc/testsuite/lib/gcc-dg.exp b/gcc/testsuite/lib/gcc-
> dg.exp
> index 4ed4233efff..6475cab46de 100644
> --- a/gcc/testsuite/lib/gcc-dg.exp
> +++ b/gcc/testsuite/lib/gcc-dg.exp
> @@ -364,6 +364,11 @@ proc gcc-dg-prune { system text } {
>      # Always remember to clear it in .exp file after executed all
> tests.
>      global dg_runtest_extra_prunes
>  
> +    # Call into multiline.exp to handle any multiline output
> directives.
> +    # This is done before the check for blank lines so that
> multiline
> +    # output directives can have blank lines within them.
> +    set text [handle-multiline-outputs $text]
> +
>      # Complain about blank lines in the output (PR other/69006)
>      global allow_blank_lines
>      if { !$allow_blank_lines } {
> diff --git a/gcc/testsuite/lib/multiline.exp
> b/gcc/testsuite/lib/multiline.exp
> index 73621a0bdbd..4c25bb76f43 100644
> --- a/gcc/testsuite/lib/multiline.exp
> +++ b/gcc/testsuite/lib/multiline.exp
> @@ -139,7 +139,7 @@ proc dg-end-multiline-output { args } {
>      verbose "within dg-end-multiline-output:
> multiline_expected_outputs: $multiline_expected_outputs" 3
>  }
>  
> -# Hook to be called by prune.exp's prune_gcc_output to
> +# Hook to be called by gcc-dg.exp's gcc-dg-prune to
>  # look for the expected multiline outputs, pruning them,
>  # reporting PASS for those that are found, and FAIL for
>  # those that weren't found.
> @@ -149,6 +149,11 @@ proc dg-end-multiline-output { args } {
>  proc handle-multiline-outputs { text } {
>      global multiline_expected_outputs
>      global testname_with_flags
> +
> +    # If dg-enable-nn-line-numbers was provided, then obscure
> source-margin
> +    # line numbers by converting them to "NN" form.
> +    set text [maybe-handle-nn-line-numbers $text]
> +    
>      set index 0
>      foreach entry $multiline_expected_outputs {
>         verbose "  entry: $entry" 3
> diff --git a/gcc/testsuite/lib/prune.exp
> b/gcc/testsuite/lib/prune.exp
> index cfe427c99ac..8d37b24e59b 100644
> --- a/gcc/testsuite/lib/prune.exp
> +++ b/gcc/testsuite/lib/prune.exp
> @@ -108,13 +108,6 @@ proc prune_gcc_output { text } {
>      # Many tests that use visibility will still pass on platforms
> that don't support it.
>      regsub -all "(^|\n)\[^\n\]*lto1: warning: visibility attribute
> not supported in this configuration; ignored\[^\n\]*" $text "" text
>  
> -    # If dg-enable-nn-line-numbers was provided, then obscure
> source-margin
> -    # line numbers by converting them to "NN" form.
> -    set text [maybe-handle-nn-line-numbers $text]
> -    
> -    # Call into multiline.exp to handle any multiline output
> directives.
> -    set text [handle-multiline-outputs $text]
> -
>      #send_user "After:$text\n"
>  
>      return $text


^ permalink raw reply	[flat|nested] 14+ messages in thread

* PING^2: Re: [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines
  2023-06-12 23:11   ` PING: " David Malcolm
@ 2023-06-20 17:21     ` David Malcolm
  2023-06-21 16:24       ` Mike Stump
  0 siblings, 1 reply; 14+ messages in thread
From: David Malcolm @ 2023-06-20 17:21 UTC (permalink / raw)
  To: gcc-patches; +Cc: Rainer Orth, Mike Stump

Does this testsuite patch look OK?

  https://gcc.gnu.org/pipermail/gcc-patches/2023-May/620275.html

Thanks
David

On Mon, 2023-06-12 at 19:11 -0400, David Malcolm wrote:
> Please can someone review this testsuite patch:
>   https://gcc.gnu.org/pipermail/gcc-patches/2023-May/620275.html
> 
> Thanks
> Dave
> 
> On Wed, 2023-05-31 at 14:06 -0400, David Malcolm wrote:
> > I have followup patches that require checking for multiline
> > patterns
> > that have blank lines within them, so this moves the handling of
> > multiline patterns before the check for blank lines, allowing for
> > such
> > multiline patterns.
> > 
> > Doing so uncovers some issues with existing multiline directives,
> > which
> > the patch fixes.
> > 
> > gcc/testsuite/ChangeLog:
> >         * c-c++-common/Wlogical-not-parentheses-2.c: Split up the
> >         multiline directive.
> >         * gcc.dg/analyzer/malloc-macro-inline-events.c: Remove
> > redundant
> >         dg-regexp directives.
> >         * gcc.dg/missing-header-fixit-5.c: Split up the multiline
> >         directives.
> >         * lib/gcc-dg.exp (gcc-dg-prune): Move call to
> >         handle-multiline-outputs from prune_gcc_output to here.
> >         * lib/multiline.exp (dg-end-multiline-output): Move call to
> >         maybe-handle-nn-line-numbers from prune_gcc_output to here.
> >         * lib/prune.exp (prune_gcc_output): Move calls to
> >         maybe-handle-nn-line-numbers and handle-multiline-outputs
> > from
> >         here to the above.
> > ---
> >  .../c-c++-common/Wlogical-not-parentheses-2.c          |  2 ++
> >  .../gcc.dg/analyzer/malloc-macro-inline-events.c       |  5 -----
> >  gcc/testsuite/gcc.dg/missing-header-fixit-5.c          | 10
> > ++++++++--
> >  gcc/testsuite/lib/gcc-dg.exp                           |  5 +++++
> >  gcc/testsuite/lib/multiline.exp                        |  7
> > ++++++-
> >  gcc/testsuite/lib/prune.exp                            |  7 ------
> > -
> >  6 files changed, 21 insertions(+), 15 deletions(-)
> > 
> > diff --git a/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-
> > 2.c
> > b/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> > index ba8dce84f5d..2d9382014c4 100644
> > --- a/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> > +++ b/gcc/testsuite/c-c++-common/Wlogical-not-parentheses-2.c
> > @@ -12,6 +12,8 @@ foo (int aaa, int bbb)
> >  /* { dg-begin-multiline-output "" }
> >     r += !aaa == bbb;
> >               ^~
> > +   { dg-end-multiline-output "" } */
> > +/* { dg-begin-multiline-output "" }
> >     r += !aaa == bbb;
> >          ^~~~
> >          (   )
> > diff --git a/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-
> > events.c b/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-
> > events.c
> > index f08aee626a5..9134bb4781e 100644
> > --- a/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
> > +++ b/gcc/testsuite/gcc.dg/analyzer/malloc-macro-inline-events.c
> > @@ -12,11 +12,6 @@ int test (void *ptr)
> >    WRAPPED_FREE (ptr); /* { dg-message "in expansion of macro
> > 'WRAPPED_FREE'" } */
> >    WRAPPED_FREE (ptr); /* { dg-message "in expansion of macro
> > 'WRAPPED_FREE'" } */
> >  
> > -  /* Erase the spans indicating the header file
> > -     (to avoid embedding path assumptions).  */
> > -  /* { dg-regexp "\[^|\]+/malloc-macro.h:\[0-9\]+:\[0-9\]+:" } */
> > -  /* { dg-regexp "\[^|\]+/malloc-macro.h:\[0-9\]+:\[0-9\]+:" } */
> > -
> >    /* { dg-begin-multiline-output "" }
> >     NN | #define WRAPPED_FREE(PTR) free(PTR)
> >        |                           ^~~~~~~~~
> > diff --git a/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> > b/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> > index 916033c689c..bf44feb24a9 100644
> > --- a/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> > +++ b/gcc/testsuite/gcc.dg/missing-header-fixit-5.c
> > @@ -12,14 +12,18 @@ foo (char *m, int i)
> >    /* { dg-begin-multiline-output "" }
> >     11 |   if (isdigit (m[0]))
> >        |       ^~~~~~~
> > +     { dg-end-multiline-output "" } */
> > +  /* { dg-begin-multiline-output "" }
> >    +++ |+#include <ctype.h>
> >      1 | 
> >       { dg-end-multiline-output "" } */
> >      {
> >        return abs (i); /* { dg-warning "implicit declaration of
> > function" } */
> >    /* { dg-begin-multiline-output "" }
> > -   19 |       return abs (i);
> > +   21 |       return abs (i);
> >        |              ^~~
> > +     { dg-end-multiline-output "" } */
> > +  /* { dg-begin-multiline-output "" }
> >    +++ |+#include <stdlib.h>
> >      1 | 
> >       { dg-end-multiline-output "" } */
> > @@ -27,8 +31,10 @@ foo (char *m, int i)
> >    else
> >      putchar (m[0]); /* { dg-warning "implicit declaration of
> > function" } */
> >    /* { dg-begin-multiline-output "" }
> > -   28 |     putchar (m[0]);
> > +   32 |     putchar (m[0]);
> >        |     ^~~~~~~
> > +     { dg-end-multiline-output "" } */
> > +  /* { dg-begin-multiline-output "" }
> >    +++ |+#include <stdio.h>
> >      1 | 
> >       { dg-end-multiline-output "" } */
> > diff --git a/gcc/testsuite/lib/gcc-dg.exp b/gcc/testsuite/lib/gcc-
> > dg.exp
> > index 4ed4233efff..6475cab46de 100644
> > --- a/gcc/testsuite/lib/gcc-dg.exp
> > +++ b/gcc/testsuite/lib/gcc-dg.exp
> > @@ -364,6 +364,11 @@ proc gcc-dg-prune { system text } {
> >      # Always remember to clear it in .exp file after executed all
> > tests.
> >      global dg_runtest_extra_prunes
> >  
> > +    # Call into multiline.exp to handle any multiline output
> > directives.
> > +    # This is done before the check for blank lines so that
> > multiline
> > +    # output directives can have blank lines within them.
> > +    set text [handle-multiline-outputs $text]
> > +
> >      # Complain about blank lines in the output (PR other/69006)
> >      global allow_blank_lines
> >      if { !$allow_blank_lines } {
> > diff --git a/gcc/testsuite/lib/multiline.exp
> > b/gcc/testsuite/lib/multiline.exp
> > index 73621a0bdbd..4c25bb76f43 100644
> > --- a/gcc/testsuite/lib/multiline.exp
> > +++ b/gcc/testsuite/lib/multiline.exp
> > @@ -139,7 +139,7 @@ proc dg-end-multiline-output { args } {
> >      verbose "within dg-end-multiline-output:
> > multiline_expected_outputs: $multiline_expected_outputs" 3
> >  }
> >  
> > -# Hook to be called by prune.exp's prune_gcc_output to
> > +# Hook to be called by gcc-dg.exp's gcc-dg-prune to
> >  # look for the expected multiline outputs, pruning them,
> >  # reporting PASS for those that are found, and FAIL for
> >  # those that weren't found.
> > @@ -149,6 +149,11 @@ proc dg-end-multiline-output { args } {
> >  proc handle-multiline-outputs { text } {
> >      global multiline_expected_outputs
> >      global testname_with_flags
> > +
> > +    # If dg-enable-nn-line-numbers was provided, then obscure
> > source-margin
> > +    # line numbers by converting them to "NN" form.
> > +    set text [maybe-handle-nn-line-numbers $text]
> > +    
> >      set index 0
> >      foreach entry $multiline_expected_outputs {
> >         verbose "  entry: $entry" 3
> > diff --git a/gcc/testsuite/lib/prune.exp
> > b/gcc/testsuite/lib/prune.exp
> > index cfe427c99ac..8d37b24e59b 100644
> > --- a/gcc/testsuite/lib/prune.exp
> > +++ b/gcc/testsuite/lib/prune.exp
> > @@ -108,13 +108,6 @@ proc prune_gcc_output { text } {
> >      # Many tests that use visibility will still pass on platforms
> > that don't support it.
> >      regsub -all "(^|\n)\[^\n\]*lto1: warning: visibility attribute
> > not supported in this configuration; ignored\[^\n\]*" $text "" text
> >  
> > -    # If dg-enable-nn-line-numbers was provided, then obscure
> > source-margin
> > -    # line numbers by converting them to "NN" form.
> > -    set text [maybe-handle-nn-line-numbers $text]
> > -    
> > -    # Call into multiline.exp to handle any multiline output
> > directives.
> > -    set text [handle-multiline-outputs $text]
> > -
> >      #send_user "After:$text\n"
> >  
> >      return $text
> 


^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: PING^2: Re: [PATCH 1/3] testsuite: move handle-multiline-outputs to before check for blank lines
  2023-06-20 17:21     ` PING^2: " David Malcolm
@ 2023-06-21 16:24       ` Mike Stump
  0 siblings, 0 replies; 14+ messages in thread
From: Mike Stump @ 2023-06-21 16:24 UTC (permalink / raw)
  To: David Malcolm; +Cc: gcc-patches, Rainer Orth

On Jun 20, 2023, at 10:21 AM, David Malcolm <dmalcolm@redhat.com> wrote:
> Does this testsuite patch look OK?
> 
>  https://gcc.gnu.org/pipermail/gcc-patches/2023-May/620275.html
> 
> Thanks
> David
> 
> On Mon, 2023-06-12 at 19:11 -0400, David Malcolm wrote:
>> Please can someone review this testsuite patch:
>>   https://gcc.gnu.org/pipermail/gcc-patches/2023-May/620275.html
>> 
>> Thanks
>> Dave
>> 
>> On Wed, 2023-05-31 at 14:06 -0400, David Malcolm wrote:
>>> I have followup patches that require checking for multiline
>>> patterns
>>> that have blank lines within them, so this moves the handling of
>>> multiline patterns before the check for blank lines, allowing for
>>> such
>>> multiline patterns.
>>> 
>>> Doing so uncovers some issues with existing multiline directives,
>>> which
>>> the patch fixes.

Ok.

^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH 2/3] diagnostics: add support for "text art" diagrams
  2023-05-31 18:06 ` [PATCH 2/3] diagnostics: add support for "text art" diagrams David Malcolm
@ 2023-06-23 11:52   ` Alex Coplan
  2023-06-23 14:36     ` [PATCH] text-art: remove explicit #include of C++ standard library headers David Malcolm
  0 siblings, 1 reply; 14+ messages in thread
From: Alex Coplan @ 2023-06-23 11:52 UTC (permalink / raw)
  To: David Malcolm; +Cc: gcc-patches

Hi David,

On 31/05/2023 14:06, David Malcolm via Gcc-patches wrote:
> Existing text output in GCC has to be implemented by writing
> sequentially to a pretty_printer instance.  This makes it
> hard to implement some kinds of diagnostic output (see e.g.
> diagnostic-show-locus.cc).
> 
> This patch adds more flexible ways of creating text output:
> - a canvas class, which can be "painted" to via random-access (rather
> that sequentially)
> - a table class for 2D grid layout, supporting items that span
> multiple rows/columns
> - a widget class for organizing diagrams hierarchically.
> 
> The patch also expands GCC's diagnostics subsystem so that diagnostics
> can have "text art" diagrams - think ASCII art, but potentially
> including some Unicode characters, such as box-drawing chars.
> 
> The new code is in a new "gcc/text-art" subdirectory and "text_art"
> namespace.

It looks like this patch breaks bootstrap on Darwin. I tried a bootstrap on
x86_64-apple-darwin and got errors building selftest-run-tests.cc:

In file included from /Users/alecop01/toolchain/src/gcc/gcc/selftest-run-tests.cc:31:
In file included from /Users/alecop01/toolchain/src/gcc/gcc/text-art/selftests.h:25:
In file included from /Users/alecop01/toolchain/src/gcc/gcc/text-art/types.h:26:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/vector:276:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__bit_reference:15:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/algorithm:653:
In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/memory:670:
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/typeinfo:377:5: error: no member named 'fancy_abort' in namespace 'std::__1'; did you mean simply 'fancy_abort'?
    _VSTD::abort();
    ^~~~~~~
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/__config:856:15: note: expanded from macro '_VSTD'
#define _VSTD std::_LIBCPP_ABI_NAMESPACE
              ^
/Users/alecop01/toolchain/src/gcc/gcc/system.h:811:13: note: 'fancy_abort' declared here
extern void fancy_abort (const char *, int, const char *)
            ^

Please could you take a look?

Thanks,
Alex

> 
> The patch adds a new "-fdiagnostics-text-art-charset=VAL" option, with
> values:
> - "none": don't emit diagrams (added to -fdiagnostics-plain-output)
> - "ascii": use pure ASCII in diagrams
> - "unicode": allow for conservative use of unicode drawing characters
> (such as box-drawing characters).
> - "emoji" (the default): as "unicode", but potentially allow for
> conservative use of emoji in the output (such as U+26A0 WARNING SIGN).
> I made it possible to disable emoji separately from unicode as I believe
> there's a generation gap in acceptance of these characters (some older
> programmers have a visceral reaction against them, whereas younger
> programmers may have no problem with them).
> 
> Diagrams are emitted to stderr by default.  With SARIF output they are
> captured as a location in "relatedLocations", with the diagram as a
> code block in Markdown within a "markdown" property of a message.
> 
> This patch doesn't add any such diagram usage to GCC, saving that for
> followups, apart from adding a plugin to the test suite to exercise the
> functionality.
> 
> One issue is that in various places in the seltftests I've embedded
> UTF-8 encoded characters in the source code.  Is that acceptable, or
> do I need to e.g. move those tests to DejaGnu?
> 
> contrib/ChangeLog:
> 	* unicode/gen-box-drawing-chars.py: New file.
> 	* unicode/gen-combining-chars.py: New file.
> 	* unicode/gen-printable-chars.py: New file.
> 
> gcc/ChangeLog:
> 	* Makefile.in (OBJS-libcommon): Add text-art/box-drawing.o,
> 	text-art/canvas.o, text-art/ruler.o, text-art/selftests.o,
> 	text-art/style.o, text-art/styled-string.o, text-art/table.o,
> 	text-art/theme.o, and text-art/widget.o.
> 	* color-macros.h (COLOR_FG_BRIGHT_BLACK): New.
> 	(COLOR_FG_BRIGHT_RED): New.
> 	(COLOR_FG_BRIGHT_GREEN): New.
> 	(COLOR_FG_BRIGHT_YELLOW): New.
> 	(COLOR_FG_BRIGHT_BLUE): New.
> 	(COLOR_FG_BRIGHT_MAGENTA): New.
> 	(COLOR_FG_BRIGHT_CYAN): New.
> 	(COLOR_FG_BRIGHT_WHITE): New.
> 	(COLOR_BG_BRIGHT_BLACK): New.
> 	(COLOR_BG_BRIGHT_RED): New.
> 	(COLOR_BG_BRIGHT_GREEN): New.
> 	(COLOR_BG_BRIGHT_YELLOW): New.
> 	(COLOR_BG_BRIGHT_BLUE): New.
> 	(COLOR_BG_BRIGHT_MAGENTA): New.
> 	(COLOR_BG_BRIGHT_CYAN): New.
> 	(COLOR_BG_BRIGHT_WHITE): New.
> 	* common.opt (fdiagnostics-text-art-charset=): New option.
> 	(diagnostic-text-art.h): New SourceInclude.
> 	(diagnostic_text_art_charset) New Enum and EnumValues.
> 	* configure: Regenerate.
> 	* configure.ac (gccdepdir): Add text-art to loop.
> 	* diagnostic-diagram.h: New file.
> 	* diagnostic-format-json.cc (json_emit_diagram): New.
> 	(diagnostic_output_format_init_json): Wire it up to
> 	context->m_diagrams.m_emission_cb.
> 	* diagnostic-format-sarif.cc: Include "diagnostic-diagram.h" and
> 	"text-art/canvas.h".
> 	(sarif_result::on_nested_diagnostic): Move code to...
> 	(sarif_result::add_related_location): ...this new function.
> 	(sarif_result::on_diagram): New.
> 	(sarif_builder::emit_diagram): New.
> 	(sarif_builder::make_message_object_for_diagram): New.
> 	(sarif_emit_diagram): New.
> 	(diagnostic_output_format_init_sarif): Set
> 	context->m_diagrams.m_emission_cb to sarif_emit_diagram.
> 	* diagnostic-text-art.h: New file.
> 	* diagnostic.cc: Include "diagnostic-text-art.h",
> 	"diagnostic-diagram.h", and "text-art/theme.h".
> 	(diagnostic_initialize): Initialize context->m_diagrams and
> 	call diagnostics_text_art_charset_init.
> 	(diagnostic_finish): Clean up context->m_diagrams.m_theme.
> 	(diagnostic_emit_diagram): New.
> 	(diagnostics_text_art_charset_init): New.
> 	* diagnostic.h (text_art::theme): New forward decl.
> 	(class diagnostic_diagram): Likewise.
> 	(diagnostic_context::m_diagrams): New field.
> 	(diagnostic_emit_diagram): New decl.
> 	* doc/invoke.texi (Diagnostic Message Formatting Options): Add
> 	-fdiagnostics-text-art-charset=.
> 	(-fdiagnostics-plain-output): Add
> 	-fdiagnostics-text-art-charset=none.
> 	* gcc.cc: Include "diagnostic-text-art.h".
> 	(driver_handle_option): Handle OPT_fdiagnostics_text_art_charset_.
> 	* opts-common.cc (decode_cmdline_options_to_array): Add
> 	"-fdiagnostics-text-art-charset=none" to expanded_args for
> 	-fdiagnostics-plain-output.
> 	* opts.cc: Include "diagnostic-text-art.h".
> 	(common_handle_option): Handle OPT_fdiagnostics_text_art_charset_.
> 	* pretty-print.cc (pp_unicode_character): New.
> 	* pretty-print.h (pp_unicode_character): New decl.
> 	* selftest-run-tests.cc: Include "text-art/selftests.h".
> 	(selftest::run_tests): Call text_art_tests.
> 	* text-art/box-drawing-chars.inc: New file, generated by
> 	contrib/unicode/gen-box-drawing-chars.py.
> 	* text-art/box-drawing.cc: New file.
> 	* text-art/box-drawing.h: New file.
> 	* text-art/canvas.cc: New file.
> 	* text-art/canvas.h: New file.
> 	* text-art/ruler.cc: New file.
> 	* text-art/ruler.h: New file.
> 	* text-art/selftests.cc: New file.
> 	* text-art/selftests.h: New file.
> 	* text-art/style.cc: New file.
> 	* text-art/styled-string.cc: New file.
> 	* text-art/table.cc: New file.
> 	* text-art/table.h: New file.
> 	* text-art/theme.cc: New file.
> 	* text-art/theme.h: New file.
> 	* text-art/types.h: New file.
> 	* text-art/widget.cc: New file.
> 	* text-art/widget.h: New file.
> 
> gcc/testsuite/ChangeLog:
> 	* gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c: New test.
> 	* gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c: New test.
> 	* gcc.dg/plugin/diagnostic-test-text-art-none.c: New test.
> 	* gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c: New test.
> 	* gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c: New test.
> 	* gcc.dg/plugin/diagnostic_plugin_test_text_art.c: New test plugin.
> 	* gcc.dg/plugin/plugin.exp (plugin_test_list): Add them.
> 
> libcpp/ChangeLog:
> 	* charset.cc (get_cppchar_property): New function template, based
> 	on...
> 	(cpp_wcwidth): ...this function.  Rework to use the above.
> 	Include "combining-chars.inc".
> 	(cpp_is_combining_char): New function
> 	Include "printable-chars.inc".
> 	(cpp_is_printable_char): New function
> 	* combining-chars.inc: New file, generated by
> 	contrib/unicode/gen-combining-chars.py.
> 	* include/cpplib.h (cpp_is_combining_char): New function decl.
> 	(cpp_is_printable_char): New function decl.
> 	* printable-chars.inc: New file, generated by
> 	contrib/unicode/gen-printable-chars.py.
> ---
>  contrib/unicode/gen-box-drawing-chars.py      |   94 ++
>  contrib/unicode/gen-combining-chars.py        |   75 +
>  contrib/unicode/gen-printable-chars.py        |   77 +
>  gcc/Makefile.in                               |   11 +-
>  gcc/color-macros.h                            |   16 +
>  gcc/common.opt                                |   23 +
>  gcc/configure                                 |    2 +-
>  gcc/configure.ac                              |    2 +-
>  gcc/diagnostic-diagram.h                      |   51 +
>  gcc/diagnostic-format-json.cc                 |   10 +
>  gcc/diagnostic-format-sarif.cc                |  106 +-
>  gcc/diagnostic-text-art.h                     |   49 +
>  gcc/diagnostic.cc                             |   72 +
>  gcc/diagnostic.h                              |   21 +
>  gcc/doc/invoke.texi                           |   25 +-
>  gcc/gcc.cc                                    |    6 +
>  gcc/opts-common.cc                            |    1 +
>  gcc/opts.cc                                   |    6 +
>  gcc/pretty-print.cc                           |   29 +
>  gcc/pretty-print.h                            |    1 +
>  gcc/selftest-run-tests.cc                     |    3 +
>  .../diagnostic-test-text-art-ascii-bw.c       |   57 +
>  .../diagnostic-test-text-art-ascii-color.c    |   58 +
>  .../plugin/diagnostic-test-text-art-none.c    |    5 +
>  .../diagnostic-test-text-art-unicode-bw.c     |   58 +
>  .../diagnostic-test-text-art-unicode-color.c  |   59 +
>  .../plugin/diagnostic_plugin_test_text_art.c  |  257 ++++
>  gcc/testsuite/gcc.dg/plugin/plugin.exp        |    6 +
>  gcc/text-art/box-drawing-chars.inc            |   18 +
>  gcc/text-art/box-drawing.cc                   |   72 +
>  gcc/text-art/box-drawing.h                    |   32 +
>  gcc/text-art/canvas.cc                        |  437 ++++++
>  gcc/text-art/canvas.h                         |   74 +
>  gcc/text-art/ruler.cc                         |  723 ++++++++++
>  gcc/text-art/ruler.h                          |  125 ++
>  gcc/text-art/selftests.cc                     |   77 +
>  gcc/text-art/selftests.h                      |   60 +
>  gcc/text-art/style.cc                         |  632 ++++++++
>  gcc/text-art/styled-string.cc                 | 1107 ++++++++++++++
>  gcc/text-art/table.cc                         | 1272 +++++++++++++++++
>  gcc/text-art/table.h                          |  262 ++++
>  gcc/text-art/theme.cc                         |  183 +++
>  gcc/text-art/theme.h                          |  123 ++
>  gcc/text-art/types.h                          |  504 +++++++
>  gcc/text-art/widget.cc                        |  275 ++++
>  gcc/text-art/widget.h                         |  246 ++++
>  libcpp/charset.cc                             |   89 +-
>  libcpp/combining-chars.inc                    |   68 +
>  libcpp/include/cpplib.h                       |    3 +
>  libcpp/printable-chars.inc                    |  231 +++
>  50 files changed, 7760 insertions(+), 33 deletions(-)
>  create mode 100755 contrib/unicode/gen-box-drawing-chars.py
>  create mode 100755 contrib/unicode/gen-combining-chars.py
>  create mode 100755 contrib/unicode/gen-printable-chars.py
>  create mode 100644 gcc/diagnostic-diagram.h
>  create mode 100644 gcc/diagnostic-text-art.h
>  create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
>  create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
>  create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
>  create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
>  create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
>  create mode 100644 gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
>  create mode 100644 gcc/text-art/box-drawing-chars.inc
>  create mode 100644 gcc/text-art/box-drawing.cc
>  create mode 100644 gcc/text-art/box-drawing.h
>  create mode 100644 gcc/text-art/canvas.cc
>  create mode 100644 gcc/text-art/canvas.h
>  create mode 100644 gcc/text-art/ruler.cc
>  create mode 100644 gcc/text-art/ruler.h
>  create mode 100644 gcc/text-art/selftests.cc
>  create mode 100644 gcc/text-art/selftests.h
>  create mode 100644 gcc/text-art/style.cc
>  create mode 100644 gcc/text-art/styled-string.cc
>  create mode 100644 gcc/text-art/table.cc
>  create mode 100644 gcc/text-art/table.h
>  create mode 100644 gcc/text-art/theme.cc
>  create mode 100644 gcc/text-art/theme.h
>  create mode 100644 gcc/text-art/types.h
>  create mode 100644 gcc/text-art/widget.cc
>  create mode 100644 gcc/text-art/widget.h
>  create mode 100644 libcpp/combining-chars.inc
>  create mode 100644 libcpp/printable-chars.inc
> 
> diff --git a/contrib/unicode/gen-box-drawing-chars.py b/contrib/unicode/gen-box-drawing-chars.py
> new file mode 100755
> index 00000000000..9a55266ab84
> --- /dev/null
> +++ b/contrib/unicode/gen-box-drawing-chars.py
> @@ -0,0 +1,94 @@
> +#!/usr/bin/env python3
> +#
> +# Script to generate gcc/text-art/box-drawing-chars.inc
> +#
> +# 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/>.  */
> +
> +import unicodedata
> +
> +def get_box_drawing_char_name(up: bool,
> +                              down: bool,
> +                              left: bool,
> +                              right: bool) -> str:
> +    if 0:
> +        print(f'{locals()=}')
> +    if up and down:
> +        vertical = True
> +        up = False
> +        down = False
> +    else:
> +        vertical = False
> +
> +    if left and right:
> +        horizontal = True
> +        left = False
> +        right = False
> +    else:
> +        horizontal = False
> +
> +    weights = []
> +    heavy = []
> +    light = []
> +    dirs = []
> +    for dir_name in ('up', 'down', 'vertical', 'left', 'right', 'horizontal'):
> +        val = locals()[dir_name]
> +        if val:
> +            dirs.append(dir_name.upper())
> +
> +    if not dirs:
> +        return 'SPACE'
> +
> +    name = 'BOX DRAWINGS'
> +    #print(f'{light=} {heavy=}')
> +
> +    if 0:
> +        print(dirs)
> +
> +    def weights_frag(weight: str, dirs: list, prefix: bool):
> +        """
> +        Generate a fragment where all directions share the same weight, e.g.:
> +        'HEAVY HORIZONTAL'
> +        'DOWN LIGHT'
> +        'LEFT DOWN HEAVY'
> +        'HEAVY DOWN AND RIGHT'
> +        """
> +        assert len(dirs) >= 1
> +        assert len(dirs) <= 2
> +        if prefix:
> +            return f' {weight} ' + (' AND '.join(dirs))
> +        else:
> +            return ' ' + (' '.join(dirs)) + f' {weight}'
> +
> +    assert(len(dirs) >= 1 and len(dirs) <= 2)
> +    name += weights_frag('LIGHT', dirs, True)
> +
> +    return name
> +
> +print('/* Generated by contrib/unicode/gen-box-drawing-chars.py.  */')
> +print()
> +for i in range(16):
> +    up = (i & 8)
> +    down = (i & 4)
> +    left = (i & 2)
> +    right = (i & 1)
> +    name = get_box_drawing_char_name(up, down, left, right)
> +    if i < 15:
> +        trailing_comma = ','
> +    else:
> +        trailing_comma = ' '
> +    unichar = unicodedata.lookup(name)
> +    print(f'0x{ord(unichar):04X}{trailing_comma} /* "{unichar}": U+{ord(unichar):04X}: {name} */')
> diff --git a/contrib/unicode/gen-combining-chars.py b/contrib/unicode/gen-combining-chars.py
> new file mode 100755
> index 00000000000..fb5ef50ba4c
> --- /dev/null
> +++ b/contrib/unicode/gen-combining-chars.py
> @@ -0,0 +1,75 @@
> +#!/usr/bin/env python3
> +#
> +# Script to generate libcpp/combining-chars.inc
> +#
> +# 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/>.  */
> +
> +from pprint import pprint
> +import unicodedata
> +
> +def is_combining_char(code_point) -> bool:
> +    return unicodedata.combining(chr(code_point)) != 0
> +
> +class Range:
> +    def __init__(self, start, end, value):
> +        self.start = start
> +        self.end = end
> +        self.value = value
> +
> +    def __repr__(self):
> +        return f'Range({self.start:x}, {self.end:x}, {self.value})'
> +
> +def make_ranges(value_callback):
> +    ranges = []
> +    for code_point in range(0x10FFFF):
> +        value = is_combining_char(code_point)
> +        if 0:
> +            print(f'{code_point=:x} {value=}')
> +        if ranges and ranges[-1].value == value:
> +            # Extend current range
> +            ranges[-1].end = code_point
> +        else:
> +            # Start a new range
> +            ranges.append(Range(code_point, code_point, value))
> +    return ranges
> +
> +ranges = make_ranges(is_combining_char)
> +if 0:
> +    pprint(ranges)
> +
> +print(f"/* Generated by contrib/unicode/gen-combining-chars.py")
> +print(f"   using version {unicodedata.unidata_version}"
> +      " of the Unicode standard.  */")
> +print("\nstatic const cppchar_t combining_range_ends[] = {", end="")
> +for i, r in enumerate(ranges):
> +    if i % 8:
> +        print(" ", end="")
> +    else:
> +        print("\n  ", end="")
> +    print("0x%x," % r.end, end="")
> +print("\n};\n")
> +print("static const bool is_combining[] = {", end="")
> +for i, r in enumerate(ranges):
> +    if i % 24:
> +        print(" ", end="")
> +    else:
> +        print("\n  ", end="")
> +    if r.value:
> +        print("1,", end="")
> +    else:
> +        print("0,", end="")
> +print("\n};")
> diff --git a/contrib/unicode/gen-printable-chars.py b/contrib/unicode/gen-printable-chars.py
> new file mode 100755
> index 00000000000..7684c086638
> --- /dev/null
> +++ b/contrib/unicode/gen-printable-chars.py
> @@ -0,0 +1,77 @@
> +#!/usr/bin/env python3
> +#
> +# Script to generate libcpp/printable-chars.inc
> +#
> +# 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/>.  */
> +
> +from pprint import pprint
> +import unicodedata
> +
> +def is_printable_char(code_point) -> bool:
> +    category = unicodedata.category(chr(code_point))
> +    # "Cc" is "control" and "Cf" is "format"
> +    return category[0] != 'C'
> +
> +class Range:
> +    def __init__(self, start, end, value):
> +        self.start = start
> +        self.end = end
> +        self.value = value
> +
> +    def __repr__(self):
> +        return f'Range({self.start:x}, {self.end:x}, {self.value})'
> +
> +def make_ranges(value_callback):
> +    ranges = []
> +    for code_point in range(0x10FFFF):
> +        value = is_printable_char(code_point)
> +        if 0:
> +            print(f'{code_point=:x} {value=}')
> +        if ranges and ranges[-1].value == value:
> +            # Extend current range
> +            ranges[-1].end = code_point
> +        else:
> +            # Start a new range
> +            ranges.append(Range(code_point, code_point, value))
> +    return ranges
> +
> +ranges = make_ranges(is_printable_char)
> +if 0:
> +    pprint(ranges)
> +
> +print(f"/* Generated by contrib/unicode/gen-printable-chars.py")
> +print(f"   using version {unicodedata.unidata_version}"
> +      " of the Unicode standard.  */")
> +print("\nstatic const cppchar_t printable_range_ends[] = {", end="")
> +for i, r in enumerate(ranges):
> +    if i % 8:
> +        print(" ", end="")
> +    else:
> +        print("\n  ", end="")
> +    print("0x%x," % r.end, end="")
> +print("\n};\n")
> +print("static const bool is_printable[] = {", end="")
> +for i, r in enumerate(ranges):
> +    if i % 24:
> +        print(" ", end="")
> +    else:
> +        print("\n  ", end="")
> +    if r.value:
> +        print("1,", end="")
> +    else:
> +        print("0,", end="")
> +print("\n};")
> diff --git a/gcc/Makefile.in b/gcc/Makefile.in
> index 1d39e6dd3f8..c1e7257ed24 100644
> --- a/gcc/Makefile.in
> +++ b/gcc/Makefile.in
> @@ -1781,7 +1781,16 @@ OBJS-libcommon = diagnostic-spec.o diagnostic.o diagnostic-color.o \
>  	json.o \
>  	sbitmap.o \
>  	vec.o input.o hash-table.o ggc-none.o memory-block.o \
> -	selftest.o selftest-diagnostic.o sort.o
> +	selftest.o selftest-diagnostic.o sort.o \
> +	text-art/box-drawing.o \
> +	text-art/canvas.o \
> +	text-art/ruler.o \
> +	text-art/selftests.o \
> +	text-art/style.o \
> +	text-art/styled-string.o \
> +	text-art/table.o \
> +	text-art/theme.o \
> +	text-art/widget.o
>  
>  # Objects in libcommon-target.a, used by drivers and by the core
>  # compiler and containing target-dependent code.
> diff --git a/gcc/color-macros.h b/gcc/color-macros.h
> index fcd79d09c01..9688f92110a 100644
> --- a/gcc/color-macros.h
> +++ b/gcc/color-macros.h
> @@ -92,6 +92,14 @@ along with GCC; see the file COPYING3.  If not see
>  #define COLOR_FG_MAGENTA	"35"
>  #define COLOR_FG_CYAN		"36"
>  #define COLOR_FG_WHITE		"37"
> +#define COLOR_FG_BRIGHT_BLACK	"90"
> +#define COLOR_FG_BRIGHT_RED	"91"
> +#define COLOR_FG_BRIGHT_GREEN	"92"
> +#define COLOR_FG_BRIGHT_YELLOW	"93"
> +#define COLOR_FG_BRIGHT_BLUE	"94"
> +#define COLOR_FG_BRIGHT_MAGENTA	"95"
> +#define COLOR_FG_BRIGHT_CYAN	"96"
> +#define COLOR_FG_BRIGHT_WHITE	"97"
>  #define COLOR_BG_BLACK		"40"
>  #define COLOR_BG_RED		"41"
>  #define COLOR_BG_GREEN		"42"
> @@ -100,6 +108,14 @@ along with GCC; see the file COPYING3.  If not see
>  #define COLOR_BG_MAGENTA	"45"
>  #define COLOR_BG_CYAN		"46"
>  #define COLOR_BG_WHITE		"47"
> +#define COLOR_BG_BRIGHT_BLACK	"100"
> +#define COLOR_BG_BRIGHT_RED	"101"
> +#define COLOR_BG_BRIGHT_GREEN	"102"
> +#define COLOR_BG_BRIGHT_YELLOW	"103"
> +#define COLOR_BG_BRIGHT_BLUE	"104"
> +#define COLOR_BG_BRIGHT_MAGENTA	"105"
> +#define COLOR_BG_BRIGHT_CYAN	"106"
> +#define COLOR_BG_BRIGHT_WHITE	"107"
>  #define SGR_START		"\33["
>  #define SGR_END			"m\33[K"
>  #define SGR_SEQ(str)		SGR_START str SGR_END
> diff --git a/gcc/common.opt b/gcc/common.opt
> index a28ca13385a..b3c82b8607c 100644
> --- a/gcc/common.opt
> +++ b/gcc/common.opt
> @@ -1502,6 +1502,29 @@ fdiagnostics-show-path-depths
>  Common Var(flag_diagnostics_show_path_depths) Init(0)
>  Show stack depths of events in paths.
>  
> +fdiagnostics-text-art-charset=
> +Driver Common Joined RejectNegative Var(flag_diagnostics_text_art_charset) Enum(diagnostic_text_art_charset) Init(DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI)
> +-fdiagnostics-text-art-charset=[none|ascii|unicode|emoji]	Determine which characters to use in text arg diagrams.
> +
> +; Required for these enum values.
> +SourceInclude
> +diagnostic-text-art.h
> +
> +Enum
> +Name(diagnostic_text_art_charset) Type(int)
> +
> +EnumValue
> +Enum(diagnostic_text_art_charset) String(none) Value(DIAGNOSTICS_TEXT_ART_CHARSET_NONE)
> +
> +EnumValue
> +Enum(diagnostic_text_art_charset) String(ascii) Value(DIAGNOSTICS_TEXT_ART_CHARSET_ASCII)
> +
> +EnumValue
> +Enum(diagnostic_text_art_charset) String(unicode) Value(DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE)
> +
> +EnumValue
> +Enum(diagnostic_text_art_charset) String(emoji) Value(DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI)
> +
>  fdiagnostics-minimum-margin-width=
>  Common Joined UInteger Var(diagnostics_minimum_margin_width) Init(6)
>  Set minimum width of left margin of source code when showing source.
> diff --git a/gcc/configure b/gcc/configure
> index 5f67808b774..e061d2b1949 100755
> --- a/gcc/configure
> +++ b/gcc/configure
> @@ -33995,7 +33995,7 @@ $as_echo "$as_me: executing $ac_file commands" >&6;}
>      "depdir":C) $SHELL $ac_aux_dir/mkinstalldirs $DEPDIR ;;
>      "gccdepdir":C)
>    ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
> -  for lang in $subdirs c-family common analyzer rtl-ssa
> +  for lang in $subdirs c-family common analyzer text-art rtl-ssa
>    do
>        ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
>    done ;;
> diff --git a/gcc/configure.ac b/gcc/configure.ac
> index cc8dd9e20bf..350d245c89f 100644
> --- a/gcc/configure.ac
> +++ b/gcc/configure.ac
> @@ -1384,7 +1384,7 @@ AC_CHECK_HEADERS(ext/hash_map)
>  ZW_CREATE_DEPDIR
>  AC_CONFIG_COMMANDS([gccdepdir],[
>    ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs build/$DEPDIR
> -  for lang in $subdirs c-family common analyzer rtl-ssa
> +  for lang in $subdirs c-family common analyzer text-art rtl-ssa
>    do
>        ${CONFIG_SHELL-/bin/sh} $ac_aux_dir/mkinstalldirs $lang/$DEPDIR
>    done], [subdirs="$subdirs" ac_aux_dir=$ac_aux_dir DEPDIR=$DEPDIR])
> diff --git a/gcc/diagnostic-diagram.h b/gcc/diagnostic-diagram.h
> new file mode 100644
> index 00000000000..fc923c512ed
> --- /dev/null
> +++ b/gcc/diagnostic-diagram.h
> @@ -0,0 +1,51 @@
> +/* Support for diagrams within diagnostics.
> +   Copyright (C) 2023 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/>.  */
> +
> +#ifndef GCC_DIAGNOSTIC_DIAGRAM_H
> +#define GCC_DIAGNOSTIC_DIAGRAM_H
> +
> +namespace text_art
> +{
> +  class canvas;
> +} // namespace text_art
> +
> +/* A text art diagram, along with an "alternative text" string
> +   describing it.  */
> +
> +class diagnostic_diagram
> +{
> + public:
> +  diagnostic_diagram (const text_art::canvas &canvas,
> +		      const char *alt_text)
> +  : m_canvas (canvas),
> +    m_alt_text (alt_text)
> +  {
> +    gcc_assert (alt_text);
> +  }
> +
> +  const text_art::canvas &get_canvas () const { return m_canvas; }
> +  const char *get_alt_text () const { return m_alt_text; }
> +
> + private:
> +  const text_art::canvas &m_canvas;
> +  const char *const m_alt_text;
> +};
> +
> +#endif /* ! GCC_DIAGNOSTIC_DIAGRAM_H */
> diff --git a/gcc/diagnostic-format-json.cc b/gcc/diagnostic-format-json.cc
> index 694dddca9e8..539b98b5e74 100644
> --- a/gcc/diagnostic-format-json.cc
> +++ b/gcc/diagnostic-format-json.cc
> @@ -324,6 +324,15 @@ json_file_final_cb (diagnostic_context *)
>    free (filename);
>  }
>  
> +/* Callback for diagnostic_context::m_diagrams.m_emission_cb.  */
> +
> +static void
> +json_emit_diagram (diagnostic_context *,
> +		   const diagnostic_diagram &)
> +{
> +  /* No-op.  */
> +}
> +
>  /* Populate CONTEXT in preparation for JSON output (either to stderr, or
>     to a file).  */
>  
> @@ -340,6 +349,7 @@ diagnostic_output_format_init_json (diagnostic_context *context)
>    context->begin_group_cb = json_begin_group;
>    context->end_group_cb =  json_end_group;
>    context->print_path = NULL; /* handled in json_end_diagnostic.  */
> +  context->m_diagrams.m_emission_cb = json_emit_diagram;
>  
>    /* The metadata is handled in JSON format, rather than as text.  */
>    context->show_cwe = false;
> diff --git a/gcc/diagnostic-format-sarif.cc b/gcc/diagnostic-format-sarif.cc
> index fd29ac2ca3b..ac2f5b844e3 100644
> --- a/gcc/diagnostic-format-sarif.cc
> +++ b/gcc/diagnostic-format-sarif.cc
> @@ -29,6 +29,8 @@ along with GCC; see the file COPYING3.  If not see
>  #include "cpplib.h"
>  #include "logical-location.h"
>  #include "diagnostic-client-data-hooks.h"
> +#include "diagnostic-diagram.h"
> +#include "text-art/canvas.h"
>  
>  class sarif_builder;
>  
> @@ -66,8 +68,13 @@ public:
>  			diagnostic_info *diagnostic,
>  			diagnostic_t orig_diag_kind,
>  			sarif_builder *builder);
> +  void on_diagram (diagnostic_context *context,
> +		   const diagnostic_diagram &diagram,
> +		   sarif_builder *builder);
>  
>  private:
> +  void add_related_location (json::object *location_obj);
> +
>    json::array *m_related_locations_arr;
>  };
>  
> @@ -135,7 +142,8 @@ public:
>  
>    void end_diagnostic (diagnostic_context *context, diagnostic_info *diagnostic,
>  		       diagnostic_t orig_diag_kind);
> -
> +  void emit_diagram (diagnostic_context *context,
> +		     const diagnostic_diagram &diagram);
>    void end_group ();
>  
>    void flush_to_file (FILE *outf);
> @@ -144,6 +152,9 @@ public:
>    json::object *make_location_object (const rich_location &rich_loc,
>  				      const logical_location *logical_loc);
>    json::object *make_message_object (const char *msg) const;
> +  json::object *
> +  make_message_object_for_diagram (diagnostic_context *context,
> +				   const diagnostic_diagram &diagram);
>  
>  private:
>    sarif_result *make_result_object (diagnostic_context *context,
> @@ -261,12 +272,6 @@ sarif_result::on_nested_diagnostic (diagnostic_context *context,
>  				    diagnostic_t /*orig_diag_kind*/,
>  				    sarif_builder *builder)
>  {
> -  if (!m_related_locations_arr)
> -    {
> -      m_related_locations_arr = new json::array ();
> -      set ("relatedLocations", m_related_locations_arr);
> -    }
> -
>    /* We don't yet generate meaningful logical locations for notes;
>       sometimes these will related to current_function_decl, but
>       often they won't.  */
> @@ -277,6 +282,39 @@ sarif_result::on_nested_diagnostic (diagnostic_context *context,
>    pp_clear_output_area (context->printer);
>    location_obj->set ("message", message_obj);
>  
> +  add_related_location (location_obj);
> +}
> +
> +/* Handle diagrams that occur within a diagnostic group.
> +   The closest thing in SARIF seems to be to add a location to the
> +   "releatedLocations" property  (SARIF v2.1.0 section 3.27.22),
> +   and to put the diagram into the "message" property of that location
> +   (SARIF v2.1.0 section 3.28.5).  */
> +
> +void
> +sarif_result::on_diagram (diagnostic_context *context,
> +			  const diagnostic_diagram &diagram,
> +			  sarif_builder *builder)
> +{
> +  json::object *location_obj = new json::object ();
> +  json::object *message_obj
> +    = builder->make_message_object_for_diagram (context, diagram);
> +  location_obj->set ("message", message_obj);
> +
> +  add_related_location (location_obj);
> +}
> +
> +/* Add LOCATION_OBJ to this result's "relatedLocations" array,
> +   creating it if it doesn't yet exist.  */
> +
> +void
> +sarif_result::add_related_location (json::object *location_obj)
> +{
> +  if (!m_related_locations_arr)
> +    {
> +      m_related_locations_arr = new json::array ();
> +      set ("relatedLocations", m_related_locations_arr);
> +    }
>    m_related_locations_arr->append (location_obj);
>  }
>  
> @@ -348,6 +386,18 @@ sarif_builder::end_diagnostic (diagnostic_context *context,
>      }
>  }
>  
> +/* Implementation of diagnostic_context::m_diagrams.m_emission_cb
> +   for SARIF output.  */
> +
> +void
> +sarif_builder::emit_diagram (diagnostic_context *context,
> +			     const diagnostic_diagram &diagram)
> +{
> +  /* We must be within the emission of a top-level diagnostic.  */
> +  gcc_assert (m_cur_group_result);
> +  m_cur_group_result->on_diagram (context, diagram, this);
> +}
> +
>  /* Implementation of "end_group_cb" for SARIF output.  */
>  
>  void
> @@ -1115,6 +1165,37 @@ sarif_builder::make_message_object (const char *msg) const
>    return message_obj;
>  }
>  
> +/* Make a message object (SARIF v2.1.0 section 3.11) for DIAGRAM.
> +   We emit the diagram as a code block within the Markdown part
> +   of the message.  */
> +
> +json::object *
> +sarif_builder::make_message_object_for_diagram (diagnostic_context *context,
> +						const diagnostic_diagram &diagram)
> +{
> +  json::object *message_obj = new json::object ();
> +
> +  /* "text" property (SARIF v2.1.0 section 3.11.8).  */
> +  message_obj->set ("text", new json::string (diagram.get_alt_text ()));
> +
> +  char *saved_prefix = pp_take_prefix (context->printer);
> +  pp_set_prefix (context->printer, NULL);
> +
> +  /* "To produce a code block in Markdown, simply indent every line of
> +     the block by at least 4 spaces or 1 tab."
> +     Here we use 4 spaces.  */
> +  diagram.get_canvas ().print_to_pp (context->printer, "    ");
> +  pp_set_prefix (context->printer, saved_prefix);
> +
> +  /* "markdown" property (SARIF v2.1.0 section 3.11.9).  */
> +  message_obj->set ("markdown",
> +		    new json::string (pp_formatted_text (context->printer)));
> +
> +  pp_clear_output_area (context->printer);
> +
> +  return message_obj;
> +}
> +
>  /* Make a multiformatMessageString object (SARIF v2.1.0 section 3.12)
>     for MSG.  */
>  
> @@ -1630,6 +1711,16 @@ sarif_ice_handler (diagnostic_context *context)
>    fnotice (stderr, "Internal compiler error:\n");
>  }
>  
> +/* Callback for diagnostic_context::m_diagrams.m_emission_cb.  */
> +
> +static void
> +sarif_emit_diagram (diagnostic_context *context,
> +		    const diagnostic_diagram &diagram)
> +{
> +  gcc_assert (the_builder);
> +  the_builder->emit_diagram (context, diagram);
> +}
> +
>  /* Populate CONTEXT in preparation for SARIF output (either to stderr, or
>     to a file).  */
>  
> @@ -1645,6 +1736,7 @@ diagnostic_output_format_init_sarif (diagnostic_context *context)
>    context->end_group_cb =  sarif_end_group;
>    context->print_path = NULL; /* handled in sarif_end_diagnostic.  */
>    context->ice_handler_cb = sarif_ice_handler;
> +  context->m_diagrams.m_emission_cb = sarif_emit_diagram;
>  
>    /* The metadata is handled in SARIF format, rather than as text.  */
>    context->show_cwe = false;
> diff --git a/gcc/diagnostic-text-art.h b/gcc/diagnostic-text-art.h
> new file mode 100644
> index 00000000000..a0d8a78f52a
> --- /dev/null
> +++ b/gcc/diagnostic-text-art.h
> @@ -0,0 +1,49 @@
> +/* Copyright (C) 2023 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/>.  */
> +
> +#ifndef GCC_DIAGNOSTIC_TEXT_ART_H
> +#define GCC_DIAGNOSTIC_TEXT_ART_H
> +
> +/* Values for -fdiagnostics-text-art-charset=.  */
> +
> +enum diagnostic_text_art_charset
> +{
> +  /* No text art diagrams shall be emitted.  */
> +  DIAGNOSTICS_TEXT_ART_CHARSET_NONE,
> +
> +  /* Use pure ASCII for text art diagrams.  */
> +  DIAGNOSTICS_TEXT_ART_CHARSET_ASCII,
> +
> +  /* Use ASCII + conservative use of other unicode characters
> +     in text art diagrams.  */
> +  DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE,
> +
> +  /* Use Emoji.  */
> +  DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI
> +};
> +
> +const enum diagnostic_text_art_charset DIAGNOSTICS_TEXT_ART_CHARSET_DEFAULT
> +  = DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI;
> +
> +extern void
> +diagnostics_text_art_charset_init (diagnostic_context *context,
> +				  enum diagnostic_text_art_charset charset);
> +
> +
> +#endif /* ! GCC_DIAGNOSTIC_TEXT_ART_H */
> diff --git a/gcc/diagnostic.cc b/gcc/diagnostic.cc
> index 0f093081161..7c2289f0634 100644
> --- a/gcc/diagnostic.cc
> +++ b/gcc/diagnostic.cc
> @@ -35,11 +35,14 @@ along with GCC; see the file COPYING3.  If not see
>  #include "diagnostic-metadata.h"
>  #include "diagnostic-path.h"
>  #include "diagnostic-client-data-hooks.h"
> +#include "diagnostic-text-art.h"
> +#include "diagnostic-diagram.h"
>  #include "edit-context.h"
>  #include "selftest.h"
>  #include "selftest-diagnostic.h"
>  #include "opts.h"
>  #include "cpplib.h"
> +#include "text-art/theme.h"
>  
>  #ifdef HAVE_TERMIOS_H
>  # include <termios.h>
> @@ -244,6 +247,10 @@ diagnostic_initialize (diagnostic_context *context, int n_opts)
>    context->ice_handler_cb = NULL;
>    context->includes_seen = NULL;
>    context->m_client_data_hooks = NULL;
> +  context->m_diagrams.m_theme = NULL;
> +  context->m_diagrams.m_emission_cb = NULL;
> +  diagnostics_text_art_charset_init (context,
> +				     DIAGNOSTICS_TEXT_ART_CHARSET_DEFAULT);
>  }
>  
>  /* Maybe initialize the color support. We require clients to do this
> @@ -320,6 +327,12 @@ diagnostic_finish (diagnostic_context *context)
>    if (context->final_cb)
>      context->final_cb (context);
>  
> +  if (context->m_diagrams.m_theme)
> +    {
> +      delete context->m_diagrams.m_theme;
> +      context->m_diagrams.m_theme = NULL;
> +    }
> +
>    diagnostic_file_cache_fini ();
>  
>    XDELETEVEC (context->classify_diagnostic);
> @@ -2174,6 +2187,33 @@ internal_error_no_backtrace (const char *gmsgid, ...)
>  
>    gcc_unreachable ();
>  }
> +
> +/* Emit DIAGRAM to CONTEXT, respecting the output format.  */
> +
> +void
> +diagnostic_emit_diagram (diagnostic_context *context,
> +			 const diagnostic_diagram &diagram)
> +{
> +  if (context->m_diagrams.m_theme == nullptr)
> +    return;
> +
> +  if (context->m_diagrams.m_emission_cb)
> +    {
> +      context->m_diagrams.m_emission_cb (context, diagram);
> +      return;
> +    }
> +
> +  /* Default implementation.  */
> +  char *saved_prefix = pp_take_prefix (context->printer);
> +  pp_set_prefix (context->printer, NULL);
> +  /* Use a newline before and after and a two-space indent
> +     to make the diagram stand out a little from the wall of text.  */
> +  pp_newline (context->printer);
> +  diagram.get_canvas ().print_to_pp (context->printer, "  ");
> +  pp_newline (context->printer);
> +  pp_set_prefix (context->printer, saved_prefix);
> +  pp_flush (context->printer);
> +}
>  \f
>  /* Special case error functions.  Most are implemented in terms of the
>     above, or should be.  */
> @@ -2316,6 +2356,38 @@ diagnostic_output_format_init (diagnostic_context *context,
>      }
>  }
>  
> +/* Initialize CONTEXT->m_diagrams based on CHARSET.
> +   Specifically, make a text_art::theme object for m_diagrams.m_theme,
> +   (or NULL for "no diagrams").  */
> +
> +void
> +diagnostics_text_art_charset_init (diagnostic_context *context,
> +				   enum diagnostic_text_art_charset charset)
> +{
> +  delete context->m_diagrams.m_theme;
> +  switch (charset)
> +    {
> +    default:
> +      gcc_unreachable ();
> +
> +    case DIAGNOSTICS_TEXT_ART_CHARSET_NONE:
> +      context->m_diagrams.m_theme = NULL;
> +      break;
> +
> +    case DIAGNOSTICS_TEXT_ART_CHARSET_ASCII:
> +      context->m_diagrams.m_theme = new text_art::ascii_theme ();
> +      break;
> +
> +    case DIAGNOSTICS_TEXT_ART_CHARSET_UNICODE:
> +      context->m_diagrams.m_theme = new text_art::unicode_theme ();
> +      break;
> +
> +    case DIAGNOSTICS_TEXT_ART_CHARSET_EMOJI:
> +      context->m_diagrams.m_theme = new text_art::emoji_theme ();
> +      break;
> +    }
> +}
> +
>  /* Implementation of diagnostic_path::num_events vfunc for
>     simple_diagnostic_path: simply get the number of events in the vec.  */
>  
> diff --git a/gcc/diagnostic.h b/gcc/diagnostic.h
> index 9a51097f146..00b828f230d 100644
> --- a/gcc/diagnostic.h
> +++ b/gcc/diagnostic.h
> @@ -24,6 +24,11 @@ along with GCC; see the file COPYING3.  If not see
>  #include "pretty-print.h"
>  #include "diagnostic-core.h"
>  
> +namespace text_art
> +{
> +  class theme;
> +} // namespace text_art
> +
>  /* An enum for controlling what units to use for the column number
>     when diagnostics are output, used by the -fdiagnostics-column-unit option.
>     Tabs will be expanded or not according to the value of -ftabstop.  The origin
> @@ -170,6 +175,7 @@ class edit_context;
>  namespace json { class value; }
>  class diagnostic_client_data_hooks;
>  class logical_location;
> +class diagnostic_diagram;
>  
>  /* This data structure bundles altogether any information relevant to
>     the context of a diagnostic message.  */
> @@ -417,6 +423,18 @@ struct diagnostic_context
>       Used by SARIF output to give metadata about the client that's
>       producing diagnostics.  */
>    diagnostic_client_data_hooks *m_client_data_hooks;
> +
> +  /* Support for diagrams.  */
> +  struct
> +  {
> +    /* Theme to use when generating diagrams.
> +       Can be NULL (if text art is disabled).  */
> +    text_art::theme *m_theme;
> +
> +    /* Callback for emitting diagrams.  */
> +    void (*m_emission_cb) (diagnostic_context *context,
> +			   const diagnostic_diagram &diagram);
> +  } m_diagrams;
>  };
>  
>  inline void
> @@ -619,4 +637,7 @@ extern bool warning_enabled_at (location_t, int);
>  
>  extern char *get_cwe_url (int cwe);
>  
> +extern void diagnostic_emit_diagram (diagnostic_context *context,
> +				     const diagnostic_diagram &diagram);
> +
>  #endif /* ! GCC_DIAGNOSTIC_H */
> diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
> index 898a88ce33e..023a56a647e 100644
> --- a/gcc/doc/invoke.texi
> +++ b/gcc/doc/invoke.texi
> @@ -316,7 +316,8 @@ Objective-C and Objective-C++ Dialects}.
>  -fno-show-column
>  -fdiagnostics-column-unit=@r{[}display@r{|}byte@r{]}
>  -fdiagnostics-column-origin=@var{origin}
> --fdiagnostics-escape-format=@r{[}unicode@r{|}bytes@r{]}}
> +-fdiagnostics-escape-format=@r{[}unicode@r{|}bytes@r{]}
> +-fdiagnostics-text-art-charset=@r{[}none@r{|}ascii@r{|}unicode@r{|}emoji@r{]}}
>  
>  @item Warning Options
>  @xref{Warning Options,,Options to Request or Suppress Warnings}.
> @@ -5066,7 +5067,8 @@ options:
>  -fno-diagnostics-show-line-numbers
>  -fdiagnostics-color=never
>  -fdiagnostics-urls=never
> --fdiagnostics-path-format=separate-events}
> +-fdiagnostics-path-format=separate-events
> +-fdiagnostics-text-art-charset=none}
>  In the future, if GCC changes the default appearance of its diagnostics, the
>  corresponding option to disable the new behavior will be added to this list.
>  
> @@ -5592,6 +5594,25 @@ Unicode characters.  For the example above, the following will be printed:
>   before<CF><80><BF>after
>  @end smallexample
>  
> +@opindex fdiagnostics-text-art-charset
> +@item -fdiagnostics-text-art-charset=@var{CHARSET}
> +Some diagnostics can contain ``text art'' diagrams: visualizations created
> +from text, intended to be viewed in a monospaced font.
> +
> +This option selects which characters should be used for printing such
> +diagrams, if any.  @var{CHARSET} is @samp{none}, @samp{ascii}, @samp{unicode},
> +or @samp{emoji}.
> +
> +The @samp{none} value suppresses the printing of such diagrams.
> +The @samp{ascii} value will ensure that such diagrams are pure ASCII
> +(``ASCII art'').  The @samp{unicode} value will allow for conservative use of
> +unicode drawing characters (such as box-drawing characters).  The @samp{emoji}
> +value further adds the possibility of emoji in the output (such as emitting
> +U+26A0 WARNING SIGN followed by U+FE0F VARIATION SELECTOR-16 to select the
> +emoji variant of the character).
> +
> +The default is @samp{emoji}.
> +
>  @opindex fdiagnostics-format
>  @item -fdiagnostics-format=@var{FORMAT}
>  Select a different format for printing diagnostics.
> diff --git a/gcc/gcc.cc b/gcc/gcc.cc
> index 2ccca00d603..f9f0a7eaad4 100644
> --- a/gcc/gcc.cc
> +++ b/gcc/gcc.cc
> @@ -46,6 +46,7 @@ compilation is specified by a string called a "spec".  */
>  #include "spellcheck.h"
>  #include "opts-jobserver.h"
>  #include "common/common-target.h"
> +#include "diagnostic-text-art.h"
>  
>  \f
>  
> @@ -4299,6 +4300,11 @@ driver_handle_option (struct gcc_options *opts,
>  	  break;
>  	}
>  
> +    case OPT_fdiagnostics_text_art_charset_:
> +      diagnostics_text_art_charset_init (dc,
> +					 (enum diagnostic_text_art_charset)value);
> +      break;
> +
>      case OPT_Wa_:
>        {
>  	int prev, j;
> diff --git a/gcc/opts-common.cc b/gcc/opts-common.cc
> index 23ddcaa3b55..f0c5f483665 100644
> --- a/gcc/opts-common.cc
> +++ b/gcc/opts-common.cc
> @@ -1068,6 +1068,7 @@ decode_cmdline_options_to_array (unsigned int argc, const char **argv,
>  	    "-fdiagnostics-color=never",
>  	    "-fdiagnostics-urls=never",
>  	    "-fdiagnostics-path-format=separate-events",
> +	    "-fdiagnostics-text-art-charset=none"
>  	  };
>  	  const int num_expanded = ARRAY_SIZE (expanded_args);
>  	  opt_array_len += num_expanded - 1;
> diff --git a/gcc/opts.cc b/gcc/opts.cc
> index 86b94d62b58..3087bdac2c6 100644
> --- a/gcc/opts.cc
> +++ b/gcc/opts.cc
> @@ -35,6 +35,7 @@ along with GCC; see the file COPYING3.  If not see
>  #include "version.h"
>  #include "selftest.h"
>  #include "file-prefix-map.h"
> +#include "diagnostic-text-art.h"
>  
>  /* In this file all option sets are explicit.  */
>  #undef OPTION_SET_P
> @@ -2887,6 +2888,11 @@ common_handle_option (struct gcc_options *opts,
>  	  break;
>  	}
>  
> +    case OPT_fdiagnostics_text_art_charset_:
> +      diagnostics_text_art_charset_init (dc,
> +					 (enum diagnostic_text_art_charset)value);
> +      break;
> +
>      case OPT_fdiagnostics_parseable_fixits:
>        dc->extra_output_kind = (value
>  			       ? EXTRA_DIAGNOSTIC_OUTPUT_fixits_v1
> diff --git a/gcc/pretty-print.cc b/gcc/pretty-print.cc
> index 7d294717f50..3d789a23812 100644
> --- a/gcc/pretty-print.cc
> +++ b/gcc/pretty-print.cc
> @@ -1828,6 +1828,35 @@ pp_string (pretty_printer *pp, const char *str)
>    pp_maybe_wrap_text (pp, str, str + strlen (str));
>  }
>  
> +/* Append code point C to the output area of PRETTY-PRINTER, encoding it
> +   as UTF-8.  */
> +
> +void
> +pp_unicode_character (pretty_printer *pp, unsigned c)
> +{
> +  static const uchar masks[6] =  { 0x00, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC };
> +  static const uchar limits[6] = { 0x80, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE };
> +  size_t nbytes;
> +  uchar buf[6], *p = &buf[6];
> +
> +  nbytes = 1;
> +  if (c < 0x80)
> +    *--p = c;
> +  else
> +    {
> +      do
> +	{
> +	  *--p = ((c & 0x3F) | 0x80);
> +	  c >>= 6;
> +	  nbytes++;
> +	}
> +      while (c >= 0x3F || (c & limits[nbytes-1]));
> +      *--p = (c | masks[nbytes-1]);
> +    }
> +
> +  pp_append_r (pp, (const char *)p, nbytes);
> +}
> +
>  /* Append the leading N characters of STRING to the output area of
>     PRETTY-PRINTER, quoting in hexadecimal non-printable characters.
>     Setting N = -1 is as if N were set to strlen (STRING).  The STRING
> diff --git a/gcc/pretty-print.h b/gcc/pretty-print.h
> index 0230a289df5..369be6e7ba7 100644
> --- a/gcc/pretty-print.h
> +++ b/gcc/pretty-print.h
> @@ -401,6 +401,7 @@ extern void pp_indent (pretty_printer *);
>  extern void pp_newline (pretty_printer *);
>  extern void pp_character (pretty_printer *, int);
>  extern void pp_string (pretty_printer *, const char *);
> +extern void pp_unicode_character (pretty_printer *, unsigned);
>  
>  extern void pp_write_text_to_stream (pretty_printer *);
>  extern void pp_write_text_as_dot_label_to_stream (pretty_printer *, bool);
> diff --git a/gcc/selftest-run-tests.cc b/gcc/selftest-run-tests.cc
> index 915f2129702..e2fc8f84b1b 100644
> --- a/gcc/selftest-run-tests.cc
> +++ b/gcc/selftest-run-tests.cc
> @@ -28,6 +28,7 @@ along with GCC; see the file COPYING3.  If not see
>  #include "stringpool.h"
>  #include "attribs.h"
>  #include "analyzer/analyzer-selftests.h"
> +#include "text-art/selftests.h"
>  
>  /* This function needed to be split out from selftest.cc as it references
>     tests from the whole source tree, and so is within
> @@ -118,6 +119,8 @@ selftest::run_tests ()
>    /* Run any lang-specific selftests.  */
>    lang_hooks.run_lang_selftests ();
>  
> +  text_art_tests ();
> +
>    /* Run the analyzer selftests (if enabled).  */
>    ana::selftest::run_analyzer_selftests ();
>  
> diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
> new file mode 100644
> index 00000000000..e4239aab032
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-bw.c
> @@ -0,0 +1,57 @@
> +/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fdiagnostics-color=never" } */
> +
> +int non_empty;
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  A
> +   B
> +    C
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
> +  ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
> +  
> +  
> +  
> +  
> +  ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
> +  ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  +--+
> +  |🙂|
> +  +--+
> +
> +   { dg-end-multiline-output "" } */
> +/* { dg-begin-multiline-output "" }
> +
> +  +-------+-----+---------------+---------------------+-----------------------+-----------------------+
> +  |Offsets|Octet|       0       |          1          |           2           |           3           |
> +  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
> +  | Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
> +  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
> +  |   0   |  0  |Version|  IHL  |     DSCP      | ECN |                 Total Length                  |
> +  +-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+
> +  |   4   | 32  |           Identification            | Flags  |           Fragment Offset            |
> +  +-------+-----+---------------+---------------------+--------+--------------------------------------+
> +  |   8   | 64  | Time To Live  |      Protocol       |                Header Checksum                |
> +  +-------+-----+---------------+---------------------+-----------------------------------------------+
> +  |  12   | 96  |                                  Source IP Address                                  |
> +  +-------+-----+-------------------------------------------------------------------------------------+
> +  |  16   | 128 |                               Destination IP Address                                |
> +  +-------+-----+-------------------------------------------------------------------------------------+
> +  |  20   | 160 |                                                                                     |
> +  +-------+-----+                                                                                     |
> +  |  ...  | ... |                                       Options                                       |
> +  +-------+-----+                                                                                     |
> +  |  56   | 448 |                                                                                     |
> +  +-------+-----+-------------------------------------------------------------------------------------+
> +
> +   { dg-end-multiline-output "" } */
> diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
> new file mode 100644
> index 00000000000..0650428b1ce
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-ascii-color.c
> @@ -0,0 +1,58 @@
> +/* { dg-additional-options "-fdiagnostics-text-art-charset=ascii -fdiagnostics-color=always" } */
> +
> +int non_empty;
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  A
> +   B
> +    C
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ^[[38;2;0;0;0;48;2;240;217;181m^[[K♜ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♞ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♝ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♛ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♚ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♝ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♞ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♜ ^[[m^[[K
> +  ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[m^[[K
> +  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
> +  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
> +  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
> +  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
> +  ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[m^[[K
> +  ^[[38;2;255;255;255;48;2;181;136;99m^[[K♖ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♘ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♗ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♕ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♔ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♗ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♘ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♖ ^[[m^[[K
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  +--+
> +  |🙂|
> +  +--+
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  +-------+-----+---------------+---------------------+-----------------------+-----------------------+
> +  |Offsets|Octet|       0       |          1          |           2           |           3           |
> +  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
> +  | Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|
> +  +-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
> +  |   0   |  0  |Version|  IHL  |     DSCP      | ECN |                 Total Length                  |
> +  +-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+
> +  |   4   | 32  |           Identification            | Flags  |           Fragment Offset            |
> +  +-------+-----+---------------+---------------------+--------+--------------------------------------+
> +  |   8   | 64  | Time To Live  |      Protocol       |                Header Checksum                |
> +  +-------+-----+---------------+---------------------+-----------------------------------------------+
> +  |  12   | 96  |                                  Source IP Address                                  |
> +  +-------+-----+-------------------------------------------------------------------------------------+
> +  |  16   | 128 |                               Destination IP Address                                |
> +  +-------+-----+-------------------------------------------------------------------------------------+
> +  |  20   | 160 |                                                                                     |
> +  +-------+-----+                                                                                     |
> +  |  ...  | ... |                                       Options                                       |
> +  +-------+-----+                                                                                     |
> +  |  56   | 448 |                                                                                     |
> +  +-------+-----+-------------------------------------------------------------------------------------+
> +
> +   { dg-end-multiline-output "" } */
> diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
> new file mode 100644
> index 00000000000..c8118b46759
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-none.c
> @@ -0,0 +1,5 @@
> +/* { dg-additional-options "-fdiagnostics-text-art-charset=none" } */
> +
> +int non_empty;
> +
> +/* We expect no output.  */
> diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
> new file mode 100644
> index 00000000000..c9f5b36571a
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-bw.c
> @@ -0,0 +1,58 @@
> +/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -fdiagnostics-color=never" } */
> +
> +int non_empty;
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  A
> +   B
> +    C
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ♜ ♞ ♝ ♛ ♚ ♝ ♞ ♜
> +  ♟ ♟ ♟ ♟ ♟ ♟ ♟ ♟
> +  
> +  
> +  
> +  
> +  ♙ ♙ ♙ ♙ ♙ ♙ ♙ ♙
> +  ♖ ♘ ♗ ♕ ♔ ♗ ♘ ♖
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ┌──┐
> +  │🙂│
> +  └──┘
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐
> +  │Offsets│Octet│       0       │          1          │           2           │           3           │
> +  ├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤
> +  │ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│
> +  ├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤
> +  │   0   │  0  │Version│  IHL  │     DSCP      │ ECN │                 Total Length                  │
> +  ├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤
> +  │   4   │ 32  │           Identification            │ Flags  │           Fragment Offset            │
> +  ├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤
> +  │   8   │ 64  │ Time To Live  │      Protocol       │                Header Checksum                │
> +  ├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤
> +  │  12   │ 96  │                                  Source IP Address                                  │
> +  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
> +  │  16   │ 128 │                               Destination IP Address                                │
> +  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
> +  │  20   │ 160 │                                                                                     │
> +  ├───────┼─────┤                                                                                     │
> +  │  ...  │ ... │                                       Options                                       │
> +  ├───────┼─────┤                                                                                     │
> +  │  56   │ 448 │                                                                                     │
> +  └───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘
> +
> +   { dg-end-multiline-output "" } */
> diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
> new file mode 100644
> index 00000000000..f402836f889
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/plugin/diagnostic-test-text-art-unicode-color.c
> @@ -0,0 +1,59 @@
> +/* { dg-additional-options "-fdiagnostics-text-art-charset=unicode -fdiagnostics-color=always" } */
> +
> +int non_empty;
> +
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  A
> +   B
> +    C
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ^[[38;2;0;0;0;48;2;240;217;181m^[[K♜ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♞ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♝ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♛ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♚ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♝ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♞ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♜ ^[[m^[[K
> +  ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[38;2;0;0;0;48;2;181;136;99m^[[K♟ ^[[38;2;0;0;0;48;2;240;217;181m^[[K♟ ^[[m^[[K
> +  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
> +  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
> +  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[m^[[K
> +  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[48;2;181;136;99m^[[K  ^[[48;2;240;217;181m^[[K  ^[[m^[[K
> +  ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♙ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♙ ^[[m^[[K
> +  ^[[38;2;255;255;255;48;2;181;136;99m^[[K♖ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♘ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♗ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♕ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♔ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♗ ^[[38;2;255;255;255;48;2;181;136;99m^[[K♘ ^[[38;2;255;255;255;48;2;240;217;181m^[[K♖ ^[[m^[[K
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ┌──┐
> +  │🙂│
> +  └──┘
> +
> +   { dg-end-multiline-output "" } */
> +
> +/* { dg-begin-multiline-output "" }
> +
> +  ┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐
> +  │Offsets│Octet│       0       │          1          │           2           │           3           │
> +  ├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤
> +  │ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│
> +  ├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤
> +  │   0   │  0  │Version│  IHL  │     DSCP      │ ECN │                 Total Length                  │
> +  ├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤
> +  │   4   │ 32  │           Identification            │ Flags  │           Fragment Offset            │
> +  ├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤
> +  │   8   │ 64  │ Time To Live  │      Protocol       │                Header Checksum                │
> +  ├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤
> +  │  12   │ 96  │                                  Source IP Address                                  │
> +  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
> +  │  16   │ 128 │                               Destination IP Address                                │
> +  ├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤
> +  │  20   │ 160 │                                                                                     │
> +  ├───────┼─────┤                                                                                     │
> +  │  ...  │ ... │                                       Options                                       │
> +  ├───────┼─────┤                                                                                     │
> +  │  56   │ 448 │                                                                                     │
> +  └───────┴─────┴─────────────────────────────────────────────────────────────────────────────────────┘
> +
> +   { dg-end-multiline-output "" } */
> diff --git a/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
> new file mode 100644
> index 00000000000..27c341b9f2f
> --- /dev/null
> +++ b/gcc/testsuite/gcc.dg/plugin/diagnostic_plugin_test_text_art.c
> @@ -0,0 +1,257 @@
> +/* { dg-options "-O" } */
> +
> +/* This plugin exercises the text_art code.  */
> +
> +#include "gcc-plugin.h"
> +#include "config.h"
> +#include "system.h"
> +#include "coretypes.h"
> +#include "plugin-version.h"
> +#include "diagnostic.h"
> +#include "diagnostic-diagram.h"
> +#include "text-art/canvas.h"
> +#include "text-art/table.h"
> +
> +int plugin_is_GPL_compatible;
> +
> +using namespace text_art;
> +
> +/* Canvas tests.  */
> +
> +static void
> +emit_canvas (const canvas &c, const char *alt_text)
> +{
> +  diagnostic_diagram diagram (c, alt_text);
> +  diagnostic_emit_diagram (global_dc, diagram);
> +}
> +
> +static void
> +test_abc ()
> +{
> +  style_manager sm;
> +  canvas c (canvas::size_t (3, 3), sm);
> +  c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
> +  c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
> +  c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));
> +  emit_canvas (c, "test_abc");
> +}
> +
> +/* Test of procedural art using 24-bit color: chess starting position.  */
> +
> +static void
> +test_chessboard ()
> +{
> +  /* With the exception of NONE, these are in order of the chess symbols
> +     in the Unicode Miscellaneous Symbols block.  */
> +  enum class piece { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN, NONE };
> +  enum class color { BLACK, WHITE, NONE };
> +
> +  style_manager sm;
> +
> +  /* We assume double-column chars for the pieces, so allow two canvas
> +     columns per square.  */
> +  canvas canvas (canvas::size_t (16, 8), sm);
> +
> +  for (int x = 0; x < 8; x++)
> +    for (int y = 0; y < 8; y++)
> +      {
> +	enum piece piece_kind;
> +	enum color piece_color;
> +	switch (y)
> +	  {
> +	  case 0:
> +	  case 7:
> +	    switch (x)
> +	      {
> +	      default:
> +		gcc_unreachable ();
> +	      case 0:
> +		piece_kind = piece::ROOK;
> +		break;
> +	      case 1:
> +		piece_kind = piece::KNIGHT;
> +		break;
> +	      case 2:
> +		piece_kind = piece::BISHOP;
> +		break;
> +	      case 3:
> +		piece_kind = piece::QUEEN;
> +		break;
> +	      case 4:
> +		piece_kind = piece::KING;
> +		break;
> +	      case 5:
> +		piece_kind = piece::BISHOP;
> +		break;
> +	      case 6:
> +		piece_kind = piece::KNIGHT;
> +		break;
> +	      case 7:
> +		piece_kind = piece::ROOK;
> +	      break;
> +	      }
> +	    piece_color = (y == 0) ? color::BLACK : color::WHITE;
> +	    break;
> +	  case 1:
> +	  case 6:
> +	    piece_kind = piece::PAWN;
> +	    piece_color = (y == 1) ? color::BLACK : color::WHITE;
> +	    break;
> +	  default:
> +	    piece_kind = piece::NONE;
> +	    piece_color = color::NONE;
> +	    break;
> +	  }
> +
> +	style s;
> +	const bool white_square = (x + y) % 2 == 0;
> +	if (white_square)
> +	  s.m_bg_color = style::color (0xf0, 0xd9, 0xb5);
> +	else
> +	  s.m_bg_color = style::color (0xb5, 0x88, 0x63);
> +	switch (piece_color)
> +	  {
> +	  default:
> +	    gcc_unreachable ();
> +	  case color::WHITE:
> +	    s.m_fg_color = style::color (0xff, 0xff, 0xff);
> +	    break;
> +	  case color::BLACK:
> +	    s.m_fg_color = style::color (0x00, 0x00, 0x00);
> +	    break;
> +	  case color::NONE:
> +	    break;
> +	  }
> +	style::id_t style_id = sm.get_or_create_id (s);
> +
> +	cppchar_t ch;
> +	if (piece_kind == piece::NONE)
> +	  ch = ' ';
> +	else
> +	  {
> +	    const cppchar_t WHITE_KING = 0x2654;
> +	    const cppchar_t BLACK_KING = 0x265A;
> +	    cppchar_t base ((piece_color == color::WHITE)
> +			    ? WHITE_KING : BLACK_KING);
> +	    ch = base + ((int)piece_kind - (int)piece::KING);
> +	  }
> +	canvas.paint (canvas::coord_t (x * 2, y),
> +		      canvas::cell_t (ch, false, style_id));
> +	canvas.paint (canvas::coord_t (x * 2 + 1, y),
> +		      canvas::cell_t (' ', false, style_id));
> +      }
> +  emit_canvas (canvas, "test_chessboard");
> +}
> +
> +/* Table tests.  */
> +
> +static void
> +emit_table (const table &table, const style_manager &sm, const char *alt_text)
> +{
> +  const text_art::theme *theme = global_dc->m_diagrams.m_theme;
> +  if (!theme)
> +    return;
> +  canvas c (table.to_canvas (*theme, sm));
> +  emit_canvas (c, alt_text);
> +}
> +
> +static void
> +test_double_width_chars ()
> +{
> +  style_manager sm;
> +  table table (table::size_t (1, 1));
> +  table.set_cell (table::coord_t (0,0),
> +		  styled_string ((cppchar_t)0x1f642));
> +
> +  emit_table (table, sm, "test_double_width_chars");
> +}
> +
> +static void
> +test_ipv4_header ()
> +{
> +  style_manager sm;
> +  table table (table::size_t (34, 10));
> +  table.set_cell (table::coord_t (0, 0), styled_string (sm, "Offsets"));
> +  table.set_cell (table::coord_t (1, 0), styled_string (sm, "Octet"));
> +  table.set_cell (table::coord_t (0, 1), styled_string (sm, "Octet"));
> +  for (int octet = 0; octet < 4; octet++)
> +    table.set_cell_span (table::rect_t (table::coord_t (2 + (octet * 8), 0),
> +					table::size_t (8, 1)),
> +			 styled_string::from_fmt (sm, nullptr, "%i", octet));
> +  table.set_cell (table::coord_t (1, 1), styled_string (sm, "Bit"));
> +  for (int bit = 0; bit < 32; bit++)
> +    table.set_cell (table::coord_t (bit + 2, 1),
> +		    styled_string::from_fmt (sm, nullptr, "%i", bit));
> +  for (int word = 0; word < 6; word++)
> +    {
> +      table.set_cell (table::coord_t (0, word + 2),
> +		      styled_string::from_fmt (sm, nullptr, "%i", word * 4));
> +      table.set_cell (table::coord_t (1, word + 2),
> +		      styled_string::from_fmt (sm, nullptr, "%i", word * 32));
> +    }
> +
> +  table.set_cell (table::coord_t (0, 8), styled_string (sm, "..."));
> +  table.set_cell (table::coord_t (1, 8), styled_string (sm, "..."));
> +  table.set_cell (table::coord_t (0, 9), styled_string (sm, "56"));
> +  table.set_cell (table::coord_t (1, 9), styled_string (sm, "448"));
> +
> +#define SET_BITS(FIRST, LAST, NAME)					\
> +  do {									\
> +    const int first = (FIRST);						\
> +    const int last = (LAST);						\
> +    const char *name = (NAME);						\
> +    const int row = first / 32;						\
> +    gcc_assert (last / 32 == row);					\
> +    table::rect_t rect (table::coord_t ((first % 32) + 2, row + 2),	\
> +			table::size_t (last + 1 - first , 1));		\
> +    table.set_cell_span (rect, styled_string (sm, name));		\
> +  } while (0)
> +
> +  SET_BITS (0, 3, "Version");
> +  SET_BITS (4, 7, "IHL");
> +  SET_BITS (8, 13, "DSCP");
> +  SET_BITS (14, 15, "ECN");
> +  SET_BITS (16, 31, "Total Length");
> +
> +  SET_BITS (32 +  0, 32 + 15, "Identification");
> +  SET_BITS (32 + 16, 32 + 18, "Flags");
> +  SET_BITS (32 + 19, 32 + 31, "Fragment Offset");
> +
> +  SET_BITS (64 +  0, 64 +  7, "Time To Live");
> +  SET_BITS (64 +  8, 64 + 15, "Protocol");
> +  SET_BITS (64 + 16, 64 + 31, "Header Checksum");
> +
> +  SET_BITS (96 +  0, 96 + 31, "Source IP Address");
> +  SET_BITS (128 +  0, 128 + 31, "Destination IP Address");
> +
> +  table.set_cell_span(table::rect_t (table::coord_t (2, 7),
> +				     table::size_t (32, 3)),
> +		      styled_string (sm, "Options"));
> +
> +  emit_table (table, sm, "test_ipv4_header");
> +}
> +
> +static void
> +show_diagrams ()
> +{
> +  test_abc ();
> +  test_chessboard ();
> +  test_double_width_chars ();
> +  test_ipv4_header ();
> +}
> +
> +int
> +plugin_init (struct plugin_name_args *plugin_info,
> +	     struct plugin_gcc_version *version)
> +{
> +  const char *plugin_name = plugin_info->base_name;
> +  int argc = plugin_info->argc;
> +  struct plugin_argument *argv = plugin_info->argv;
> +
> +  if (!plugin_default_version_check (version, &gcc_version))
> +    return 1;
> +
> +  show_diagrams ();
> +
> +  return 0;
> +}
> diff --git a/gcc/testsuite/gcc.dg/plugin/plugin.exp b/gcc/testsuite/gcc.dg/plugin/plugin.exp
> index 4d6304cd100..60723a20eda 100644
> --- a/gcc/testsuite/gcc.dg/plugin/plugin.exp
> +++ b/gcc/testsuite/gcc.dg/plugin/plugin.exp
> @@ -114,6 +114,12 @@ set plugin_test_list [list \
>  	  diagnostic-path-format-inline-events-1.c \
>  	  diagnostic-path-format-inline-events-2.c \
>  	  diagnostic-path-format-inline-events-3.c } \
> +    { diagnostic_plugin_test_text_art.c \
> +	  diagnostic-test-text-art-none.c \
> +	  diagnostic-test-text-art-ascii-bw.c \
> +	  diagnostic-test-text-art-ascii-color.c \
> +	  diagnostic-test-text-art-unicode-bw.c \
> +	  diagnostic-test-text-art-unicode-color.c } \
>      { location_overflow_plugin.c \
>  	  location-overflow-test-1.c \
>  	  location-overflow-test-2.c \
> diff --git a/gcc/text-art/box-drawing-chars.inc b/gcc/text-art/box-drawing-chars.inc
> new file mode 100644
> index 00000000000..a370255d56d
> --- /dev/null
> +++ b/gcc/text-art/box-drawing-chars.inc
> @@ -0,0 +1,18 @@
> +/* Generated by contrib/unicode/gen-box-drawing-chars.py.  */
> +
> +0x0020, /* " ": U+0020: SPACE */
> +0x2576, /* "╶": U+2576: BOX DRAWINGS LIGHT RIGHT */
> +0x2574, /* "╴": U+2574: BOX DRAWINGS LIGHT LEFT */
> +0x2500, /* "─": U+2500: BOX DRAWINGS LIGHT HORIZONTAL */
> +0x2577, /* "╷": U+2577: BOX DRAWINGS LIGHT DOWN */
> +0x250C, /* "┌": U+250C: BOX DRAWINGS LIGHT DOWN AND RIGHT */
> +0x2510, /* "┐": U+2510: BOX DRAWINGS LIGHT DOWN AND LEFT */
> +0x252C, /* "┬": U+252C: BOX DRAWINGS LIGHT DOWN AND HORIZONTAL */
> +0x2575, /* "╵": U+2575: BOX DRAWINGS LIGHT UP */
> +0x2514, /* "└": U+2514: BOX DRAWINGS LIGHT UP AND RIGHT */
> +0x2518, /* "┘": U+2518: BOX DRAWINGS LIGHT UP AND LEFT */
> +0x2534, /* "┴": U+2534: BOX DRAWINGS LIGHT UP AND HORIZONTAL */
> +0x2502, /* "│": U+2502: BOX DRAWINGS LIGHT VERTICAL */
> +0x251C, /* "├": U+251C: BOX DRAWINGS LIGHT VERTICAL AND RIGHT */
> +0x2524, /* "┤": U+2524: BOX DRAWINGS LIGHT VERTICAL AND LEFT */
> +0x253C  /* "┼": U+253C: BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL */
> diff --git a/gcc/text-art/box-drawing.cc b/gcc/text-art/box-drawing.cc
> new file mode 100644
> index 00000000000..981d0b095cf
> --- /dev/null
> +++ b/gcc/text-art/box-drawing.cc
> @@ -0,0 +1,72 @@
> +/* Procedural lookup of box drawing characters.
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#include "system.h"
> +#include "coretypes.h"
> +#include "text-art/box-drawing.h"
> +#include "selftest.h"
> +#include "text-art/selftests.h"
> +
> +
> +/* According to
> +     https://en.wikipedia.org/wiki/Box-drawing_character#Character_code
> +   "DOS line- and box-drawing characters are not ordered in any programmatic
> +   manner, so calculating a particular character shape needs to use a look-up
> +   table. "
> +   Hence this array.  */
> +static const cppchar_t box_drawing_chars[] = {
> +#include "text-art/box-drawing-chars.inc"
> +};
> +
> +cppchar_t
> +text_art::get_box_drawing_char (directions line_dirs)
> +{
> +  const size_t idx = line_dirs.as_index ();
> +  gcc_assert (idx < 16);
> +  return box_drawing_chars[idx];
> +}
> +
> +#if CHECKING_P
> +
> +namespace selftest {
> +
> +/* Run all selftests in this file.  */
> +
> +void
> +text_art_box_drawing_cc_tests ()
> +{
> +  ASSERT_EQ (text_art::get_box_drawing_char
> +	      (text_art::directions (false, false, false, false)),
> +	     ' ');
> +  ASSERT_EQ (text_art::get_box_drawing_char
> +	       (text_art::directions (false, false, true, true)),
> +	     0x2500); /* BOX DRAWINGS LIGHT HORIZONTAL */
> +  ASSERT_EQ (text_art::get_box_drawing_char
> +	       (text_art::directions (true, true, false, false)),
> +	     0x2502); /* BOX DRAWINGS LIGHT VERTICAL */
> +  ASSERT_EQ (text_art::get_box_drawing_char
> +	       (text_art::directions (true, false, true, false)),
> +	     0x2518); /* BOX DRAWINGS LIGHT UP AND LEFT */
> +}
> +
> +} // namespace selftest
> +
> +#endif /* #if CHECKING_P */
> diff --git a/gcc/text-art/box-drawing.h b/gcc/text-art/box-drawing.h
> new file mode 100644
> index 00000000000..29f4d9921b3
> --- /dev/null
> +++ b/gcc/text-art/box-drawing.h
> @@ -0,0 +1,32 @@
> +/* Procedural lookup of box drawing characters.
> +   Copyright (C) 2023 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/>.  */
> +
> +#ifndef GCC_TEXT_ART_BOX_DRAWING_H
> +#define GCC_TEXT_ART_BOX_DRAWING_H
> +
> +#include "text-art/types.h"
> +
> +namespace text_art {
> +
> +extern cppchar_t get_box_drawing_char (directions line_dirs);
> +
> +} // namespace text_art
> +
> +#endif /* GCC_TEXT_ART_BOX_DRAWING_H */
> diff --git a/gcc/text-art/canvas.cc b/gcc/text-art/canvas.cc
> new file mode 100644
> index 00000000000..f229612c919
> --- /dev/null
> +++ b/gcc/text-art/canvas.cc
> @@ -0,0 +1,437 @@
> +/* Canvas for random-access procedural text art.
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#include "system.h"
> +#include "coretypes.h"
> +#include "pretty-print.h"
> +#include "selftest.h"
> +#include "text-art/selftests.h"
> +#include "text-art/canvas.h"
> +
> +using namespace text_art;
> +
> +canvas::canvas (size_t size, const style_manager &style_mgr)
> +: m_cells (size_t (size.w, size.h)),
> +  m_style_mgr (style_mgr)
> +{
> +  m_cells.fill (cell_t (' '));
> +}
> +
> +void
> +canvas::paint (coord_t coord, styled_unichar ch)
> +{
> +  m_cells.set (coord, std::move (ch));
> +}
> +
> +void
> +canvas::paint_text (coord_t coord, const styled_string &text)
> +{
> +  for (auto ch : text)
> +    {
> +      paint (coord, ch);
> +      if (ch.double_width_p ())
> +	coord.x += 2;
> +      else
> +	coord.x++;
> +    }
> +}
> +
> +void
> +canvas::fill (rect_t rect, cell_t c)
> +{
> +  for (int y = rect.get_min_y (); y < rect.get_next_y (); y++)
> +    for (int x = rect.get_min_x (); x < rect.get_next_x (); x++)
> +      paint(coord_t (x, y), c);
> +}
> +
> +void
> +canvas::debug_fill ()
> +{
> +  fill (rect_t (coord_t (0, 0), get_size ()), cell_t ('*'));
> +}
> +
> +void
> +canvas::print_to_pp (pretty_printer *pp,
> +		     const char *per_line_prefix) const
> +{
> +  for (int y = 0; y < m_cells.get_size ().h; y++)
> +    {
> +      style::id_t curr_style_id = 0;
> +      if (per_line_prefix)
> +	pp_string (pp, per_line_prefix);
> +
> +      pretty_printer line_pp;
> +      line_pp.show_color = pp->show_color;
> +      line_pp.url_format = pp->url_format;
> +      const int final_x_in_row = get_final_x_in_row (y);
> +      for (int x = 0; x <= final_x_in_row; x++)
> +	{
> +	  if (x > 0)
> +	    {
> +	      const cell_t prev_cell = m_cells.get (coord_t (x - 1, y));
> +	      if (prev_cell.double_width_p ())
> +		 /* This cell is just a placeholder for the
> +		    2nd column of a double width cell; skip it.  */
> +		continue;
> +	    }
> +	  const cell_t cell = m_cells.get (coord_t (x, y));
> +	  if (cell.get_style_id () != curr_style_id)
> +	    {
> +	      m_style_mgr.print_any_style_changes (&line_pp,
> +						   curr_style_id,
> +						   cell.get_style_id ());
> +	      curr_style_id = cell.get_style_id ();
> +	    }
> +	  pp_unicode_character (&line_pp, cell.get_code ());
> +	  if (cell.emoji_variant_p ())
> +	    /* Append U+FE0F VARIATION SELECTOR-16 to select the emoji
> +	       variation of the char.  */
> +	    pp_unicode_character (&line_pp, 0xFE0F);
> +	}
> +      /* Reset the style at the end of each line.  */
> +      m_style_mgr.print_any_style_changes (&line_pp, curr_style_id, 0);
> +
> +      /* Print from line_pp to pp, stripping trailing whitespace from
> +	 the line.  */
> +      const char *line_buf = pp_formatted_text (&line_pp);
> +      ::size_t len = strlen (line_buf);
> +      while (len > 0)
> +	{
> +	  if (line_buf[len - 1] == ' ')
> +	    len--;
> +	  else
> +	    break;
> +	}
> +      pp_append_text (pp, line_buf, line_buf + len);
> +      pp_newline (pp);
> +    }
> +}
> +
> +DEBUG_FUNCTION void
> +canvas::debug (bool styled) const
> +{
> +  pretty_printer pp;
> +  if (styled)
> +    {
> +      pp_show_color (&pp) = true;
> +      pp.url_format = determine_url_format (DIAGNOSTICS_URL_AUTO);
> +    }
> +  print_to_pp (&pp);
> +  fprintf (stderr, "%s\n", pp_formatted_text (&pp));
> +}
> +
> +/* Find right-most non-default cell in this row,
> +   or -1 if all are default.  */
> +
> +int
> +canvas::get_final_x_in_row (int y) const
> +{
> +  for (int x = m_cells.get_size ().w - 1; x >= 0; x--)
> +    {
> +      cell_t cell = m_cells.get (coord_t (x, y));
> +      if (cell.get_code () != ' '
> +	  || cell.get_style_id () != style::id_plain)
> +	return x;
> +    }
> +  return -1;
> +}
> +
> +#if CHECKING_P
> +
> +namespace selftest {
> +
> +static void
> +test_blank ()
> +{
> +  style_manager sm;
> +  canvas c (canvas::size_t (5, 5), sm);
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       ("\n"
> +			"\n"
> +			"\n"
> +			"\n"
> +			"\n"));
> +}
> +
> +static void
> +test_abc ()
> +{
> +  style_manager sm;
> +  canvas c (canvas::size_t (3, 3), sm);
> +  c.paint (canvas::coord_t (0, 0), styled_unichar ('A'));
> +  c.paint (canvas::coord_t (1, 1), styled_unichar ('B'));
> +  c.paint (canvas::coord_t (2, 2), styled_unichar ('C'));
> +
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       "A\n B\n  C\n");
> +}
> +
> +static void
> +test_debug_fill ()
> +{
> +  style_manager sm;
> +  canvas c (canvas::size_t (5, 3), sm);
> +  c.debug_fill();
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       ("*****\n"
> +			"*****\n"
> +			"*****\n"));
> +}
> +
> +static void
> +test_text ()
> +{
> +  style_manager sm;
> +  canvas c (canvas::size_t (6, 1), sm);
> +  c.paint_text (canvas::coord_t (0, 0), styled_string (sm, "012345"));
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       ("012345\n"));
> +
> +  /* Paint an emoji character that should occupy two canvas columns when
> +     printed.  */
> +  c.paint_text (canvas::coord_t (2, 0), styled_string ((cppchar_t)0x1f642));
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       ("01🙂45\n"));
> +}
> +
> +static void
> +test_circle ()
> +{
> +  canvas::size_t sz (30, 30);
> +  style_manager sm;
> +  canvas canvas (sz, sm);
> +  canvas::coord_t center (sz.w / 2, sz.h / 2);
> +  const int radius = 12;
> +  const int radius_squared = radius * radius;
> +  for (int x = 0; x < sz.w; x++)
> +    for (int y = 0; y < sz.h; y++)
> +      {
> +	int dx = x - center.x;
> +	int dy = y - center.y;
> +	char ch = "AB"[(x + y) % 2];
> +	if (dx * dx + dy * dy < radius_squared)
> +	  canvas.paint (canvas::coord_t (x, y), styled_unichar (ch));
> +      }
> +  ASSERT_CANVAS_STREQ
> +    (canvas, false,
> +     ("\n"
> +      "\n"
> +      "\n"
> +      "\n"
> +      "           BABABABAB\n"
> +      "         ABABABABABABA\n"
> +      "        ABABABABABABABA\n"
> +      "       ABABABABABABABABA\n"
> +      "      ABABABABABABABABABA\n"
> +      "     ABABABABABABABABABABA\n"
> +      "     BABABABABABABABABABAB\n"
> +      "    BABABABABABABABABABABAB\n"
> +      "    ABABABABABABABABABABABA\n"
> +      "    BABABABABABABABABABABAB\n"
> +      "    ABABABABABABABABABABABA\n"
> +      "    BABABABABABABABABABABAB\n"
> +      "    ABABABABABABABABABABABA\n"
> +      "    BABABABABABABABABABABAB\n"
> +      "    ABABABABABABABABABABABA\n"
> +      "    BABABABABABABABABABABAB\n"
> +      "     BABABABABABABABABABAB\n"
> +      "     ABABABABABABABABABABA\n"
> +      "      ABABABABABABABABABA\n"
> +      "       ABABABABABABABABA\n"
> +      "        ABABABABABABABA\n"
> +      "         ABABABABABABA\n"
> +      "           BABABABAB\n"
> +      "\n"
> +      "\n"
> +      "\n"));
> +}
> +
> +static void
> +test_color_circle ()
> +{
> +  const canvas::size_t sz (10, 10);
> +  const canvas::coord_t center (sz.w / 2, sz.h / 2);
> +  const int outer_r2 = 25;
> +  const int inner_r2 = 10;
> +  style_manager sm;
> +  canvas c (sz, sm);
> +  for (int x = 0; x < sz.w; x++)
> +    for (int y = 0; y < sz.h; y++)
> +      {
> +	const int dist_from_center_squared
> +	  = ((x - center.x) * (x - center.x) + (y - center.y) * (y - center.y));
> +	if (dist_from_center_squared < outer_r2)
> +	  {
> +	    style s;
> +	    if (dist_from_center_squared < inner_r2)
> +	      s.m_fg_color = style::named_color::RED;
> +	    else
> +	      s.m_fg_color = style::named_color::GREEN;
> +	    c.paint (canvas::coord_t (x, y),
> +		     styled_unichar ('*', false, sm.get_or_create_id (s)));
> +	  }
> +      }
> +  ASSERT_EQ (sm.get_num_styles (), 3);
> +  ASSERT_CANVAS_STREQ
> +    (c, false,
> +     ("\n"
> +      "   *****\n"
> +      "  *******\n"
> +      " *********\n"
> +      " *********\n"
> +      " *********\n"
> +      " *********\n"
> +      " *********\n"
> +      "  *******\n"
> +      "   *****\n"));
> +  ASSERT_CANVAS_STREQ
> +    (c, true,
> +     ("\n"
> +      "   ^[[32m^[[K*****^[[m^[[K\n"
> +      "  ^[[32m^[[K***^[[31m^[[K*^[[32m^[[K***^[[m^[[K\n"
> +      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
> +      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
> +      " ^[[32m^[[K*^[[31m^[[K*******^[[32m^[[K*^[[m^[[K\n"
> +      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
> +      " ^[[32m^[[K**^[[31m^[[K*****^[[32m^[[K**^[[m^[[K\n"
> +      "  ^[[32m^[[K***^[[31m^[[K*^[[32m^[[K***^[[m^[[K\n"
> +      "   ^[[32m^[[K*****^[[m^[[K\n"));
> +}
> +
> +static void
> +test_bold ()
> +{
> +  auto_fix_quotes fix_quotes;
> +  style_manager sm;
> +  styled_string s (styled_string::from_fmt (sm, nullptr,
> +					    "before %qs after", "foo"));
> +  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
> +  c.paint_text (canvas::coord_t (0, 0), s);
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       "before `foo' after\n");
> +  ASSERT_CANVAS_STREQ (c, true,
> +		       "before `^[[00;01m^[[Kfoo^[[00m^[[K' after\n");
> +}
> +
> +static void
> +test_emoji ()
> +{
> +  style_manager sm;
> +  styled_string s (0x26A0, /* U+26A0 WARNING SIGN.  */
> +		   true);
> +  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
> +  c.paint_text (canvas::coord_t (0, 0), s);
> +  ASSERT_CANVAS_STREQ (c, false, "⚠️\n");
> +  ASSERT_CANVAS_STREQ (c, true, "⚠️\n");
> +}
> +
> +static void
> +test_emoji_2 ()
> +{
> +  style_manager sm;
> +  styled_string s;
> +  s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN.  */
> +			   true));
> +  s.append (styled_string (sm, "test"));
> +  ASSERT_EQ (s.size (), 5);
> +  ASSERT_EQ (s.calc_canvas_width (), 5);
> +  canvas c (canvas::size_t (s.calc_canvas_width (), 1), sm);
> +  c.paint_text (canvas::coord_t (0, 0), s);
> +  ASSERT_CANVAS_STREQ (c, false,
> +		       /* U+26A0 WARNING SIGN, as UTF-8: 0xE2 0x9A 0xA0.  */
> +		       "\xE2\x9A\xA0"
> +		       /* U+FE0F VARIATION SELECTOR-16, as UTF-8: 0xEF 0xB8 0x8F.  */
> +		       "\xEF\xB8\x8F"
> +		       "test\n");
> +}
> +
> +static void
> +test_canvas_urls ()
> +{
> +  style_manager sm;
> +  canvas canvas (canvas::size_t (9, 3), sm);
> +  styled_string foo_ss (sm, "foo");
> +  foo_ss.set_url (sm, "https://www.example.com/foo");
> +  styled_string bar_ss (sm, "bar");
> +  bar_ss.set_url (sm, "https://www.example.com/bar");
> +  canvas.paint_text(canvas::coord_t (1, 1), foo_ss);
> +  canvas.paint_text(canvas::coord_t (5, 1), bar_ss);
> +
> +  ASSERT_CANVAS_STREQ (canvas, false,
> +		       ("\n"
> +			" foo bar\n"
> +			"\n"));
> +  {
> +    pretty_printer pp;
> +    pp_show_color (&pp) = true;
> +    pp.url_format = URL_FORMAT_ST;
> +    assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
> +			 (/* Line 1.  */
> +			  "\n"
> +			  /* Line 2.  */
> +			  " "
> +			  "\33]8;;https://www.example.com/foo\33\\foo\33]8;;\33\\"
> +			  " "
> +			  "\33]8;;https://www.example.com/bar\33\\bar\33]8;;\33\\"
> +			  "\n"
> +			  /* Line 3.  */
> +			  "\n"));
> +  }
> +
> +  {
> +    pretty_printer pp;
> +    pp_show_color (&pp) = true;
> +    pp.url_format = URL_FORMAT_BEL;
> +    assert_canvas_streq (SELFTEST_LOCATION, canvas, &pp,
> +			 (/* Line 1.  */
> +			  "\n"
> +			  /* Line 2.  */
> +			  " "
> +			  "\33]8;;https://www.example.com/foo\afoo\33]8;;\a"
> +			  " "
> +			  "\33]8;;https://www.example.com/bar\abar\33]8;;\a"
> +			  "\n"
> +			  /* Line 3.  */
> +			  "\n"));
> +  }
> +}
> +
> +/* Run all selftests in this file.  */
> +
> +void
> +text_art_canvas_cc_tests ()
> +{
> +  test_blank ();
> +  test_abc ();
> +  test_debug_fill ();
> +  test_text ();
> +  test_circle ();
> +  test_color_circle ();
> +  test_bold ();
> +  test_emoji ();
> +  test_emoji_2 ();
> +  test_canvas_urls ();
> +}
> +
> +} // namespace selftest
> +
> +
> +#endif /* #if CHECKING_P */
> diff --git a/gcc/text-art/canvas.h b/gcc/text-art/canvas.h
> new file mode 100644
> index 00000000000..495497754f5
> --- /dev/null
> +++ b/gcc/text-art/canvas.h
> @@ -0,0 +1,74 @@
> +/* Canvas for random-access procedural text art.
> +   Copyright (C) 2023 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/>.  */
> +
> +#ifndef GCC_TEXT_ART_CANVAS_H
> +#define GCC_TEXT_ART_CANVAS_H
> +
> +#include "text-art/types.h"
> +
> +namespace text_art {
> +
> +class canvas;
> +
> +/* A 2 dimensional grid of text cells (a "canvas"), which
> +   can be written to ("painted") via random access, and then
> +   written out to a pretty_printer once the picture is complete.
> +
> +   Each text cell can be styled independently (colorization,
> +   URLs, etc).  */
> +
> +class canvas
> +{
> + public:
> +  typedef styled_unichar cell_t;
> +  typedef size<class canvas> size_t;
> +  typedef coord<class canvas> coord_t;
> +  typedef range<class canvas> range_t;
> +  typedef rect<class canvas> rect_t;
> +
> +  canvas (size_t size, const style_manager &style_mgr);
> +
> +  size_t get_size () const { return m_cells.get_size (); }
> +
> +  void paint (coord_t coord, cell_t c);
> +  void paint_text (coord_t coord, const styled_string &text);
> +
> +  void fill (rect_t rect, cell_t c);
> +  void debug_fill ();
> +
> +  void print_to_pp (pretty_printer *pp,
> +		    const char *per_line_prefix = NULL) const;
> +  void debug (bool styled) const;
> +
> +  const cell_t &get (coord_t coord) const
> +  {
> +    return m_cells.get (coord);
> +  }
> +
> + private:
> +  int get_final_x_in_row (int y) const;
> +
> +  array2<cell_t, size_t, coord_t> m_cells;
> +  const style_manager &m_style_mgr;
> +};
> +
> +} // namespace text_art
> +
> +#endif /* GCC_TEXT_ART_CANVAS_H */
> diff --git a/gcc/text-art/ruler.cc b/gcc/text-art/ruler.cc
> new file mode 100644
> index 00000000000..80c623f77ba
> --- /dev/null
> +++ b/gcc/text-art/ruler.cc
> @@ -0,0 +1,723 @@
> +/* Classes for printing labelled rulers.
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#define INCLUDE_ALGORITHM
> +#include "system.h"
> +#include "coretypes.h"
> +#include "pretty-print.h"
> +#include "selftest.h"
> +#include "text-art/selftests.h"
> +#include "text-art/ruler.h"
> +#include "text-art/theme.h"
> +
> +using namespace text_art;
> +
> +void
> +x_ruler::add_label (const canvas::range_t &r,
> +		    styled_string text,
> +		    style::id_t style_id,
> +		    label_kind kind)
> +{
> +  m_labels.push_back (label (r, std::move (text), style_id, kind));
> +  m_has_layout = false;
> +}
> +
> +int
> +x_ruler::get_canvas_y (int rel_y) const
> +{
> +  gcc_assert (rel_y >= 0);
> +  gcc_assert (rel_y < m_size.h);
> +  switch (m_label_dir)
> +    {
> +    default:
> +      gcc_unreachable ();
> +    case label_dir::ABOVE:
> +      return m_size.h - (rel_y + 1);
> +    case label_dir::BELOW:
> +      return rel_y;
> +    }
> +}
> +
> +void
> +x_ruler::paint_to_canvas (canvas &canvas,
> +			  canvas::coord_t offset,
> +			  const theme &theme)
> +{
> +  ensure_layout ();
> +
> +  if (0)
> +    canvas.fill (canvas::rect_t (offset, m_size),
> +		 canvas::cell_t ('*'));
> +
> +  for (size_t idx = 0; idx < m_labels.size (); idx++)
> +    {
> +      const label &iter_label = m_labels[idx];
> +
> +      /* Paint the ruler itself.  */
> +      const int ruler_rel_y = get_canvas_y (0);
> +      for (int rel_x = iter_label.m_range.start;
> +	   rel_x < iter_label.m_range.next;
> +	   rel_x++)
> +	{
> +	  enum theme::cell_kind kind = theme::cell_kind::X_RULER_MIDDLE;
> +
> +	  if (rel_x == iter_label.m_range.start)
> +	    {
> +	      kind = theme::cell_kind::X_RULER_LEFT_EDGE;
> +	      if (idx > 0)
> +		{
> +		  const label &prev_label = m_labels[idx - 1];
> +		  if (prev_label.m_range.get_max () == iter_label.m_range.start)
> +		    kind = theme::cell_kind::X_RULER_INTERNAL_EDGE;
> +		}
> +	    }
> +	  else if (rel_x == iter_label.m_range.get_max ())
> +	    kind = theme::cell_kind::X_RULER_RIGHT_EDGE;
> +	  else if (rel_x == iter_label.m_connector_x)
> +	    {
> +	      switch (m_label_dir)
> +		{
> +		default:
> +		  gcc_unreachable ();
> +		case label_dir::ABOVE:
> +		  kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE;
> +		  break;
> +		case label_dir::BELOW:
> +		  kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW;
> +		  break;
> +		}
> +	    }
> +	  canvas.paint (canvas::coord_t (rel_x, ruler_rel_y) + offset,
> +			theme.get_cell (kind, iter_label.m_style_id));
> +	}
> +
> +      /* Paint the connector to the text.  */
> +      for (int connector_rel_y = 1;
> +	   connector_rel_y < iter_label.m_text_rect.get_min_y ();
> +	   connector_rel_y++)
> +	{
> +	  canvas.paint
> +	    ((canvas::coord_t (iter_label.m_connector_x,
> +			       get_canvas_y (connector_rel_y))
> +	      + offset),
> +	     theme.get_cell (theme::cell_kind::X_RULER_VERTICAL_CONNECTOR,
> +			     iter_label.m_style_id));
> +	}
> +
> +      /* Paint the text.  */
> +      switch (iter_label.m_kind)
> +	{
> +	default:
> +	  gcc_unreachable ();
> +	case x_ruler::label_kind::TEXT:
> +	  canvas.paint_text
> +	    ((canvas::coord_t (iter_label.m_text_rect.get_min_x (),
> +			       get_canvas_y (iter_label.m_text_rect.get_min_y ()))
> +	      + offset),
> +	     iter_label.m_text);
> +	  break;
> +
> +	case x_ruler::label_kind::TEXT_WITH_BORDER:
> +	  {
> +	    const canvas::range_t rel_x_range
> +	      (iter_label.m_text_rect.get_x_range ());
> +
> +	    enum theme::cell_kind inner_left_kind;
> +	    enum theme::cell_kind inner_connector_kind;
> +	    enum theme::cell_kind inner_right_kind;
> +	    enum theme::cell_kind outer_left_kind;
> +	    enum theme::cell_kind outer_right_kind;
> +
> +	      switch (m_label_dir)
> +		{
> +		default:
> +		  gcc_unreachable ();
> +		case label_dir::ABOVE:
> +		  outer_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT;
> +		  outer_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT;
> +		  inner_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT;
> +		  inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_BELOW;
> +		  inner_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT;
> +		  break;
> +		case label_dir::BELOW:
> +		  inner_left_kind = theme::cell_kind::TEXT_BORDER_TOP_LEFT;
> +		  inner_connector_kind = theme::cell_kind::X_RULER_CONNECTOR_TO_LABEL_ABOVE;
> +		  inner_right_kind = theme::cell_kind::TEXT_BORDER_TOP_RIGHT;
> +		  outer_left_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_LEFT;
> +		  outer_right_kind = theme::cell_kind::TEXT_BORDER_BOTTOM_RIGHT;
> +		  break;
> +		}
> +	    /* Inner border.  */
> +	    {
> +	      const int rel_canvas_y
> +		= get_canvas_y (iter_label.m_text_rect.get_min_y ());
> +	      /* Left corner.  */
> +	      canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
> +					      rel_canvas_y)
> +			     + offset),
> +			    theme.get_cell (inner_left_kind,
> +					    iter_label.m_style_id));
> +	      /* Edge.  */
> +	      const canvas::cell_t edge_border_cell
> +		= theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL,
> +				  iter_label.m_style_id);
> +	      const canvas::cell_t connector_border_cell
> +		= theme.get_cell (inner_connector_kind,
> +				  iter_label.m_style_id);
> +	      for (int rel_x = rel_x_range.get_min () + 1;
> +		   rel_x < rel_x_range.get_max ();
> +		   rel_x++)
> +		if (rel_x == iter_label.m_connector_x)
> +		  canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
> +				 + offset),
> +				connector_border_cell);
> +		else
> +		  canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
> +				 + offset),
> +				edge_border_cell);
> +
> +	      /* Right corner.  */
> +	      canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
> +					      rel_canvas_y)
> +			     + offset),
> +			    theme.get_cell (inner_right_kind,
> +					    iter_label.m_style_id));
> +	    }
> +
> +	    {
> +	      const int rel_canvas_y
> +		= get_canvas_y (iter_label.m_text_rect.get_min_y () + 1);
> +	      const canvas::cell_t border_cell
> +		= theme.get_cell (theme::cell_kind::TEXT_BORDER_VERTICAL,
> +				  iter_label.m_style_id);
> +
> +	      /* Left border.  */
> +	      canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
> +					      rel_canvas_y)
> +			     + offset),
> +			    border_cell);
> +	      /* Text.  */
> +	      canvas.paint_text ((canvas::coord_t (rel_x_range.get_min () + 1,
> +						   rel_canvas_y)
> +				  + offset),
> +				 iter_label.m_text);
> +	      /* Right border.  */
> +	      canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
> +					      rel_canvas_y)
> +			     + offset),
> +			    border_cell);
> +	    }
> +
> +	    /* Outer border.  */
> +	    {
> +	      const int rel_canvas_y
> +		= get_canvas_y (iter_label.m_text_rect.get_max_y ());
> +	      /* Left corner.  */
> +	      canvas.paint ((canvas::coord_t (rel_x_range.get_min (),
> +					      rel_canvas_y)
> +			     + offset),
> +			    theme.get_cell (outer_left_kind,
> +					    iter_label.m_style_id));
> +	      /* Edge.  */
> +	      const canvas::cell_t border_cell
> +		= theme.get_cell (theme::cell_kind::TEXT_BORDER_HORIZONTAL,
> +				  iter_label.m_style_id);
> +	      for (int rel_x = rel_x_range.get_min () + 1;
> +		   rel_x < rel_x_range.get_max ();
> +		   rel_x++)
> +		canvas.paint ((canvas::coord_t (rel_x, rel_canvas_y)
> +			       + offset),
> +			      border_cell);
> +
> +	      /* Right corner.  */
> +	      canvas.paint ((canvas::coord_t (rel_x_range.get_max (),
> +					      rel_canvas_y)
> +			     + offset),
> +			    theme.get_cell (outer_right_kind,
> +					    iter_label.m_style_id));
> +	    }
> +	  }
> +	  break;
> +	}
> +    }
> +}
> +
> +DEBUG_FUNCTION void
> +x_ruler::debug (const style_manager &sm)
> +{
> +  canvas c (get_size (), sm);
> +  paint_to_canvas (c, canvas::coord_t (0, 0), unicode_theme ());
> +  c.debug (true);
> +}
> +
> +x_ruler::label::label (const canvas::range_t &range,
> +		       styled_string text,
> +		       style::id_t style_id,
> +		       label_kind kind)
> +: m_range (range),
> +  m_text (std::move (text)),
> +  m_style_id (style_id),
> +  m_kind (kind),
> +  m_text_rect (canvas::coord_t (0, 0),
> +	       canvas::size_t (m_text.calc_canvas_width (), 1)),
> +  m_connector_x ((m_range.get_min () + m_range.get_max ()) / 2)
> +{
> +  if (kind == label_kind::TEXT_WITH_BORDER)
> +    {
> +      m_text_rect.m_size.w += 2;
> +      m_text_rect.m_size.h += 2;
> +    }
> +}
> +
> +bool
> +x_ruler::label::operator< (const label &other) const
> +{
> +  int cmp = m_range.start - other.m_range.start;
> +  if (cmp)
> +    return cmp < 0;
> +  return m_range.next < other.m_range.next;
> +}
> +
> +void
> +x_ruler::ensure_layout ()
> +{
> +  if (m_has_layout)
> +    return;
> +  update_layout ();
> +  m_has_layout = true;
> +}
> +
> +void
> +x_ruler::update_layout ()
> +{
> +  if (m_labels.empty ())
> +    return;
> +
> +  std::sort (m_labels.begin (), m_labels.end ());
> +
> +  /* Place labels.  */
> +  int ruler_width = m_labels.back ().m_range.get_next ();
> +  int width_with_labels = ruler_width;
> +
> +  /* Get x coordinates of text parts of each label
> +     (m_text_rect.m_top_left.x for each label).  */
> +  for (size_t idx = 0; idx < m_labels.size (); idx++)
> +    {
> +      label &iter_label = m_labels[idx];
> +      /* Attempt to center the text label.  */
> +      int min_x;
> +      if (idx > 0)
> +	{
> +	  /* ...but don't overlap with the connector to the left.  */
> +	  int left_neighbor_connector_x = m_labels[idx - 1].m_connector_x;
> +	  min_x = left_neighbor_connector_x + 1;
> +	}
> +      else
> +	{
> +	  /* ...or go beyond the leftmost column.  */
> +	  min_x = 0;
> +	}
> +      int connector_x = iter_label.m_connector_x;
> +      int centered_x
> +	= connector_x - ((int)iter_label.m_text_rect.get_width () / 2);
> +      int text_x = std::max (min_x, centered_x);
> +      iter_label.m_text_rect.m_top_left.x = text_x;
> +    }
> +
> +  /* Now walk backwards trying to place them vertically,
> +     setting m_text_rect.m_top_left.y for each label,
> +     consolidating the rows where possible.
> +     The y cooordinates are stored with respect to label_dir::BELOW.  */
> +  int label_y = 2;
> +  for (int idx = m_labels.size () - 1; idx >= 0; idx--)
> +    {
> +      label &iter_label = m_labels[idx];
> +      /* Does it fit on the same row as the text label to the right?  */
> +      size_t text_len = iter_label.m_text_rect.get_width ();
> +      /* Get the x-coord of immediately beyond iter_label's text.  */
> +      int next_x = iter_label.m_text_rect.get_min_x () + text_len;
> +      if (idx < (int)m_labels.size () - 1)
> +	{
> +	  if (next_x >= m_labels[idx + 1].m_text_rect.get_min_x ())
> +	    {
> +	      /* If not, start a new row.  */
> +	      label_y += m_labels[idx + 1].m_text_rect.get_height ();
> +	    }
> +	}
> +      iter_label.m_text_rect.m_top_left.y = label_y;
> +      width_with_labels = std::max (width_with_labels, next_x);
> +    }
> +
> +  m_size = canvas::size_t (width_with_labels,
> +			   label_y + m_labels[0].m_text_rect.get_height ());
> +}
> +
> +#if CHECKING_P
> +
> +namespace selftest {
> +
> +static void
> +assert_x_ruler_streq (const location &loc,
> +		      x_ruler &ruler,
> +		      const theme &theme,
> +		      const style_manager &sm,
> +		      bool styled,
> +		      const char *expected_str)
> +{
> +  canvas c (ruler.get_size (), sm);
> +  ruler.paint_to_canvas (c, canvas::coord_t (0, 0), theme);
> +  if (0)
> +    c.debug (styled);
> +  assert_canvas_streq (loc, c, styled, expected_str);
> +}
> +
> +#define ASSERT_X_RULER_STREQ(RULER, THEME, SM, STYLED, EXPECTED_STR)	\
> +  SELFTEST_BEGIN_STMT							\
> +    assert_x_ruler_streq ((SELFTEST_LOCATION),				\
> +			  (RULER),					\
> +			  (THEME),					\
> +			  (SM),						\
> +			  (STYLED),					\
> +			  (EXPECTED_STR));				\
> +  SELFTEST_END_STMT
> +
> +static void
> +test_single ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
> +	       style::id_plain, x_ruler::label_kind::TEXT);
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     ("|~~~~+~~~~|\n"
> +      "     |\n"
> +      "    foo\n"));
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├────┬────┤\n"
> +      "     │\n"
> +      "    foo\n"));
> +}
> +
> +static void
> +test_single_above ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::ABOVE);
> +  r.add_label (canvas::range_t (0, 11), styled_string (sm, "hello world"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     ("hello world\n"
> +      "     |\n"
> +      "|~~~~+~~~~|\n"));
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("hello world\n"
> +      "     │\n"
> +      "├────┴────┤\n"));
> +}
> +
> +static void
> +test_multiple_contiguous ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     ("|~~~~+~~~~|~+~~|\n"
> +      "     |      |\n"
> +      "    foo    bar\n"));
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├────┬────┼─┬──┤\n"
> +      "     │      │\n"
> +      "    foo    bar\n"));
> +}
> +
> +static void
> +test_multiple_contiguous_above ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::ABOVE);
> +  r.add_label (canvas::range_t (0, 11), styled_string (sm, "foo"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 16), styled_string (sm, "bar"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     ("    foo    bar\n"
> +      "     |      |\n"
> +      "|~~~~+~~~~|~+~~|\n"));
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("    foo    bar\n"
> +      "     │      │\n"
> +      "├────┴────┼─┴──┤\n"));
> +}
> +
> +static void
> +test_multiple_contiguous_abutting_labels ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 11), styled_string (sm, "12345678"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 16), styled_string (sm, "1234678"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├────┬────┼─┬──┤\n"
> +      "     │      │\n"
> +      "     │   1234678\n"
> +      " 12345678\n"));
> +}
> +
> +static void
> +test_multiple_contiguous_overlapping_labels ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 11), styled_string (sm, "123456789"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 16), styled_string (sm, "12346789"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├────┬────┼─┬──┤\n"
> +      "     │      │\n"
> +      "     │  12346789\n"
> +      " 123456789\n"));
> +}
> +static void
> +test_abutting_left_border ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 6),
> +	       styled_string (sm, "this is a long label"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├─┬──┤\n"
> +      "  │\n"
> +      "this is a long label\n"));
> +}
> +
> +static void
> +test_too_long_to_consolidate_vertically ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 11),
> +	       styled_string (sm, "long string A"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 16),
> +	       styled_string (sm, "long string B"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├────┬────┼─┬──┤\n"
> +      "     │      │\n"
> +      "     │long string B\n"
> +      "long string A\n"));
> +}
> +
> +static void
> +test_abutting_neighbor ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 11),
> +	       styled_string (sm, "very long string A"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 16),
> +	       styled_string (sm, "very long string B"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, unicode_theme (), sm, true,
> +     ("├────┬────┼─┬──┤\n"
> +      "     │      │\n"
> +      "     │very long string B\n"
> +      "very long string A\n"));
> +}
> +
> +static void
> +test_gaps ()
> +{
> +  style_manager sm;
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 5),
> +	       styled_string (sm, "foo"),
> +	       style::id_plain);
> +  r.add_label (canvas::range_t (10, 15),
> +	       styled_string (sm, "bar"),
> +	       style::id_plain);
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     ("|~+~|     |~+~|\n"
> +      "  |         |\n"
> +      " foo       bar\n"));
> +}
> +
> +static void
> +test_styled ()
> +{
> +  style_manager sm;
> +  style s1, s2;
> +  s1.m_bold = true;
> +  s1.m_fg_color = style::named_color::YELLOW;
> +  s2.m_bold = true;
> +  s2.m_fg_color = style::named_color::BLUE;
> +  style::id_t sid1 = sm.get_or_create_id (s1);
> +  style::id_t sid2 = sm.get_or_create_id (s2);
> +
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 5), styled_string (sm, "foo"), sid1);
> +  r.add_label (canvas::range_t (10, 15), styled_string (sm, "bar"), sid2);
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     ("^[[00;01;33m^[[K|~+~|^[[00m^[[K     ^[[00;01;34m^[[K|~+~|^[[00m^[[K\n"
> +      "  ^[[00;01;33m^[[K|^[[00m^[[K         ^[[00;01;34m^[[K|^[[00m^[[K\n"
> +      " foo       bar\n"));
> +}
> +
> +static void
> +test_borders ()
> +{
> +  style_manager sm;
> +  {
> +    x_ruler r (x_ruler::label_dir::BELOW);
> +    r.add_label (canvas::range_t (0, 5),
> +		 styled_string (sm, "label 1"),
> +		 style::id_plain,
> +		 x_ruler::label_kind::TEXT_WITH_BORDER);
> +    r.add_label (canvas::range_t (10, 15),
> +		 styled_string (sm, "label 2"),
> +		 style::id_plain);
> +    r.add_label (canvas::range_t (20, 25),
> +		 styled_string (sm, "label 3"),
> +		 style::id_plain,
> +		 x_ruler::label_kind::TEXT_WITH_BORDER);
> +    ASSERT_X_RULER_STREQ
> +      (r, ascii_theme (), sm, true,
> +       "|~+~|     |~+~|     |~+~|\n"
> +       "  |         |         |\n"
> +       "  |      label 2  +---+---+\n"
> +       "+-+-----+         |label 3|\n"
> +       "|label 1|         +-------+\n"
> +       "+-------+\n");
> +    ASSERT_X_RULER_STREQ
> +      (r, unicode_theme (), sm, true,
> +       "├─┬─┤     ├─┬─┤     ├─┬─┤\n"
> +       "  │         │         │\n"
> +       "  │      label 2  ╭───┴───╮\n"
> +       "╭─┴─────╮         │label 3│\n"
> +       "│label 1│         ╰───────╯\n"
> +       "╰───────╯\n");
> +  }
> +  {
> +    x_ruler r (x_ruler::label_dir::ABOVE);
> +    r.add_label (canvas::range_t (0, 5),
> +		 styled_string (sm, "label 1"),
> +		 style::id_plain,
> +		 x_ruler::label_kind::TEXT_WITH_BORDER);
> +    r.add_label (canvas::range_t (10, 15),
> +		 styled_string (sm, "label 2"),
> +		 style::id_plain);
> +    r.add_label (canvas::range_t (20, 25),
> +		 styled_string (sm, "label 3"),
> +		 style::id_plain,
> +		 x_ruler::label_kind::TEXT_WITH_BORDER);
> +    ASSERT_X_RULER_STREQ
> +      (r, ascii_theme (), sm, true,
> +       "+-------+\n"
> +       "|label 1|         +-------+\n"
> +       "+-+-----+         |label 3|\n"
> +       "  |      label 2  +---+---+\n"
> +       "  |         |         |\n"
> +       "|~+~|     |~+~|     |~+~|\n");
> +    ASSERT_X_RULER_STREQ
> +      (r, unicode_theme (), sm, true,
> +       "╭───────╮\n"
> +       "│label 1│         ╭───────╮\n"
> +       "╰─┬─────╯         │label 3│\n"
> +       "  │      label 2  ╰───┬───╯\n"
> +       "  │         │         │\n"
> +       "├─┴─┤     ├─┴─┤     ├─┴─┤\n");
> +  }
> +}
> +
> +static void
> +test_emoji ()
> +{
> +  style_manager sm;
> +
> +  styled_string s;
> +  s.append (styled_string (0x26A0, /* U+26A0 WARNING SIGN.  */
> +			   true));
> +  s.append (styled_string (sm, "  "));
> +  s.append (styled_string (sm, "this is a warning"));
> +
> +  x_ruler r (x_ruler::label_dir::BELOW);
> +  r.add_label (canvas::range_t (0, 5),
> +	       std::move (s),
> +	       style::id_plain,
> +	       x_ruler::label_kind::TEXT_WITH_BORDER);
> +
> +  ASSERT_X_RULER_STREQ
> +    (r, ascii_theme (), sm, true,
> +     "|~+~|\n"
> +     "  |\n"
> +     "+-+------------------+\n"
> +     "|⚠️  this is a warning|\n"
> +     "+--------------------+\n");
> +}
> +
> +/* Run all selftests in this file.  */
> +
> +void
> +text_art_ruler_cc_tests ()
> +{
> +  test_single ();
> +  test_single_above ();
> +  test_multiple_contiguous ();
> +  test_multiple_contiguous_above ();
> +  test_multiple_contiguous_abutting_labels ();
> +  test_multiple_contiguous_overlapping_labels ();
> +  test_abutting_left_border ();
> +  test_too_long_to_consolidate_vertically ();
> +  test_abutting_neighbor ();
> +  test_gaps ();
> +  test_styled ();
> +  test_borders ();
> +  test_emoji ();
> +}
> +
> +} // namespace selftest
> +
> +
> +#endif /* #if CHECKING_P */
> diff --git a/gcc/text-art/ruler.h b/gcc/text-art/ruler.h
> new file mode 100644
> index 00000000000..31f53549836
> --- /dev/null
> +++ b/gcc/text-art/ruler.h
> @@ -0,0 +1,125 @@
> +/* Classes for printing labelled rulers.
> +   Copyright (C) 2023 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/>.  */
> +
> +#ifndef GCC_TEXT_ART_RULER_H
> +#define GCC_TEXT_ART_RULER_H
> +
> +#include "text-art/canvas.h"
> +
> +namespace text_art {
> +
> +/* A way to annotate a series of ranges of canvas coordinates
> +   with text labels either above or, in this example, below:
> +     ├───────┬───────┼───────┬───────┼───────┬───────┤
> +             │               │               │
> +           label A         label B          label C
> +   with logic to ensure that the text labels don't overlap
> +   when printed.  */
> +
> +class x_ruler
> +{
> + public:
> +  enum class label_dir { ABOVE, BELOW };
> +  enum class label_kind
> +  {
> +    TEXT,
> +    TEXT_WITH_BORDER
> +  };
> +
> +  x_ruler (label_dir dir)
> +  : m_label_dir (dir),
> +    m_size (canvas::size_t (0, 0)),
> +    m_has_layout (false)
> +  {}
> +
> +  void add_label (const canvas::range_t &r,
> +		  styled_string text,
> +		  style::id_t style_id,
> +		  label_kind kind = label_kind::TEXT);
> +
> +  canvas::size_t get_size ()
> +  {
> +    ensure_layout ();
> +    return m_size;
> +  }
> +
> +  void paint_to_canvas (canvas &canvas,
> +			canvas::coord_t offset,
> +			const theme &theme);
> +
> +  void debug (const style_manager &sm);
> +
> + private:
> +  /* A particular label within an x_ruler.
> +     Consider e.g.:
> +
> +     #   x:  01234567890123456789012345678901234567890123456789
> +     # y: 0: ├───────┬───────┼───────┬───────┼───────┬───────┤
> +     #    1:         │               │               │
> +     #    2:       label A         label B          label C
> +     #
> +
> +     Then "label A" is:
> +
> +     #               m_connector_x == 8
> +     #               V
> +     #   x:  0123456789012
> +     # y: 0:         ┬
> +     #    1:         │
> +     #    2:       label A
> +     #   x:  0123456789012
> +     #             ^
> +     #             m_text_coord.x == 6
> +
> +     and m_text_coord is (2, 6).
> +     The y cooordinates are stored with respect to label_dir::BELOW;
> +     for label_dir::ABOVE we flip them when painting the ruler.  */
> +  class label
> +  {
> +    friend class x_ruler;
> +  public:
> +    label (const canvas::range_t &range, styled_string text, style::id_t style_id,
> +	   label_kind kind);
> +
> +    bool operator< (const label &other) const;
> +
> +  private:
> +    canvas::range_t m_range;
> +    styled_string m_text;
> +    style::id_t m_style_id;
> +    label_kind m_kind;
> +    canvas::rect_t m_text_rect; // includes any border
> +    int m_connector_x;
> +  };
> +
> +  void ensure_layout ();
> +  void update_layout ();
> +  int get_canvas_y (int rel_y) const;
> +
> +  label_dir m_label_dir;
> +  std::vector<label> m_labels;
> +  canvas::size_t m_size;
> +  bool m_has_layout = false;
> +
> +};
> +
> +} // namespace text_art
> +
> +#endif /* GCC_TEXT_ART_RULER_H */
> diff --git a/gcc/text-art/selftests.cc b/gcc/text-art/selftests.cc
> new file mode 100644
> index 00000000000..60ad003b549
> --- /dev/null
> +++ b/gcc/text-art/selftests.cc
> @@ -0,0 +1,77 @@
> +/* Selftests for text art.
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#include "system.h"
> +#include "coretypes.h"
> +#include "selftest.h"
> +#include "pretty-print.h"
> +#include "text-art/selftests.h"
> +#include "text-art/canvas.h"
> +
> +#if CHECKING_P
> +
> +/* Run all tests, aborting if any fail.  */
> +
> +void
> +selftest::text_art_tests ()
> +{
> +  text_art_style_cc_tests ();
> +  text_art_styled_string_cc_tests ();
> +
> +  text_art_box_drawing_cc_tests ();
> +  text_art_canvas_cc_tests ();
> +  text_art_ruler_cc_tests ();
> +  text_art_table_cc_tests ();
> +  text_art_widget_cc_tests ();
> +}
> +
> +/* Implementation detail of ASSERT_CANVAS_STREQ.  */
> +
> +void
> +selftest::assert_canvas_streq (const location &loc,
> +			       const text_art::canvas &canvas,
> +			       pretty_printer *pp,
> +			       const char *expected_str)
> +{
> +  canvas.print_to_pp (pp);
> +  if (0)
> +    fprintf (stderr, "%s\n", pp_formatted_text (pp));
> +  ASSERT_STREQ_AT (loc, pp_formatted_text (pp), expected_str);
> +}
> +
> +/* Implementation detail of ASSERT_CANVAS_STREQ.  */
> +
> +void
> +selftest::assert_canvas_streq (const location &loc,
> +			       const text_art::canvas &canvas,
> +			       bool styled,
> +			       const char *expected_str)
> +{
> +  pretty_printer pp;
> +  if (styled)
> +    {
> +      pp_show_color (&pp) = true;
> +      pp.url_format = URL_FORMAT_DEFAULT;
> +    }
> +  assert_canvas_streq (loc, canvas, &pp, expected_str);
> +}
> +
> +#endif /* #if CHECKING_P */
> diff --git a/gcc/text-art/selftests.h b/gcc/text-art/selftests.h
> new file mode 100644
> index 00000000000..706a1d8b5d6
> --- /dev/null
> +++ b/gcc/text-art/selftests.h
> @@ -0,0 +1,60 @@
> +/* Copyright (C) 2023 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/>.  */
> +
> +#ifndef GCC_TEXT_ART_SELFTESTS_H
> +#define GCC_TEXT_ART_SELFTESTS_H
> +
> +#if CHECKING_P
> +
> +#include "text-art/types.h"
> +
> +namespace selftest {
> +
> +extern void text_art_box_drawing_cc_tests ();
> +extern void text_art_canvas_cc_tests ();
> +extern void text_art_ruler_cc_tests ();
> +extern void text_art_style_cc_tests ();
> +extern void text_art_styled_string_cc_tests ();
> +extern void text_art_table_cc_tests ();
> +extern void text_art_widget_cc_tests ();
> +
> +extern void text_art_tests ();
> +
> +extern void assert_canvas_streq (const location &loc,
> +				 const text_art::canvas &canvas,
> +				 pretty_printer *pp,
> +				 const char *expected_str);
> +extern void assert_canvas_streq (const location &loc,
> +				 const text_art::canvas &canvas,
> +				 bool styled,
> +				 const char *expected_str);
> +
> +#define ASSERT_CANVAS_STREQ(CANVAS, STYLED, EXPECTED_STR)		\
> +  SELFTEST_BEGIN_STMT							\
> +    assert_canvas_streq ((SELFTEST_LOCATION),				\
> +			 (CANVAS),					\
> +			 (STYLED), 					\
> +			 (EXPECTED_STR));				\
> +  SELFTEST_END_STMT
> +
> +} /* end of namespace selftest.  */
> +
> +#endif /* #if CHECKING_P */
> +
> +#endif /* GCC_TEXT_ART_SELFTESTS_H */
> diff --git a/gcc/text-art/style.cc b/gcc/text-art/style.cc
> new file mode 100644
> index 00000000000..00b056336fc
> --- /dev/null
> +++ b/gcc/text-art/style.cc
> @@ -0,0 +1,632 @@
> +/* Classes for styling text cells (color, URLs).
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#define INCLUDE_ALGORITHM
> +#define INCLUDE_MEMORY
> +#include "system.h"
> +#include "coretypes.h"
> +#include "make-unique.h"
> +#include "pretty-print.h"
> +#include "intl.h"
> +#include "selftest.h"
> +#include "text-art/selftests.h"
> +#include "text-art/types.h"
> +#include "color-macros.h"
> +
> +using namespace text_art;
> +
> +/* class text_art::style.  */
> +
> +style &
> +style::set_style_url (const char *url)
> +{
> +  m_url.clear ();
> +  while (*url)
> +    m_url.push_back (*(url++));
> +  return *this;
> +}
> +
> +/* class text_art::style::color.  */
> +
> +bool
> +style::color::operator== (const style::color &other) const
> +{
> +  if (m_kind != other.m_kind)
> +    return false;
> +  switch (m_kind)
> +    {
> +    default:
> +      gcc_unreachable ();
> +    case kind::NAMED:
> +      return (u.m_named.m_name == other.u.m_named.m_name
> +	      && u.m_named.m_bright == other.u.m_named.m_bright);
> +    case kind::BITS_8:
> +      return u.m_8bit == other.u.m_8bit;
> +    case kind::BITS_24:
> +      return (u.m_24bit.r == other.u.m_24bit.r
> +	      && u.m_24bit.g == other.u.m_24bit.g
> +	      && u.m_24bit.b == other.u.m_24bit.b);
> +    }
> +}
> +
> +static void
> +ensure_separator (pretty_printer *pp, bool &need_separator)
> +{
> +  if (need_separator)
> +    pp_string (pp, COLOR_SEPARATOR);
> +  need_separator = true;
> +}
> +
> +void
> +style::color::print_sgr (pretty_printer *pp,
> +			 bool fg,
> +			 bool &need_separator) const
> +{
> +  switch (m_kind)
> +    {
> +    default:
> +      gcc_unreachable ();
> +    case kind::NAMED:
> +      {
> +	static const char * const fg_normal[] = {"", // reset, for DEFAULT
> +						 COLOR_FG_BLACK,
> +						 COLOR_FG_RED,
> +						 COLOR_FG_GREEN,
> +						 COLOR_FG_YELLOW,
> +						 COLOR_FG_BLUE,
> +						 COLOR_FG_MAGENTA,
> +						 COLOR_FG_CYAN,
> +						 COLOR_FG_WHITE};
> +	static const char * const fg_bright[] = {"", // reset, for DEFAULT
> +						 COLOR_FG_BRIGHT_BLACK,
> +						 COLOR_FG_BRIGHT_RED,
> +						 COLOR_FG_BRIGHT_GREEN,
> +						 COLOR_FG_BRIGHT_YELLOW,
> +						 COLOR_FG_BRIGHT_BLUE,
> +						 COLOR_FG_BRIGHT_MAGENTA,
> +						 COLOR_FG_BRIGHT_CYAN,
> +						 COLOR_FG_BRIGHT_WHITE};
> +	static const char * const bg_normal[] = {"", // reset, for DEFAULT
> +						 COLOR_BG_BLACK,
> +						 COLOR_BG_RED,
> +						 COLOR_BG_GREEN,
> +						 COLOR_BG_YELLOW,
> +						 COLOR_BG_BLUE,
> +						 COLOR_BG_MAGENTA,
> +						 COLOR_BG_CYAN,
> +						 COLOR_BG_WHITE};
> +	static const char * const bg_bright[] = {"", // reset, for DEFAULT
> +						 COLOR_BG_BRIGHT_BLACK,
> +						 COLOR_BG_BRIGHT_RED,
> +						 COLOR_BG_BRIGHT_GREEN,
> +						 COLOR_BG_BRIGHT_YELLOW,
> +						 COLOR_BG_BRIGHT_BLUE,
> +						 COLOR_BG_BRIGHT_MAGENTA,
> +						 COLOR_BG_BRIGHT_CYAN,
> +						 COLOR_BG_BRIGHT_WHITE};
> +	STATIC_ASSERT (ARRAY_SIZE (fg_normal) == ARRAY_SIZE (fg_bright));
> +	STATIC_ASSERT (ARRAY_SIZE (fg_normal) == ARRAY_SIZE (bg_normal));
> +	STATIC_ASSERT (ARRAY_SIZE (fg_normal) == ARRAY_SIZE (bg_bright));
> +	gcc_assert ((size_t)u.m_named.m_name < ARRAY_SIZE (fg_normal));
> +	const char *const *arr;
> +	if (fg)
> +	  arr = u.m_named.m_bright ? fg_bright : fg_normal;
> +	else
> +	  arr = u.m_named.m_bright ? bg_bright : bg_normal;
> +	const char *str = arr[(size_t)u.m_named.m_name];
> +	if (strlen (str) > 0)
> +	  {
> +	    ensure_separator (pp, need_separator);
> +	    pp_string (pp, str);
> +	  }
> +      }
> +      break;
> +    case kind::BITS_8:
> +      {
> +	ensure_separator (pp, need_separator);
> +	if (fg)
> +	  pp_string (pp, "38");
> +	else
> +	  pp_string (pp, "48");
> +	pp_printf (pp, ";5;%i", (int)u.m_8bit);
> +      }
> +      break;
> +    case kind::BITS_24:
> +      {
> +	ensure_separator (pp, need_separator);
> +	if (fg)
> +	  pp_string (pp, "38");
> +	else
> +	  pp_string (pp, "48");
> +	pp_printf (pp, ";2;%i;%i;%i",
> +		   (int)u.m_24bit.r,
> +		   (int)u.m_24bit.g,
> +		   (int)u.m_24bit.b);
> +      }
> +      break;
> +    }
> +}
> +
> +/* class text_art::style.  */
> +
> +/* See https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf
> +   GRCM - GRAPHIC RENDITION COMBINATION MODE can be "REPLACING" or
> +   "CUMULATIVE", which affects whether we need to respecify all attributes
> +   at each SGR, or can accumulate them.  Looks like we can't rely on the value
> +   of this, so we have to emit a single SGR for all changes, with a "0" reset
> +   at the front, forcing it to be effectively replacing.  */
> +
> +void
> +style::print_changes (pretty_printer *pp,
> +		      const style &old_style,
> +		      const style &new_style)
> +{
> +  if (pp_show_color (pp))
> +    {
> +      bool needs_sgr = ((old_style.m_bold != new_style.m_bold)
> +			|| (old_style.m_underscore != new_style.m_underscore)
> +			|| (old_style.m_blink != new_style.m_blink)
> +			|| (old_style.m_fg_color != new_style.m_fg_color)
> +			|| (old_style.m_bg_color != new_style.m_bg_color));
> +      if (needs_sgr)
> +	{
> +	  bool emit_reset = (old_style.m_bold
> +			     || new_style.m_bold
> +			     || old_style.m_underscore
> +			     || new_style.m_underscore
> +			     || old_style.m_blink
> +			     || new_style.m_blink);
> +	  bool need_separator = false;
> +
> +	  pp_string (pp, SGR_START);
> +	  if (emit_reset)
> +	    {
> +	      pp_string (pp, COLOR_NONE);
> +	      need_separator = true;
> +	    }
> +	  if (new_style.m_bold)
> +	    {
> +	      gcc_assert (emit_reset);
> +	      ensure_separator (pp, need_separator);
> +	      pp_string (pp, COLOR_BOLD);
> +	    }
> +	  if (new_style.m_underscore)
> +	    {
> +	      gcc_assert (emit_reset);
> +	      ensure_separator (pp, need_separator);
> +	      pp_string (pp, COLOR_UNDERSCORE);
> +	    }
> +	  if (new_style.m_blink)
> +	    {
> +	      gcc_assert (emit_reset);
> +	      ensure_separator (pp, need_separator);
> +	      pp_string (pp, COLOR_BLINK);
> +	    }
> +	  new_style.m_fg_color.print_sgr (pp, true, need_separator);
> +	  new_style.m_bg_color.print_sgr (pp, false, need_separator);
> +	  pp_string (pp, SGR_END);
> +	}
> +    }
> +
> +  if (old_style.m_url != new_style.m_url)
> +    {
> +      if (!old_style.m_url.empty ())
> +	pp_end_url (pp);
> +      if (pp->url_format != URL_FORMAT_NONE
> +	  && !new_style.m_url.empty ())
> +	{
> +	  /* Adapted from pp_begin_url, but encoding the
> +	     chars to UTF-8 on the fly, rather than converting
> +	     to a buffer.  */
> +	  pp_string (pp, "\33]8;;");
> +	  for (auto ch : new_style.m_url)
> +	    pp_unicode_character (pp, ch);
> +	  switch (pp->url_format)
> +	    {
> +	    default:
> +	    case URL_FORMAT_NONE:
> +	      gcc_unreachable ();
> +	    case URL_FORMAT_ST:
> +	      pp_string (pp, "\33\\");
> +	      break;
> +	    case URL_FORMAT_BEL:
> +	      pp_string (pp, "\a");
> +	      break;
> +	    }
> +	}
> +    }
> +}
> +
> +/* class text_art::style_manager.  */
> +
> +style_manager::style_manager ()
> +{
> +  // index 0 will be the default style
> +  m_styles.push_back (style ());
> +}
> +
> +style::id_t
> +style_manager::get_or_create_id (const style &s)
> +{
> +  // For now, linear search
> +  std::vector<style>::iterator existing
> +    (std::find (m_styles.begin (), m_styles.end (), s));
> +
> +  /* If found, return index of slot.  */
> +  if (existing != m_styles.end ())
> +    return std::distance (m_styles.begin (), existing);
> +
> +  /* Not found.  */
> +
> +  /* styled_str uses 7 bits for style information, so we can only support
> +     up to 128 different style combinations.
> +     Gracefully fail by turning off styling when this limit is reached.  */
> +  if (m_styles.size () >= 127)
> +    return 0;
> +
> +  m_styles.push_back (s);
> +  return m_styles.size () - 1;
> +}
> +
> +void
> +style_manager::print_any_style_changes (pretty_printer *pp,
> +					style::id_t old_id,
> +					style::id_t new_id) const
> +{
> +  gcc_assert (pp);
> +  if (old_id == new_id)
> +    return;
> +
> +  const style &old_style = m_styles[old_id];
> +  const style &new_style = m_styles[new_id];
> +  gcc_assert (!(old_style == new_style));
> +  style::print_changes (pp, old_style, new_style);
> +}
> +
> +#if CHECKING_P
> +
> +namespace selftest {
> +
> +void
> +assert_style_change_streq (const location &loc,
> +			   const style &old_style,
> +			   const style &new_style,
> +			   const char *expected_str)
> +{
> +  pretty_printer pp;
> +  pp_show_color (&pp) = true;
> +  style::print_changes (&pp, old_style, new_style);
> +  ASSERT_STREQ_AT (loc, pp_formatted_text (&pp), expected_str);
> +}
> +
> +#define ASSERT_STYLE_CHANGE_STREQ(OLD_STYLE, NEW_STYLE, EXPECTED_STR) \
> +  SELFTEST_BEGIN_STMT						      \
> +    assert_style_change_streq ((SELFTEST_LOCATION),		      \
> +			       (OLD_STYLE),			      \
> +			       (NEW_STYLE),			      \
> +			       (EXPECTED_STR));			      \
> +  SELFTEST_END_STMT
> +
> +static void
> +test_bold ()
> +{
> +  style_manager sm;
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style plain;
> +  ASSERT_EQ (sm.get_or_create_id (plain), 0);
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style bold;
> +  bold.m_bold = true;
> +
> +  ASSERT_EQ (sm.get_or_create_id (bold), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +  ASSERT_EQ (sm.get_or_create_id (bold), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +
> +  ASSERT_STYLE_CHANGE_STREQ (plain, bold, "\33[00;01m\33[K");
> +  ASSERT_STYLE_CHANGE_STREQ (bold, plain, "\33[00m\33[K");
> +}
> +
> +static void
> +test_underscore ()
> +{
> +  style_manager sm;
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style plain;
> +  ASSERT_EQ (sm.get_or_create_id (plain), 0);
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style underscore;
> +  underscore.m_underscore = true;
> +
> +  ASSERT_EQ (sm.get_or_create_id (underscore), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +  ASSERT_EQ (sm.get_or_create_id (underscore), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +
> +  ASSERT_STYLE_CHANGE_STREQ (plain, underscore, "\33[00;04m\33[K");
> +  ASSERT_STYLE_CHANGE_STREQ (underscore, plain, "\33[00m\33[K");
> +}
> +
> +static void
> +test_blink ()
> +{
> +  style_manager sm;
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style plain;
> +  ASSERT_EQ (sm.get_or_create_id (plain), 0);
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style blink;
> +  blink.m_blink = true;
> +
> +  ASSERT_EQ (sm.get_or_create_id (blink), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +  ASSERT_EQ (sm.get_or_create_id (blink), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +
> +  ASSERT_STYLE_CHANGE_STREQ (plain, blink, "\33[00;05m\33[K");
> +  ASSERT_STYLE_CHANGE_STREQ (blink, plain, "\33[00m\33[K");
> +}
> +
> +#define ASSERT_NAMED_COL_STREQ(NAMED_COLOR, FG, BRIGHT, EXPECTED_STR) \
> +  SELFTEST_BEGIN_STMT						      \
> +  {								      \
> +    style plain;						      \
> +    style s;							      \
> +    if (FG)							      \
> +      s.m_fg_color = style::color ((NAMED_COLOR), (BRIGHT));	      \
> +    else							      \
> +      s.m_bg_color = style::color ((NAMED_COLOR), (BRIGHT));	      \
> +    assert_style_change_streq ((SELFTEST_LOCATION),		      \
> +			       plain,				      \
> +			       s,				      \
> +			       (EXPECTED_STR));			      \
> +  }								      \
> +  SELFTEST_END_STMT
> +
> +static void
> +test_named_colors ()
> +{
> +  /* Foreground colors.  */
> +  {
> +    const bool fg = true;
> +    {
> +      const bool bright = false;
> +      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright, "");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
> +			      "^[[30m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
> +			      "^[[31m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
> +			      "^[[32m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
> +			      "^[[33m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
> +			      "^[[34m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
> +			      "^[[35m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
> +			      "^[[36m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
> +			      "^[[37m^[[K");
> +    }
> +    {
> +      const bool bright = true;
> +      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright,
> +			      "^[[m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
> +			      "^[[90m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
> +			      "^[[91m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
> +			      "^[[92m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
> +			      "^[[93m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
> +			      "^[[94m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
> +			      "^[[95m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
> +			      "^[[96m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
> +			      "^[[97m^[[K");
> +    }
> +  }
> +
> +  /* Background colors.  */
> +  {
> +    const bool fg = false;
> +    {
> +      const bool bright = false;
> +      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright, "");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
> +			      "^[[40m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
> +			      "^[[41m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
> +			      "^[[42m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
> +			      "^[[43m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
> +			      "^[[44m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
> +			      "^[[45m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
> +			      "^[[46m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
> +			      "^[[47m^[[K");
> +    }
> +    {
> +      const bool bright = true;
> +      ASSERT_NAMED_COL_STREQ (style::named_color::DEFAULT, fg, bright,
> +			      "^[[m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLACK, fg, bright,
> +			      "^[[100m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::RED, fg, bright,
> +			      "^[[101m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::GREEN, fg, bright,
> +			      "^[[102m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::YELLOW, fg, bright,
> +			      "^[[103m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::BLUE, fg, bright,
> +			      "^[[104m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::MAGENTA, fg, bright,
> +			      "^[[105m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::CYAN, fg, bright,
> +			      "^[[106m^[[K");
> +      ASSERT_NAMED_COL_STREQ (style::named_color::WHITE, fg, bright,
> +			      "^[[107m^[[K");
> +    }
> +  }
> +}
> +
> +#define ASSERT_8_BIT_COL_STREQ(COL_VAL, FG, EXPECTED_STR) \
> +  SELFTEST_BEGIN_STMT						      \
> +  {								      \
> +    style plain;						      \
> +    style s;							      \
> +    if (FG)							      \
> +      s.m_fg_color = style::color (COL_VAL);			      \
> +    else							      \
> +      s.m_bg_color = style::color (COL_VAL);			      \
> +    assert_style_change_streq ((SELFTEST_LOCATION),		      \
> +			       plain,				      \
> +			       s,				      \
> +			       (EXPECTED_STR));			      \
> +  }								      \
> +  SELFTEST_END_STMT
> +
> +static void
> +test_8_bit_colors ()
> +{
> +  /* Foreground colors.  */
> +  {
> +    const bool fg = true;
> +    /* 0-15: standard and high-intensity standard colors.  */
> +    ASSERT_8_BIT_COL_STREQ (0, fg, "^[[38;5;0m^[[K");
> +    ASSERT_8_BIT_COL_STREQ (15, fg, "^[[38;5;15m^[[K");
> +    /* 16-231: 6x6x6 color cube.  */
> +    ASSERT_8_BIT_COL_STREQ (16, fg, "^[[38;5;16m^[[K");
> +    ASSERT_8_BIT_COL_STREQ (231, fg, "^[[38;5;231m^[[K");
> +    /* 232-255: grayscale.  */
> +    ASSERT_8_BIT_COL_STREQ (232, fg, "^[[38;5;232m^[[K");
> +    ASSERT_8_BIT_COL_STREQ (255, fg, "^[[38;5;255m^[[K");
> +  }
> +  /* Background colors.  */
> +  {
> +    const bool fg = false;
> +    /* 0-15: standard and high-intensity standard colors.  */
> +    ASSERT_8_BIT_COL_STREQ (0, fg, "^[[48;5;0m^[[K");
> +    ASSERT_8_BIT_COL_STREQ (15, fg, "^[[48;5;15m^[[K");
> +    /* 16-231: 6x6x6 color cube.  */
> +    ASSERT_8_BIT_COL_STREQ (16, fg, "^[[48;5;16m^[[K");
> +    ASSERT_8_BIT_COL_STREQ (231, fg, "^[[48;5;231m^[[K");
> +    /* 232-255: grayscale.  */
> +    ASSERT_8_BIT_COL_STREQ (232, fg, "^[[48;5;232m^[[K");
> +    ASSERT_8_BIT_COL_STREQ (255, fg, "^[[48;5;255m^[[K");
> +  }
> +}
> +
> +#define ASSERT_24_BIT_COL_STREQ(R, G, B, FG, EXPECTED_STR)	      \
> +  SELFTEST_BEGIN_STMT						      \
> +  {								      \
> +    style plain;						      \
> +    style s;							      \
> +    if (FG)							      \
> +      s.m_fg_color = style::color ((R), (G), (B));		      \
> +    else							      \
> +      s.m_bg_color = style::color ((R), (G), (B));		      \
> +    assert_style_change_streq ((SELFTEST_LOCATION),		      \
> +			       plain,				      \
> +			       s,				      \
> +			       (EXPECTED_STR));			      \
> +  }								      \
> +  SELFTEST_END_STMT
> +
> +static void
> +test_24_bit_colors ()
> +{
> +  /* Foreground colors.  */
> +  {
> +    const bool fg = true;
> +    // #F3FAF2:
> +    ASSERT_24_BIT_COL_STREQ (0xf3, 0xfa, 0xf2, fg,
> +			     "^[[38;2;243;250;242m^[[K");
> +  }
> +  /* Background colors.  */
> +  {
> +    const bool fg = false;
> +    // #FDF7E7
> +    ASSERT_24_BIT_COL_STREQ (0xfd, 0xf7, 0xe7, fg,
> +			     "^[[48;2;253;247;231m^[[K");
> +  }
> +}
> +
> +static void
> +test_style_combinations ()
> +{
> +  style_manager sm;
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style plain;
> +  ASSERT_EQ (sm.get_or_create_id (plain), 0);
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  style bold;
> +  bold.m_bold = true;
> +
> +  ASSERT_EQ (sm.get_or_create_id (bold), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +  ASSERT_EQ (sm.get_or_create_id (bold), 1);
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +
> +  style magenta_on_blue;
> +  magenta_on_blue.m_fg_color = style::named_color::MAGENTA;
> +  magenta_on_blue.m_bg_color = style::named_color::BLUE;
> +  ASSERT_EQ (sm.get_or_create_id (magenta_on_blue), 2);
> +  ASSERT_EQ (sm.get_num_styles (), 3);
> +  ASSERT_EQ (sm.get_or_create_id (magenta_on_blue), 2);
> +  ASSERT_EQ (sm.get_num_styles (), 3);
> +}
> +
> +/* Run all selftests in this file.  */
> +
> +void
> +text_art_style_cc_tests ()
> +{
> +  test_bold ();
> +  test_underscore ();
> +  test_blink ();
> +  test_named_colors ();
> +  test_8_bit_colors ();
> +  test_24_bit_colors ();
> +  test_style_combinations ();
> +}
> +
> +} // namespace selftest
> +
> +
> +#endif /* #if CHECKING_P */
> diff --git a/gcc/text-art/styled-string.cc b/gcc/text-art/styled-string.cc
> new file mode 100644
> index 00000000000..cd176b2313f
> --- /dev/null
> +++ b/gcc/text-art/styled-string.cc
> @@ -0,0 +1,1107 @@
> +/* Implementation of text_art::styled_string.
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#define INCLUDE_MEMORY
> +#include "system.h"
> +#include "coretypes.h"
> +#include "make-unique.h"
> +#include "pretty-print.h"
> +#include "intl.h"
> +#include "diagnostic.h"
> +#include "selftest.h"
> +#include "text-art/selftests.h"
> +#include "text-art/types.h"
> +#include "color-macros.h"
> +
> +using namespace text_art;
> +
> +namespace {
> +
> +/* Support class for parsing text containing escape codes.
> +   See e.g. https://en.wikipedia.org/wiki/ANSI_escape_code
> +   We only support the codes that pretty-print.cc can generate.  */
> +
> +class escape_code_parser
> +{
> +public:
> +  escape_code_parser (style_manager &sm,
> +		      std::vector<styled_unichar> &out)
> +  : m_sm (sm),
> +    m_out (out),
> +    m_cur_style_obj (),
> +    m_cur_style_id (style::id_plain),
> +    m_state (state::START)
> +  {
> +  }
> +
> +  void on_char (cppchar_t ch)
> +  {
> +    switch (m_state)
> +      {
> +      default:
> +	gcc_unreachable ();
> +      case state::START:
> +	if (ch == '\033')
> +	  {
> +	    /* The start of an escape sequence.  */
> +	    m_state = state::AFTER_ESC;
> +	    return;
> +	  }
> +	break;
> +      case state::AFTER_ESC:
> +	if (ch == '[')
> +	  {
> +	    /* ESC [ is a Control Sequence Introducer.  */
> +	    m_state = state::CS_PARAMETER_BYTES;
> +	    return;
> +	  }
> +	else if (ch == ']')
> +	  {
> +	    /* ESC ] is an Operating System Command.  */
> +	    m_state = state::WITHIN_OSC;
> +	    return;
> +	  }
> +	break;
> +      case state::CS_PARAMETER_BYTES:
> +	if (parameter_byte_p (ch))
> +	  {
> +	    m_parameter_bytes.push_back ((char)ch);
> +	    return;
> +	  }
> +	else if (intermediate_byte_p (ch))
> +	  {
> +	    m_intermediate_bytes.push_back ((char)ch);
> +	    m_state = state::CS_INTERMEDIATE_BYTES;
> +	    return;
> +	  }
> +	else if (final_byte_p (ch))
> +	  {
> +	    on_final_csi_char (ch);
> +	    return;
> +	  }
> +	break;
> +      case state::CS_INTERMEDIATE_BYTES:
> +	/* Expect zero or more intermediate bytes.  */
> +	if (intermediate_byte_p (ch))
> +	  {
> +	    m_intermediate_bytes.push_back ((char)ch);
> +	    return;
> +	  }
> +	else if (final_byte_p (ch))
> +	  {
> +	    on_final_csi_char (ch);
> +	    return;
> +	  }
> +	break;
> +      case state::WITHIN_OSC:
> +	/* Accumulate chars into m_osc_string, until we see an ST or a BEL.  */
> +	{
> +	  /* Check for ESC \, the String Terminator (aka "ST").  */
> +	  if (ch == '\\'
> +	      && m_osc_string.size () > 0
> +	      && m_osc_string.back () == '\033')
> +	    {
> +	      m_osc_string.pop_back ();
> +	      on_final_osc_char ();
> +	      return;
> +	    }
> +	  else if (ch == '\a')
> +	    {
> +	      // BEL
> +	      on_final_osc_char ();
> +	      return;
> +	    }
> +	  m_osc_string.push_back (ch);
> +	  return;
> +	}
> +	break;
> +      }
> +
> +    /* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji
> +       variation for the previous character.  */
> +    if (ch == 0xFE0F)
> +      {
> +	if (m_out.size () > 0)
> +	  m_out.back ().set_emoji_variant ();
> +	return;
> +      }
> +
> +    if (cpp_is_combining_char (ch))
> +      {
> +	if (m_out.size () > 0)
> +	  {
> +	    m_out.back ().add_combining_char (ch);
> +	    return;
> +	  }
> +      }
> +    /* By default, add the char.  */
> +    m_out.push_back (styled_unichar (ch, false, m_cur_style_id));
> +  }
> +
> +private:
> +  void on_final_csi_char (cppchar_t ch)
> +  {
> +    switch (ch)
> +      {
> +      default:
> +	/* Unrecognized.  */
> +	break;
> +      case 'm':
> +	{
> +	  /* SGR control sequence.  */
> +	  if (m_parameter_bytes.empty ())
> +	    reset_style ();
> +	  std::vector<int> params (params_from_decimal ());
> +	  for (auto iter = params.begin (); iter != params.end (); )
> +	    {
> +	      const int param = *iter;
> +	      switch (param)
> +		{
> +		default:
> +		  /* Unrecognized SGR parameter.  */
> +		  break;
> +		case 0:
> +		  reset_style ();
> +		  break;
> +		case 1:
> +		  set_style_bold ();
> +		  break;
> +		case 4:
> +		  set_style_underscore ();
> +		  break;
> +		case 5:
> +		  set_style_blink ();
> +		  break;
> +
> +		/* Named foreground colors.  */
> +		case 30:
> +		  set_style_fg_color (style::named_color::BLACK);
> +		  break;
> +		case 31:
> +		  set_style_fg_color (style::named_color::RED);
> +		  break;
> +		case 32:
> +		  set_style_fg_color (style::named_color::GREEN);
> +		  break;
> +		case 33:
> +		  set_style_fg_color (style::named_color::YELLOW);
> +		  break;
> +		case 34:
> +		  set_style_fg_color (style::named_color::BLUE);
> +		  break;
> +		case 35:
> +		  set_style_fg_color (style::named_color::MAGENTA);
> +		  break;
> +		case 36:
> +		  set_style_fg_color (style::named_color::CYAN);
> +		  break;
> +		case 37:
> +		  set_style_fg_color (style::named_color::WHITE);
> +		  break;
> +
> +		  /* 8-bit and 24-bit color */
> +		case 38:
> +		case 48:
> +		  {
> +		    const bool fg = (param == 38);
> +		    iter++;
> +		    if (iter != params.end ())
> +		      switch (*(iter++))
> +			{
> +			default:
> +			  break;
> +			case 5:
> +			  /* 8-bit color.  */
> +			  if (iter != params.end ())
> +			    {
> +			      const uint8_t col = *(iter++);
> +			      if (fg)
> +				set_style_fg_color (style::color (col));
> +			      else
> +				set_style_bg_color (style::color (col));
> +			    }
> +			  continue;
> +			case 2:
> +			  /* 24-bit color.  */
> +			  if (iter != params.end ())
> +			    {
> +			      const uint8_t r = *(iter++);
> +			      if (iter != params.end ())
> +				{
> +				  const uint8_t g = *(iter++);
> +				  if (iter != params.end ())
> +				    {
> +				      const uint8_t b = *(iter++);
> +				      if (fg)
> +					set_style_fg_color (style::color (r,
> +									  g,
> +									  b));
> +				      else
> +					set_style_bg_color (style::color (r,
> +									  g,
> +									  b));
> +				    }
> +				}
> +			    }
> +			  continue;
> +			}
> +		    continue;
> +		  }
> +		  break;
> +
> +		/* Named background colors.  */
> +		case 40:
> +		  set_style_bg_color (style::named_color::BLACK);
> +		  break;
> +		case 41:
> +		  set_style_bg_color (style::named_color::RED);
> +		  break;
> +		case 42:
> +		  set_style_bg_color (style::named_color::GREEN);
> +		  break;
> +		case 43:
> +		  set_style_bg_color (style::named_color::YELLOW);
> +		  break;
> +		case 44:
> +		  set_style_bg_color (style::named_color::BLUE);
> +		  break;
> +		case 45:
> +		  set_style_bg_color (style::named_color::MAGENTA);
> +		  break;
> +		case 46:
> +		  set_style_bg_color (style::named_color::CYAN);
> +		  break;
> +		case 47:
> +		  set_style_bg_color (style::named_color::WHITE);
> +		  break;
> +
> +		/* Named foreground colors, bright.  */
> +		case 90:
> +		  set_style_fg_color (style::color (style::named_color::BLACK,
> +						    true));
> +		  break;
> +		case 91:
> +		  set_style_fg_color (style::color (style::named_color::RED,
> +						    true));
> +		  break;
> +		case 92:
> +		  set_style_fg_color (style::color (style::named_color::GREEN,
> +						    true));
> +		  break;
> +		case 93:
> +		  set_style_fg_color (style::color (style::named_color::YELLOW,
> +						    true));
> +		  break;
> +		case 94:
> +		  set_style_fg_color (style::color (style::named_color::BLUE,
> +						    true));
> +		  break;
> +		case 95:
> +		  set_style_fg_color (style::color (style::named_color::MAGENTA,
> +						    true));
> +		  break;
> +		case 96:
> +		  set_style_fg_color (style::color (style::named_color::CYAN,
> +						    true));
> +		  break;
> +		case 97:
> +		  set_style_fg_color (style::color (style::named_color::WHITE,
> +						    true));
> +		  break;
> +
> +		/* Named foreground colors, bright.  */
> +		case 100:
> +		  set_style_bg_color (style::color (style::named_color::BLACK,
> +						    true));
> +		  break;
> +		case 101:
> +		  set_style_bg_color (style::color (style::named_color::RED,
> +						    true));
> +		  break;
> +		case 102:
> +		  set_style_bg_color (style::color (style::named_color::GREEN,
> +						    true));
> +		  break;
> +		case 103:
> +		  set_style_bg_color (style::color (style::named_color::YELLOW,
> +						    true));
> +		  break;
> +		case 104:
> +		  set_style_bg_color (style::color (style::named_color::BLUE,
> +						    true));
> +		  break;
> +		case 105:
> +		  set_style_bg_color (style::color (style::named_color::MAGENTA,
> +						    true));
> +		  break;
> +		case 106:
> +		  set_style_bg_color (style::color (style::named_color::CYAN,
> +						    true));
> +		  break;
> +		case 107:
> +		  set_style_bg_color (style::color (style::named_color::WHITE,
> +						    true));
> +		  break;
> +		}
> +	      ++iter;
> +	    }
> +	}
> +	break;
> +      }
> +    m_parameter_bytes.clear ();
> +    m_intermediate_bytes.clear ();
> +    m_state = state::START;
> +  }
> +
> +  void on_final_osc_char ()
> +  {
> +    if (!m_osc_string.empty ())
> +      {
> +	switch (m_osc_string[0])
> +	  {
> +	  default:
> +	    break;
> +	  case '8':
> +	    /* Hyperlink support; see:
> +	       https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
> +	       We don't support params, so we expect either:
> +	       (a) "8;;URL" to begin a url (see pp_begin_url), or
> +	       (b) "8;;" to end a URL (see pp_end_url).  */
> +	    if (m_osc_string.size () >= 3
> +		&& m_osc_string[1] == ';'
> +		&& m_osc_string[2] == ';')
> +	      {
> +		set_style_url (m_osc_string.begin () + 3,
> +			       m_osc_string.end ());
> +	      }
> +	    break;
> +	  }
> +      }
> +    m_osc_string.clear ();
> +    m_state = state::START;
> +  }
> +
> +  std::vector<int> params_from_decimal () const
> +  {
> +    std::vector<int> result;
> +
> +    int curr_int = -1;
> +    for (auto param_ch : m_parameter_bytes)
> +      {
> +	if (param_ch >= '0' && param_ch <= '9')
> +	  {
> +	    if (curr_int == -1)
> +	      curr_int = 0;
> +	    else
> +	      curr_int *= 10;
> +	    curr_int += param_ch - '0';
> +	  }
> +	else
> +	  {
> +	    if (curr_int != -1)
> +	      {
> +		result.push_back (curr_int);
> +		curr_int = -1;
> +	      }
> +	  }
> +      }
> +    if (curr_int != -1)
> +      result.push_back (curr_int);
> +    return result;
> +  }
> +
> +  void refresh_style_id ()
> +  {
> +    m_cur_style_id = m_sm.get_or_create_id (m_cur_style_obj);
> +  }
> +  void reset_style ()
> +  {
> +    m_cur_style_obj = style ();
> +    refresh_style_id ();
> +  }
> +  void set_style_bold ()
> +  {
> +    m_cur_style_obj.m_bold = true;
> +    refresh_style_id ();
> +  }
> +  void set_style_underscore ()
> +  {
> +    m_cur_style_obj.m_underscore = true;
> +    refresh_style_id ();
> +  }
> +  void set_style_blink ()
> +  {
> +    m_cur_style_obj.m_blink = true;
> +    refresh_style_id ();
> +  }
> +  void set_style_fg_color (style::color color)
> +  {
> +    m_cur_style_obj.m_fg_color = color;
> +    refresh_style_id ();
> +  }
> +  void set_style_bg_color (style::color color)
> +  {
> +    m_cur_style_obj.m_bg_color = color;
> +    refresh_style_id ();
> +  }
> +  void set_style_url (std::vector<cppchar_t>::iterator begin,
> +		      std::vector<cppchar_t>::iterator end)
> +  {
> +    // The empty string means "no URL"
> +    m_cur_style_obj.m_url = std::vector<cppchar_t> (begin, end);
> +    refresh_style_id ();
> +  }
> +
> +  static bool parameter_byte_p (cppchar_t ch)
> +  {
> +    return ch >= 0x30 && ch <= 0x3F;
> +  }
> +
> +  static bool intermediate_byte_p (cppchar_t ch)
> +  {
> +    return ch >= 0x20 && ch <= 0x2F;
> +  }
> +
> +  static bool final_byte_p (cppchar_t ch)
> +  {
> +    return ch >= 0x40 && ch <= 0x7E;
> +  }
> +
> +  style_manager &m_sm;
> +  std::vector<styled_unichar> &m_out;
> +
> +  style m_cur_style_obj;
> +  style::id_t m_cur_style_id;
> +
> +  /* Handling of control sequences.  */
> +  enum class state
> +  {
> +   START,
> +
> +   /* After ESC, expecting '['.  */
> +   AFTER_ESC,
> +
> +   /* Expecting zero or more parameter bytes, an
> +      intermediate byte, or a final byte.  */
> +   CS_PARAMETER_BYTES,
> +
> +   /* Expecting zero or more intermediate bytes, or a final byte.  */
> +   CS_INTERMEDIATE_BYTES,
> +
> +   /* Within OSC.  */
> +   WITHIN_OSC
> +
> +  } m_state;
> +  std::vector<char> m_parameter_bytes;
> +  std::vector<char> m_intermediate_bytes;
> +  std::vector<cppchar_t> m_osc_string;
> +};
> +
> +} // anon namespace
> +
> +/* class text_art::styled_string.  */
> +
> +/* Construct a styled_string from STR.
> +   STR is assumed to be UTF-8 encoded and 0-terminated.
> +
> +   Parse SGR formatting chars from being in-band (within in the sequence
> +   of chars) to being out-of-band, as style elements.
> +   We only support parsing the subset of SGR chars that can be emitted
> +   by pretty-print.cc   */
> +
> +styled_string::styled_string (style_manager &sm, const char *str)
> +: m_chars ()
> +{
> +  escape_code_parser parser (sm, m_chars);
> +
> +  /* We don't actually want the display widths here, but
> +     it's an easy way to decode UTF-8.  */
> +  cpp_char_column_policy policy (8, cpp_wcwidth);
> +  cpp_display_width_computation dw (str, strlen (str), policy);
> +  while (!dw.done ())
> +    {
> +      cpp_decoded_char decoded_char;
> +      dw.process_next_codepoint (&decoded_char);
> +
> +      if (!decoded_char.m_valid_ch)
> +	/* Skip bytes that aren't valid UTF-8.  */
> +	continue;
> +
> +      /* Decode SGR formatting.  */
> +      cppchar_t ch = decoded_char.m_ch;
> +      parser.on_char (ch);
> +    }
> +}
> +
> +styled_string::styled_string (cppchar_t cppchar, bool emoji)
> +{
> +  m_chars.push_back (styled_unichar (cppchar, emoji, style::id_plain));
> +}
> +
> +styled_string
> +styled_string::from_fmt_va (style_manager &sm,
> +			    printer_fn format_decoder,
> +			    const char *fmt,
> +			    va_list *args)
> +{
> +  text_info text;
> +  text.err_no = errno;
> +  text.args_ptr = args;
> +  text.format_spec = fmt;
> +  pretty_printer pp;
> +  pp_show_color (&pp) = true;
> +  pp.url_format = URL_FORMAT_DEFAULT;
> +  pp_format_decoder (&pp) = format_decoder;
> +  pp_format (&pp, &text);
> +  pp_output_formatted_text (&pp);
> +  styled_string result (sm, pp_formatted_text (&pp));
> +  return result;
> +}
> +
> +styled_string
> +styled_string::from_fmt (style_manager &sm,
> +			 printer_fn format_decoder,
> +			 const char *fmt, ...)
> +{
> +  va_list ap;
> +  va_start (ap, fmt);
> +  styled_string result = from_fmt_va (sm, format_decoder, fmt, &ap);
> +  va_end (ap);
> +  return result;
> +}
> +
> +int
> +styled_string::calc_canvas_width () const
> +{
> +  int result = 0;
> +  for (auto ch : m_chars)
> +    result += ch.get_canvas_width ();
> +  return result;
> +}
> +
> +void
> +styled_string::append (const styled_string &suffix)
> +{
> +  m_chars.insert<std::vector<styled_unichar>::const_iterator> (m_chars.end (),
> +							       suffix.begin (),
> +							       suffix.end ());
> +}
> +
> +void
> +styled_string::set_url (style_manager &sm, const char *url)
> +{
> +  for (auto& ch : m_chars)
> +    {
> +      const style &existing_style = sm.get_style (ch.get_style_id ());
> +      style with_url (existing_style);
> +      with_url.set_style_url (url);
> +      ch.m_style_id = sm.get_or_create_id (with_url);
> +    }
> +}
> +
> +#if CHECKING_P
> +
> +namespace selftest {
> +
> +static void
> +test_combining_chars ()
> +{
> +  /* This really ought to be in libcpp, but we don't have
> +     selftests there.  */
> +  ASSERT_FALSE (cpp_is_combining_char (0));
> +  ASSERT_FALSE (cpp_is_combining_char ('a'));
> +
> +  /* COMBINING BREVE (U+0306).  */
> +  ASSERT_TRUE (cpp_is_combining_char (0x0306));
> +
> +  /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57.  */
> +  ASSERT_FALSE (cpp_is_combining_char (0x5B57));
> +
> +  /* U+FE0F VARIATION SELECTOR-16.  */
> +  ASSERT_FALSE (cpp_is_combining_char (0xFE0F));
> +}
> +
> +static void
> +test_empty ()
> +{
> +  style_manager sm;
> +  styled_string s (sm, "");
> +  ASSERT_EQ (s.size (), 0);
> +  ASSERT_EQ (s.calc_canvas_width (), 0);
> +}
> +
> +/* Test of a pure ASCII string with no escape codes.  */
> +
> +static void
> +test_simple ()
> +{
> +  const char *c_str = "hello world!";
> +  style_manager sm;
> +  styled_string s (sm, c_str);
> +  ASSERT_EQ (s.size (), strlen (c_str));
> +  ASSERT_EQ (s.calc_canvas_width (), (int)strlen (c_str));
> +  for (size_t i = 0; i < strlen (c_str); i++)
> +    {
> +      ASSERT_EQ (s[i].get_code (), (cppchar_t)c_str[i]);
> +      ASSERT_EQ (s[i].get_style_id (), 0);
> +    }
> +}
> +
> +/* Test of decoding UTF-8.  */
> +
> +static void
> +test_pi_from_utf8 ()
> +{
> +  /* U+03C0 "GREEK SMALL LETTER PI".  */
> +  const char * const pi_utf8 = "\xCF\x80";
> +
> +  style_manager sm;
> +  styled_string s (sm, pi_utf8);
> +  ASSERT_EQ (s.size (), 1);
> +  ASSERT_EQ (s.calc_canvas_width (), 1);
> +  ASSERT_EQ (s[0].get_code (), 0x03c0);
> +  ASSERT_EQ (s[0].emoji_variant_p (), false);
> +  ASSERT_EQ (s[0].double_width_p (), false);
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +}
> +
> +/* Test of double-width character.  */
> +
> +static void
> +test_emoji_from_utf8 ()
> +{
> +  /* U+1F642 "SLIGHTLY SMILING FACE".  */
> +  const char * const emoji_utf8 = "\xF0\x9F\x99\x82";
> +
> +  style_manager sm;
> +  styled_string s (sm, emoji_utf8);
> +  ASSERT_EQ (s.size (), 1);
> +  ASSERT_EQ (s.calc_canvas_width (), 2);
> +  ASSERT_EQ (s[0].get_code (), 0x1f642);
> +  ASSERT_EQ (s[0].double_width_p (), true);
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +}
> +
> +/* Test of handling U+FE0F VARIATION SELECTOR-16 to select the emoji
> +   variation for the previous character.  */
> +
> +static void
> +test_emoji_variant_from_utf8 ()
> +{
> +  const char * const emoji_utf8
> +    = (/* U+26A0 WARNING SIGN.  */
> +       "\xE2\x9A\xA0"
> +       /* U+FE0F VARIATION SELECTOR-16 (emoji variation selector).  */
> +       "\xEF\xB8\x8F");
> +
> +  style_manager sm;
> +  styled_string s (sm, emoji_utf8);
> +  ASSERT_EQ (s.size (), 1);
> +  ASSERT_EQ (s.calc_canvas_width (), 1);
> +  ASSERT_EQ (s[0].get_code (), 0x26a0);
> +  ASSERT_EQ (s[0].emoji_variant_p (), true);
> +  ASSERT_EQ (s[0].double_width_p (), false);
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +}
> +
> +static void
> +test_emoji_from_codepoint ()
> +{
> +  styled_string s ((cppchar_t)0x1f642);
> +  ASSERT_EQ (s.size (), 1);
> +  ASSERT_EQ (s.calc_canvas_width (), 2);
> +  ASSERT_EQ (s[0].get_code (), 0x1f642);
> +  ASSERT_EQ (s[0].double_width_p (), true);
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +}
> +
> +static void
> +test_from_mixed_width_utf8 ()
> +{
> +  /* This UTF-8 string literal is of the form
> +     before mojibake after
> +   where the Japanese word "mojibake" is written as the following
> +   four unicode code points:
> +     U+6587 CJK UNIFIED IDEOGRAPH-6587
> +     U+5B57 CJK UNIFIED IDEOGRAPH-5B57
> +     U+5316 CJK UNIFIED IDEOGRAPH-5316
> +     U+3051 HIRAGANA LETTER KE.
> +   Each of these is 3 bytes wide when encoded in UTF-8, whereas the
> +   "before" and "after" are 1 byte per unicode character.  */
> +  const char * const mixed_width_utf8
> +    = ("before "
> +
> +       /* U+6587 CJK UNIFIED IDEOGRAPH-6587
> +	  UTF-8: 0xE6 0x96 0x87
> +	  C octal escaped UTF-8: \346\226\207.  */
> +       "\346\226\207"
> +
> +       /* U+5B57 CJK UNIFIED IDEOGRAPH-5B57
> +	  UTF-8: 0xE5 0xAD 0x97
> +	  C octal escaped UTF-8: \345\255\227.  */
> +       "\345\255\227"
> +
> +       /* U+5316 CJK UNIFIED IDEOGRAPH-5316
> +	  UTF-8: 0xE5 0x8C 0x96
> +	  C octal escaped UTF-8: \345\214\226.  */
> +       "\345\214\226"
> +
> +       /* U+3051 HIRAGANA LETTER KE
> +	  UTF-8: 0xE3 0x81 0x91
> +	  C octal escaped UTF-8: \343\201\221.  */
> +       "\343\201\221"
> +
> +       " after");
> +
> +  style_manager sm;
> +  styled_string s (sm, mixed_width_utf8);
> +  ASSERT_EQ (s.size (), 6 + 1 + 4 + 1 + 5);
> +  ASSERT_EQ (sm.get_num_styles (), 1);
> +
> +  // We expect the Japanese characters to be double width.
> +  ASSERT_EQ (s.calc_canvas_width (), 6 + 1 + (2 * 4) + 1 + 5);
> +
> +  ASSERT_EQ (s[0].get_code (), 'b');
> +  ASSERT_EQ (s[0].double_width_p (), false);
> +  ASSERT_EQ (s[1].get_code (), 'e');
> +  ASSERT_EQ (s[2].get_code (), 'f');
> +  ASSERT_EQ (s[3].get_code (), 'o');
> +  ASSERT_EQ (s[4].get_code (), 'r');
> +  ASSERT_EQ (s[5].get_code (), 'e');
> +  ASSERT_EQ (s[6].get_code (), ' ');
> +  ASSERT_EQ (s[7].get_code (), 0x6587);
> +  ASSERT_EQ (s[7].double_width_p (), true);
> +  ASSERT_EQ (s[8].get_code (), 0x5B57);
> +  ASSERT_EQ (s[9].get_code (), 0x5316);
> +  ASSERT_EQ (s[10].get_code (), 0x3051);
> +  ASSERT_EQ (s[11].get_code (), ' ');
> +  ASSERT_EQ (s[12].get_code (), 'a');
> +  ASSERT_EQ (s[13].get_code (), 'f');
> +  ASSERT_EQ (s[14].get_code (), 't');
> +  ASSERT_EQ (s[15].get_code (), 'e');
> +  ASSERT_EQ (s[16].get_code (), 'r');
> +
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +}
> +
> +static void
> +assert_style_urleq (const location &loc,
> +		    const style &s,
> +		    const char *expected_str)
> +{
> +  ASSERT_EQ_AT (loc, s.m_url.size (), strlen (expected_str));
> +  for (size_t i = 0; i < s.m_url.size (); i++)
> +    ASSERT_EQ_AT (loc, s.m_url[i], (cppchar_t)expected_str[i]);
> +}
> +
> +#define ASSERT_STYLE_URLEQ(STYLE, EXPECTED_STR) \
> +  assert_style_urleq ((SELFTEST_LOCATION), (STYLE), (EXPECTED_STR))
> +
> +static void
> +test_url ()
> +{
> +  // URL_FORMAT_ST
> +  {
> +    style_manager sm;
> +    styled_string s
> +      (sm, "\33]8;;http://example.com\33\\This is a link\33]8;;\33\\");
> +    const char *expected = "This is a link";
> +    ASSERT_EQ (s.size (), strlen (expected));
> +    ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected));
> +    ASSERT_EQ (sm.get_num_styles (), 2);
> +    for (size_t i = 0; i < strlen (expected); i++)
> +      {
> +	ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]);
> +	ASSERT_EQ (s[i].get_style_id (), 1);
> +      }
> +    ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com");
> +  }
> +
> +  // URL_FORMAT_BEL
> +  {
> +    style_manager sm;
> +    styled_string s
> +      (sm, "\33]8;;http://example.com\aThis is a link\33]8;;\a");
> +    const char *expected = "This is a link";
> +    ASSERT_EQ (s.size (), strlen (expected));
> +    ASSERT_EQ (s.calc_canvas_width (), (int)strlen (expected));
> +    ASSERT_EQ (sm.get_num_styles (), 2);
> +    for (size_t i = 0; i < strlen (expected); i++)
> +      {
> +	ASSERT_EQ (s[i].get_code (), (cppchar_t)expected[i]);
> +	ASSERT_EQ (s[i].get_style_id (), 1);
> +      }
> +    ASSERT_STYLE_URLEQ (sm.get_style (1), "http://example.com");
> +  }
> +}
> +
> +static void
> +test_from_fmt ()
> +{
> +  style_manager sm;
> +  styled_string s (styled_string::from_fmt (sm, NULL, "%%i: %i", 42));
> +  ASSERT_EQ (s[0].get_code (), '%');
> +  ASSERT_EQ (s[1].get_code (), 'i');
> +  ASSERT_EQ (s[2].get_code (), ':');
> +  ASSERT_EQ (s[3].get_code (), ' ');
> +  ASSERT_EQ (s[4].get_code (), '4');
> +  ASSERT_EQ (s[5].get_code (), '2');
> +  ASSERT_EQ (s.size (), 6);
> +  ASSERT_EQ (s.calc_canvas_width (), 6);
> +}
> +
> +static void
> +test_from_fmt_qs ()
> +{
> +  auto_fix_quotes fix_quotes;
> +  open_quote = "\xe2\x80\x98";
> +  close_quote = "\xe2\x80\x99";
> +
> +  style_manager sm;
> +  styled_string s (styled_string::from_fmt (sm, NULL, "%qs", "msg"));
> +  ASSERT_EQ (sm.get_num_styles (), 2);
> +  ASSERT_EQ (s[0].get_code (), 0x2018);
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +  ASSERT_EQ (s[1].get_code (), 'm');
> +  ASSERT_EQ (s[1].get_style_id (), 1);
> +  ASSERT_EQ (s[2].get_code (), 's');
> +  ASSERT_EQ (s[2].get_style_id (), 1);
> +  ASSERT_EQ (s[3].get_code (), 'g');
> +  ASSERT_EQ (s[3].get_style_id (), 1);
> +  ASSERT_EQ (s[4].get_code (), 0x2019);
> +  ASSERT_EQ (s[4].get_style_id (), 0);
> +  ASSERT_EQ (s.size (), 5);
> +}
> +
> +// Test of parsing SGR codes.
> +
> +static void
> +test_from_str_with_bold ()
> +{
> +  style_manager sm;
> +  /* This is the result of pp_printf (pp, "%qs", "foo")
> +     with auto_fix_quotes.  */
> +  styled_string s (sm, "`\33[01m\33[Kfoo\33[m\33[K'");
> +  ASSERT_EQ (s[0].get_code (), '`');
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +  ASSERT_EQ (s[1].get_code (), 'f');
> +  ASSERT_EQ (s[1].get_style_id (), 1);
> +  ASSERT_EQ (s[2].get_code (), 'o');
> +  ASSERT_EQ (s[2].get_style_id (), 1);
> +  ASSERT_EQ (s[3].get_code (), 'o');
> +  ASSERT_EQ (s[3].get_style_id (), 1);
> +  ASSERT_EQ (s[4].get_code (), '\'');
> +  ASSERT_EQ (s[4].get_style_id (), 0);
> +  ASSERT_EQ (s.size (), 5);
> +  ASSERT_TRUE (sm.get_style (1).m_bold);
> +}
> +
> +static void
> +test_from_str_with_underscore ()
> +{
> +  style_manager sm;
> +  styled_string s (sm, "\33[04m\33[KA");
> +  ASSERT_EQ (s[0].get_code (), 'A');
> +  ASSERT_EQ (s[0].get_style_id (), 1);
> +  ASSERT_TRUE (sm.get_style (1).m_underscore);
> +}
> +
> +static void
> +test_from_str_with_blink ()
> +{
> +  style_manager sm;
> +  styled_string s (sm, "\33[05m\33[KA");
> +  ASSERT_EQ (s[0].get_code (), 'A');
> +  ASSERT_EQ (s[0].get_style_id (), 1);
> +  ASSERT_TRUE (sm.get_style (1).m_blink);
> +}
> +
> +// Test of parsing SGR codes.
> +
> +static void
> +test_from_str_with_color ()
> +{
> +  style_manager sm;
> +
> +  styled_string s (sm,
> +		   ("0"
> +		    SGR_SEQ (COLOR_FG_RED)
> +		    "R"
> +		    SGR_RESET
> +		    "2"
> +		    SGR_SEQ (COLOR_FG_GREEN)
> +		    "G"
> +		    SGR_RESET
> +		    "4"));
> +  ASSERT_EQ (s.size (), 5);
> +  ASSERT_EQ (sm.get_num_styles (), 3);
> +  ASSERT_EQ (s[0].get_code (), '0');
> +  ASSERT_EQ (s[0].get_style_id (), 0);
> +  ASSERT_EQ (s[1].get_code (), 'R');
> +  ASSERT_EQ (s[1].get_style_id (), 1);
> +  ASSERT_EQ (s[2].get_code (), '2');
> +  ASSERT_EQ (s[2].get_style_id (), 0);
> +  ASSERT_EQ (s[3].get_code (), 'G');
> +  ASSERT_EQ (s[3].get_style_id (), 2);
> +  ASSERT_EQ (s[4].get_code (), '4');
> +  ASSERT_EQ (s[4].get_style_id (), 0);
> +  ASSERT_EQ (sm.get_style (1).m_fg_color, style::named_color::RED);
> +  ASSERT_EQ (sm.get_style (2).m_fg_color, style::named_color::GREEN);
> +}
> +
> +static void
> +test_from_str_with_named_color ()
> +{
> +  style_manager sm;
> +  styled_string s (sm,
> +		   ("F"
> +		    SGR_SEQ (COLOR_FG_BLACK) "F"
> +		    SGR_SEQ (COLOR_FG_RED) "F"
> +		    SGR_SEQ (COLOR_FG_GREEN) "F"
> +		    SGR_SEQ (COLOR_FG_YELLOW) "F"
> +		    SGR_SEQ (COLOR_FG_BLUE) "F"
> +		    SGR_SEQ (COLOR_FG_MAGENTA) "F"
> +		    SGR_SEQ (COLOR_FG_CYAN) "F"
> +		    SGR_SEQ (COLOR_FG_WHITE) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_BLACK) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_RED) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_GREEN) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_YELLOW) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_BLUE) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_MAGENTA) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_CYAN) "F"
> +		    SGR_SEQ (COLOR_FG_BRIGHT_WHITE) "F"
> +		    SGR_SEQ (COLOR_BG_BLACK) "B"
> +		    SGR_SEQ (COLOR_BG_RED) "B"
> +		    SGR_SEQ (COLOR_BG_GREEN) "B"
> +		    SGR_SEQ (COLOR_BG_YELLOW) "B"
> +		    SGR_SEQ (COLOR_BG_BLUE) "B"
> +		    SGR_SEQ (COLOR_BG_MAGENTA) "B"
> +		    SGR_SEQ (COLOR_BG_CYAN) "B"
> +		    SGR_SEQ (COLOR_BG_WHITE) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_BLACK) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_RED) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_GREEN) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_YELLOW) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_BLUE) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_MAGENTA) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_CYAN) "B"
> +		    SGR_SEQ (COLOR_BG_BRIGHT_WHITE) "B"));
> +  ASSERT_EQ (s.size (), 33);
> +  for (size_t i = 0; i < s.size (); i++)
> +    ASSERT_EQ (s[i].get_style_id (), i);
> +  for (size_t i = 0; i < 17; i++)
> +    ASSERT_EQ (s[i].get_code (), 'F');
> +  for (size_t i = 17; i < 33; i++)
> +    ASSERT_EQ (s[i].get_code (), 'B');
> +}
> +
> +static void
> +test_from_str_with_8_bit_color ()
> +{
> +  {
> +    style_manager sm;
> +    styled_string s (sm,
> +		     ("^[[38;5;232m^[[KF"));
> +    ASSERT_EQ (s.size (), 1);
> +    ASSERT_EQ (s[0].get_code (), 'F');
> +    ASSERT_EQ (s[0].get_style_id (), 1);
> +    ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (232));
> +  }
> +  {
> +    style_manager sm;
> +    styled_string s (sm,
> +		     ("^[[48;5;231m^[[KB"));
> +    ASSERT_EQ (s.size (), 1);
> +    ASSERT_EQ (s[0].get_code (), 'B');
> +    ASSERT_EQ (s[0].get_style_id (), 1);
> +    ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (231));
> +  }
> +}
> +
> +static void
> +test_from_str_with_24_bit_color ()
> +{
> +  {
> +    style_manager sm;
> +    styled_string s (sm,
> +		     ("^[[38;2;243;250;242m^[[KF"));
> +    ASSERT_EQ (s.size (), 1);
> +    ASSERT_EQ (s[0].get_code (), 'F');
> +    ASSERT_EQ (s[0].get_style_id (), 1);
> +    ASSERT_EQ (sm.get_style (1).m_fg_color, style::color (243, 250, 242));
> +  }
> +  {
> +    style_manager sm;
> +    styled_string s (sm,
> +		     ("^[[48;2;253;247;231m^[[KB"));
> +    ASSERT_EQ (s.size (), 1);
> +    ASSERT_EQ (s[0].get_code (), 'B');
> +    ASSERT_EQ (s[0].get_style_id (), 1);
> +    ASSERT_EQ (sm.get_style (1).m_bg_color, style::color (253, 247, 231));
> +  }
> +}
> +
> +static void
> +test_from_str_combining_characters ()
> +{
> +  style_manager sm;
> +  styled_string s (sm,
> +		   /* CYRILLIC CAPITAL LETTER U (U+0423).  */
> +		   "\xD0\xA3"
> +		   /* COMBINING BREVE (U+0306).  */
> +		   "\xCC\x86");
> +  ASSERT_EQ (s.size (), 1);
> +  ASSERT_EQ (s[0].get_code (), 0x423);
> +  ASSERT_EQ (s[0].get_combining_chars ().size (), 1);
> +  ASSERT_EQ (s[0].get_combining_chars ()[0], 0x306);
> +}
> +
> +/* Run all selftests in this file.  */
> +
> +void
> +text_art_styled_string_cc_tests ()
> +{
> +  test_combining_chars ();
> +  test_empty ();
> +  test_simple ();
> +  test_pi_from_utf8 ();
> +  test_emoji_from_utf8 ();
> +  test_emoji_variant_from_utf8 ();
> +  test_emoji_from_codepoint ();
> +  test_from_mixed_width_utf8 ();
> +  test_url ();
> +  test_from_fmt ();
> +  test_from_fmt_qs ();
> +  test_from_str_with_bold ();
> +  test_from_str_with_underscore ();
> +  test_from_str_with_blink ();
> +  test_from_str_with_color ();
> +  test_from_str_with_named_color ();
> +  test_from_str_with_8_bit_color ();
> +  test_from_str_with_24_bit_color ();
> +  test_from_str_combining_characters ();
> +}
> +
> +} // namespace selftest
> +
> +
> +#endif /* #if CHECKING_P */
> diff --git a/gcc/text-art/table.cc b/gcc/text-art/table.cc
> new file mode 100644
> index 00000000000..42cc4228ea6
> --- /dev/null
> +++ b/gcc/text-art/table.cc
> @@ -0,0 +1,1272 @@
> +/* Support for tabular/grid-based content.
> +   Copyright (C) 2023 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/>.  */
> +
> +#include "config.h"
> +#define INCLUDE_MEMORY
> +#include "system.h"
> +#include "coretypes.h"
> +#include "make-unique.h"
> +#include "pretty-print.h"
> +#include "diagnostic.h"
> +#include "selftest.h"
> +#include "text-art/selftests.h"
> +#include "text-art/table.h"
> +
> +using namespace text_art;
> +
> +/* class text_art::table_cell_content.  */
> +
> +table_cell_content::table_cell_content (styled_string &&s)
> +: m_str (std::move (s)),
> +  /* We assume here that the content occupies a single canvas row.  */
> +  m_size (m_str.calc_canvas_width (), 1)
> +{
> +}
> +
> +void
> +table_cell_content::paint_to_canvas (canvas &canvas,
> +				     canvas::coord_t top_left) const
> +{
> +  canvas.paint_text (top_left, m_str);
> +}
> +
> +/* struct text_art::table_dimension_sizes.  */
> +
> +table_dimension_sizes::table_dimension_sizes (unsigned num)
> +: m_requirements (num, 0)
> +{
> +}
> +
> +/* class text_art::table::cell_placement.  */
> +
> +void
> +table::cell_placement::paint_cell_contents_to_canvas(canvas &canvas,
> +						     canvas::coord_t offset,
> +						     const table_geometry &tg) const
> +{
> +  const canvas::size_t req_canvas_size = get_min_canvas_size ();
> +  const canvas::size_t alloc_canvas_size = tg.get_canvas_size (m_rect);
> +  gcc_assert (req_canvas_size.w <= alloc_canvas_size.w);
> +  gcc_assert (req_canvas_size.h <= alloc_canvas_size.h);
> +  const int x_padding = alloc_canvas_size.w - req_canvas_size.w;
> +  const int y_padding = alloc_canvas_size.h - req_canvas_size.h;
> +  const table::coord_t table_top_left = m_rect.m_top_left;
> +  const canvas::coord_t canvas_top_left = tg.table_to_canvas (table_top_left);
> +
> +  gcc_assert (x_padding >= 0);
> +  int x_align_offset;
> +  switch (m_x_align)
> +    {
> +    default:
> +      gcc_unreachable ();
> +    case x_align::LEFT:
> +      x_align_offset = 0;
> +      break;
> +    case x_align::CENTER:
> +      x_align_offset = x_padding / 2;
> +      break;
> +    case x_align::RIGHT:
> +      x_align_offset = x_padding;
> +      break;
> +    }
> +
> +  gcc_assert (y_padding >= 0);
> +  int y_align_offset;
> +  switch (m_y_align)
> +    {
> +    default:
> +      gcc_unreachable ();
> +    case y_align::TOP:
> +      y_align_offset = 0;
> +      break;
> +    case y_align::CENTER:
> +      y_align_offset = y_padding / 2;
> +      break;
> +    case y_align::BOTTOM:
> +      y_align_offset = y_padding;
> +      break;
> +    }
> +  const canvas::coord_t content_rel_coord
> +    (canvas_top_left.x + 1 + x_align_offset,
> +     canvas_top_left.y + 1 + y_align_offset);
> +  m_content.paint_to_canvas (canvas, offset + content_rel_coord);
> +}
> +
> +/* class text_art::table.  */
> +
> +
> +table::table (size_t size)
> +: m_size (size),
> +  m_placements (),
> +  m_occupancy (size)
> +{
> +  m_occupancy.fill (-1);
> +}
> +
> +void
> +table::set_cell (coord_t coord,
> +		 table_cell_content &&content,
> +		 enum x_align x_align,
> +		 enum y_align y_align)
> +{
> +  set_cell_span (rect_t (coord, table::size_t (1, 1)),
> +		 std::move (content), x_align, y_align);
> +}
> +
> +void
> +table::set_cell_span (rect_t span,
> +		      table_cell_content &&content,
> +		      enum x_align x_align,
> +		      enum y_align y_align)
> +{
> +  gcc_assert (span.m_size.w > 0);
> +  gcc_assert (span.m_size.h > 0);
> +  int placement_idx = m_placements.size ();
> +  m_placements.emplace_back (cell_placement (span, std::move (content),
> +					     x_align, y_align));
> +  for (int y = span.get_min_y (); y < span.get_next_y (); y++)
> +    for (int x = span.get_min_x (); x < span.get_next_x (); x++)
> +      {
> +	gcc_assert (m_occupancy.get (coord_t (x, y)) == -1);
> +	m_occupancy.set (coord_t (x, y), placement_idx);
> +      }
> +}
> +
> +canvas
> +table::to_canvas (const theme &theme, const style_manager &sm) const
> +{
> +  table_dimension_sizes col_widths (m_size.w);
> +  table_dimension_sizes row_heights (m_size.h);
> +  table_cell_sizes cell_sizes (col_widths, row_heights);
> +  cell_sizes.pass_1 (*this);
> +  cell_sizes.pass_2 (*this);
> +  table_geometry tg (*this, cell_sizes);
> +  canvas canvas (tg.get_canvas_size (), sm);
> +  paint_to_canvas (canvas, canvas::coord_t (0, 0), tg, theme);
> +  return canvas;
> +}
> +
> +void
> +table::paint_to_canvas (canvas &canvas,
> +			canvas::coord_t offset,
> +			const table_geometry &tg,
> +			const theme &theme) const
> +{
> +  canvas.fill (canvas::rect_t (offset, tg.get_canvas_size ()),
> +	       styled_unichar (' '));
> +  paint_cell_borders_to_canvas (canvas, offset, tg, theme);
> +  paint_cell_contents_to_canvas (canvas, offset, tg);
> +}
> +
> +/* Print this table to stderr.  */
> +
> +DEBUG_FUNCTION void
> +table::debug () const
> +{
> +  /* Use a temporary style manager.
> +     Styles in the table will be meaningless, so
> +     print the canvas with styling disabled.  */
> +  style_manager sm;
> +  canvas canvas (to_canvas (unicode_theme (), sm));
> +  canvas.debug (false);
> +}
> +
> +const table::cell_placement *
> +table::get_placement_at (coord_t coord) const
> +{
> +  const int placement_idx = m_occupancy.get (coord);
> +  if (placement_idx == -1)
> +    return nullptr;
> +  return &m_placements[placement_idx];
> +}
> +
> +int
> +table::get_occupancy_safe (coord_t coord) const
> +{
> +  if (coord.x < 0)
> +    return -1;
> +  if (coord.x >= m_size.w)
> +    return -1;
> +  if (coord.y < 0)
> +    return -1;
> +  if (coord.y >= m_size.h)
> +    return -1;
> +  return m_occupancy.get (coord);
> +}
> +
> +/* Determine if the "?" edges need borders for table cell D
> +   in the following, for the directions relative to "X", based
> +   on whether each of table cell boundaries AB, CD, AC, and BD
> +   are boundaries between cell spans:
> +
> +   #            up?
> +   #      +-----+-----+
> +   #      |           |
> +   #      |     ?     |
> +   #      |  A  ?  B  |
> +   #      |     ?     |
> +   #      |           |
> +   # left?+ ??? X ??? + right?
> +   #      |           |
> +   #      |     ?     |
> +   #      |  C  ?  D  |
> +   #      |     ?     |
> +   #      |           |
> +   #      +-----+-----+
> +   #          down?
> +*/
> +
> +directions
> +table::get_connections (int table_x, int table_y) const
> +{
> +  int cell_a = get_occupancy_safe (coord_t (table_x - 1, table_y - 1));
> +  int cell_b = get_occupancy_safe (coord_t (table_x, table_y - 1));
> +  int cell_c = get_occupancy_safe (coord_t (table_x - 1, table_y));
> +  int cell_d = get_occupancy_safe (coord_t (table_x, table_y));
> +  const bool up = (cell_a != cell_b);
> +  const bool down = (cell_c != cell_d);
> +  const bool left = (cell_a != cell_c);
> +  const bool right = (cell_b != cell_d);
> +  return directions (up, down, left, right);
> +}
> +
> +/* Paint the grid lines.
> +
> +   Consider painting
> +   - a grid of cells,
> +   - plus a right-hand border
> +   - and a bottom border
> +
> +   Then we need to paint to the canvas like this:
> +
> +   #         PER-TABLE-COLUMN     R BORDER
> +   #      +-------------------+   +-----+
> +   #
> +   #             TABLE CELL WIDTH (in canvas units)
> +   #            +-------------+
> +   #      .     .     . .     .   .     .
> +   #   ...+-----+-----+.+-----+...+-----+ +
> +   #      |  U  |     |.|     |   |  U  | |
> +   #      |  U  |     |.|     |   |  U  | |
> +   #      |LL+RR|RRRRR|.|RRRRR|   |LL+  | |
> +   #      |  D  |     |.|     |   |  D  | |
> +   #      |  D  |     |.|     |   |  D  | |
> +   #   ...+-----+-----+.+-----+...+-----+ |
> +   #      .....................   ......  +-- PER-TABLE-ROW
> +   #   ...+-----+-----+.+-----+...+-----+ | +
> +   #      |  D  |     |.|     |   |  D  | | |
> +   #      |  D  |     |.|     |   |  D  | | |
> +   #      |  D  |     |.|     |   |  D  | | +---- TABLE CELL HEIGHT (in canvas units)
> +   #      |  D  |     |.|     |   |  D  | | |
> +   #      |  D  |     |.|     |   |  D  | | |
> +   #   ...+-----+-----+.+-----+...+-----+ + +
> +   #      .     .     .     .   .     .
> +   #   ...+-----+-----+.+-----+...+-----+  +
> +   #      |  D  |     |.|     |   |  U  |  |
> +   #      |  D  |     |.|     |   |  U  |  |
> +   #      |LL+RR|RRRRR|.|RRRRR|   |LL+  |  | BOTTOM BORDER
> +   #      |     |     |.|     |   |     |  |
> +   #      |     |     |.|     |   |     |  |
> +   #   ...+-----+-----+.+-----+...+-----+  +
> +
> +   where each:
> +
> +   #    +-----+
> +   #    |     |
> +   #    |     |
> +   #    |     |
> +   #    |     |
> +   #    |     |
> +   #    +-----+
> +
> +   is a canvas cell, and the U, L, R, D express the connections
> +   that are present with neighboring table cells.  These affect
> +   the kinds of borders that we draw for a particular table cell.  */
> +
> +void
> +table::paint_cell_borders_to_canvas (canvas &canvas,
> +				     canvas::coord_t offset,
> +				     const table_geometry &tg,
> +				     const theme &theme) const
> +{
> +  /* The per-table-cell left and top borders are either drawn or not,
> +     but if they are, they aren't affected by per-table-cell connections.  */
> +  const canvas::cell_t left_border
> +    = theme.get_line_art (directions (true, /* up */
> +				      true, /* down */
> +				      false, /* left */
> +				      false /* right */));
> +  const canvas::cell_t top_border
> +    = theme.get_line_art (directions (false, /* up */
> +				      false, /* down */
> +				      true, /* left */
> +				      true)); /* right */
> +  for (int table_y = 0; table_y < m_size.h; table_y++)
> +    {
> +      const int canvas_y = tg.table_y_to_canvas_y (table_y);
> +      for (int table_x = 0; table_x < m_size.w; table_x++)
> +	{
> +	  canvas::coord_t canvas_top_left
> +	    = tg.table_to_canvas(table::coord_t (table_x, table_y));
> +
> +	  const directions c (get_connections (table_x, table_y));
> +
> +	  /* Paint top-left corner of border, if any.  */
> +	  canvas.paint (offset + canvas_top_left,
> +			theme.get_line_art (c));
> +
> +	  /* Paint remainder of left border of cell, if any.
> +	     We assume here that the content occupies a single canvas row.  */
> +	  if (c.m_down)
> +	    canvas.paint (offset + canvas::coord_t (canvas_top_left.x,
> +						    canvas_y + 1),
> +			  left_border);
> +
> +	  /* Paint remainder of top border of cell, if any.  */
> +	  if (c.m_right)
> +	    {
> +	      const int col_width = tg.get_col_width (table_x);
> +	      for (int x_offset = 0; x_offset < col_width; x_offset++)
> +		{
> +		  const int canvas_x = canvas_top_left.x + 1 + x_offset;
> +		  canvas.paint (offset + canvas::coord_t (canvas_x, canvas_y),
> +				top_border);
> +		}
> +	    }
> +	}
> +
> +      /* Paint right-hand border of row.  */
> +      const int table_x = m_size.w;
> +      const int canvas_x = tg.table_x_to_canvas_x (table_x);
> +      const directions c (get_connections (m_size.w, table_y));
> +      canvas.paint(offset + canvas::coord_t (canvas_x, canvas_y),
> +		   theme.get_line_art (directions (c.m_up,
> +						   c.m_down,
> +						   c.m_left,
> +						   false))); /* right */
> +      /* We assume here that the content occupies a single canvas row.  */
> +      canvas.paint(offset + canvas::coord_t (canvas_x, canvas_y + 1),
> +		   theme.get_line_art (directions (c.m_down, /* up */
> +						   c.m_down, /* down */
> +						   false, /* left */
> +						   false))); /* right */
> +    }
> +
> +  /* Draw bottom border of table.  */
> +  {
> +    const int canvas_y = tg.get_canvas_size ().h - 1;
> +    for (int table_x = 0; table_x < m_size.w; table_x++)
> +      {
> +	const directions c (get_connections (table_x, m_size.h));
> +	const int left_canvas_x = tg.table_x_to_canvas_x (table_x);
> +	canvas.paint (offset + canvas::coord_t (left_canvas_x, canvas_y),
> +		      theme.get_line_art (directions (c.m_up,
> +						      false, /* down */
> +						      c.m_left,
> +						      c.m_right)));
> +	const int col_width = tg.get_col_width (table_x);
> +	for (int x_offset = 0; x_offset < col_width; x_offset++)
> +	  {
> +	    const int canvas_x = left_canvas_x + 1 + x_offset;
> +	    canvas.paint(offset + canvas::coord_t (canvas_x, canvas_y),
> +			 theme.get_line_art (directions (false, // up
> +							 false, // down
> +							 c.m_right, // left
> +							 c.m_right))); // right
> +	  }
> +      }
> +
> +    /* Bottom-right corner of table.  */
> +    const int table_x = m_size.w;
> +    const int canvas_x = tg.table_x_to_canvas_x (table_x);
> +    const directions c (get_connections (m_size.w, m_size.h));
> +    canvas.paint (offset + canvas::coord_t (canvas_x, canvas_y),
> +		  theme.get_line_art (directions (c.m_up, // up
> +						  false, // down
> +						  c.m_left, // left
> +						  false))); // right
> +  }
> +}
> +
> +void
> +table::paint_cell_contents_to_canvas(canvas &canvas,
> +				     canvas::coord_t offset,
> +				     const table_geometry &tg) const
> +{
> +  for (auto &placement : m_placements)
> +    placement.paint_cell_contents_to_canvas (canvas, offset, tg);
> +}
> +
> +/* class table_cell_sizes.  */
> +
> +/* Consider 1x1 cells.  */
> +
> +void
> +table_cell_sizes::pass_1 (const table &table)
> +{
> +  for (auto &placement : table.m_placements)
> +    if (placement.one_by_one_p ())
> +      {
> +	canvas::size_t canvas_size (placement.get_min_canvas_size ());
> +	table::coord_t table_coord (placement.m_rect.m_top_left);
> +	m_col_widths.require (table_coord.x, canvas_size.w);
> +	m_row_heights.require (table_coord.y, canvas_size.h);
> +      }
> +}
> +
> +/* Consider cells that span more than one row or column.  */
> +
> +void
> +table_cell_sizes::pass_2 (const table &table)
> +{
> +  for (auto &placement : table.m_placements)
> +    if (!placement.one_by_one_p ())
> +      {
> +	const canvas::size_t req_canvas_size (placement.get_min_canvas_size ());
> +	const canvas::size_t current_canvas_size
> +	  = get_canvas_size (placement.m_rect);
> +	/* Grow columns as necessary.  */
> +	if (req_canvas_size.w > current_canvas_size.w)
> +	  {
> +	    /* Spread the deficit amongst the columns.  */
> +	    int deficit = req_canvas_size.w - current_canvas_size.w;
> +	    const int per_col = deficit / placement.m_rect.m_size.w;
> +	    for (int table_x = placement.m_rect.get_min_x ();
> +		 table_x < placement.m_rect.get_next_x ();
> +		 table_x++)
> +	    {
> +	      m_col_widths.m_requirements[table_x] += per_col;
> +	      deficit -= per_col;
> +	    }
> +	    /* Make sure we allocate all of the deficit.  */
> +	    if (deficit > 0)
> +	      {
> +		const int table_x = placement.m_rect.get_max_x ();
> +		m_col_widths.m_requirements[table_x] += deficit;
> +	      }
> +	  }
> +	/* Grow rows as necessary.  */
> +	if (req_canvas_size.h > current_canvas_size.h)
> +	  {
> +	    /* Spread the deficit amongst the rows.  */
> +	    int deficit = req_canvas_size.h - current_canvas_size.h;
> +	    const int per_row = deficit / placement.m_rect.m_size.h;
> +	    for (int table_y = placement.m_rect.get_min_y ();
> +		 table_y < placement.m_rect.get_next_y ();
> +		 table_y++)
> +	    {
> +	      m_row_heights.m_requirements[table_y] += per_row;
> +	      deficit -= per_row;
> +	    }
> +	    /* Make sure we allocate all of the deficit.  */
> +	    if (deficit > 0)
> +	      {
> +		const int table_y = placement.m_rect.get_max_y ();
> +		m_row_heights.m_requirements[table_y] += deficit;
> +	      }
> +	  }
> +      }
> +}
> +
> +canvas::size_t
> +table_cell_sizes::get_canvas_size (const table::rect_t &rect) const
> +{
> +  canvas::size_t result (0, 0);
> +  for (int table_x = rect.get_min_x ();
> +       table_x < rect.get_next_x ();
> +       table_x ++)
> +    result.w += m_col_widths.m_requirements[table_x];
> +  for (int table_y = rect.get_min_y ();
> +       table_y < rect.get_next_y ();
> +       table_y ++)
> +    result.h += m_row_heights.m_requirements[table_y];
> +  /* Allow space for the borders.  */
> +  result.w += rect.m_size.w - 1;
> +  result.h += rect.m_size.h - 1;
> +  return result;
> +}
> +
> +/* class text_art::table_geometry.  */
> +
> +table_geometry::table_geometry (const table &table, table_cell_sizes &cell_sizes)
> +: m_table (table),
> +  m_cell_sizes (cell_sizes),
> +  m_canvas_size (canvas::size_t (0, 0)),
> +  m_col_start_x (table.get_size ().w),
> +  m_row_start_y (table.get_size ().h)
> +{
> +  recalc_coords ();
> +}
> +
> +void
> +table_geometry::recalc_coords ()
> +{
> +  /* Start canvas column of table cell, including leading border.  */
> +  m_col_start_x.clear ();
> +  int iter_canvas_x = 0;
> +  for (auto w : m_cell_sizes.m_col_widths.m_requirements)
> +    {
> +      m_col_start_x.push_back (iter_canvas_x);
> +      iter_canvas_x += w + 1;
> +    }
> +
> +  /* Start canvas row of table cell, including leading border.  */
> +  m_row_start_y.clear ();
> +  int iter_canvas_y = 0;
> +  for (auto h : m_cell_sizes.m_row_heights.m_requirements)
> +    {
> +      m_row_start_y.push_back (iter_canvas_y);
> +      iter_canvas_y += h + 1;
> +    }
> +
> +  m_canvas_size = canvas::size_t (iter_canvas_x + 1,
> +				  iter_canvas_y + 1);
> +}
> +
> +/* Get the TL corner of the table cell at TABLE_COORD
> +   in canvas coords (including the border).  */
> +
> +canvas::coord_t
> +table_geometry::table_to_canvas (table::coord_t table_coord) const
> +{
> +  return canvas::coord_t (table_x_to_canvas_x (table_coord.x),
> +			  table_y_to_canvas_y (table_coord.y));
> +}
> +
> +/* Get the left border of the table cell at column TABLE_X
> +   in canvas coords (including the border).  */
> +
> +int
> +table_geometry::table_x_to_canvas_x (int table_x) const
> +{
> +  /* Allow one beyond the end, for the right-hand border of the table.  */
> +  if (table_x == m_col_start_x.size ())
> +    return m_canvas_size.w - 1;
> +  return m_col_start_x[table_x];
> +}
> +
> +/* Get the top border of the table cell at column TABLE_Y
> +   in canvas coords (including the border).  */
> +
> +int
> +table_geometry::table_y_to_canvas_y (int table_y) const
> +{
> +  /* Allow one beyond the end, for the right-hand border of the table.  */
> +  if (table_y == m_row_start_y.size ())
> +    return m_canvas_size.h - 1;
> +  return m_row_start_y[table_y];
> +}
> +
> +/* class text_art::simple_table_geometry.  */
> +
> +simple_table_geometry::simple_table_geometry (const table &table)
> +: m_col_widths (table.get_size ().w),
> +  m_row_heights (table.get_size ().h),
> +  m_cell_sizes (m_col_widths, m_row_heights),
> +  m_tg (table, m_cell_sizes)
> +{
> +  m_cell_sizes.pass_1 (table);
> +  m_cell_sizes.pass_2 (table);
> +  m_tg.recalc_coords ();
> +}
> +
> +#if CHECKING_P
> +
> +namespace selftest {
> +
> +static void
> +test_tic_tac_toe ()
> +{
> +  style_manager sm;
> +  table t (table::size_t (3, 3));
> +  t.set_cell (table::coord_t (0, 0), styled_string (sm, "X"));
> +  t.set_cell (table::coord_t (1, 0), styled_string (sm, ""));
> +  t.set_cell (table::coord_t (2, 0), styled_string (sm, ""));
> +  t.set_cell (table::coord_t (0, 1), styled_string (sm, "O"));
> +  t.set_cell (table::coord_t (1, 1), styled_string (sm, "O"));
> +  t.set_cell (table::coord_t (2, 1), styled_string (sm, ""));
> +  t.set_cell (table::coord_t (0, 2), styled_string (sm, "X"));
> +  t.set_cell (table::coord_t (1, 2), styled_string (sm, ""));
> +  t.set_cell (table::coord_t (2, 2), styled_string (sm, "O"));
> +
> +  {
> +    canvas canvas (t.to_canvas (ascii_theme (), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       ("+-+-+-+\n"
> +	"|X| | |\n"
> +	"+-+-+-+\n"
> +	"|O|O| |\n"
> +	"+-+-+-+\n"
> +	"|X| |O|\n"
> +	"+-+-+-+\n"));
> +  }
> +
> +  {
> +    canvas canvas (t.to_canvas (unicode_theme (), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("┌─┬─┬─┐\n"
> +	"│X│ │ │\n"
> +	"├─┼─┼─┤\n"
> +	"│O│O│ │\n"
> +	"├─┼─┼─┤\n"
> +	"│X│ │O│\n"
> +	"└─┴─┴─┘\n"));
> +  }
> +}
> +
> +static table
> +make_text_table ()
> +{
> +  style_manager sm;
> +  table t (table::size_t (3, 3));
> +  t.set_cell (table::coord_t (0, 0), styled_string (sm, "top left"));
> +  t.set_cell (table::coord_t (1, 0), styled_string (sm, "top middle"));
> +  t.set_cell (table::coord_t (2, 0), styled_string (sm, "top right"));
> +  t.set_cell (table::coord_t (0, 1), styled_string (sm, "middle left"));
> +  t.set_cell (table::coord_t (1, 1), styled_string (sm, "middle middle"));
> +  t.set_cell (table::coord_t (2, 1), styled_string (sm, "middle right"));
> +  t.set_cell (table::coord_t (0, 2), styled_string (sm, "bottom left"));
> +  t.set_cell (table::coord_t (1, 2), styled_string (sm, "bottom middle"));
> +  t.set_cell (table::coord_t (2, 2), styled_string (sm, "bottom right"));
> +  return t;
> +}
> +
> +static void
> +test_text_table ()
> +{
> +  style_manager sm;
> +  table table = make_text_table ();
> +  {
> +    canvas canvas (table.to_canvas (ascii_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       ("+-----------+-------------+------------+\n"
> +	"| top left  | top middle  | top right  |\n"
> +	"+-----------+-------------+------------+\n"
> +	"|middle left|middle middle|middle right|\n"
> +	"+-----------+-------------+------------+\n"
> +	"|bottom left|bottom middle|bottom right|\n"
> +	"+-----------+-------------+------------+\n"));
> +  }
> +  {
> +    canvas canvas (table.to_canvas (unicode_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("┌───────────┬─────────────┬────────────┐\n"
> +	"│ top left  │ top middle  │ top right  │\n"
> +	"├───────────┼─────────────┼────────────┤\n"
> +	"│middle left│middle middle│middle right│\n"
> +	"├───────────┼─────────────┼────────────┤\n"
> +	"│bottom left│bottom middle│bottom right│\n"
> +	"└───────────┴─────────────┴────────────┘\n"));
> +  }
> +}
> +
> +static void
> +test_offset_table ()
> +{
> +  style_manager sm;
> +  table table = make_text_table ();
> +  simple_table_geometry stg (table);
> +  const canvas::size_t tcs = stg.m_tg.get_canvas_size();
> +  {
> +    canvas canvas (canvas::size_t (tcs.w + 5, tcs.h + 5), sm);
> +    canvas.debug_fill ();
> +    table.paint_to_canvas (canvas, canvas::coord_t (3, 3),
> +			   stg.m_tg,
> +			   ascii_theme());
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       ("*********************************************\n"
> +	"*********************************************\n"
> +	"*********************************************\n"
> +	"***+-----------+-------------+------------+**\n"
> +	"***| top left  | top middle  | top right  |**\n"
> +	"***+-----------+-------------+------------+**\n"
> +	"***|middle left|middle middle|middle right|**\n"
> +	"***+-----------+-------------+------------+**\n"
> +	"***|bottom left|bottom middle|bottom right|**\n"
> +	"***+-----------+-------------+------------+**\n"
> +	"*********************************************\n"
> +	"*********************************************\n"));
> +  }
> +  {
> +    canvas canvas (canvas::size_t (tcs.w + 5, tcs.h + 5), sm);
> +    canvas.debug_fill ();
> +    table.paint_to_canvas (canvas, canvas::coord_t (3, 3),
> +			   stg.m_tg,
> +			   unicode_theme());
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("*********************************************\n"
> +	"*********************************************\n"
> +	"*********************************************\n"
> +	"***┌───────────┬─────────────┬────────────┐**\n"
> +	"***│ top left  │ top middle  │ top right  │**\n"
> +	"***├───────────┼─────────────┼────────────┤**\n"
> +	"***│middle left│middle middle│middle right│**\n"
> +	"***├───────────┼─────────────┼────────────┤**\n"
> +	"***│bottom left│bottom middle│bottom right│**\n"
> +	"***└───────────┴─────────────┴────────────┘**\n"
> +	"*********************************************\n"
> +	"*********************************************\n"));
> +  }
> +}
> +
> +#define ASSERT_TABLE_CELL_STREQ(TABLE, TABLE_X, TABLE_Y, EXPECTED_STR)	\
> +  SELFTEST_BEGIN_STMT							\
> +    table::coord_t coord ((TABLE_X), (TABLE_Y));			\
> +    const table::cell_placement *cp = (TABLE).get_placement_at (coord);	\
> +    ASSERT_NE (cp, nullptr);						\
> +    ASSERT_EQ (cp->get_content (), styled_string (sm, EXPECTED_STR)); \
> +  SELFTEST_END_STMT
> +
> +#define ASSERT_TABLE_NULL_CELL(TABLE, TABLE_X, TABLE_Y)			\
> +  SELFTEST_BEGIN_STMT							\
> +    table::coord_t coord ((TABLE_X), (TABLE_Y));			\
> +    const table::cell_placement *cp = (TABLE).get_placement_at (coord);	\
> +    ASSERT_EQ (cp, nullptr);						\
> +  SELFTEST_END_STMT
> +
> +static void
> +test_spans ()
> +{
> +  style_manager sm;
> +  table table (table::size_t (3, 3));
> +  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
> +				      table::size_t (3, 1)),
> +		       styled_string (sm, "ABC"));
> +  table.set_cell_span (table::rect_t (table::coord_t (0, 1),
> +				      table::size_t (2, 1)),
> +		       styled_string (sm, "DE"));
> +  table.set_cell_span (table::rect_t (table::coord_t (2, 1),
> +				      table::size_t (1, 1)),
> +		       styled_string (sm, "F"));
> +  table.set_cell (table::coord_t (0, 2), styled_string (sm, "G"));
> +  table.set_cell (table::coord_t (1, 2), styled_string (sm, "H"));
> +  table.set_cell (table::coord_t (2, 2), styled_string (sm, "I"));
> +  {
> +    canvas canvas (table.to_canvas (ascii_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       ("+-----+\n"
> +	"| ABC |\n"
> +	"+---+-+\n"
> +	"|DE |F|\n"
> +	"+-+-+-+\n"
> +	"|G|H|I|\n"
> +	"+-+-+-+\n"));
> +  }
> +  {
> +    canvas canvas (table.to_canvas (unicode_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("┌─────┐\n"
> +	"│ ABC │\n"
> +	"├───┬─┤\n"
> +	"│DE │F│\n"
> +	"├─┬─┼─┤\n"
> +	"│G│H│I│\n"
> +	"└─┴─┴─┘\n"));
> +  }
> +}
> +
> +/* Verify building this 5x5 table with spans:
> +     |0|1|2|3|4|
> +     +-+-+-+-+-+
> +    0|A A A|B|C|0
> +     +     +-+ +
> +    1|A A A|D|C|1
> +     +     +-+-+
> +    2|A A A|E|F|2
> +     +-+-+-+-+-+
> +    3|G G|H|I I|3
> +     |   | +-+-+
> +    4|G G|H|J J|4
> +     +-+-+-+-+-+
> +     |0|1|2|3|4|
> +*/
> +
> +static void
> +test_spans_2 ()
> +{
> +  style_manager sm;
> +  table table (table::size_t (5, 5));
> +  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
> +				      table::size_t (3, 3)),
> +		       styled_string (sm, "A"));
> +  table.set_cell_span (table::rect_t (table::coord_t (3, 0),
> +				      table::size_t (1, 1)),
> +		       styled_string (sm, "B"));
> +  table.set_cell_span (table::rect_t (table::coord_t (4, 0),
> +				      table::size_t (1, 2)),
> +		       styled_string (sm, "C"));
> +  table.set_cell_span (table::rect_t (table::coord_t (3, 1),
> +				      table::size_t (1, 1)),
> +		       styled_string (sm, "D"));
> +  table.set_cell_span (table::rect_t (table::coord_t (3, 2),
> +				      table::size_t (1, 1)),
> +		       styled_string (sm, "E"));
> +  table.set_cell_span (table::rect_t (table::coord_t (4, 2),
> +				      table::size_t (1, 1)),
> +		       styled_string (sm, "F"));
> +  table.set_cell_span (table::rect_t (table::coord_t (0, 3),
> +				      table::size_t (2, 2)),
> +		       styled_string (sm, "G"));
> +  table.set_cell_span (table::rect_t (table::coord_t (2, 3),
> +				      table::size_t (1, 2)),
> +		       styled_string (sm, "H"));
> +  table.set_cell_span (table::rect_t (table::coord_t (3, 3),
> +				      table::size_t (2, 1)),
> +		       styled_string (sm, "I"));
> +  table.set_cell_span (table::rect_t (table::coord_t (3, 4),
> +				      table::size_t (2, 1)),
> +		       styled_string (sm, "J"));
> +
> +  /* Check occupancy at each table coordinate.  */
> +  ASSERT_TABLE_CELL_STREQ (table, 0, 0, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 1, 0, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 2, 0, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 3, 0, "B");
> +  ASSERT_TABLE_CELL_STREQ (table, 4, 0, "C");
> +
> +  ASSERT_TABLE_CELL_STREQ (table, 0, 1, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 1, 1, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 2, 1, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 3, 1, "D");
> +  ASSERT_TABLE_CELL_STREQ (table, 4, 1, "C");
> +
> +  ASSERT_TABLE_CELL_STREQ (table, 0, 2, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 1, 2, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 2, 2, "A");
> +  ASSERT_TABLE_CELL_STREQ (table, 3, 2, "E");
> +  ASSERT_TABLE_CELL_STREQ (table, 4, 2, "F");
> +
> +  ASSERT_TABLE_CELL_STREQ (table, 0, 3, "G");
> +  ASSERT_TABLE_CELL_STREQ (table, 1, 3, "G");
> +  ASSERT_TABLE_CELL_STREQ (table, 2, 3, "H");
> +  ASSERT_TABLE_CELL_STREQ (table, 3, 3, "I");
> +  ASSERT_TABLE_CELL_STREQ (table, 4, 3, "I");
> +
> +  ASSERT_TABLE_CELL_STREQ (table, 0, 4, "G");
> +  ASSERT_TABLE_CELL_STREQ (table, 1, 4, "G");
> +  ASSERT_TABLE_CELL_STREQ (table, 2, 4, "H");
> +  ASSERT_TABLE_CELL_STREQ (table, 3, 4, "J");
> +  ASSERT_TABLE_CELL_STREQ (table, 4, 4, "J");
> +
> +  {
> +    canvas canvas (table.to_canvas (ascii_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       ("+---+-+-+\n"
> +	"|   |B| |\n"
> +	"|   +-+C|\n"
> +	"| A |D| |\n"
> +	"|   +-+-+\n"
> +	"|   |E|F|\n"
> +	"+-+-+-+-+\n"
> +	"| | | I |\n"
> +	"|G|H+---+\n"
> +	"| | | J |\n"
> +	"+-+-+---+\n"));
> +  }
> +  {
> +    canvas canvas (table.to_canvas (unicode_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("┌───┬─┬─┐\n"
> +	"│   │B│ │\n"
> +	"│   ├─┤C│\n"
> +	"│ A │D│ │\n"
> +	"│   ├─┼─┤\n"
> +	"│   │E│F│\n"
> +	"├─┬─┼─┴─┤\n"
> +	"│ │ │ I │\n"
> +	"│G│H├───┤\n"
> +	"│ │ │ J │\n"
> +	"└─┴─┴───┘\n"));
> +  }
> +}
> +
> +/* Experiment with adding a 1-table-column gap at the boundary between
> +   valid vs invalid for visualizing a buffer overflow.  */
> +static void
> +test_spans_3 ()
> +{
> +  const char * const str = "hello world!";
> +  const size_t buf_size = 10;
> +  const size_t str_size = strlen (str) + 1;
> +
> +  style_manager sm;
> +  table table (table::size_t (str_size + 1, 3));
> +
> +  table.set_cell_span (table::rect_t (table::coord_t (0, 0),
> +				      table::size_t (str_size + 1, 1)),
> +		       styled_string (sm, "String literal"));
> +
> +  for (size_t i = 0; i < str_size; i++)
> +    {
> +      table::coord_t c (i, 1);
> +      if (i >= buf_size)
> +	c.x++;
> +      if (str[i] == '\0')
> +	table.set_cell (c, styled_string (sm, "NUL"));
> +      else
> +	table.set_cell (c, styled_string ((cppchar_t)str[i]));
> +    }
> +
> +  table.set_cell_span (table::rect_t (table::coord_t (0, 2),
> +				      table::size_t (buf_size, 1)),
> +		       styled_string::from_fmt (sm,
> +						     nullptr,
> +						     "'buf' (char[%i])",
> +						     (int)buf_size));
> +  table.set_cell_span (table::rect_t (table::coord_t (buf_size + 1, 2),
> +				      table::size_t (str_size - buf_size, 1)),
> +		       styled_string (sm, "overflow"));
> +
> +  {
> +    canvas canvas (table.to_canvas (ascii_theme (), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       "+-----------------------------+\n"
> +       "|       String literal        |\n"
> +       "+-+-+-+-+-+-+-+-+-+-++-+-+----+\n"
> +       "|h|e|l|l|o| |w|o|r|l||d|!|NUL |\n"
> +       "+-+-+-+-+-+-+-+-+-+-++-+-+----+\n"
> +       "| 'buf' (char[10])  ||overflow|\n"
> +       "+-------------------++--------+\n");
> +  }
> +  {
> +    canvas canvas (table.to_canvas (unicode_theme (), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("┌─────────────────────────────┐\n"
> +	"│       String literal        │\n"
> +	"├─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬┬─┬─┬────┤\n"
> +	"│h│e│l│l│o│ │w│o│r│l││d│!│NUL │\n"
> +	"├─┴─┴─┴─┴─┴─┴─┴─┴─┴─┤├─┴─┴────┤\n"
> +	"│ 'buf' (char[10])  ││overflow│\n"
> +	"└───────────────────┘└────────┘\n"));
> +  }
> +}
> +
> +static void
> +test_double_width_chars ()
> +{
> +  table_cell_content tcc (styled_string ((cppchar_t)0x1f642));
> +  ASSERT_EQ (tcc.get_canvas_size ().w, 2);
> +  ASSERT_EQ (tcc.get_canvas_size ().h, 1);
> +
> +  style_manager sm;
> +  table table (table::size_t (1, 1));
> +  table.set_cell (table::coord_t (0,0),
> +		  styled_string ((cppchar_t)0x1f642));
> +
> +  canvas canvas (table.to_canvas (unicode_theme(), sm));
> +  ASSERT_CANVAS_STREQ
> +    (canvas, false,
> +     // FIXME: are we allowed unicode chars in string literals in our source?
> +     ("┌──┐\n"
> +      "│🙂│\n"
> +      "└──┘\n"));
> +}
> +
> +static void
> +test_ipv4_header ()
> +{
> +  style_manager sm;
> +  table table (table::size_t (34, 10));
> +  table.set_cell (table::coord_t (0, 0), styled_string (sm, "Offsets"));
> +  table.set_cell (table::coord_t (1, 0), styled_string (sm, "Octet"));
> +  table.set_cell (table::coord_t (0, 1), styled_string (sm, "Octet"));
> +  for (int octet = 0; octet < 4; octet++)
> +    table.set_cell_span (table::rect_t (table::coord_t (2 + (octet * 8), 0),
> +					table::size_t (8, 1)),
> +			 styled_string::from_fmt (sm, nullptr, "%i", octet));
> +  table.set_cell (table::coord_t (1, 1), styled_string (sm, "Bit"));
> +  for (int bit = 0; bit < 32; bit++)
> +    table.set_cell (table::coord_t (bit + 2, 1),
> +		    styled_string::from_fmt (sm, nullptr, "%i", bit));
> +  for (int word = 0; word < 6; word++)
> +    {
> +      table.set_cell (table::coord_t (0, word + 2),
> +		      styled_string::from_fmt (sm, nullptr, "%i", word * 4));
> +      table.set_cell (table::coord_t (1, word + 2),
> +		      styled_string::from_fmt (sm, nullptr, "%i", word * 32));
> +    }
> +
> +  table.set_cell (table::coord_t (0, 8), styled_string (sm, "..."));
> +  table.set_cell (table::coord_t (1, 8), styled_string (sm, "..."));
> +  table.set_cell (table::coord_t (0, 9), styled_string (sm, "56"));
> +  table.set_cell (table::coord_t (1, 9), styled_string (sm, "448"));
> +
> +#define SET_BITS(FIRST, LAST, NAME)					\
> +  do {									\
> +    const int first = (FIRST);						\
> +    const int last = (LAST);						\
> +    const char *name = (NAME);						\
> +    const int row = first / 32;						\
> +    gcc_assert (last / 32 == row);					\
> +    table::rect_t rect (table::coord_t ((first % 32) + 2, row + 2),	\
> +			table::size_t (last + 1 - first , 1));		\
> +    table.set_cell_span (rect, styled_string (sm, name));		\
> +  } while (0)
> +
> +  SET_BITS (0, 3, "Version");
> +  SET_BITS (4, 7, "IHL");
> +  SET_BITS (8, 13, "DSCP");
> +  SET_BITS (14, 15, "ECN");
> +  SET_BITS (16, 31, "Total Length");
> +
> +  SET_BITS (32 +  0, 32 + 15, "Identification");
> +  SET_BITS (32 + 16, 32 + 18, "Flags");
> +  SET_BITS (32 + 19, 32 + 31, "Fragment Offset");
> +
> +  SET_BITS (64 +  0, 64 +  7, "Time To Live");
> +  SET_BITS (64 +  8, 64 + 15, "Protocol");
> +  SET_BITS (64 + 16, 64 + 31, "Header Checksum");
> +
> +  SET_BITS (96 +  0, 96 + 31, "Source IP Address");
> +  SET_BITS (128 +  0, 128 + 31, "Destination IP Address");
> +
> +  table.set_cell_span(table::rect_t (table::coord_t (2, 7),
> +				     table::size_t (32, 3)),
> +		      styled_string (sm, "Options"));
> +  {
> +    canvas canvas (table.to_canvas (ascii_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       ("+-------+-----+---------------+---------------------+-----------------------+-----------------------+\n"
> +	"|Offsets|Octet|       0       |          1          |           2           |           3           |\n"
> +	"+-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\n"
> +	"| Octet | Bit |0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|\n"
> +	"+-------+-----+-+-+-+-+-+-+-+-+-+-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+\n"
> +	"|   0   |  0  |Version|  IHL  |     DSCP      | ECN |                 Total Length                  |\n"
> +	"+-------+-----+-------+-------+---------------+-----+--------+--------------------------------------+\n"
> +	"|   4   | 32  |           Identification            | Flags  |           Fragment Offset            |\n"
> +	"+-------+-----+---------------+---------------------+--------+--------------------------------------+\n"
> +	"|   8   | 64  | Time To Live  |      Protocol       |                Header Checksum                |\n"
> +	"+-------+-----+---------------+---------------------+-----------------------------------------------+\n"
> +	"|  12   | 96  |                                  Source IP Address                                  |\n"
> +	"+-------+-----+-------------------------------------------------------------------------------------+\n"
> +	"|  16   | 128 |                               Destination IP Address                                |\n"
> +	"+-------+-----+-------------------------------------------------------------------------------------+\n"
> +	"|  20   | 160 |                                                                                     |\n"
> +	"+-------+-----+                                                                                     |\n"
> +	"|  ...  | ... |                                       Options                                       |\n"
> +	"+-------+-----+                                                                                     |\n"
> +	"|  56   | 448 |                                                                                     |\n"
> +	"+-------+-----+-------------------------------------------------------------------------------------+\n"));
> +  }
> +  {
> +    canvas canvas (table.to_canvas (unicode_theme(), sm));
> +    ASSERT_CANVAS_STREQ
> +      (canvas, false,
> +       // FIXME: are we allowed unicode chars in string literals in our source?
> +       ("┌───────┬─────┬───────────────┬─────────────────────┬───────────────────────┬───────────────────────┐\n"
> +	"│Offsets│Octet│       0       │          1          │           2           │           3           │\n"
> +	"├───────┼─────┼─┬─┬─┬─┬─┬─┬─┬─┼─┬─┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┼──┬──┬──┬──┬──┬──┬──┬──┤\n"
> +	"│ Octet │ Bit │0│1│2│3│4│5│6│7│8│9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│24│25│26│27│28│29│30│31│\n"
> +	"├───────┼─────┼─┴─┴─┴─┼─┴─┴─┴─┼─┴─┴──┴──┴──┴──┼──┴──┼──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┤\n"
> +	"│   0   │  0  │Version│  IHL  │     DSCP      │ ECN │                 Total Length                  │\n"
> +	"├───────┼─────┼───────┴───────┴───────────────┴─────┼────────┬──────────────────────────────────────┤\n"
> +	"│   4   │ 32  │           Identification            │ Flags  │           Fragment Offset            │\n"
> +	"├───────┼─────┼───────────────┬─────────────────────┼────────┴──────────────────────────────────────┤\n"
> +	"│   8   │ 64  │ Time To Live  │      Protocol       │                Header Checksum                │\n"
> +	"├───────┼─────┼───────────────┴─────────────────────┴───────────────────────────────────────────────┤\n"
> +	"│  12   │ 96  │                                  Source IP Address                                  │\n"
> +	"├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤\n"
> +	"│  16   │ 128 │                               Destination IP Address                                │\n"
> +	"├───────┼─────┼─────────────────────────────────────────────────────────────────────────────────────┤\n"
> +	"│  20   │ 160 │                                                                                     │\n"
> +	"├───────┼─────┤                                                                                     │\n"
> +	"│  ...  │ ... │                                       Options                                       │\n"
> +	"├───────┼─────┤                                                                                     │\n"
> +	"│  56   │ 448 │                                                                                     │\n"
> +	"└───────┴─────┴────────────────────────────────────────────────────────────────────────────────────