chiark / gitweb /
@@@ tty mess
[mLib] / test / tvec-output.c
index 9ca4f29ef7da1fd8010a9caca486b1bb8516319e..40ab769868fa396730301b3afa45f86f2cd25eb3 100644 (file)
@@ -36,6 +36,7 @@
 #include <string.h>
 
 #include <unistd.h>
+#include <sys/stat.h>
 
 #include "alloc.h"
 #include "bench.h"
 #include "macros.h"
 #include "quis.h"
 #include "report.h"
+#include "tty.h"
+#include "ttycolour.h"
+
 #include "tvec.h"
+#include "tvec-bench.h"
+#include "tvec-output.h"
 
 /*----- Common machinery --------------------------------------------------*/
 
@@ -61,43 +67,57 @@ static const char *regdisp(unsigned disp)
     case TVRD_INPUT: return "input";
     case TVRD_OUTPUT: return "output";
     case TVRD_MATCH: return "matched";
-    case TVRD_EXPECT: return "expected";
     case TVRD_FOUND: return "found";
+    case TVRD_EXPECT: return "expected";
     default: abort();
   }
 }
 
-/* --- @getenv_boolean@ --- *
+/* --- @interpret_boolean@, @getenv_boolean@ --- *
  *
- * Arguments:  @const char *var@ = environment variable name
+ * Arguments:  @const char *val@ = a string
+ *             @const char *var@ = environment variable name
  *             @int dflt@ = default value
+ *             @const char *what, ...@ = format string describing where the
+ *                     setting came from
  *
- * Returns:    @0@ if the variable is set to something falseish, @1@ if it's
- *             set to something truish, or @dflt@ otherwise.
+ * Returns:    @0@ if the string, or variable, is set to something
+ *             falseish, @1@ if it's set to something truish, or @dflt@
+ *             otherwise.
  */
 
-static int getenv_boolean(const char *var, int dflt)
+static PRINTF_LIKE(3, 4)
+  int interpret_boolean(const char *val, int dflt, const char *what, ...)
 {
-  const char *p;
-
-  p = getenv(var);
-  if (!p)
-    return (dflt);
-  else if (STRCMP(p, ==, "y") || STRCMP(p, ==, "yes") ||
-          STRCMP(p, ==, "t") || STRCMP(p, ==, "true") ||
-          STRCMP(p, ==, "on") || STRCMP(p, ==, "force") ||
-          STRCMP(p, ==, "1"))
-    return (1);
-  else if (STRCMP(p, ==, "n") || STRCMP(p, ==, "no") ||
-          STRCMP(p, ==, "f") || STRCMP(p, ==, "false") ||
-          STRCMP(p, ==, "nil") || STRCMP(p, ==, "off") ||
-          STRCMP(p, ==, "0"))
-    return (0);
+  dstr d = DSTR_INIT;
+  va_list ap;
+  int rc;
+
+  if (!val)
+    rc = dflt;
+  else if (STRCMP(val, ==, "y") || STRCMP(val, ==, "yes") ||
+          STRCMP(val, ==, "t") || STRCMP(val, ==, "true") ||
+          STRCMP(val, ==, "on") || STRCMP(val, ==, "force") ||
+          STRCMP(val, ==, "1"))
+    rc = 1;
+  else if (STRCMP(val, ==, "n") || STRCMP(val, ==, "no") ||
+          STRCMP(val, ==, "nil") || STRCMP(val, ==, "f") ||
+            STRCMP(val, ==, "false") ||
+          STRCMP(val, ==, "off") || STRCMP(val, ==, "inhibit") ||
+          STRCMP(val, ==, "0"))
+    rc = 0;
   else {
-    moan("ignoring unexpected value `%s' for environment variable `%s'",
-        var, p);
-    return (dflt);
+    va_start(ap, what); dstr_vputf(&d, what, &ap); va_end(ap);
+    moan("ignoring unexpected value `%s' for %s", val, d.buf);
+    rc = dflt;
   }
+  dstr_destroy(&d); return (rc);
+}
+
+static int getenv_boolean(const char *var, int dflt)
+{
+  return (interpret_boolean(getenv(var), dflt,
+                           "environment variable `%s'", var));
 }
 
 /* --- @register_maxnamelen@ --- *
@@ -110,31 +130,66 @@ static int getenv_boolean(const char *var, int dflt)
 static int register_maxnamelen(const struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
-  int maxlen = 6, n;
+  int maxlen = 10, n;
 
   for (rd = tv->test->regs; rd->name; rd++)
     { n = strlen(rd->name); if (n > maxlen) maxlen = n; }
   return (maxlen);
 }
 
-/*----- Output formatting -------------------------------------------------*/
+/* --- @print_ident@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned style@ = style to use for register dumps
+ *             @const struct gprintf_ops *gops@ = output operations
+ *             @void *go@ = output state
+ *
+ * Returns:    ---
+ *
+ * Use:                Write a benchmark identification to the output.
+ */
+
+static void print_ident(struct tvec_state *tv, unsigned style,
+                       const struct gprintf_ops *gops, void *go)
+{
+  const struct tvec_regdef *rd;
+  unsigned f = 0;
+
+#define f_any 1u
+
+  for (rd = tv->test->regs; rd->name; rd++)
+    if (rd->f&TVRF_ID) {
+      if (!(f&f_any)) f |= f_any;
+      else if (style&TVSF_RAW) gops->putch(go, ' ');
+      else gprintf(gops, go, ", ");
+      gprintf(gops, go, "%s", rd->name);
+      if (style&TVSF_RAW) gops->putch(go, '=');
+      else gprintf(gops, go, " = ");
+      rd->ty->dump(&TVEC_REG(tv, in, rd->i)->v, rd, style, gops, go);
+    }
+
+#undef f_any
+}
+
+/*----- Output layout -----------------------------------------------------*/
 
-/* We have two main jobs in output formatting: trimming trailing blanks; and
+/* We have two main jobs in output layout: trimming trailing blanks; and
  * adding a prefix to each line.
  *
  * This is somehow much more complicated than it ought to be.
  */
 
-struct format {
+struct layout {
   FILE *fp;                             /* output file */
   const char *prefix, *pfxtail, *pfxlim; /* prefix pointers */
   dstr w;                              /* trailing whitespace */
+  dstr ctrl;                           /* control sequence for next word */
   unsigned f;                          /* flags */
-#define FMTF_NEWL 1u                   /*   start of output line */
+#define LYTF_NEWL 1u                   /*   start of output line */
 };
 
-/* Support macros.  These assume `fmt' is defined as a pointer to the `struct
- * format' state.
+/* Support macros.  These assume `lyt' is defined as a pointer to the `struct
+ * layout' state.
  */
 
 #define SPLIT_RANGE(tail, base, limit) do {                            \
@@ -151,27 +206,34 @@ struct format {
    * file.  Return immediately on error.                               \
    */                                                                  \
                                                                        \
-  size_t n = limit - base;                                             \
-  if (fwrite(base, 1, n, fmt->fp) < n) return (-1);                    \
+  size_t _n = limit - base;                                            \
+  if (_n && fwrite(base, 1, _n, lyt->fp) < _n) return (-1);            \
 } while (0)
 
 #define PUT_CHAR(ch) do {                                              \
   /* Write CH to the output. Return immediately on error. */           \
                                                                        \
-  if (putc(ch, fmt->fp) == EOF) return (-1);                           \
+  if (putc(ch, lyt->fp) == EOF) return (-1);                           \
 } while (0)
 
 #define PUT_PREFIX do {                                                        \
   /* Output the prefix, if there is one.  Return immediately on error. */ \
                                                                        \
-  if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxlim);                        \
+  if (lyt->prefix) PUT_RANGE(lyt->prefix, lyt->pfxlim);                        \
 } while (0)
 
 #define PUT_SAVED do {                                                 \
   /* Output the saved trailing blank material in the buffer. */                \
                                                                        \
-  size_t n = fmt->w.len;                                               \
-  if (n && fwrite(fmt->w.buf, 1, n, fmt->fp) < n) return (-1);         \
+  size_t _n = lyt->w.len;                                              \
+  if (_n && fwrite(lyt->w.buf, 1, _n, lyt->fp) < _n) return (-1);      \
+} while (0)
+
+#define PUT_CTRL do {                                                  \
+  /* Output the accumulated control string. */                         \
+                                                                       \
+  size_t _n = lyt->ctrl.len;                                           \
+  if (_n && fwrite(lyt->ctrl.buf, 1, _n, lyt->fp) < _n) return (-1);   \
 } while (0)
 
 #define PUT_PFXINB do {                                                        \
@@ -179,69 +241,82 @@ struct format {
    * one.  Return immediately on error.                                        \
    */                                                                  \
                                                                        \
-  if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxtail);               \
+  if (lyt->prefix) PUT_RANGE(lyt->prefix, lyt->pfxtail);               \
 } while (0)
 
 #define SAVE_PFXTAIL do {                                              \
   /* Save the trailing blank portion of the prefix. */                 \
                                                                        \
-  if (fmt->prefix)                                                     \
-    DPUTM(&fmt->w, fmt->pfxtail, fmt->pfxlim - fmt->pfxtail);          \
+  if (lyt->prefix)                                                     \
+    DPUTM(&lyt->w, lyt->pfxtail, lyt->pfxlim - lyt->pfxtail);          \
 } while (0)
 
-/* --- @init_fmt@ --- *
+/* --- @set_layout_prefix@ --- *
  *
- * Arguments:  @struct format *fmt@ = formatting state to initialize
- *             @FILE *fp@ = output file
- *             @const char *prefix@ = prefix string (or null if empty)
+ * Arguments:  @struct layout *lyt@ = layout state
+ *             @const char *prefix@ = new prefix string or null
  *
  * Returns:    ---
  *
- * Use:                Initialize a formatting state.
+ * Use:                Change the configured prefix string.  The change takes effect
+ *             at the start of the next line (or the current line if it's
+ *             empty or only whitespace so far).
  */
 
-static void init_fmt(struct format *fmt, FILE *fp, const char *prefix)
+static void set_layout_prefix(struct layout *lyt, const char *prefix)
 {
   const char *q, *l;
 
-  /* Basics. */
-  fmt->fp = fp;
-  fmt->f = FMTF_NEWL;
-  dstr_create(&fmt->w);
-
-  /* Prefix portions. */
-  if (!prefix || !*prefix)
-    fmt->prefix = fmt->pfxtail = fmt->pfxlim = 0;
+ if (!prefix || !*prefix)
+    lyt->prefix = lyt->pfxtail = lyt->pfxlim = 0;
   else {
-    fmt->prefix = prefix;
-    l = fmt->pfxlim = prefix + strlen(prefix);
-    SPLIT_RANGE(q, prefix, l); fmt->pfxtail = q;
-    DPUTM(&fmt->w, q, l - q);
+    lyt->prefix = prefix;
+    l = lyt->pfxlim = prefix + strlen(prefix);
+    SPLIT_RANGE(q, prefix, l); lyt->pfxtail = q;
   }
 }
 
-/* --- @destroy_fmt@ --- *
+/* --- @init_layout@ --- *
+ *
+ * Arguments:  @struct layout *lyt@ = layout state to initialize
+ *             @FILE *fp@ = output file
+ *             @const char *prefix@ = prefix string (or null if empty)
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a layout state.
+ */
+
+static void init_layout(struct layout *lyt, FILE *fp, const char *prefix)
+{
+  lyt->fp = fp;
+  lyt->f = LYTF_NEWL;
+  dstr_create(&lyt->w); dstr_create(&lyt->ctrl);
+  set_layout_prefix(lyt, prefix);
+}
+
+/* --- @destroy_layout@ --- *
  *
- * Arguments:  @struct format *fmt@ = formatting state
- *             @unsigned f@ = flags (@DFF_...@)
+ * Arguments:  @struct layout *lyt@ = layout state
+ *             @unsigned f@ = flags (@DLF_...@)
  *
  * Returns:    ---
  *
- * Use:                Releases a formatting state and the resources it holds.
- *             Close the file if @DFF_CLOSE@ is set in @f@; otherwise leave
+ * Use:                Releases a layout state and the resources it holds.
+ *             Close the file if @DLF_CLOSE@ is set in @f@; otherwise leave
  *             it open (in case it's @stderr@ or something).
  */
 
-#define DFF_CLOSE 1u
-static void destroy_fmt(struct format *fmt, unsigned f)
+#define DLF_CLOSE 1u
+static void destroy_layout(struct layout *lyt, unsigned f)
 {
-  if (f&DFF_CLOSE) fclose(fmt->fp);
-  dstr_destroy(&fmt->w);
+  if (f&DLF_CLOSE) fclose(lyt->fp);
+  dstr_destroy(&lyt->w); dstr_destroy(&lyt->ctrl);
 }
 
-/* --- @format_char@ --- *
+/* --- @layout_char@ --- *
  *
- * Arguments:  @struct format *fmt@ = formatting state
+ * Arguments:  @struct layout *lyt@ = layout state
  *             @int ch@ = character to write
  *
  * Returns:    Zero on success, @-1@ on failure.
@@ -249,23 +324,23 @@ static void destroy_fmt(struct format *fmt, unsigned f)
  * Use:                Write a single character to the output.
  */
 
-static int format_char(struct format *fmt, int ch)
+static int layout_char(struct layout *lyt, int ch)
 {
   if (ch == '\n') {
-    if (fmt->f&FMTF_NEWL) PUT_PFXINB;
-    PUT_CHAR('\n'); fmt->f |= FMTF_NEWL; DRESET(&fmt->w);
+    if (lyt->f&LYTF_NEWL) PUT_PFXINB;
+    PUT_CHAR('\n'); lyt->f |= LYTF_NEWL; DRESET(&lyt->w);
   } else if (isspace(ch))
-    DPUTC(&fmt->w, ch);
+    DPUTC(&lyt->w, ch);
   else {
-    if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; }
-    PUT_SAVED; PUT_CHAR(ch); DRESET(&fmt->w);
+    if (lyt->f&LYTF_NEWL) { PUT_PFXINB; lyt->f &= ~LYTF_NEWL; }
+    PUT_SAVED; PUT_CHAR(ch); DRESET(&lyt->w);
   }
   return (0);
 }
 
-/* --- @format_string@ --- *
+/* --- @layout_string@ --- *
  *
- * Arguments:  @struct format *fmt@ = formatting state
+ * Arguments:  @struct layout *lyt@ = layout state
  *             @const char *p@ = string to write
  *             @size_t sz@ = length of string
  *
@@ -274,7 +349,7 @@ static int format_char(struct format *fmt, int ch)
  * Use:                Write a string to the output.
  */
 
-static int format_string(struct format *fmt, const char *p, size_t sz)
+static int layout_string(struct layout *lyt, const char *p, size_t sz)
 {
   const char *q, *r, *l = p + sz;
 
@@ -326,7 +401,7 @@ static int format_string(struct format *fmt, const char *p, size_t sz)
    * be omitted.                                                       \
    */                                                                  \
                                                                        \
-  DPUTM(&fmt->w, r, l - r);                                            \
+  DPUTM(&lyt->w, r, l - r);                                            \
 } while (0)
 
   /* Determine the bounds of the first segment.  Handling this is the most
@@ -343,12 +418,15 @@ static int format_string(struct format *fmt, const char *p, size_t sz)
      * need to write that.  Otherwise, there's only blank stuff, which we
      * accumulate in the buffer.
      *
-     * If we're at the start of a line here, then
+     * If we're at the start of a line here, then put the prefix followed by
+     * any saved whitespace, and then our initial nonblank portion.  Then
+     * save our new trailing space.
      */
 
     if (r > p) {
-      if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; }
-      PUT_SAVED; PUT_NONBLANK; DRESET(&fmt->w);
+      if (lyt->f&LYTF_NEWL) { PUT_PREFIX; lyt->f &= ~LYTF_NEWL; }
+      PUT_SAVED; PUT_CTRL; PUT_NONBLANK;
+      DRESET(&lyt->w); DRESET(&lyt->ctrl);
     }
     SAVE_TAIL;
     return (0);
@@ -357,9 +435,13 @@ static int format_string(struct format *fmt, const char *p, size_t sz)
   /* There is at least one more segment, so we know that there'll be a line
    * to output.
    */
-  if (fmt->f&FMTF_NEWL) PUT_PFXINB;
-  if (r > p) { PUT_SAVED; PUT_NONBLANK; }
-  PUT_NEWLINE; DRESET(&fmt->w);
+  if (r > p) {
+    if (lyt->f&LYTF_NEWL) PUT_PREFIX;
+    PUT_SAVED; PUT_CTRL; PUT_NONBLANK;
+    DRESET(&lyt->ctrl);
+  } else if (lyt->f&LYTF_NEWL)
+    PUT_PFXINB;
+  PUT_NEWLINE; DRESET(&lyt->w);
   SPLIT_SEGMENT;
 
   /* Main loop over whole segments with trailing newlines.  For each one, we
@@ -367,7 +449,9 @@ static int format_string(struct format *fmt, const char *p, size_t sz)
    * newline, so we write the initial prefix and drop the trailing blanks.
    */
   while (q) {
-    PUT_PREFIX; PUT_NONBLANK; PUT_NEWLINE;
+    if (r > p) { PUT_PREFIX; PUT_CTRL; PUT_NONBLANK; DRESET(&lyt->ctrl); }
+    else PUT_PFXINB;
+    PUT_NEWLINE;
     SPLIT_SEGMENT;
   }
 
@@ -376,8 +460,8 @@ static int format_string(struct format *fmt, const char *p, size_t sz)
    * the blank stuff (including the trailing blanks of the prefix) and leave
    * the newline flag set.
    */
-  if (r > p) { PUT_PREFIX; PUT_NONBLANK; fmt->f &= ~FMTF_NEWL; }
-  else { fmt->f |= FMTF_NEWL; SAVE_PFXTAIL; }
+  if (r > p) { PUT_PREFIX; PUT_NONBLANK; lyt->f &= ~LYTF_NEWL; }
+  else { lyt->f |= LYTF_NEWL; SAVE_PFXTAIL; }
   SAVE_TAIL;
 
 #undef SPLIT_SEGMENT
@@ -396,120 +480,140 @@ static int format_string(struct format *fmt, const char *p, size_t sz)
 #undef PUT_CHAR
 #undef SAVE_PFXTAIL
 
-/*----- Skeleton ----------------------------------------------------------*/
-/*
-static void ..._bsession(struct tvec_output *o, struct tvec_state *tv)
-static int ..._esession(struct tvec_output *o)
-static void ..._bgroup(struct tvec_output *o)
-static void ..._skipgroup(struct tvec_output *o,
-                         const char *excuse, va_list *ap)
-static void ..._egroup(struct tvec_output *o)
-static void ..._btest(struct tvec_output *o)
-static void ..._skip(struct tvec_output *o, const char *excuse, va_list *ap)
-static void ..._fail(struct tvec_output *o, const char *detail, va_list *ap)
-static void ..._dumpreg(struct tvec_output *o, unsigned disp,
-                       union tvec_regval *rv, const struct tvec_regdef *rd)
-static void ..._etest(struct tvec_output *o, unsigned outcome)
-static void ..._bbench(struct tvec_output *o,
-                      const char *ident, unsigned unit)
-static void ..._ebench(struct tvec_output *o,
-                      const char *ident, unsigned unit,
-                      const struct tvec_timing *t)
-static void ..._error(struct tvec_output *o, const char *msg, va_list *ap)
-static void ..._notice(struct tvec_output *o, const char *msg, va_list *ap)
-static void ..._destroy(struct tvec_output *o)
-
-static const struct tvec_outops ..._ops = {
-  ..._bsession, ..._esession,
-  ..._bgroup, ..._egroup, ..._skip,
-  ..._btest, ..._skip, ..._fail, ..._dumpreg, ..._etest,
-  ..._bbench, ..._ebench,
-  ..._error, ..._notice,
-  ..._destroy
-};
-*/
 /*----- Human-readable output ---------------------------------------------*/
 
-#define HAF_FGMASK 0x0f
-#define HAF_FGSHIFT 0
-#define HAF_BGMASK 0xf0
-#define HAF_BGSHIFT 4
-#define HAF_FG 256u
-#define HAF_BG 512u
-#define HAF_BOLD 1024u
-#define HCOL_BLACK 0u
-#define HCOL_RED 1u
-#define HCOL_GREEN 2u
-#define HCOL_YELLOW 3u
-#define HCOL_BLUE 4u
-#define HCOL_MAGENTA 5u
-#define HCOL_CYAN 6u
-#define HCOL_WHITE 7u
-#define HCF_BRIGHT 8u
-#define HFG(col) (HAF_FG | (HCOL_##col) << HAF_FGSHIFT)
-#define HBG(col) (HAF_BG | (HCOL_##col) << HAF_BGSHIFT)
-
-#define HA_WIN (HFG(GREEN))
-#define HA_LOSE (HFG(RED) | HAF_BOLD)
-#define HA_SKIP (HFG(YELLOW))
-#define HA_ERR (HFG(MAGENTA) | HAF_BOLD)
+#define GENATTR(want, attrs, fgspc, fgcol, bgspc, bgcol)               \
+  { (want), (want),                                                    \
+    { ((fgspc) << TTAF_FGSPCSHIFT) |                                   \
+      ((bgspc) << TTAF_BGSPCSHIFT) | (attrs),                          \
+      0, (fgcol), (bgcol) } }
+
+#define FGBG(want, attrs, fgcol, bgcol)                                        \
+  GENATTR(want | TTACF_FG | TTACF_BG, attrs,                           \
+         TTCSPC_1BPCBR, TTCOL_##fgcol, TTCSPC_1BPCBR, TTCOL_##bgcol)
+#define FG(want, attrs, fgcol)                                         \
+  GENATTR(want | TTACF_FG, attrs,                                      \
+         TTCSPC_1BPCBR, TTCOL_##fgcol, TTCSPC_NONE, 0)
+#define BG(want, attrs, bgcol)                                         \
+  GENATTR(want | TTACF_BG, attrs,                                      \
+         TTCSPC_NONE, 0, TTCSPC_1BPCBR, TTCOL_##bgcol)
+#define ATTR(want, attrs)                                              \
+  GENATTR(want, attrs, TTCSPC_NONE, 0, TTCSPC_NONE, 0)
+
+#define BOLD (TTWT_BOLD << TTAF_WTSHIFT)
+
+static const struct tty_attrlist
+  loc_attrs[] = { FG(0, 0, CYN), TTY_ATTRLIST_CLEAR },
+  locsep_attrs[] = { FG(0, 0, BRBLU), TTY_ATTRLIST_CLEAR },
+  note_attrs[] = { FG(0, 0, YLW), TTY_ATTRLIST_CLEAR },
+  err_attrs[] = { FG(0, BOLD, MGN), TTY_ATTRLIST_CLEAR },
+  unklv_attrs[] = { FGBG(0, BOLD, BRWHT, BRRED), ATTR(0, TTAF_INVV) },
+  vfound_attrs[] = { FG(0, 0, RED), TTY_ATTRLIST_CLEAR },
+  vexpect_attrs[] = { FG(0, 0, GRN), TTY_ATTRLIST_CLEAR },
+  vunset_attrs[] = { FG(0, 0, YLW), TTY_ATTRLIST_CLEAR },
+  lose_attrs[] = { FG(0, BOLD, RED), ATTR(0, BOLD | TTAF_INVV) },
+  skip_attrs[] = { FG(0, 0, YLW), TTY_ATTRLIST_CLEAR },
+  xfail_attrs[] = { FG(0, BOLD, BLU), ATTR(0, BOLD) },
+  win_attrs[] = { FG(0, 0, GRN), TTY_ATTRLIST_CLEAR },
+  sblose_attrs[] = { FG(0, BOLD, RED), ATTR(0, BOLD) };
+
+#undef GENATTR
+#undef FGBG
+#undef FG
+#undef BG
+#undef ATTR
+#undef BOLD
+
+/* Predefined attributes. */
+#define HIGHLIGHTS(_st, _)                                             \
+  _(_st, LOCFN,           "lf", loc_attrs)     /* location filename */         \
+  _(_st, LOCLN,           "ln", loc_attrs)     /* location line number */      \
+  _(_st, LOCSEP,   "ls", locsep_attrs) /* location separator `:' */    \
+  _(_st, INFO,    "mi", 0)             /* information */               \
+  _(_st, NOTE,    "mn", note_attrs)    /* notices */                   \
+  _(_st, ERR,     "me", err_attrs)     /* error messages */            \
+  _(_st, UNKLV,           "mu", unklv_attrs)   /* unknown-level messages */    \
+  _(_st, DSINPUT,  "di", 0)            /* disposition for input value */ \
+  _(_st, DSOUTPUT, "do", 0)            /* ... unsolicited output */    \
+  _(_st, DSMATCH,  "dm", 0)            /* ... matching output */       \
+  _(_st, DSFOUND,  "df", 0)            /* ... incorrect output */      \
+  _(_st, DSEXPECT, "dx", 0)            /* ... reference output */      \
+  _(_st, RNINPUT,  "ri", 0)            /* register name for input value */ \
+  _(_st, RNOUTPUT, "ro", 0)            /* ... unsolicited output */    \
+  _(_st, RNMATCH,  "rm", 0)            /* ... matching output */       \
+  _(_st, RNFOUND,  "rf", 0)            /* ... incorrect output */      \
+  _(_st, RNEXPECT, "rx", 0)            /* ... reference output */      \
+  _(_st, VINPUT,   "vi", 0)            /* input value */               \
+  _(_st, VOUTPUT,  "vo", 0)            /* unsolicited output value */  \
+  _(_st, VMATCH,   "vm", 0)            /* matching output value */     \
+  _(_st, VFOUND,   "vf", vfound_attrs) /* incorrect output value */    \
+  _(_st, VEXPECT,  "vx", vexpect_attrs)        /* reference output value */    \
+  _(_st, VUNSET,   "vu", vunset_attrs) /* register not set */          \
+  _(_st, LOSE,    "ol", lose_attrs)    /* report failure */            \
+  _(_st, SKIP,    "os", skip_attrs)    /* report a skipped test/group */ \
+  _(_st, XFAIL,           "ox", xfail_attrs)   /* report expected fail */      \
+  _(_st, WIN,     "ow", win_attrs)     /* report success */            \
+  _(_st, SBLOSE,   "sl", sblose_attrs) /* scoreboard failure */        \
+  _(_st, SBSKIP,   "ss", skip_attrs)   /* scoreboard skipped test */   \
+  _(_st, SBXFAIL,  "sx", xfail_attrs)  /* scoreboard xfail */          \
+  _(_st, SBWIN,           "sw", 0)             /* scoreboard success */
+
+TTYCOLOUR_DEFENUM(HIGHLIGHTS, HL_);
+#define HL_PLAIN (-1)
+
+/* Scoreboard indicators. */
+static const char scoreboard[] = { 'x', '_', 'o', '.' };
 
 struct human_output {
-  struct tvec_output _o;
-  struct tvec_state *tv;
-  struct format fmt;
-  char *outbuf; size_t outsz;
-  dstr scoreboard;
-  unsigned attr;
-  int maxlen;
-  unsigned f;
-#define HOF_TTY 1u
-#define HOF_DUPERR 2u
-#define HOF_COLOUR 4u
-#define HOF_PROGRESS 8u
+  struct tvec_output _o;               /* output base class */
+  struct tvec_state *tv;               /* stashed testing state */
+  arena *a;                            /* arena for memory allocation */
+  struct tty *tty;                     /* output terminal, or null */
+  struct layout lyt;                   /* output layout */
+  char *outbuf; size_t outsz;          /* buffer for formatted output */
+  dstr scoreboard;                     /* history of test group results */
+  struct tty_attr attr[HL__LIMIT];     /* highlight attribute map */
+  int maxlen;                          /* longest register name */
+  unsigned f;                          /* flags */
+                                       /*   bits 0--7 from @TVHF_...@ */
+#define HOF_DUPERR 0x0100u             /*   duplicate errors to stderr */
+#define HOF_PROGRESS 0x0200u           /*   progress display is active */
 };
 
-static void set_colour(FILE *fp, int *sep_inout,
-                      const char *norm, const char *bright,
-                      unsigned colour)
-{
-  if (*sep_inout) putc(*sep_inout, fp);
-  fprintf(fp, "%s%d", colour&HCF_BRIGHT ? bright : norm, colour&7);
-  *sep_inout = ';';
-}
+/* --- @setattr@, @setattr_layout@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @int hi@ = highlight code to set
+ *
+ * Returns:    ---
+ *
+ * Use:                Send a control sequence to the output stream so that
+ *             subsequent text is printed with the given attributes.
+ */
 
-static void setattr(struct human_output *h, unsigned attr)
+static void setattr_common(struct human_output *h,
+                          const struct gprintf_ops *gops, void *go, int hl)
 {
-  unsigned diff = h->attr ^ attr;
-  int sep = 0;
-
-  if (!diff || !(h->f&HOF_COLOUR)) return;
-  fputs("\x1b[", h->fmt.fp);
+  if (h->f&TVHF_COLOUR)
+    tty_setattrg(h->tty, gops, go, hl >= 0 ? &h->attr[hl] : 0);
+}
 
-  if (diff&HAF_BOLD) {
-    if (attr&HAF_BOLD) putc('1', h->fmt.fp);
-    else { putc('0', h->fmt.fp); diff = h->attr; }
-    sep = ';';
-  }
-  if (diff&(HAF_FG | HAF_FGMASK)) {
-    if (attr&HAF_FG)
-      set_colour(h->fmt.fp, &sep, "3", "9",
-                (attr&HAF_FGMASK) >> HAF_FGSHIFT);
-    else
-      { if (sep) putc(sep, h->fmt.fp); fputs("39", h->fmt.fp); sep = ';'; }
-  }
-  if (diff&(HAF_BG | HAF_BGMASK)) {
-    if (attr&HAF_BG)
-      set_colour(h->fmt.fp, &sep, "4", "10",
-                (attr&HAF_BGMASK) >> HAF_BGSHIFT);
-    else
-      { if (sep) putc(sep, h->fmt.fp); fputs("49", h->fmt.fp); sep = ';'; }
-  }
+static void setattr(struct human_output *h, int hl)
+  { setattr_common(h, &file_printops, h->lyt.fp, hl); }
 
-  putc('m', h->fmt.fp); h->attr = attr;
+static void setattr_layout(struct human_output *h, int hl)
+  { setattr_common(h, &dstr_printops, &h->lyt.ctrl, hl); }
 
-#undef f_any
-}
+/* --- @clear_progress@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *
+ * Returns:    ---
+ *
+ * Use:                Remove the progress display from the terminal.
+ *
+ *             If the progress display isn't active then do nothing.
+ */
 
 static void clear_progress(struct human_output *h)
 {
@@ -517,68 +621,78 @@ static void clear_progress(struct human_output *h)
 
   if (h->f&HOF_PROGRESS) {
     n = strlen(h->tv->test->name) + 2 + h->scoreboard.len;
-    for (i = 0; i < n; i++) fputs("\b \b", h->fmt.fp);
+    for (i = 0; i < n; i++) fputs("\b \b", h->lyt.fp);
     h->f &= ~HOF_PROGRESS;
   }
 }
 
+/* --- @write_scoreboard_char@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @int ch@ = scoreboard character to print
+ *
+ * Returns:    ---
+ *
+ * Use:                Write a scoreboard character, indicating the outcome of a
+ *             test, to the output stream, with appropriate highlighting.
+ */
+
 static void write_scoreboard_char(struct human_output *h, int ch)
 {
-  switch (ch) {
-    case 'x': setattr(h, HA_LOSE); break;
-    case '_': setattr(h, HA_SKIP); break;
-    default: setattr(h, 0); break;
-  }
-  putc(ch, h->fmt.fp); setattr(h, 0);
+  assert(0 <= ch && ch < TVOUT_LIMIT);
+  setattr(h, HL_SBLOSE + ch); putc(scoreboard[ch], h->lyt.fp);
 }
 
+/* --- @show_progress@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *
+ * Returns:    ---
+ *
+ * Use:                Show the progress display, with the record of outcomes for
+ *             the current test group.
+ *
+ *             If the progress display is already active, or the output
+ *             stream is not interactive, then nothing happens.
+ */
+
 static void show_progress(struct human_output *h)
 {
   struct tvec_state *tv = h->tv;
   const char *p, *l;
 
-  if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
-    fprintf(h->fmt.fp, "%s: ", tv->test->name);
-    if (!(h->f&HOF_COLOUR))
-      dstr_write(&h->scoreboard, h->fmt.fp);
-    else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
+  if (tv->test && (h->f&TVHF_TTY) && !(h->f&HOF_PROGRESS)) {
+    fprintf(h->lyt.fp, "%s: ", tv->test->name);
+    for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
       write_scoreboard_char(h, *p);
-    fflush(h->fmt.fp); h->f |= HOF_PROGRESS;
+    setattr(h, HL_PLAIN);
+    fflush(h->lyt.fp); h->f |= HOF_PROGRESS;
   }
 }
 
-static void report_location(struct human_output *h, FILE *fp,
-                           const char *file, unsigned lno)
-{
-  unsigned f = 0;
-#define f_flush 1u
-
-#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
-
-  if (fp != h->fmt.fp) f |= f_flush;
-
-  if (file) {
-    setattr(h, HFG(CYAN));     FLUSH(h->fmt.fp);
-    fputs(file, fp);           FLUSH(fp);
-    setattr(h, HFG(BLUE));     FLUSH(h->fmt.fp);
-    fputc(':', fp);            FLUSH(fp);
-    setattr(h, HFG(CYAN));     FLUSH(h->fmt.fp);
-    fprintf(fp, "%u", lno);    FLUSH(fp);
-    setattr(h, HFG(BLUE));     FLUSH(h->fmt.fp);
-    fputc(':', fp);            FLUSH(fp);
-    setattr(h, 0);             FLUSH(h->fmt.fp);
-    fputc(' ', fp);
-  }
-
-#undef f_flush
-#undef FLUSH
-}
+/* --- @human_writech@, @human_write@, @human_writef@ --- *
+ *
+ * Arguments:  @void *go@ = output sink, secretly a @struct human_output@
+ *             @int ch@ = character to write
+ *             @const char *@p@, @size_t sz@ = string (with explicit length)
+ *                     to write
+ *             @const char *p, ...@ = format control string and arguments to
+ *                     write
+ *
+ * Returns:    ---
+ *
+ * Use:                Write characters, strings, or formatted strings to the
+ *             output, applying appropriate layout.
+ *
+ *             For the human output driver, the layout machinery just strips
+ *             trailing spaces.
+ */
 
 static int human_writech(void *go, int ch)
-  { struct human_output *h = go; return (format_char(&h->fmt, ch)); }
+  { struct human_output *h = go; return (layout_char(&h->lyt, ch)); }
 
 static int human_writem(void *go, const char *p, size_t sz)
-  { struct human_output *h = go; return (format_string(&h->fmt, p, sz)); }
+  { struct human_output *h = go; return (layout_string(&h->lyt, p, sz)); }
 
 static int human_nwritef(void *go, size_t maxsz, const char *p, ...)
 {
@@ -587,64 +701,136 @@ static int human_nwritef(void *go, size_t maxsz, const char *p, ...)
   va_list ap;
 
   va_start(ap, p);
-  n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap);
+  n = gprintf_memputf(h->a, &h->outbuf, &h->outsz, maxsz, p, ap);
   va_end(ap);
-  return (format_string(&h->fmt, h->outbuf, n));
+  if (layout_string(&h->lyt, h->outbuf, n)) return (-1);
+  return (n);
 }
 
 static const struct gprintf_ops human_printops =
   { human_writech, human_writem, human_nwritef };
 
+/* --- @human_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The human driver just records the test state for later
+ *             reference.
+ */
+
 static void human_bsession(struct tvec_output *o, struct tvec_state *tv)
   { struct human_output *h = (struct human_output *)o; h->tv = tv; }
 
-static void report_skipped(struct human_output *h, unsigned n)
+/* --- @human_report_unusual@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output sink
+ *             @unsigned nxfail, nskip@ = number of expected failures and
+ *                     skipped tests
+ *
+ * Returns:    ---
+ *
+ * Use:                Write (directly on the output stream) a note about expected
+ *             failures and/or skipped tests, if there were any.
+ */
+
+static void human_report_unusual(struct human_output *h,
+                                unsigned nxfail, unsigned nskip)
 {
-  if (n) {
-    fprintf(h->fmt.fp, " (%u ", n);
-    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
-    fputc(')', h->fmt.fp);
+  unsigned f = 0;
+#define f_any 1u
+
+  if (nxfail) {
+    fprintf(h->lyt.fp, "%s%u ", f&f_any ? ", " : " (", nxfail);
+    setattr(h, HL_XFAIL);
+    fprintf(h->lyt.fp, "expected %s", nxfail == 1 ? "failure" : "failures");
+    setattr(h, HL_PLAIN);
+    f |= f_any;
+  }
+
+  if (nskip) {
+    fprintf(h->lyt.fp, "%s%u ", f&f_any ? ", " : " (", nskip);
+    setattr(h, HL_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HL_PLAIN);
+    f |= f_any;
   }
+
+  if (f&f_any) fputc(')', h->lyt.fp);
+
+#undef f_any
 }
 
+/* --- @human_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The human driver prints a final summary of the rest results
+ *             and returns a suitable exit code.
+ */
+
 static int human_esession(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
   struct tvec_state *tv = h->tv;
   unsigned
     all_win = tv->all[TVOUT_WIN], grps_win = tv->grps[TVOUT_WIN],
+    all_xfail = tv->all[TVOUT_XFAIL],
     all_lose = tv->all[TVOUT_LOSE], grps_lose = tv->grps[TVOUT_LOSE],
     all_skip = tv->all[TVOUT_SKIP], grps_skip = tv->grps[TVOUT_SKIP],
-    all_run = all_win + all_lose, grps_run = grps_win + grps_lose;
+    all_pass = all_win + all_xfail, all_run = all_pass + all_lose,
+    grps_run = grps_win + grps_lose;
 
   if (!all_lose) {
-    setattr(h, HA_WIN); fputs("PASSED", h->fmt.fp); setattr(h, 0);
-    fprintf(h->fmt.fp, " %s%u %s",
+    setattr(h, HL_WIN); fputs("PASSED", h->lyt.fp); setattr(h, HL_PLAIN);
+    fprintf(h->lyt.fp, " %s%u %s",
            !(all_skip || grps_skip) ? "all " : "",
-           all_win, all_win == 1 ? "test" : "tests");
-    report_skipped(h, all_skip);
-    fprintf(h->fmt.fp, " in %u %s",
+           all_pass, all_pass == 1 ? "test" : "tests");
+    human_report_unusual(h, all_xfail, all_skip);
+    fprintf(h->lyt.fp, " in %u %s",
            grps_win, grps_win == 1 ? "group" : "groups");
-    report_skipped(h, grps_skip);
+    human_report_unusual(h, 0, grps_skip);
   } else {
-    setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
-    fprintf(h->fmt.fp, " %u out of %u %s",
+    setattr(h, HL_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HL_PLAIN);
+    fprintf(h->lyt.fp, " %u out of %u %s",
            all_lose, all_run, all_run == 1 ? "test" : "tests");
-    report_skipped(h, all_skip);
-    fprintf(h->fmt.fp, " in %u out of %u %s",
+    human_report_unusual(h, all_xfail, all_skip);
+    fprintf(h->lyt.fp, " in %u out of %u %s",
            grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
-    report_skipped(h, grps_skip);
+    human_report_unusual(h, 0, grps_skip);
   }
-  fputc('\n', h->fmt.fp);
+  fputc('\n', h->lyt.fp);
 
   if (tv->f&TVSF_ERROR) {
-    setattr(h, HA_ERR); fputs("ERRORS", h->fmt.fp); setattr(h, 0);
-    fputs(" found in input; tests may not have run correctly\n", h->fmt.fp);
+    setattr(h, HL_ERR); fputs("ERRORS", h->lyt.fp); setattr(h, HL_PLAIN);
+    fputs(" found in input; tests may not have run correctly\n", h->lyt.fp);
   }
 
-  h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0);
+  h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : all_lose ? 1 : 0);
 }
 
+/* --- @human_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The human driver determines the length of the longest
+ *             register name, resets the group progress scoreboard, and
+ *             activates the progress display.
+ */
+
 static void human_bgroup(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
@@ -653,74 +839,173 @@ static void human_bgroup(struct tvec_output *o)
   dstr_reset(&h->scoreboard); show_progress(h);
 }
 
+/* --- @human_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The human driver just reports the situation to its output
+ *             stream.
+ */
+
 static void human_skipgroup(struct tvec_output *o,
                            const char *excuse, va_list *ap)
 {
   struct human_output *h = (struct human_output *)o;
 
-  if (!(~h->f&(HOF_TTY | HOF_PROGRESS))) {
-    h->f &= ~HOF_PROGRESS;
-    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
-  } else {
-    fprintf(h->fmt.fp, "%s: ", h->tv->test->name);
-    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
+  if (!(h->f&TVHF_TTY))
+    fprintf(h->lyt.fp, "%s ", h->tv->test->name);
+  else {
+    show_progress(h); h->f &= ~HOF_PROGRESS;
+    if (h->scoreboard.len) putc(' ', h->lyt.fp);
   }
-  if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
-  fputc('\n', h->fmt.fp);
+  setattr(h, HL_SKIP); fputs("skipped", h->lyt.fp); setattr(h, HL_PLAIN);
+  if (excuse) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, excuse, *ap); }
+  fputc('\n', h->lyt.fp);
 }
 
+/* --- @human_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The human driver reports a summary of the group's tests.
+ */
+
 static void human_egroup(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
   struct tvec_state *tv = h->tv;
-  unsigned win = tv->curr[TVOUT_WIN], lose = tv->curr[TVOUT_LOSE],
-    skip = tv->curr[TVOUT_SKIP], run = win + lose;
+  unsigned win = tv->curr[TVOUT_WIN], xfail = tv->curr[TVOUT_XFAIL],
+    lose = tv->curr[TVOUT_LOSE], skip = tv->curr[TVOUT_SKIP],
+    run = win + lose + xfail;
 
-  if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS;
-  else fprintf(h->fmt.fp, "%s:", h->tv->test->name);
+  if (h->f&TVHF_TTY) h->f &= ~HOF_PROGRESS;
+  else fprintf(h->lyt.fp, "%s:", h->tv->test->name);
 
   if (lose) {
-    fprintf(h->fmt.fp, " %u/%u ", lose, run);
-    setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
-    report_skipped(h, skip);
+    fprintf(h->lyt.fp, " %u/%u ", lose, run);
+    setattr(h, HL_LOSE); fputs("FAILED", h->lyt.fp); setattr(h, HL_PLAIN);
+    human_report_unusual(h, xfail, skip);
   } else {
-    fputc(' ', h->fmt.fp); setattr(h, HA_WIN);
-    fputs("ok", h->fmt.fp); setattr(h, 0);
-    report_skipped(h, skip);
+    fputc(' ', h->lyt.fp); setattr(h, HL_WIN);
+    fputs("ok", h->lyt.fp); setattr(h, HL_PLAIN);
+    human_report_unusual(h, xfail, skip);
   }
-  fputc('\n', h->fmt.fp);
+  fputc('\n', h->lyt.fp);
 }
 
+/* --- @human_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The human driver makes sure the progress display is active.
+ */
+
 static void human_btest(struct tvec_output *o)
   { struct human_output *h = (struct human_output *)o; show_progress(h); }
 
-static void human_skip(struct tvec_output *o,
-                      const char *excuse, va_list *ap)
+/* --- @human_report_location@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @FILE *fp@ = stream to write the location on
+ *             @const char *file@ = filename
+ *             @unsigned lno@ = line number
+ *
+ * Returns:    ---
+ *
+ * Use:                Print the filename and line number to the output stream @fp@.
+ *             Also, if appropriate, print interleaved highlighting control
+ *             codes to our usual output stream.  If @file@ is null then do
+ *             nothing.
+ */
+
+static void human_report_location(struct human_output *h, FILE *fp,
+                                 const char *file, unsigned lno)
+{
+  if (!file)
+    /* nothing to do */;
+  else if (fp != h->lyt.fp || !(h->f&TVHF_COLOUR))
+    fprintf(fp, "%s:%u: ", file, lno);
+  else {
+    setattr(h, HL_LOCFN);      fputs(file, fp);
+    setattr(h, HL_LOCSEP);     fputc(':', fp);
+    setattr(h, HL_LOCLN);      fprintf(fp, "%u", lno);
+    setattr(h, HL_LOCSEP);     fputc(':', fp);
+    setattr(h, HL_PLAIN);      fputc(' ', fp);
+  }
+}
+
+/* --- @human_outcome@, @human_skip@, @human_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @unsigned attr@ = attribute to apply to the outcome
+ *             @const char *outcome@ = outcome string to report
+ *             @const char *detail@, @va_list *ap@ = a detail message
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     test
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has been skipped or failed.
+ *
+ *             The human driver reports the situation on its output stream.
+ */
+
+static void human_outcome(struct tvec_output *o,
+                         unsigned attr, const char *outcome,
+                         const char *detail, va_list *ap)
 {
   struct human_output *h = (struct human_output *)o;
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  report_location(h, h->fmt.fp, tv->infile, tv->test_lno);
-  fprintf(h->fmt.fp, "`%s' ", tv->test->name);
-  setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
-  if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
-  fputc('\n', h->fmt.fp);
+  human_report_location(h, h->lyt.fp, tv->infile, tv->test_lno);
+  fprintf(h->lyt.fp, "`%s' ", tv->test->name);
+  setattr(h, attr); fputs(outcome, h->lyt.fp); setattr(h, HL_PLAIN);
+  if (detail) { fputs(": ", h->lyt.fp); vfprintf(h->lyt.fp, detail, *ap); }
+  fputc('\n', h->lyt.fp);
 }
 
+static void human_skip(struct tvec_output *o,
+                      const char *excuse, va_list *ap)
+  { human_outcome(o, HL_SKIP, "skipped", excuse, ap); }
 static void human_fail(struct tvec_output *o,
                       const char *detail, va_list *ap)
-{
-  struct human_output *h = (struct human_output *)o;
-  struct tvec_state *tv = h->tv;
+  { human_outcome(o, HL_LOSE, "FAILED", detail, ap); }
 
-  clear_progress(h);
-  report_location(h, h->fmt.fp, tv->infile, tv->test_lno);
-  fprintf(h->fmt.fp, "`%s' ", tv->test->name);
-  setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
-  if (detail) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, detail, *ap); }
-  fputc('\n', h->fmt.fp);
-}
+/* --- @human_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The human driver applies highlighting to mismatching output
+ *             registers, but otherwise delegates to the register type
+ *             handler and the layout machinery.
+ */
 
 static void human_dumpreg(struct tvec_output *o,
                          unsigned disp, const union tvec_regval *rv,
@@ -730,345 +1015,1706 @@ static void human_dumpreg(struct tvec_output *o,
   const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
 
   clear_progress(h);
-  gprintf(&human_printops, h, "%*s%s %s = ",
-         10 + h->maxlen - n, "", ds, rd->name);
-  if (h->f&HOF_COLOUR) {
-    if (!rv) setattr(h, HFG(YELLOW));
-    else if (disp == TVRD_FOUND) setattr(h, HFG(RED));
-    else if (disp == TVRD_EXPECT) setattr(h, HFG(GREEN));
+  gprintf(&human_printops, h, "%*s", 10 + h->maxlen - n, "");
+  setattr_layout(h, HL_DSINPUT + disp);
+  gprintf(&human_printops, h, "%s", ds);
+  setattr(h, HL_PLAIN); layout_char(&h->lyt, ' ');
+  setattr_layout(h, HL_RNINPUT + disp);
+  gprintf(&human_printops, h, "%s", rd->name);
+  setattr(h, HL_PLAIN); gprintf(&human_printops, h, " = ");
+  if (!rv) {
+    setattr_layout(h, HL_VUNSET);
+    gprintf(&human_printops, h, "#unset");
+  } else {
+    setattr_layout(h, HL_VINPUT + disp);
+    rd->ty->dump(rv, rd, 0, &human_printops, h);
   }
-  if (!rv) gprintf(&human_printops, h, "#unset");
-  else rd->ty->dump(rv, rd, 0, &human_printops, h);
-  setattr(h, 0); format_char(&h->fmt, '\n');
+  setattr(h, HL_PLAIN); layout_char(&h->lyt, '\n');
 }
 
+/* --- @human_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The human driver reactivates the progress display, if
+ *             necessary, and adds a new character for the completed test.
+ */
+
 static void human_etest(struct tvec_output *o, unsigned outcome)
 {
   struct human_output *h = (struct human_output *)o;
-  int ch;
 
-  if (h->f&HOF_TTY) {
+  if (h->f&TVHF_TTY) {
     show_progress(h);
-    switch (outcome) {
-      case TVOUT_WIN: ch = '.'; break;
-      case TVOUT_LOSE: ch = 'x'; break;
-      case TVOUT_SKIP: ch = '_'; break;
-      default: abort();
-    }
-    dstr_putc(&h->scoreboard, ch);
-    write_scoreboard_char(h, ch); fflush(h->fmt.fp);
+    dstr_putc(&h->scoreboard, outcome); write_scoreboard_char(h, outcome);
+    setattr(h, HL_PLAIN); fflush(h->lyt.fp);
   }
 }
 
-static void human_bbench(struct tvec_output *o,
-                        const char *ident, unsigned unit)
-{
-  struct human_output *h = (struct human_output *)o;
-  struct tvec_state *tv = h->tv;
-
-  clear_progress(h);
-  fprintf(h->fmt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->fmt.fp);
-}
-
-static void human_ebench(struct tvec_output *o,
-                        const char *ident, unsigned unit,
-                        const struct bench_timing *tm)
-{
-  struct human_output *h = (struct human_output *)o;
-
-  tvec_benchreport(&human_printops, h->fmt.fp, unit, tm);
-  fputc('\n', h->fmt.fp);
-}
+/* --- @human_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @unsigned level@ = message level (@TVLV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             The human driver arranges to show the message on @stderr@ as
+ *             well as the usual output, with a certain amount of
+ *             intelligence in case they're both actually the same device.
+ */
 
-static void human_report(struct tvec_output *o, const char *msg, va_list *ap)
+static void human_report(struct tvec_output *o, unsigned level,
+                        const char *msg, va_list *ap)
 {
   struct human_output *h = (struct human_output *)o;
   struct tvec_state *tv = h->tv;
+  const char *levstr; unsigned levhl;
   dstr d = DSTR_INIT;
+  unsigned f = 0;
+#define f_progress 1u
 
   dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n');
 
-  clear_progress(h); fflush(h->fmt.fp);
-  fprintf(stderr, "%s: ", QUIS);
-  report_location(h, stderr, tv->infile, tv->lno);
-  fwrite(d.buf, 1, d.len, stderr);
+  switch (level) {
+#define CASE(tag, name, val)                                           \
+    case TVLV_##tag: levstr = name; levhl = HL_##tag; break;
+    TVEC_LEVELS(CASE)
+    default: levstr = "??"; levhl = HL_UNKLV; break;
+  }
+
+  if (h->f&HOF_PROGRESS) { clear_progress(h); f |= f_progress; }
 
   if (h->f&HOF_DUPERR) {
-    report_location(h, h->fmt.fp, tv->infile, tv->lno);
-    fwrite(d.buf, 1, d.len, h->fmt.fp);
+    fprintf(stderr, "%s: ", QUIS);
+    human_report_location(h, stderr, tv->infile, tv->lno);
+    fprintf(stderr, "%s: ", levstr);
+    fwrite(d.buf, 1, d.len, stderr);
   }
-  show_progress(h);
+
+  human_report_location(h, h->lyt.fp, tv->infile, tv->lno);
+  setattr(h, levhl); fputs(levstr, h->lyt.fp);
+  setattr(h, HL_PLAIN); fputs(": ", h->lyt.fp);
+  fwrite(d.buf, 1, d.len, h->lyt.fp);
+
+  if (f&f_progress) show_progress(h);
+  dstr_destroy(&d);
+
+#undef f_progress
 }
 
-static void human_destroy(struct tvec_output *o)
+/* --- @human_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The human driver just prints the start of the benchmark
+ *             report.
+ */
+
+static void human_bbench(struct tvec_output *o,
+                        const char *desc, unsigned unit)
 {
   struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->tv;
 
-  destroy_fmt(&h->fmt, h->f&HOF_DUPERR ? DFF_CLOSE : 0);
-  dstr_destroy(&h->scoreboard);
-  xfree(h->outbuf); xfree(h);
+  clear_progress(h);
+  gprintf(&human_printops, h, "%s ", tv->test->name);
+  if (desc) gprintf(&human_printops, h, "%s", desc);
+  else print_ident(tv, TVSF_COMPACT, &human_printops, h);
+  gprintf(&human_printops, h, ": ");
+  if (h->f&TVHF_TTY) fflush(h->lyt.fp);
 }
 
-static const struct tvec_outops human_ops = {
-  human_bsession, human_esession,
-  human_bgroup, human_skipgroup, human_egroup,
-  human_btest, human_skip, human_fail, human_dumpreg, human_etest,
-  human_bbench, human_ebench,
-  human_report, human_report,
-  human_destroy
-};
+/* --- @human_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *             @const struct bench_timing *t@ = measurement
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a benchmark's results.
+ *
+ *             The human driver just delegates to the default benchmark
+ *             reporting, via the layout machinery.
+ */
 
-struct tvec_output *tvec_humanoutput(FILE *fp)
+static void human_ebench(struct tvec_output *o,
+                        const char *desc, unsigned unit,
+                        const struct bench_timing *t)
 {
-  struct human_output *h;
-  const char *p;
+  struct human_output *h = (struct human_output *)o;
 
-  h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops;
-  h->f = 0; h->attr = 0;
+  tvec_benchreport(&human_printops, h, unit, 0, t);
+  layout_char(&h->lyt, '\n');
+}
+
+static const struct tvec_benchoutops human_benchops =
+  { human_bbench, human_ebench };
+
+/* --- @human_extend@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *             @const char *name@ = extension name
+ *
+ * Returns:    A pointer to the extension implementation, or null.
+ */
+
+static const void *human_extend(struct tvec_output *o, const char *name)
+{
+  if (STRCMP(name, ==, TVEC_BENCHOUTEXT)) return (&human_benchops);
+  else return (0);
+}
+
+/* --- @human_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct human_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
+static void human_destroy(struct tvec_output *o)
+{
+  struct human_output *h = (struct human_output *)o;
+
+  tty_close(h->tty);
+  destroy_layout(&h->lyt,
+                h->lyt.fp == stdout || h->lyt.fp == stderr ? 0 : DLF_CLOSE);
+  dstr_destroy(&h->scoreboard);
+  x_free(h->a, h->outbuf); x_free(h->a, h);
+}
+
+static const struct tvec_outops human_ops = {
+  human_bsession, human_esession,
+  human_bgroup, human_skipgroup, human_egroup,
+  human_btest, human_skip, human_fail, human_dumpreg, human_etest,
+  human_report, human_extend, human_destroy
+};
 
-  init_fmt(&h->fmt, fp, 0);
+/* --- @tvec_humanoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *             @unsigned f, m@ = flags and mask
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Return an output formatter which writes on @fp@ with the
+ *             expectation that a human will interpret the output.
+ *
+ *             The flags @f@ and mask @m@ operate together.  Flag bits not
+ *             covered by the mask must be left clear, i.e., @f&~m$ must be
+ *             zero; the semantics are that a set mask bit indicates that
+ *             the corresponding bit of @f@ should control the indicated
+ *             behaviour; a clear mask bit indicates that a suitable default
+ *             should be chosen based on environmental conditions.
+ *
+ *             If @TVHF_TTY@ is set, then the output shows a `scoreboard'
+ *             indicating the outcome of each test case attempted, providing
+ *             a visual indication of progress.  If @TVHF_COLOUR@ is set,
+ *             then the output uses control codes for colour and other
+ *             highlighting.  It is unusual to set @TVHF_COLOUR@ without
+ *             @TVHF_TTY@, this is permitted anyway.
+ *
+ *             The environment variables %|TVEC_TTY|% and %|TVEC_COLOUR|%
+ *             provide default values for these settings.  If they are not
+ *             set, then @TVHF_TTY@ is set if @fp@ refers to a terminal, and
+ *             @TVHF_COLOUR@ is set if @TVHF_TTY@ is set and, additionally,
+ *             the %|TERM|% environment variable is set to a value other
+ *             than %|dumb|%.
+ */
+
+struct tvec_output *tvec_humanoutput(FILE *fp, unsigned f, unsigned m)
+{
+  struct human_output *h;
+  struct stat st_out, st_err;
+  int rc_out, rc_err;
+
+  static const struct ttycolour_style hltab[] =
+    TTYCOLOUR_INITTAB(HIGHLIGHTS);
+
+  assert(!(f&~m));
+
+  if (!(m&TVHF_TTY))
+    switch (getenv_boolean("MLIB_TVEC_TTY", -1)) {
+      case 1: f |= TVHF_TTY; break;
+      case 0: break;
+      default:
+       if (isatty(fileno(fp))) f |= TVHF_TTY;
+       break;
+    }
+  if (!(m&TVHF_COLOUR))
+    switch (getenv_boolean("MLIB_TVEC_COLOUR", -1)) {
+      case 1: f |= TVHF_COLOUR; break;
+      case 0: break;
+      default:
+       if (ttycolour_enablep((f&TVHF_TTY ? TCEF_TTY : 0) | TCEF_DFLT))
+         f |= TVHF_COLOUR;
+       break;
+    }
+
+  /* Decide whether to write copies of reports to stderr.
+   *
+   * There's not much point if the output is already going to stderr.
+   * Otherwise, check to see whether stdout is the same underlying file.
+   */
+  if (fp != stderr) {
+    rc_out = fstat(fileno(fp), &st_out);
+    rc_err = fstat(STDERR_FILENO, &st_err);
+    if (!rc_err && (rc_out ||
+                   st_out.st_dev != st_err.st_dev ||
+                   st_out.st_ino != st_err.st_ino))
+      f |= HOF_DUPERR;
+  }
+
+  XNEW(h); h->a = arena_global; h->_o.ops = &human_ops;
+  h->f = f;
+
+  /* Initialize the colour tables. */
+  if (!(h->f&TVHF_COLOUR))
+    h->tty = 0;
+  else {
+    h->tty = tty_open(fp, TTF_BORROW, 0);
+    if (!h->tty)
+      h->f &= ~TVHF_COLOUR;
+    else
+      ttycolour_config(h->attr, "MLIB_TVEC_COLOURS",
+                      TCIF_GETENV | TCIF_REPORT, h->tty, hltab);
+  }
+
+  init_layout(&h->lyt, fp, 0);
   h->outbuf = 0; h->outsz = 0;
 
-  switch (getenv_boolean("TVEC_TTY", -1)) {
-    case 1: h->f |= HOF_TTY; break;
-    case 0: break;
-    default:
-      if (isatty(fileno(fp))) h->f |= HOF_TTY;
+  dstr_create(&h->scoreboard);
+  return (&h->_o);
+}
+
+/*----- Machine-readable output -------------------------------------------*/
+
+struct machine_output {
+  struct tvec_output _o;               /* output base class */
+  struct tvec_state *tv;               /* stashed testing state */
+  arena *a;                            /* arena for memory allocation */
+  FILE *fp;                            /* output stream */
+  char *outbuf; size_t outsz;          /* buffer for formatted output */
+  unsigned grpix, testix;              /* group and test indices */
+  unsigned f;                          /* flags */
+#define MF_BENCH 1u                    /*   current test is a benchmark */
+};
+
+/* --- @machine_writech@, @machine_write@, @machine_writef@ --- *
+ *
+ * Arguments:  @void *go@ = output sink, secretly a @struct machine_output@
+ *             @int ch@ = character to write
+ *             @const char *@p@, @size_t sz@ = string (with explicit length)
+ *                     to write
+ *             @const char *p, ...@ = format control string and arguments to
+ *                     write
+ *
+ * Returns:    ---
+ *
+ * Use:                Write characters, strings, or formatted strings to the
+ *             output, applying appropriate quoting.
+ */
+
+static void machine_escape(struct machine_output *m, int ch)
+{
+  switch (ch) {
+    case 0: fputs("\\0", m->fp); break;
+    case '\a': fputs("\\a", m->fp); break;
+    case '\b': fputs("\\b", m->fp); break;
+    case '\x1b': fputs("\\e", m->fp); break;
+    case '\f': fputs("\\f", m->fp); break;
+    case '\n': fputs("\\n", m->fp); break;
+    case '\r': fputs("\\r", m->fp); break;
+    case '\t': fputs("\\t", m->fp); break;
+    case '\v': fputs("\\v", m->fp); break;
+    case '"': fputs("\\\"", m->fp); break;
+    case '\\': fputs("\\\\", m->fp); break;
+    default: fprintf(m->fp, "\\x{%02x}", ch);
+  }
+}
+
+static int machine_writech(void *go, int ch)
+{
+  struct machine_output *m = go;
+
+  if (ISPRINT(ch)) putc(ch, m->fp);
+  else machine_escape(m, ch);
+  return (1);
+}
+
+static int machine_writem(void *go, const char *p, size_t sz)
+{
+  struct machine_output *m = go;
+  const char *q, *l = p + sz;
+
+  for (;;) {
+    q = p;
+    for (;;) {
+      if (q >= l) goto final;
+      if (!ISPRINT(*q) || *q == '\\' || *q == '"') break;
+      q++;
+    }
+    if (p < q) fwrite(p, 1, q - p, m->fp);
+    p = q; machine_escape(m, (unsigned char)*p++);
+  }
+final:
+  if (p < l) fwrite(p, 1, l - p, m->fp);
+  return (sz);
+}
+
+static int machine_nwritef(void *go, size_t maxsz, const char *p, ...)
+{
+  struct machine_output *m = go;
+  int n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(m->a, &m->outbuf, &m->outsz, maxsz, p, ap);
+  va_end(ap);
+  return (machine_writem(m, m->outbuf, n));
+}
+
+static const struct gprintf_ops machine_printops =
+  { machine_writech, machine_writem, machine_nwritef };
+
+/* --- @machine_maybe_quote@ --- *
+ *
+ * Arguments:  @struct machine_output *m@ = output sink
+ *             @const char *p@ = pointer to string
+ *
+ * Returns:    ---
+ *
+ * Use:                Print the string @p@, quoting it if necessary.
+ */
+
+static void machine_maybe_quote(struct machine_output *m, const char *p)
+{
+  const char *q;
+
+  for (q = p; *q; q++)
+    if (!ISPRINT(*q) || ISSPACE(*q)) goto quote;
+  fputs(p, m->fp); return;
+quote:
+  putc('"', m->fp); machine_writem(m, p, strlen(p)); putc('"', m->fp);
+}
+
+/* --- @machine_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The TAP driver records the test state for later reference,
+ *             initializes the group index counter, and prints the version
+ *             number.
+ */
+
+static void machine_bsession(struct tvec_output *o, struct tvec_state *tv)
+{
+  struct machine_output *m = (struct machine_output *)o;
+
+  m->tv = tv; m->grpix = 0;
+}
+
+/* --- @machine_show_stats@ --- *
+ *
+ * Arguments:  @struct machine_output *m@ = output sink
+ *             @unsigned *out[TVOUT_LIMIT]@ = outcome counter table
+ *
+ * Returns:    ---
+ *
+ * Use:                Print a machine readable outcome statistics table
+ */
+
+static void machine_show_stats(struct machine_output *m,
+                              unsigned out[TVOUT_LIMIT])
+{
+  static const char *outtab[] = { "lose", "skip", "xfail", "win" };
+  unsigned i;
+
+  for (i = 0; i < TVOUT_LIMIT; i++) {
+    if (i) putc(' ', m->fp);
+    fprintf(m->fp, "%s=%u", outtab[i], out[i]);
+  }
+}
+
+/* --- @machine_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The TAP driver prints a final summary of the rest results
+ *             and returns a suitable exit code.  If errors occurred, it
+ *             instead prints a `Bail out!' line forcing the reader to
+ *             report a failure.
+ */
+
+static int machine_esession(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("END groups: ", m->fp);
+  machine_show_stats(m, tv->grps);
+  fputs("; tests: ", m->fp);
+  machine_show_stats(m, tv->all);
+  putc('\n', m->fp);
+  m->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0);
+}
+
+/* --- @machine_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The TAP driver determines the length of the longest
+ *             register name, resets the group progress scoreboard, and
+ *             activates the progress display.
+ */
+
+static void machine_bgroup(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("BGROUP ", m->fp);
+  machine_maybe_quote(m, tv->test->name);
+  putc('\n', m->fp);
+  m->grpix++; m->testix = 0;
+}
+
+/* --- @machine_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The TAP driver just reports the situation to its output
+ *             stream.
+ */
+
+static void machine_skipgroup(struct tvec_output *o,
+                         const char *excuse, va_list *ap)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fputs("SKIPGRP ", m->fp);
+  machine_maybe_quote(m, tv->test->name);
+  if (excuse) {
+    fputs(" \"", m->fp);
+    vgprintf(&machine_printops, m, excuse, ap);
+    putc('\"', m->fp);
+  }
+  putc('\n', m->fp);
+}
+
+/* --- @machine_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The TAP driver reports a summary of the group's tests.
+ */
+
+static void machine_egroup(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  if (!(tv->f&TVSF_SKIP)) {
+    fputs("EGROUP ", m->fp); machine_maybe_quote(m, tv->test->name);
+    putc(' ', m->fp); machine_show_stats(m, tv->curr);
+    putc('\n', m->fp);
+  }
+}
+
+/* --- @machine_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The TAP driver advances its test counter.  (We could do this
+ *             by adding up up the counters in @tv->curr@, and add on the
+ *             current test, but it's easier this way.)
+ */
+
+static void machine_btest(struct tvec_output *o) { ; }
+
+/* --- @machine_report_location@ --- *
+ *
+ * Arguments:  @struct human_output *h@ = output state
+ *             @const char *file@ = filename
+ *             @unsigned lno@ = line number
+ *
+ * Returns:    ---
+ *
+ * Use:                Print the filename and line number to the output stream.
+ */
+
+static void machine_report_location(struct machine_output *m,
+                                   const char *file, unsigned lno)
+{
+  if (file) {
+    putc(' ', m->fp);
+    machine_maybe_quote(m, file);
+    fprintf(m->fp, ":%u", lno);
+  }
+}
+
+/* --- @machine_outcome@, @machine_skip@, @machine_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @const char *outcome@ = outcome string to report
+ *             @const char *detail@, @va_list *ap@ = a detail message
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     test
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has been skipped or failed.
+ */
+
+static void machine_outcome(struct tvec_output *o, const char *outcome,
+                           const char *detail, va_list *ap)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fprintf(m->fp, "%s %u", outcome, m->testix);
+  machine_report_location(m, tv->infile, tv->test_lno);
+  if (detail)
+    { putc(' ', m->fp); vgprintf(&machine_printops, m, detail, ap); }
+  putc('\n', m->fp);
+}
+
+static void machine_skip(struct tvec_output *o,
+                        const char *excuse, va_list *ap)
+  { machine_outcome(o, "SKIP", excuse, ap); }
+static void machine_fail(struct tvec_output *o,
+                        const char *detail, va_list *ap)
+  { machine_outcome(o, "FAIL", detail, ap); }
+
+/* --- @machine_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The machine driver applies highlighting to mismatching output
+ *             registers, but otherwise delegates to the register type
+ *             handler.
+ */
+
+static void machine_dumpreg(struct tvec_output *o,
+                       unsigned disp, const union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  unsigned f = 0;
+#define f_reg 1u
+#define f_nl 2u
+
+  switch (disp) {
+    case TVRD_INPUT: fputs("\tINPUT ", m->fp); f |= f_reg | f_nl; break;
+    case TVRD_OUTPUT: fputs("\tOUTPUT ", m->fp); f |= f_reg | f_nl; break;
+    case TVRD_MATCH: fputs("\tMATCH ", m->fp); f |= f_reg | f_nl; break;
+    case TVRD_FOUND: fputs("\tMISMATCH ", m->fp); f |= f_reg; break;
+    case TVRD_EXPECT: fputs(" /= ", m->fp); f |= f_nl; break;
+    default: abort();
+  }
+
+  if (f&f_reg) fprintf(m->fp, "%s = ", rd->name);
+  if (!rv) fputs("#unset", m->fp);
+  else rd->ty->dump(rv, rd, TVSF_RAW, &file_printops, m->fp);
+  if (f&f_nl) putc('\n', m->fp);
+
+#undef f_reg
+#undef f_newline
+}
+
+/* --- @machine_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The machine driver reports the outcome of the test, if that's
+ *             not already decided.
+ */
+
+static void machine_etest(struct tvec_output *o, unsigned outcome)
+{
+  struct machine_output *m = (struct machine_output *)o;
+
+  if (!(m->f&MF_BENCH)) switch (outcome) {
+    case TVOUT_WIN: machine_outcome(o, "WIN", 0, 0); break;
+    case TVOUT_XFAIL: machine_outcome(o, "XFAIL", 0, 0); break;
+  }
+  m->testix++; m->f &= ~MF_BENCH;
+}
+
+/* --- @machine_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @unsigned level@ = message level (@TVLV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             Each report level has its own output tag
+ */
+
+static void machine_report(struct tvec_output *o, unsigned level,
+                          const char *msg, va_list *ap)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+  const char *p;
+
+  for (p = tvec_strlevel(level); *p; p++) putc(TOUPPER(*p), m->fp);
+  machine_report_location(m, tv->infile, tv->lno);
+  fputs(" \"", m->fp); vgprintf(&machine_printops, m, msg, ap);
+  putc('"', m->fp);
+  putc('\n', m->fp);
+}
+
+/* --- @machine_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The machine driver does nothing here.  All of the reporting
+ *             happens in @machine_ebench@.
+ */
+
+static void machine_bbench(struct tvec_output *o,
+                          const char *desc, unsigned unit)
+  { ; }
+
+/* --- @machine_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *             @const struct bench_timing *t@ = measurement
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a benchmark's results.
+ *
+ *             The machine driver prints the result as a `BENCH' output
+ *             line, in raw format.
+ */
+
+static void machine_ebench(struct tvec_output *o,
+                          const char *desc, unsigned unit,
+                          const struct bench_timing *t)
+{
+  struct machine_output *m = (struct machine_output *)o;
+  struct tvec_state *tv = m->tv;
+
+  fprintf(m->fp, "BENCH %u", m->testix);
+  machine_report_location(m, tv->infile, tv->test_lno);
+  putc(' ', m->fp);
+  if (desc) machine_maybe_quote(m, desc);
+  else print_ident(tv, TVSF_RAW, &file_printops, m->fp);
+  fputs(": ", m->fp);
+  tvec_benchreport(&file_printops, m->fp, unit, TVSF_RAW, t);
+  putc('\n', m->fp); m->f |= MF_BENCH;
+}
+
+static const struct tvec_benchoutops machine_benchops =
+  { machine_bbench, machine_ebench };
+
+/* --- @machine_extend@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *             @const char *name@ = extension name
+ *
+ * Returns:    A pointer to the extension implementation, or null.
+ */
+
+static const void *machine_extend(struct tvec_output *o, const char *name)
+{
+  if (STRCMP(name, ==, TVEC_BENCHOUTEXT)) return (&machine_benchops);
+  else return (0);
+}
+
+/* --- @machine_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct machine_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
+static void machine_destroy(struct tvec_output *o)
+{
+  struct machine_output *m = (struct machine_output *)o;
+
+  if (m->fp != stdout && m->fp != stderr) fclose(m->fp);
+  x_free(m->a, m->outbuf); x_free(m->a, m);
+}
+
+static const struct tvec_outops machine_ops = {
+  machine_bsession, machine_esession,
+  machine_bgroup, machine_skipgroup, machine_egroup,
+  machine_btest, machine_skip, machine_fail, machine_dumpreg, machine_etest,
+  machine_report, machine_extend, machine_destroy
+};
+
+/* --- @tvec_machineoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Return an output formatter which writes on @fp@ in a
+ *             moderately simple machine-readable format.
+ */
+
+struct tvec_output *tvec_machineoutput(FILE *fp)
+{
+  struct machine_output *m;
+
+  XNEW(m); m->a = arena_global; m->_o.ops = &machine_ops;
+  m->f = 0; m->fp = fp; m->outbuf = 0; m->outsz = 0; m->testix = 0;
+  return (&m->_o);
+}
+
+/*----- Perl's `Test Anything Protocol' -----------------------------------*/
+
+struct tap_output {
+  struct tvec_output _o;               /* output base class */
+  struct tvec_state *tv;               /* stashed testing state */
+  arena *a;                            /* arena for memory allocation */
+  struct layout lyt;                   /* output layout */
+  char *outbuf; size_t outsz;          /* buffer for formatted output */
+  unsigned grpix, testix;              /* group and test indices */
+  unsigned previx;                     /* previously reported test index */
+  int maxlen;                          /* longest register name */
+};
+
+/* --- @tap_writech@, @tap_write@, @tap_writef@ --- *
+ *
+ * Arguments:  @void *go@ = output sink, secretly a @struct tap_output@
+ *             @int ch@ = character to write
+ *             @const char *@p@, @size_t sz@ = string (with explicit length)
+ *                     to write
+ *             @const char *p, ...@ = format control string and arguments to
+ *                     write
+ *
+ * Returns:    ---
+ *
+ * Use:                Write characters, strings, or formatted strings to the
+ *             output, applying appropriate layout.
+ *
+ *             For the TAP output driver, the layout machinery prefixes each
+ *             line with `    ## ' and strips trailing spaces.
+ */
+
+static int tap_writech(void *go, int ch)
+{
+  struct tap_output *t = go;
+
+  if (layout_char(&t->lyt, ch)) return (-1);
+  else return (1);
+}
+
+static int tap_writem(void *go, const char *p, size_t sz)
+{
+  struct tap_output *t = go;
+
+  if (layout_string(&t->lyt, p, sz)) return (-1);
+  else return (sz);
+}
+
+static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
+{
+  struct tap_output *t = go;
+  size_t n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(t->a, &t->outbuf, &t->outsz, maxsz, p, ap);
+  va_end(ap);
+  if (layout_string(&t->lyt, t->outbuf, n)) return (-1);
+  return (n);
+}
+
+static const struct gprintf_ops tap_printops =
+  { tap_writech, tap_writem, tap_nwritef };
+
+/* --- @tap_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The TAP driver records the test state for later reference,
+ *             initializes the group index counter, and prints the version
+ *             number.
+ */
+
+static void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
+{
+  struct tap_output *t = (struct tap_output *)o;
+
+  t->tv = tv; t->grpix = 0;
+  fputs("TAP version 13\n", t->lyt.fp); /* but secretly 14 really */
+}
+
+/* --- @tap_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The TAP driver prints a final summary of the rest results
+ *             and returns a suitable exit code.  If errors occurred, it
+ *             instead prints a `Bail out!' line forcing the reader to
+ *             report a failure.
+ */
+
+static int tap_esession(struct tvec_output *o)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  if (tv->f&TVSF_ERROR) {
+    fputs("Bail out!  "
+         "Errors found in input; tests may not have run correctly\n",
+         t->lyt.fp);
+    return (2);
+  }
+
+  fprintf(t->lyt.fp, "1..%u\n", t->grpix);
+  t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
+}
+
+/* --- @tap_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The TAP driver determines the length of the longest
+ *             register name, resets the group progress scoreboard, and
+ *             activates the progress display.
+ */
+
+static void tap_bgroup(struct tvec_output *o)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  t->grpix++; t->testix = t->previx = 0;
+  t->maxlen = register_maxnamelen(t->tv);
+  fprintf(t->lyt.fp, "# Subtest: %s\n", tv->test->name);
+}
+
+/* --- @tap_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The TAP driver just reports the situation to its output
+ *             stream.
+ */
+
+static void tap_skipgroup(struct tvec_output *o,
+                         const char *excuse, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+
+  fprintf(t->lyt.fp, "    1..%u\n", t->testix);
+  fprintf(t->lyt.fp, "ok %u %s # SKIP", t->grpix, t->tv->test->name);
+  if (excuse) { fputc(' ', t->lyt.fp); vfprintf(t->lyt.fp, excuse, *ap); }
+  fputc('\n', t->lyt.fp);
+}
+
+/* --- @tap_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The TAP driver reports a summary of the group's tests.
+ */
+
+static void tap_egroup(struct tvec_output *o)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  fprintf(t->lyt.fp, "    1..%u\n", t->testix);
+  fprintf(t->lyt.fp, "%s %u - %s\n",
+         tv->curr[TVOUT_LOSE] ? "not ok" : "ok",
+         t->grpix, tv->test->name);
+}
+
+/* --- @tap_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The TAP driver advances its test counter.  (We could do this
+ *             by adding up up the counters in @tv->curr@, and add on the
+ *             current test, but it's easier this way.)
+ */
+
+static void tap_btest(struct tvec_output *o)
+  { struct tap_output *t = (struct tap_output *)o; t->testix++; }
+
+/* --- @tap_outcome@, @tap_skip@, @tap_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @const char *head, *tail@ = outcome strings to report
+ *             @const char *detail@, @va_list *ap@ = a detail message
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     test
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has been skipped or failed.
+ *
+ *             The TAP driver reports the situation on its output stream.
+ *             TAP only allows us to report a single status for each
+ *             subtest, so we notice when we've already reported a status
+ *             for the current test and convert the second report as a
+ *             comment.  This should only happen in the case of multiple
+ *             failures.
+ */
+
+static void tap_outcome(struct tvec_output *o,
+                       const char *head, const char *tail,
+                       const char *detail, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  fprintf(t->lyt.fp, "    %s %u - %s:%u%s",
+         t->testix == t->previx ? "##" : head,
+         t->testix, tv->infile, tv->test_lno, tail);
+  if (detail)
+    { fputc(' ', t->lyt.fp); vfprintf(t->lyt.fp, detail, *ap); }
+  fputc('\n', t->lyt.fp);
+  t->previx = t->testix;
+}
+
+static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+  { tap_outcome(o, "ok", " # SKIP", excuse, ap); }
+static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap)
+  { tap_outcome(o, "not ok", "", detail, ap); }
+
+/* --- @tap_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The TAP driver applies highlighting to mismatching output
+ *             registers, but otherwise delegates to the register type
+ *             handler and the layout machinery.  The result is that the
+ *             register dump is marked as a comment and indented.
+ */
+
+static void tap_dumpreg(struct tvec_output *o,
+                       unsigned disp, const union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
+
+  set_layout_prefix(&t->lyt, "    ## ");
+  gprintf(&tap_printops, t, "%*s%s %s = ",
+         10 + t->maxlen - n, "", ds, rd->name);
+  if (!rv) gprintf(&tap_printops, t, "#<unset>");
+  else rd->ty->dump(rv, rd, 0, &tap_printops, t);
+  layout_char(&t->lyt, '\n');
+}
+
+/* --- @tap_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The TAP driver reports the outcome of the test, if that's not
+ *             already decided.
+ */
+
+static void tap_etest(struct tvec_output *o, unsigned outcome)
+{
+  switch (outcome) {
+    case TVOUT_WIN:
+      tap_outcome(o, "ok", "", 0, 0);
       break;
-  }
-  switch (getenv_boolean("TVEC_COLOUR", -1)) {
-    case 1: h->f |= HOF_COLOUR; break;
-    case 0: break;
-    default:
-      if (h->f&HOF_TTY) {
-       p = getenv("TERM");
-       if (p && STRCMP(p, !=, "dumb")) h->f |= HOF_COLOUR;
-      }
+    case TVOUT_XFAIL:
+      tap_outcome(o, "not ok", " # TODO expected failure", 0, 0);
       break;
   }
+}
 
-  if (fp != stderr && (fp != stdout || !(h->f&HOF_TTY))) h->f |= HOF_DUPERR;
-  dstr_create(&h->scoreboard);
-  return (&h->_o);
+/* --- @tap_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @unsigned level@ = message level (@TVLV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             Messages are reported as comments, so that they can be
+ *             accumulated by the reader.  An error will cause a later
+ *             bailout or, if we crash before then, a missing plan line,
+ *             either of which will cause the reader to report a serious
+ *             problem.
+ */
+
+static void tap_report(struct tvec_output *o, unsigned level,
+                      const char *msg, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->tv;
+
+  if (tv->test) set_layout_prefix(&t->lyt, "    ## ");
+  else set_layout_prefix(&t->lyt, "## ");
+
+  if (tv->infile) gprintf(&tap_printops, t, "%s:%u: ", tv->infile, tv->lno);
+  gprintf(&tap_printops, t, "%s: ", tvec_strlevel(level));
+  vgprintf(&tap_printops, t, msg, ap);
+  layout_char(&t->lyt, '\n');
 }
 
-/*----- Perl's `Test Anything Protocol' -----------------------------------*/
+/* --- @tap_extend@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *             @const char *name@ = extension name
+ *
+ * Returns:    A pointer to the extension implementation, or null.
+ */
 
-struct tap_output {
-  struct tvec_output _o;
-  struct tvec_state *tv;
-  struct format fmt;
-  char *outbuf; size_t outsz;
-  int maxlen;
-};
+static const void *tap_extend(struct tvec_output *o, const char *name)
+  { return (0); }
 
-static int tap_writech(void *go, int ch)
-  { struct tap_output *t = go; return (format_char(&t->fmt, ch)); }
+/* --- @tap_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct tap_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
 
-static int tap_writem(void *go, const char *p, size_t sz)
-  { struct human_output *t = go; return (format_string(&t->fmt, p, sz)); }
+static void tap_destroy(struct tvec_output *o)
+{
+  struct tap_output *t = (struct tap_output *)o;
 
-static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
+  destroy_layout(&t->lyt,
+                t->lyt.fp == stdout || t->lyt.fp == stderr ? 0 : DLF_CLOSE);
+  x_free(t->a, t->outbuf); x_free(t->a, t);
+}
+
+static const struct tvec_outops tap_ops = {
+  tap_bsession, tap_esession,
+  tap_bgroup, tap_skipgroup, tap_egroup,
+  tap_btest, tap_skip, tap_fail, tap_dumpreg, tap_etest,
+  tap_report, tap_extend, tap_destroy
+};
+
+/* --- @tvec_tapoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Return an output formatter which writes on @fp@ in `TAP'
+ *             (`Test Anything Protocol') format.
+ *
+ *             TAP comes from the Perl community, but has spread rather
+ *             further.  This driver produces TAP version 14, but pretends
+ *             to be version 13.  The driver produces a TAP `test point' --
+ *             i.e., a result reported as `ok' or `not ok' -- for each input
+ *             test group.  Failure reports and register dumps are produced
+ *             as diagnostic messages before the final group result.  (TAP
+ *             permits structuerd YAML data after the test-point result,
+ *             which could be used to report details, but (a) postponing the
+ *             details until after the report is inconvenient, and (b) there
+ *             is no standardization for the YAML anyway, so in practice
+ *             it's no more useful than the unstructured diagnostics.
+ */
+
+struct tvec_output *tvec_tapoutput(FILE *fp)
 {
-  struct human_output *t = go;
-  size_t n;
-  va_list ap;
+  struct tap_output *t;
 
-  va_start(ap, p);
-  n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap);
-  va_end(ap);
-  return (format_string(&t->fmt, t->outbuf, n));
+  XNEW(t); t->a = arena_global; t->_o.ops = &tap_ops;
+  init_layout(&t->lyt, fp, 0);
+  t->outbuf = 0; t->outsz = 0;
+  return (&t->_o);
 }
 
-static const struct gprintf_ops tap_printops =
-  { tap_writech, tap_writem, tap_nwritef };
+/*----- Automake support --------------------------------------------------*/
 
-static void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
+struct automake_output {
+  struct tvec_output _o;
+  arena *a;                            /* arena */
+  struct tvec_state *tv;               /* test-vector state */
+  struct tvec_output *progress;                /* real-time progress output */
+  struct tvec_output *log;             /* log file output */
+  FILE *trs;                           /* test result stream */
+};
+
+/* --- @am_bsession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @struct tvec_state *tv@ = the test state producing output
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test session.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_bsession(struct tvec_output *o, struct tvec_state *tv)
 {
-  struct tap_output *t = (struct tap_output *)o;
+  struct automake_output *am = (struct automake_output *)o;
 
-  t->tv = tv;
-  fputs("TAP version 13\n", t->fmt.fp);
+  am->tv = tv;
+  human_bsession(am->progress, tv);
+  machine_bsession(am->log, tv);
 }
 
-static unsigned tap_grpix(struct tap_output *t)
+/* --- @am_esession@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    Suggested exit code.
+ *
+ * Use:                End a test session.
+ *
+ *             The Automake driver completes the test-results file and
+ *             passes  the event on to its subordinates.
+ */
+
+static void am_report_unusual(struct automake_output *am,
+                             unsigned xfail, unsigned skip)
 {
-  struct tvec_state *tv = t->tv;
+  unsigned f = 0;
+#define f_any 1u
 
-  return (tv->grps[TVOUT_WIN] +
-         tv->grps[TVOUT_LOSE] +
-         tv->grps[TVOUT_SKIP]);
+  if (xfail) {
+    fprintf(am->trs, "%s%u expected %s", f&f_any ? ", " : " (",
+           xfail, xfail == 1 ? "failure" : "failures");
+    f |= f_any;
+  }
+  if (skip) {
+    fprintf(am->trs, "%s%u skipped", f&f_any ? ", " : " (", skip);
+    f |= f_any;
+  }
+  if (f&f_any) fputc(')', am->trs);
+
+#undef f_any
 }
 
-static int tap_esession(struct tvec_output *o)
+static int am_esession(struct tvec_output *o)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->tv;
+  struct automake_output *am = (struct automake_output *)o;
+  struct tvec_state *tv = am->tv;
+  unsigned
+    all_win = tv->all[TVOUT_WIN], grps_win = tv->grps[TVOUT_WIN],
+    all_xfail = tv->all[TVOUT_XFAIL],
+    all_lose = tv->all[TVOUT_LOSE], grps_lose = tv->grps[TVOUT_LOSE],
+    all_skip = tv->all[TVOUT_SKIP], grps_skip = tv->grps[TVOUT_SKIP],
+    all_pass = all_win + all_xfail, all_run = all_pass + all_lose,
+    grps_run = grps_win + grps_lose;
 
-  if (tv->f&TVSF_ERROR) {
-    fputs("Bail out!  "
-         "Errors found in input; tests may not have run correctly\n",
-         t->fmt.fp);
-    return (2);
+  human_esession(am->progress);
+  machine_esession(am->log);
+
+  fputs(":test-global-result: ", am->trs);
+  if (tv->f&TVSF_ERROR) fputs("ERRORS; ", am->trs);
+  if (!all_lose) {
+    fprintf(am->trs, "PASSED %s%u %s",
+           !(all_skip || grps_skip) ? "all " : "",
+           all_win, all_win == 1 ? "test" : "tests");
+    am_report_unusual(am, all_xfail, all_skip);
+    fprintf(am->trs, " in %u %s",
+           grps_win, grps_win == 1 ? "group" : "groups");
+    am_report_unusual(am, 0, grps_skip);
+  } else {
+    fprintf(am->trs, "FAILED %u out of %u %s",
+           all_lose, all_run, all_run == 1 ? "test" : "tests");
+    am_report_unusual(am, all_xfail, all_skip);
+    fprintf(am->trs, " in %u out of %u %s",
+           grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
+    am_report_unusual(am, 0, grps_skip);
   }
+  fputc('\n', am->trs);
 
-  fprintf(t->fmt.fp, "1..%u\n", tap_grpix(t));
-  t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
+  fprintf(am->trs, ":copy-in-global-log: %s\n",
+         !all_lose && !(tv->f&TVSF_ERROR) ? "no" : "yes");
+  fprintf(am->trs, ":recheck: %s\n",
+         !all_lose && !(tv->f&TVSF_ERROR) ? "no" : "yes");
+
+  return (0);
 }
 
-static void tap_bgroup(struct tvec_output *o)
+/* --- @am_bgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin a test group.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_bgroup(struct tvec_output *o)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  t->maxlen = register_maxnamelen(t->tv);
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_bgroup(am->progress);
+  machine_bgroup(am->log);
 }
 
-static void tap_skipgroup(struct tvec_output *o,
-                         const char *excuse, va_list *ap)
+/* --- @am_skipgroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     group, or null
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group is being skipped.
+ *
+ *             The Automake driver makes a note in the test-results file and
+ *             passes the event on to its subordinates.
+ */
+
+static void am_skipgroup(struct tvec_output *o,
+                        const char *excuse, va_list *ap)
 {
-  struct tap_output *t = (struct tap_output *)o;
+  struct automake_output *am = (struct automake_output *)o;
+  struct tvec_state *tv = am->tv;
 
-  fprintf(t->fmt.fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name);
-  if (excuse) { fputc(' ', t->fmt.fp); vfprintf(t->fmt.fp, excuse, *ap); }
-  fputc('\n', t->fmt.fp);
+  fprintf(am->trs, ":test-result: SKIP %s\n", tv->test->name);
+  human_skipgroup(am->progress, excuse, ap);
+  machine_skipgroup(am->log, excuse, ap);
 }
 
-static void tap_egroup(struct tvec_output *o)
+/* --- @am_egroup@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test group has finished.
+ *
+ *             The Automake driver makes a note in the test-results file and
+ *             passes the event on to its subordinates.
+ */
+
+static void am_egroup(struct tvec_output *o)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->tv;
-  unsigned
-    grpix = tap_grpix(t),
-    win = tv->curr[TVOUT_WIN],
-    lose = tv->curr[TVOUT_LOSE],
-    skip = tv->curr[TVOUT_SKIP];
+  struct automake_output *am = (struct automake_output *)o;
+  struct tvec_state *tv = am->tv;
 
-  if (lose) {
-    fprintf(t->fmt.fp, "not ok %u - %s: FAILED %u/%u",
-           grpix, tv->test->name, lose, win + lose);
-    if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip);
-  } else {
-    fprintf(t->fmt.fp, "ok %u - %s: passed %u", grpix, tv->test->name, win);
-    if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip);
-  }
-  fputc('\n', t->fmt.fp);
+  fprintf(am->trs, ":test-result: %s %s\n",
+         tv->curr[TVOUT_LOSE] ? "FAIL" : "PASS", tv->test->name);
+  human_egroup(am->progress);
+  machine_egroup(am->log);
 }
 
-static void tap_btest(struct tvec_output *o) { ; }
+/* --- @am_btest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test is starting.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
 
-static void tap_outcome(struct tvec_output *o, const char *outcome,
-                       const char *detail, va_list *ap)
+static void am_btest(struct tvec_output *o)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->tv;
+  struct automake_output *am = (struct automake_output *)o;
 
-  gprintf(&tap_printops, t, "%s:%u: `%s' %s",
-         tv->infile, tv->test_lno, tv->test->name, outcome);
-  if (detail) {
-    format_string(&t->fmt, ": ", 2);
-    vgprintf(&tap_printops, t, detail, ap);
-  }
-  format_char(&t->fmt, '\n');
+  human_btest(am->progress);
+  machine_btest(am->log);
 }
 
-static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
-  { tap_outcome(o, "skipped", excuse, ap); }
-static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap)
-  { tap_outcome(o, "FAILED", detail, ap); }
+/* --- @am_skip@, @am_fail@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *head, *tail@ = outcome strings to report
+ *             @const char *detail@, @va_list *ap@ = a detail message
+ *             @const char *excuse@, @va_list *ap@ = reason for skipping the
+ *                     test
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has been skipped or failed.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
 
-static void tap_dumpreg(struct tvec_output *o,
-                       unsigned disp, const union tvec_regval *rv,
-                       const struct tvec_regdef *rd)
+static void am_skip(struct tvec_output *o, const char *excuse, va_list *ap)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
+  struct automake_output *am = (struct automake_output *)o;
 
-  gprintf(&tap_printops, t, "%*s%s %s = ",
-         10 + t->maxlen - n, "", ds, rd->name);
-  if (!rv) gprintf(&tap_printops, t, "#<unset>");
-  else rd->ty->dump(rv, rd, 0, &tap_printops, t);
-  format_char(&t->fmt, '\n');
+  human_skip(am->progress, excuse, ap);
+  machine_skip(am->log, excuse, ap);
 }
 
-static void tap_etest(struct tvec_output *o, unsigned outcome) { ; }
+static void am_fail(struct tvec_output *o, const char *detail, va_list *ap)
+{
+  struct automake_output *am = (struct automake_output *)o;
 
-static void tap_bbench(struct tvec_output *o,
-                      const char *ident, unsigned unit)
-  { ; }
+  human_fail(am->progress, detail, ap);
+  machine_fail(am->log, detail, ap);
+}
+
+/* --- @am_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @unsigned disp@ = register disposition
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
 
-static void tap_ebench(struct tvec_output *o,
-                      const char *ident, unsigned unit,
-                      const struct bench_timing *tm)
+static void am_dumpreg(struct tvec_output *o,
+                      unsigned disp, const union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
 {
-  struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->tv;
+  struct automake_output *am = (struct automake_output *)o;
 
-  gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident);
-  tvec_benchreport(&tap_printops, t, unit, tm);
-  format_char(&t->fmt, '\n');
+  human_dumpreg(am->progress, disp, rv, rd);
+  machine_dumpreg(am->log, disp, rv, rd);
 }
 
-static void tap_report(struct tap_output *t,
-                      const struct gprintf_ops *gops, void *go,
-                      const char *msg, va_list *ap)
+/* --- @am_etest@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @unsigned outcome@ = the test outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a test has finished.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_etest(struct tvec_output *o, unsigned outcome)
 {
-  struct tvec_state *tv = t->tv;
+  struct automake_output *am = (struct automake_output *)o;
 
-  if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno);
-  gprintf(gops, go, msg, ap); gops->putch(go, '\n');
+  human_etest(am->progress, outcome);
+  machine_etest(am->log, outcome);
 }
 
-static void tap_error(struct tvec_output *o, const char *msg, va_list *ap)
+/* --- @am_report@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @unsigned level@ = message level (@TVLV_...@)
+ *             @const char *msg@, @va_list *ap@ = format string and
+ *                     arguments
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a message to the user.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_report(struct tvec_output *o, unsigned level,
+                     const char *msg, va_list *ap)
 {
-  struct tap_output *t = (struct tap_output *)o;
+  struct automake_output *am = (struct automake_output *)o;
 
-  fputs("Bail out!  ", t->fmt.fp);
-  tap_report(t, &file_printops, t->fmt.fp, msg, ap);
+  human_report(am->progress, level, msg, ap);
+  machine_report(am->log, level, msg, ap);
 }
 
-static void tap_notice(struct tvec_output *o, const char *msg, va_list *ap)
+/* --- @am_bbench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Report that a benchmark has started.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_bbench(struct tvec_output *o,
+                     const char *desc, unsigned unit)
 {
-  struct tap_output *t = (struct tap_output *)o;
+  struct automake_output *am = (struct automake_output *)o;
 
-  tap_report(t, &tap_printops, t, msg, ap);
+  human_bbench(am->progress, desc, unit);
+  machine_bbench(am->progress, desc, unit);
 }
 
-static void tap_destroy(struct tvec_output *o)
+/* --- @am_ebench@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *desc@ = adhoc test description
+ *             @unsigned unit@ = measurement unit (@BTU_...@)
+ *             @const struct bench_timing *t@ = measurement
+ *
+ * Returns:    ---
+ *
+ * Use:                Report a benchmark's results.
+ *
+ *             The Automake driver passes the event on to its subordinates.
+ */
+
+static void am_ebench(struct tvec_output *o,
+                        const char *desc, unsigned unit,
+                        const struct bench_timing *t)
 {
-  struct tap_output *t = (struct tap_output *)o;
+  struct automake_output *am = (struct automake_output *)o;
 
-  destroy_fmt(&t->fmt,
-             t->fmt.fp == stdout || t->fmt.fp == stderr ? 0 : DFF_CLOSE);
-  xfree(t->outbuf); xfree(t);
+  human_ebench(am->progress, desc, unit, t);
+  machine_ebench(am->progress, desc, unit, t);
 }
 
-static const struct tvec_outops tap_ops = {
-  tap_bsession, tap_esession,
-  tap_bgroup, tap_skipgroup, tap_egroup,
-  tap_btest, tap_skip, tap_fail, tap_dumpreg, tap_etest,
-  tap_bbench, tap_ebench,
-  tap_error, tap_notice,
-  tap_destroy
+static const struct tvec_benchoutops am_benchops =
+  { am_bbench, am_ebench };
+
+/* --- @am_extend@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *             @const char *name@ = extension name
+ *
+ * Returns:    A pointer to the extension implementation, or null.
+ */
+
+static const void *am_extend(struct tvec_output *o, const char *name)
+{
+  if (STRCMP(name, ==, TVEC_BENCHOUTEXT)) return (&am_benchops);
+  else return (0);
+}
+
+/* --- @am_destroy@ --- *
+ *
+ * Arguments:  @struct tvec_output *o@ = output sink, secretly a
+ *                      @struct automake_output@
+ *
+ * Returns:    ---
+ *
+ * Use:                Release the resources held by the output driver.
+ */
+
+static void am_destroy(struct tvec_output *o)
+{
+  struct automake_output *am = (struct automake_output *)o;
+
+  human_destroy(am->progress);
+  machine_destroy(am->log);
+  fclose(am->trs); x_free(am->a, am);
+}
+
+static const struct tvec_outops automake_ops = {
+  am_bsession, am_esession,
+  am_bgroup, am_skipgroup, am_egroup,
+  am_btest, am_skip, am_fail, am_dumpreg, am_etest,
+  am_report, am_extend, am_destroy
 };
 
-struct tvec_output *tvec_tapoutput(FILE *fp)
+/* --- @tvec_amoutput@ --- *
+ *
+ * Arguments:  @const struct tvec_amargs *a@ = arguments from Automake
+ *                     command-line protocol
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Returns an output formatter which writes on standard output
+ *             in human format, pretending that the output is to a terminal
+ *             (in order to cope with %%\manpage{make}{1}%%'s output-
+ *             buffering behaviour, writes to the log file @a->log@ in
+ *             machine-readable format, and writes an Automake rST-format
+ *             test result file to @a->trs@.  The `test name' is currently
+ *             ignored, because the framework has its own means of
+ *             determining test names.
+ */
+
+struct tvec_output *tvec_amoutput(const struct tvec_amargs *a)
 {
-  struct tap_output *t;
+  struct automake_output *am;
+  unsigned f;
 
-  t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops;
-  init_fmt(&t->fmt, fp, "## ");
-  t->outbuf = 0; t->outsz = 0;
-  return (&t->_o);
+  f = TVHF_TTY;
+  if (a->f&TVAF_COLOUR) f |= TVHF_COLOUR;
+
+  XNEW(am); am->a = arena_global; am->_o.ops = &automake_ops;
+  am->progress = tvec_humanoutput(stdout, f, TVHF_TTY | TVHF_COLOUR);
+  am->log = tvec_machineoutput(a->log); am->trs = a->trs;
+  return (&am->_o);
 }
 
 /*----- Default output ----------------------------------------------------*/
 
-struct tvec_output *tvec_dfltout(FILE *fp)
+/* --- @tvec_dfltoutput@ --- *
+ *
+ * Arguments:  @FILE *fp@ = output file to write on
+ *
+ * Returns:    An output formatter.
+ *
+ * Use:                Selects and instantiates an output formatter suitable for
+ *             writing on @fp@.  The policy is subject to change, but
+ *             currently the `human' output format is selected if @fp@ is
+ *             interactive (i.e., if @isatty(fileno(fp))@ is true), and
+ *             otherwise the `machine' format is used.
+ */
+
+struct tvec_output *tvec_dfltoutput(FILE *fp)
 {
-  int ttyp = getenv_boolean("TVEC_TTY", -1);
+  int ttyp = getenv_boolean("MLIB_TVEC_TTY", -1);
 
   if (ttyp == -1) ttyp = isatty(fileno(fp));
-  if (ttyp) return (tvec_humanoutput(fp));
-  else return (tvec_tapoutput(fp));
+  if (ttyp) return (tvec_humanoutput(fp, TVHF_TTY, TVHF_TTY));
+  else return (tvec_machineoutput(fp));
 }
 
 /*----- That's all, folks -------------------------------------------------*/