@@@ misc wip
[mLib] / test / tvec-types.c
index a5d57db..dc70dbd 100644 (file)
@@ -30,7 +30,9 @@
 #include <assert.h>
 #include <ctype.h>
 #include <errno.h>
+#include <float.h>
 #include <limits.h>
+#include <math.h>
 #include <stdio.h>
 #include <string.h>
 
 #  include "base64.h"
 #  include "hex.h"
 #include "dstr.h"
+#include "maths.h"
 #include "tvec.h"
 
 /*----- Preliminary utilities ---------------------------------------------*/
 
+/* --- @trivial_release@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = a register value
+ *             @const struct tvec_regdef@ = the register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Does nothing.  Used for register values which don't retain
+ *             resources.
+ */
+
+static void trivial_release(union tvec_regval *rv,
+                           const struct tvec_regdef *rd)
+  { ; }
+
+/*----- Integer utilities -------------------------------------------------*/
+
+/* --- @unsigned_to_buf@, @signed_to_buf@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer to write on
+ *             @unsigned long u@ or @long i@ = integer to write
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write @i@ to the buffer, in big-endian (two's-complement, it
+ *             signed) format.
+ */
+
+static int unsigned_to_buf(buf *b, unsigned long u)
+  { kludge64 k; ASSIGN64(k, u); return (buf_putk64l(b, k)); }
+
 static int signed_to_buf(buf *b, long i)
 {
   kludge64 k;
@@ -55,23 +89,17 @@ static int signed_to_buf(buf *b, long i)
   return (buf_putk64l(b, k));
 }
 
-static int signed_from_buf(buf *b, long *i_out)
-{
-  kludge64 k, lmax, not_lmin;
-
-  ASSIGN64(lmax, LONG_MAX); ASSIGN64(not_lmin, ~(unsigned long)LONG_MIN);
-  if (buf_getk64l(b, &k)) return (-1);
-  if (CMP64(k, <=, lmax)) *i_out = (long)GET64(unsigned long, k);
-  else {
-    CPL64(k, k);
-    if (CMP64(k, <=, not_lmin)) *i_out = -(long)GET64(unsigned long, k) - 1;
-    else return (-1);
-  }
-  return (0);
-}
-
-static int unsigned_to_buf(buf *b, unsigned long u)
-  { kludge64 k; ASSIGN64(k, u); return (buf_putk64l(b, k)); }
+/* --- @unsigned_from_buf@, @signed_from_buf@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer to write on
+ *             @unsigned long *u_out@ or @long *i_out@ = where to put the
+ *                     result
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Read an integer, in big-endian (two's-complement, if signed)
+ *             format, from the buffer.
+ */
 
 static int unsigned_from_buf(buf *b, unsigned long *u_out)
 {
@@ -79,10 +107,19 @@ static int unsigned_from_buf(buf *b, unsigned long *u_out)
 
   ASSIGN64(ulmax, ULONG_MAX);
   if (buf_getk64l(b, &k)) return (-1);
-  if (CMP64(k, >, ulmax)) return (-1);
+  if (CMP64(k, >, ulmax)) { buf_break(b); return (-1); }
   *u_out = GET64(unsigned long, k); return (0);
 }
 
+/* --- @hex_width@ --- *
+ *
+ * Arguments:  @unsigned long u@ = an integer
+ *
+ * Returns:    A suitable number of digits to use in order to display @u@ in
+ *             hex.  Currently, we select a power of two sufficient to show
+ *             the value, but at least 2.
+ */
+
 static int hex_width(unsigned long u)
 {
   int wd;
@@ -92,734 +129,2944 @@ static int hex_width(unsigned long u)
   return (wd/4);
 }
 
-static void check_signed_range(long i,
-                              const struct tvec_irange *ir,
-                              struct tvec_state *tv)
-{
-  if (ir && (ir->min > i || i > ir->max))
-    tvec_error(tv, "integer %ld out of range (must be in [%ld .. %ld])",
-              i, ir->min, ir->max);
-}
+/* --- @format_unsigned_hex@, @format_signed_hex@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @unsigned long u@ or @long i@ = integer to print
+ *
+ * Returns:    ---
+ *
+ * Use:                Print an unsigned or signed integer in hexadecimal.
+ */
 
-static void check_unsigned_range(unsigned long u,
-                                const struct tvec_urange *ur,
-                                struct tvec_state *tv)
+static void format_unsigned_hex(const struct gprintf_ops *gops, void *go,
+                               unsigned long u)
+  { gprintf(gops, go, "0x%0*lx", hex_width(u), u); }
+
+static void format_signed_hex(const struct gprintf_ops *gops, void *go,
+                             long i)
 {
-  if (ur && (ur->min > u || u > ur->max))
-    tvec_error(tv, "integer %lu out of range (must be in [%lu .. %lu])",
-              u, ur->min, ur->max);
+  unsigned long u = i >= 0 ? i : -(unsigned long)i;
+  gprintf(gops, go, "%s0x%0*lx", i < 0 ? "-" : "", hex_width(u), u);
 }
 
-static void parse_signed(long *i_out, const char *p,
-                        const struct tvec_irange *ir,
-                        struct tvec_state *tv)
+static int signed_from_buf(buf *b, long *i_out)
 {
-  char *q; const char *pp;
-  int olderr;
-  long i;
+  kludge64 k, lmax, not_lmin;
 
-  olderr = errno; errno = 0;
-  pp = p; if (*pp == '-' || *pp == '+') pp++;
-  if (!ISDIGIT(*pp)) tvec_syntax(tv, *pp, "signed integer");
-  i = strtol(p, &q, 0);
-  if (*q && !ISSPACE(*q)) tvec_syntax(tv, *q, "end-of-line");
-  if (errno) tvec_error(tv, "invalid integer `%s'", p);
-  check_signed_range(i, ir, tv);
-  errno = olderr; *i_out = i;
+  ASSIGN64(lmax, LONG_MAX); ASSIGN64(not_lmin, ~(unsigned long)LONG_MIN);
+  if (buf_getk64l(b, &k)) return (-1);
+  if (CMP64(k, <=, lmax)) *i_out = (long)GET64(unsigned long, k);
+  else {
+    CPL64(k, k);
+    if (CMP64(k, <=, not_lmin)) *i_out = -(long)GET64(unsigned long, k) - 1;
+    else { buf_break(b); return (-1); }
+  }
+  return (0);
 }
 
-static void parse_unsigned(unsigned long *u_out, const char *p,
-                          const struct tvec_urange *ur,
-                          struct tvec_state *tv)
+/* --- @check_unsigned_range@, @check_signed_range@ --- *
+ *
+ * Arguments:  @unsigned long u@ or @long i@ = an integer
+ *             @const struct tvec_urange *ur@ or
+ *                     @const struct tvec_irange *ir@ = range specification,
+ *                     or null
+ *             @struct tvec_state *tv@ = test vector state
+ *             @const char *what@ = description of value
+ *
+ * Returns:    Zero on success, or @-1@ on error.
+ *
+ * Use:                Check that the integer is within bounds.  If not, report a
+ *             suitable error and return a failure indication.
+ */
+
+static int check_signed_range(long i,
+                             const struct tvec_irange *ir,
+                             struct tvec_state *tv, const char *what)
 {
-  char *q;
-  int olderr;
-  unsigned long u;
+  if (ir && (ir->min > i || i > ir->max)) {
+    tvec_error(tv, "%s %ld out of range (must be in [%ld .. %ld])",
+              what, i, ir->min, ir->max);
+    return (-1);
+  }
+  return (0);
+}
 
-  olderr = errno; errno = 0;
-  if (!ISDIGIT(*p)) tvec_syntax(tv, *p, "unsigned integer");
-  u = strtoul(p, &q, 0);
-  if (*q && !ISSPACE(*q)) tvec_syntax(tv, *q, "end-of-line");
-  if (errno) tvec_error(tv, "invalid integer `%s'", p);
-  check_unsigned_range(u, ur, tv);
-  errno = olderr; *u_out = u;
+static int check_unsigned_range(unsigned long u,
+                               const struct tvec_urange *ur,
+                               struct tvec_state *tv, const char *what)
+{
+  if (ur && (ur->min > u || u > ur->max)) {
+    tvec_error(tv, "%s %lu out of range (must be in [%lu .. %lu])",
+              what, u, ur->min, ur->max);
+    return (-1);
+  }
+  return (0);
 }
 
-static int convert_hex(char ch, int *v_out)
+/* --- @chtodig@ --- *
+ *
+ * Arguments:  @int ch@ = a character
+ *
+ * Returns:    The numeric value of the character as a digit, or @-1@ if
+ *             it's not a digit.  Letters count as extended digits starting
+ *             with value 10; case is not significant.
+ */
+
+static int chtodig(int ch)
 {
-  if ('0' <= ch && ch <= '9') { *v_out = ch - '0'; return (0); }
-  else if ('a' <= ch && ch <= 'f') { *v_out = ch - 'a' + 10; return (0); }
-  else if ('A' <= ch && ch <= 'F') { *v_out = ch - 'A' + 10; return (0); }
+  if ('0' <= ch && ch <= '9') return (ch - '0');
+  else if ('a' <= ch && ch <= 'z') return (ch - 'a' + 10);
+  else if ('A' <= ch && ch <= 'Z') return (ch - 'A' + 10);
   else return (-1);
 }
 
-static void read_quoted_string(dstr *d, int quote, struct tvec_state *tv)
+/* --- @parse_unsigned_integer@, @parse_signed_integer@ --- *
+ *
+ * Arguments:  @unsigned long *u_out@, @long *i_out@ = where to put the
+ *                     result
+ *             @const char **q_out@ = where to put the end position
+ *             @const char *p@ = pointer to the string to parse
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Parse an integer from a string in the test-vector format.
+ *             This is mostly extension of the traditional C @strtoul@
+ *             format: supported inputs include:
+ *
+ *               * NNN -- a decimal number (even if it starts with `0');
+ *               * 0xNNN -- hexadecimal;
+ *               * 0oNNN -- octal;
+ *               * 0bNNN -- binary;
+ *               * NNrNNN -- base NN.
+ *
+ *             Furthermore, single underscores are permitted internally as
+ *             an insignificant digit separator.
+ */
+
+static int parse_unsigned_integer(unsigned long *u_out, const char **q_out,
+                                 const char *p)
 {
-  char expect[4];
-  int ch, i, esc;
+  unsigned long u;
+  int ch, d, r;
+  const char *q;
   unsigned f = 0;
-#define f_brace 1u
-
-  sprintf(expect, "`%c'", quote);
+#define f_implicit 1u                  /* implicitly reading base 10 */
+#define f_digit 2u                     /* read a real digit */
+#define f_uscore 4u                    /* found an underscore */
+
+  /* Initial setup
+   *
+   * This will deal with the traditional `0[box]...' prefixes.  We'll leave
+   * our new `NNr...' syntax for later.
+   */
+  if (p[0] != '0' || !p[1]) {
+    d = chtodig(*p); if (0 > d || d >= 10) return (-1);
+    r = 10; u = d; p++; f |= f_implicit | f_digit;
+  } else {
+    u = 0; d = chtodig(p[2]);
+    if (d < 0) { r = 10; f |= f_implicit | f_digit; p++; }
+    else if ((p[1] == 'x' || p[1] == 'X') && d < 16) { r = 16; p += 2; }
+    else if ((p[1] == 'o' || p[1] == 'O') && d < 8) { r = 8; p += 2; }
+    else if ((p[1] == 'b' || p[1] == 'B') && d < 2) { r = 2; p += 2; }
+    else { r = 10; f |= f_digit; p++; }
+  }
 
+  q = p;
   for (;;) {
-    ch = getc(tv->fp);
-  reinsert:
-    switch (ch) {
-      case EOF: case '\n':
-       tvec_syntax(tv, ch, expect);
+    /* Work through the string a character at a time. */
 
-      case '\\':
-       if (quote == '\'') goto ordinary;
-       ch = getc(tv->fp);
-       switch (ch) {
-         case EOF: tvec_syntax(tv, ch, expect);
-         case '\n': tv->lno++; break;
-         case '\'': DPUTC(d, '\''); break;
-         case '\\': DPUTC(d, '\\'); break;
-         case '"': DPUTC(d, '"'); break;
-         case 'a': DPUTC(d, '\a'); break;
-         case 'b': DPUTC(d, '\b'); break;
-         case 'e': DPUTC(d, '\x1b'); break;
-         case 'f': DPUTC(d, '\f'); break;
-         case 'n': DPUTC(d, '\n'); break;
-         case 'r': DPUTC(d, '\r'); break;
-         case 't': DPUTC(d, '\t'); break;
-         case 'v': DPUTC(d, '\v'); break;
-
-         case 'x':
-           ch = getc(tv->fp);
-           if (ch == '{') { f |= f_brace; ch = getc(tv->fp); }
-           else f &= ~f_brace;
-           if (convert_hex(ch, &esc)) tvec_syntax(tv, ch, "hex digit");
-           for (;;) {
-             ch = getc(tv->fp); if (convert_hex(ch, &i)) break;
-             esc = 8*esc + i;
-             if (esc > UCHAR_MAX)
-               tvec_error(tv, "character code %d out of range", esc);
-           }
-           DPUTC(d, esc);
-           if (!(f&f_brace)) goto reinsert;
-           else if (ch != '}') tvec_syntax(tv, ch, "`}'");
-           break;
+    ch = *p; switch (ch) {
 
-         default:
-           if ('0' <= ch && ch < '8') {
-             i = 1; esc = ch - '0';
-             for (;;) {
-               ch = getc(tv->fp);
-               if (i > 3 || '0' > ch || ch >= '8') break;
-               esc = 8*esc + ch - '0'; i++;
-             }
-             if (esc > UCHAR_MAX)
-               tvec_error(tv, "character code %d out of range", esc);
-             DPUTC(d, esc);
-             goto reinsert;
-           }
-           tvec_syntax(tv, ch, "string escape");
-           break;
-       }
-       break;
+      case '_':
+       /* An underscore is OK if we haven't just seen one. */
 
-      default:
-       if (ch == quote) goto end;
-      ordinary:
-       DPUTC(d, ch);
+       if (f&f_uscore) goto done;
+       p++; f = (f&~f_implicit) | f_uscore;
        break;
-    }
-  }
-
-end:
-  DPUTZ(d);
-
-#undef f_brace
-}
 
-enum { TVCODE_BARE, TVCODE_HEX, TVCODE_BASE64, TVCODE_BASE32 };
-
-static int collect_bare(dstr *d, struct tvec_state *tv)
-{
-  size_t pos = d->len;
-  enum { WORD, SPACE, ESCAPE }; unsigned s = WORD;
-  int ch, rc;
+      case 'r': case 'R':
+       /* An `r' is OK if the number so far is small enough to be a sensible
+        * base, and we're scanning decimal implicitly.
+        */
 
-  for (;;) {
-    ch = getc(tv->fp);
-    switch (ch) {
-      case EOF:
-       goto bad;
-      case '\n':
-       if (s == ESCAPE) { tv->lno++; goto addch; }
-       if (s == WORD) pos = d->len;
-       ungetc(ch, tv->fp); if (tvec_nexttoken(tv)) { rc = -1; goto done; }
-       DPUTC(d, ' '); s = SPACE;
-       break;
-      case '"': case '\'': case '!':
-       if (s == SPACE) { ungetc(ch, tv->fp); rc = 0; goto done; }
-       goto addch;
-      case '\\':
-       s = ESCAPE;
+       if (!(f&f_implicit) || !u || u >= 36) goto done;
+       d = chtodig(p[1]); if (0 > d || d >= u) goto done;
+       r = u; u = d; f = (f&~f_implicit) | f_digit; p += 2; q = p;
        break;
+
       default:
-       if (s != ESCAPE && isspace(ch)) {
-         if (s == WORD) pos = d->len;
-         DPUTC(d, ch); s = SPACE;
-         break;
-       }
-      addch:
-       DPUTC(d, ch); s = WORD;
+       /* Otherwise we expect a valid digit and accumulate it. */
+       d = chtodig(ch); if (d < 0 || d >= r) goto done;
+       if (u > ULONG_MAX/r) return (-1);
+       u *= r; if (u > ULONG_MAX - d) return (-1);
+       u += d; f = (f&~f_uscore) | f_digit; p++; q = p;
+       break;
     }
   }
 
 done:
-  if (s == SPACE) d->len = pos;
-  DPUTZ(d); return (rc);
+  if (!(f&f_digit)) return (-1);
+  *u_out = u; *q_out = q; return (0);
 
-bad:
-  tvec_syntax(tv, ch, "bareword");
+#undef f_implicit
+#undef f_digit
+#undef f_uscore
 }
 
-static void set_up_encoding(const codec_class **ccl_out, unsigned *f_out,
-                           unsigned code)
+static int parse_signed_integer(long *i_out, const char **q_out,
+                               const char *p)
 {
-  switch (code) {
-    case TVCODE_BARE:
-      *ccl_out = 0; *f_out = 0;
-      break;
-    case TVCODE_HEX:
-      *ccl_out = &hex_class; *f_out = CDCF_IGNCASE;
-      break;
-    case TVCODE_BASE32:
-      *ccl_out = &base32_class; *f_out = CDCF_IGNCASE | CDCF_IGNEQPAD;
-      break;
-    case TVCODE_BASE64:
-      *ccl_out = &base64_class; *f_out = CDCF_IGNEQPAD;
-      break;
-    default:
-      abort();
+  unsigned long u;
+  unsigned f = 0;
+#define f_neg 1u
+
+  /* Read an initial sign. */
+  if (*p == '+') p++;
+  else if (*p == '-') { f |= f_neg; p++; }
+
+  /* Scan an unsigned number. */
+  if (parse_unsigned_integer(&u, q_out, p)) return (-1);
+
+  /* Check for signed overflow and apply the sign. */
+  if (!(f&f_neg)) {
+    if (u > LONG_MAX) return (-1);
+    *i_out = u;
+  } else {
+    if (u && u - 1 > -(LONG_MIN + 1)) return (-1);
+    *i_out = u ? -(long)(u - 1) - 1 : 0;
   }
+
+  return (0);
+
+#undef f_neg
 }
 
-static void read_compound_string(void **p_inout, size_t *sz_inout,
-                                unsigned code, struct tvec_state *tv)
-{
-  const codec_class *ccl; unsigned f;
-  codec *cdc;
-  dstr d = DSTR_INIT, w = DSTR_INIT;
-  char *p;
-  int ch, err;
+/* --- @parse_unsigned@, @parse_signed@ --- *
+ *
+ * Arguments:  @unsigned long *u_out@ or @long *i_out@ = where to put the
+ *                     result
+ *             @const char *p@ = string to parse
+ *             @const struct tvec_urange *ur@ or
+ *                     @const struct tvec_irange *ir@ = range specification,
+ *                     or null
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Parse and range-check an integer.  Unlike @parse_(un)signed_
+ *             integer@, these functions check that there's no cruft
+ *             following the final digit, and report errors as they find
+ *             them rather than leaving that to the caller.
+ */
 
-  set_up_encoding(&ccl, &f, code);
-  if (tvec_nexttoken(tv)) tvec_syntax(tv, fgetc(tv->fp), "string");
-  do {
-    ch = getc(tv->fp);
-    if (ch == '"' || ch == '\'')
-      read_quoted_string(&d, ch, tv);
-    else if (ch == '!') {
-      ungetc(ch, tv->fp);
-      DRESET(&w); tvec_readword(tv, &w, ";", "`!'-keyword");
-      if (STRCMP(w.buf, ==, "!bare")) code = TVCODE_BARE;
-      else if (STRCMP(w.buf, ==, "!hex")) code = TVCODE_HEX;
-      else if (STRCMP(w.buf, ==, "!base32")) code = TVCODE_BASE32;
-      else if (STRCMP(w.buf, ==, "!base64")) code = TVCODE_BASE64;
-      else tvec_error(tv, "unknown string keyword `%s'", w.buf);
-      set_up_encoding(&ccl, &f, code);
-    } else if (ccl) {
-      ungetc(ch, tv->fp);
-      DRESET(&w);
-       tvec_readword(tv, &w, ";", "%s-encoded fragment", ccl->name);
-      cdc = ccl->decoder(f);
-      err = cdc->ops->code(cdc, w.buf, w.len, &d);
-      if (!err) err = cdc->ops->code(cdc, 0, 0, &d);
-      if (err)
-       tvec_error(tv, "invalid %s fragment `%s': %s",
-                  ccl->name, w.buf, codec_strerror(err));
-      cdc->ops->destroy(cdc);
-    } else switch (code) {
-      case TVCODE_BARE:
-       ungetc(ch, tv->fp);
-       if (collect_bare(&d, tv)) goto done;
-       break;
-      default:
-       abort();
-    }
-  } while (!tvec_nexttoken(tv));
+static int parse_unsigned(unsigned long *u_out, const char *p,
+                         const struct tvec_urange *ur,
+                         struct tvec_state *tv)
+{
+  unsigned long u;
+  const char *q;
 
-done:
-  if (*sz_inout <= d.len)
-    { xfree(*p_inout); *p_inout = xmalloc(d.len + 1); }
-  p = *p_inout; memcpy(p, d.buf, d.len); p[d.len] = 0; *sz_inout = d.len;
-  dstr_destroy(&d); dstr_destroy(&w);
+  if (parse_unsigned_integer(&u, &q, p))
+    return (tvec_error(tv, "invalid unsigned integer `%s'", p));
+  if (*q) return (tvec_syntax(tv, *q, "end-of-line"));
+  if (check_unsigned_range(u, ur, tv, "integer")) return (-1);
+  *u_out = u; return (0);
 }
 
-/*----- Skeleton ----------------------------------------------------------*/
-/*
-static void init_...(union tvec_regval *rv, const struct tvec_regdef *rd)
-static void release_...(union tvec_regval *rv, const struct tvec_regdef *rd)
-static int eq_...(const union tvec_regval *rv0, const union tvec_regval *rv1,
-                 const struct tvec_regdef *rd)
-static size_t measure_...(const union tvec_regval *rv,
-                         const struct tvec_regdef *rd)
-static int tobuf_...(buf *b, const union tvec_regval *rv,
-                    const struct tvec_regdef *rd)
-static int frombuf_...(buf *b, union tvec_regval *rv,
-                      const struct tvec_regdef *rd)
-static void parse_...(union tvec_regval *rv, const struct tvec_regdef *rd,
-                     struct tvec_state *tv)
-static void dump_...(const union tvec_regval *rv,
-                    const struct tvec_regdef *rd,
-                    struct tvec_state *tv, unsigned style)
-
-const struct tvec_regty tvty_... = {
-  init_..., release_..., eq_..., measure_...,
-  tobuf_..., frombuf_...,
-  parse_..., dump_...
-};
-*/
-/*----- Signed and unsigned integer types ---------------------------------*/
+static int parse_signed(long *i_out, const char *p,
+                       const struct tvec_irange *ir,
+                       struct tvec_state *tv)
+{
+  long i;
+  const char *q;
 
-static void init_int(union tvec_regval *rv, const struct tvec_regdef *rd)
-  { rv->i = 0; }
+  if (parse_signed_integer(&i, &q, p))
+    return (tvec_error(tv, "invalid signed integer `%s'", p));
+  if (*q) return (tvec_syntax(tv, *q, "end-of-line"));
+  if (check_signed_range(i, ir, tv, "integer")) return (-1);
+  *i_out = i; return (0);
+}
+static const char size_units[] = "kMGTPEZY";
 
-static void init_uint(union tvec_regval *rv, const struct tvec_regdef *rd)
-  { rv->u = 0; }
+/* --- @parse_szint@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned long *u_out@ = where to put the answer
+ *             @const char *delims@ = delimiters
+ *             @const char *what@ = description of what we're parsing
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Parse a memory size.
+ */
 
-static void release_int(union tvec_regval *rv, const struct tvec_regdef *rd)
-  { ; }
+static int parse_szint(struct tvec_state *tv, unsigned long *u_out,
+                      const char *delims, const char *what)
+{
+  dstr d = DSTR_INIT;
+  const char *p, *unit;
+  unsigned long u, t;
+  int rc;
+  unsigned f = 0;
+#define f_range 1u
 
-static int eq_int(const union tvec_regval *rv0, const union tvec_regval *rv1,
-                 const struct tvec_regdef *rd)
-  { return (rv0->i == rv1->i); }
+  if (tvec_readword(tv, &d, 0, delims, what)) { rc = -1; goto end; }
+  p = d.buf;
+  if (parse_unsigned_integer(&u, &p, p)) goto bad;
+  if (!*p) tvec_readword(tv, &d, &p, delims, 0);
 
-static int eq_uint(const union tvec_regval *rv0,
-                  const union tvec_regval *rv1,
-                  const struct tvec_regdef *rd)
-  { return (rv0->u == rv1->u); }
+  for (t = u, unit = size_units; *unit; unit++) {
+    if (t > ULONG_MAX/1024) f |= f_range;
+    else t *= 1024;
+    if (*p == *unit) {
+      if (f&f_range) goto rangerr;
+      u = t; p++; break;
+    }
+  }
+  if (*p == 'B') p++;
+  if (*p) goto bad;
 
-static size_t measure_int(const union tvec_regval *rv,
-                         const struct tvec_regdef *rd)
-  { return (8); }
+  *u_out = u; rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
 
-static int tobuf_int(buf *b, const union tvec_regval *rv,
-                    const struct tvec_regdef *rd)
-  { return (signed_to_buf(b, rv->i)); }
+bad:
+  tvec_error(tv, "invalid %s `%s'", what, d.buf);
+  rc = -1; goto end;
 
-static int tobuf_uint(buf *b, const union tvec_regval *rv,
-                      const struct tvec_regdef *rd)
-  { return (unsigned_to_buf(b, rv->u)); }
+rangerr:
+  tvec_error(tv, "%s `%s' out of range", what, d.buf);
+  rc = -1; goto end;
 
-static int frombuf_int(buf *b, union tvec_regval *rv,
-                      const struct tvec_regdef *rd)
-  { return signed_from_buf(b, &rv->i); }
+#undef f_range
+}
 
-static int frombuf_uint(buf *b, union tvec_regval *rv,
-                       const struct tvec_regdef *rd)
-  { return (unsigned_from_buf(b, &rv->u)); }
+/* --- @format_size@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @unsigned long u@ = a size
+ *             @unsigned style@ = style (@TVSF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Format @u@ as a size in bytes to the destination, expressing
+ *             it with a unit prefix if this is possible exactly.
+ */
 
-static void parse_int(union tvec_regval *rv, const struct tvec_regdef *rd,
-                     struct tvec_state *tv)
+static void format_size(const struct gprintf_ops *gops, void *go,
+                       unsigned long u, unsigned style)
 {
-  dstr d = DSTR_INIT;
+  const char *unit;
 
-  tvec_readword(tv, &d, ";", "signed integer");
-  parse_signed(&rv->i, d.buf, rd->arg.p, tv);
-  tvec_flushtoeol(tv, 0);
-  dstr_destroy(&d);
+  if (style&TVSF_RAW)
+    gprintf(gops, go, "%lu", u);
+  else if (!u || u%1024)
+    gprintf(gops, go, "%lu%sB", u, style&TVSF_COMPACT ? "" : " ");
+  else {
+    for (unit = size_units, u /= 1024;
+        !(u%1024) && unit[1];
+        u /= 1024, unit++);
+    gprintf(gops, go, "%lu%s%cB", u, style&TVSF_COMPACT ? "" : " ", *unit);
+  }
 }
 
-static void parse_uint(union tvec_regval *rv, const struct tvec_regdef *rd,
-                      struct tvec_state *tv)
-{
-  dstr d = DSTR_INIT;
+/*----- Floating-point utilities ------------------------------------------*/
 
-  tvec_readword(tv, &d, ";", "unsigned integer");
-  parse_unsigned(&rv->u, d.buf, rd->arg.p, tv);
-  tvec_flushtoeol(tv, 0);
-  dstr_destroy(&d);
+/* --- @eqish_floating_p@ --- *
+ *
+ * Arguments:  @double x, y@ = two numbers to compare
+ *             @const struct tvec_floatinfo *fi@ = floating-point info
+ *
+ * Returns:    Nonzero if  the comparand @x@ is sufficiently close to the
+ *             reference @y@, or zero if it's definitely different.
+ */
+
+static int eqish_floating_p(double x, double y,
+                           const struct tvec_floatinfo *fi)
+{
+  double t;
+
+  if (NANP(x)) return (NANP(y)); else if (NANP(y)) return (0);
+  if (INFP(x)) return (x == y); else if (INFP(y)) return (0);
+
+  switch (fi ? fi->f&TVFF_EQMASK : TVFF_EXACT) {
+    case TVFF_EXACT:
+      return (x == y && NEGP(x) == NEGP(y));
+    case TVFF_ABSDELTA:
+      t = x - y; if (t < 0) t = -t; return (t < fi->delta);
+    case TVFF_RELDELTA:
+      t = 1.0 - x/y; if (t < 0) t = -t; return (t < fi->delta);
+    default:
+      abort();
+  }
+}
+
+/* --- @format_floating@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @double x@ = number to print
+ *
+ * Returns:    ---
+ *
+ * Use:                Print a floating-point number, accurately.
+ */
+
+static void format_floating(const struct gprintf_ops *gops, void *go,
+                           double x)
+{
+  int prec;
+
+  if (NANP(x))
+    gprintf(gops, go, "#nan");
+  else if (INFP(x))
+    gprintf(gops, go, x > 0 ? "#+inf" : "#-inf");
+  else {
+    /* Ugh.  C doesn't provide any function for just printing a
+     * floating-point number /correctly/, i.e., so that you can read the
+     * result back and recover the number you first thought of.  There are
+     * complicated algorithms published for doing this, but I really don't
+     * want to get into that here.  So we have this.
+     *
+     * The sign doesn't cause significant difficulty so we're going to ignore
+     * it for now.  So suppose we're given a number %$x = f b^e$%, in
+     * base-%$b$% format, so %$f b^n$% and %$e$% are integers, with
+     * %$0 \le f < 1$%.  We're going to convert it into the nearest integer
+     * of the form %$X = F B^E$%, with similar conditions, only with the
+     * additional requirement that %$X$% is normalized, i.e., that %$X = 0$%
+     * or %$F \ge B^{-N}$%.
+     *
+     * We're rounding to the nearest such %$X$%.  If there is to be ambiguity
+     * in the conversion, then some %$x = f b^e$% and the next smallest
+     * representable number %$x' = x + b^{e-n}$% must both map to the same
+     * %$X$%, which means both %$x$% and %$x'$% must be nearer to %$X$% than
+     * any other number representable in the target system.  The nest larger
+     * number is %$X' = X + B^{E-N}$%; the next smaller number will normally
+     * be %$W = X - B^{E-N}$%, but if %$F = 1/B$ then the next smaller number
+     * is actually %$X - B^{E-N-1}$%.  We ignore this latter possibility in
+     * the pursuit of a conservative estimate (though actually it doesn't
+     * matter).
+     *
+     * If both %$x$% and %$x'$% map to %$X$% then we must have
+     * %$L = X - B^{E-N}/2 \le x$% and %$x + b^{e-n} \le R = X + B^{E-N}/2$%;
+     * so firstly %$f b^e = x \ge L = W + B^{E-N}/2 > W = (F - B^{-N}) B^E$%,
+     * and secondly %$b^{e-n} \le B^{E-N}$%.  Since these inequalities are in
+     * opposite senses, we can divide, giving
+     *
+     *        %$f b^e/b^{e-n} > (F - B^{-N}) B^E/B^{E-N}$% ,
+     *
+     * whence
+     *
+     *        %$f b^n > (F - B^{-N}) B^N = F B^N - 1$% .
+     *
+     * Now %$f \le 1 - b^{-n}$%, and %$F \ge B^{-1}$%, so, for this to be
+     * possible, it must be the case that
+     *
+     *        %$(1 - b^{-n}) b^n = b^n - 1 > B^{N-1} - 1$% .
+     *
+     * Then rearrange and take logarithms, obtaining
+     *
+     *        %$(N - 1) \log B < n \log b$% ,
+     *
+     * and so
+     *
+     *        %$N < n \log b/\log B + 1$% .
+     *
+     * Recall that this is a necessary condition for a collision to occur; we
+     * are therefore safe whenever
+     *
+     *        %$N \ge n \log b/\log B + 1$% ;
+     *
+     * so, taking ceilings,
+     *
+     *        %$N \ge \lceil n \log b/\log B \rceil + 1$% .
+     *
+     * So that's why we have this.
+     *
+     * I'm going to assume that @n = DBL_MANT_DIG@ is sufficiently small that
+     * we can calculate this without ending up on the wrong side of an
+     * integer boundary.
+     *
+     * In C11, we have @DBL_DECIMAL_DIG@, which should be the same value only
+     * as a constant.  Except that modern compilers are more than clever
+     * enough to work out that this is a constant anyway.
+     *
+     * This is sometimes an overestimate: we'll print out meaningless digits
+     * that don't represent anything we actually know about the number in
+     * question.  To fix that, we'd need a complicated algorithm like Steele
+     * and White's Dragon4, Gay's @dtoa@, or Burger and Dybvig's algorithm
+     * (note that Loitsch's Grisu2 is conservative, and Grisu3 hands off to
+     * something else in difficult situations).
+     */
+
+    prec = ceil(DBL_MANT_DIG*log(FLT_RADIX)/log(10)) + 1;
+    gprintf(gops, go, "%.*g", prec, x);
+  }
+}
+
+/* --- @parse_floating@ --- *
+ *
+ * Arguments:  @double *x_out@ = where to put the result
+ *             @const char *q_out@ = where to leave end pointer, or null
+ *             @const char *p@ = string to parse
+ *             @const struct tvec_floatinfo *fi@ = floating-point info
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Parse a floating-point number from a string.  Reports any
+ *             necessary errors.  If @q_out@ is not null then trailing
+ *             material is permitted and a pointer to it (or the end of the
+ *             string) is left in @*q_out@.
+ */
+
+static int parse_floating(double *x_out, const char **q_out, const char *p,
+                         const struct tvec_floatinfo *fi,
+                         struct tvec_state *tv)
+{
+  const char *pp; char *q;
+  dstr d = DSTR_INIT;
+  double x;
+  int olderr, rc;
+
+  /* Check for special tokens. */
+  if (STRCMP(p, ==, "#nan")) {
+#ifdef NAN
+    if (q_out) *q_out = p + strlen(p);
+    x = NAN; rc = 0;
+#else
+    tvec_error(tv, "NaN not supported on this system");
+    rc = -1; goto end;
+#endif
+  }
+
+  else if (STRCMP(p, ==, "#inf") ||
+          STRCMP(p, ==, "#+inf") || STRCMP(p, ==, "+#inf")) {
+#ifdef INFINITY
+    if (q_out) *q_out = p + strlen(p);
+    x = INFINITY; rc = 0;
+#else
+    tvec_error(tv, "infinity not supported on this system");
+    rc = -1; goto end;
+#endif
+  }
+
+  else if (STRCMP(p, ==, "#-inf") || STRCMP(p, ==, "-#inf")) {
+#ifdef INFINITY
+    if (q_out) *q_out = p + strlen(p);
+    x = -INFINITY; rc = 0;
+#else
+    tvec_error(tv, "infinity not supported on this system");
+    rc = -1; goto end;
+#endif
+  }
+
+  /* Check that this looks like a number, so we can exclude `strtod'
+   * recognizing its own non-finite number tokens.
+   */
+  else {
+    pp = p;
+    if (*pp == '+' || *pp == '-') pp++;
+    if (*pp == '.') pp++;
+    if (!ISDIGIT(*pp)) {
+      tvec_syntax(tv, *p ? *p : fgetc(tv->fp), "floating-point number");
+      rc = -1; goto end;
+    }
+
+    /* Parse the number using the system parser. */
+    olderr = errno; errno = 0;
+    x = strtod(p, &q);
+    if (q_out) *q_out = q;
+    else if (*q) { tvec_syntax(tv, *q, "end-of-line"); rc = -1; goto end; }
+    if (errno && (errno != ERANGE || (x > 0 ? -x : x) == HUGE_VAL)) {
+      tvec_error(tv, "invalid floating-point number `%.*s': %s",
+                (int)(q - p), p, strerror(errno));
+      rc = -1; goto end;
+    }
+    errno = olderr;
+  }
+
+  /* Check that the number is acceptable. */
+  if (NANP(x) && fi && !(fi->f&TVFF_NANOK)) {
+    tvec_error(tv, "#nan not allowed here");
+    rc = -1; goto end;
+  }
+
+  if (fi && ((!(fi->f&TVFF_NOMIN) && x < fi->min) ||
+            (!(fi->f&TVFF_NOMAX) && x > fi->max))) {
+    dstr_puts(&d, "floating-point number ");
+    format_floating(&dstr_printops, &d, x);
+    dstr_puts(&d, " out of range (must be in ");
+    if (fi->f&TVFF_NOMIN)
+      dstr_puts(&d, "(#-inf");
+    else
+      { dstr_putc(&d, '['); format_floating(&dstr_printops, &d, fi->min); }
+    dstr_puts(&d, " .. ");
+    if (fi->f&TVFF_NOMAX)
+      dstr_puts(&d, "#+inf)");
+    else
+      { format_floating(&dstr_printops, &d, fi->max); dstr_putc(&d, ']'); }
+    dstr_putc(&d, ')'); dstr_putz(&d);
+    tvec_error(tv, "%s", d.buf); rc = -1; goto end;
+  }
+
+  /* All done. */
+  *x_out = x; rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
+}
+
+/*----- String utilities --------------------------------------------------*/
+
+/* Special character name table. */
+static const struct chartab {
+  const char *name;                    /* character name */
+  int ch;                              /* character value */
+  unsigned f;                          /* flags: */
+#define CTF_PREFER 1u                  /*   preferred name */
+#define CTF_SHORT 2u                   /*   short name (compact style) */
+} chartab[] = {
+  { "#eof",            EOF,    CTF_PREFER | CTF_SHORT },
+  { "#nul",            '\0',   CTF_PREFER },
+  { "#bell",           '\a',   CTF_PREFER },
+  { "#ding",           '\a',   0 },
+  { "#bel",            '\a',   CTF_SHORT },
+  { "#backspace",      '\b',   CTF_PREFER },
+  { "#bs",             '\b',   CTF_SHORT },
+  { "#escape",         '\x1b', CTF_PREFER },
+  { "#esc",            '\x1b', CTF_SHORT },
+  { "#formfeed",       '\f',   CTF_PREFER },
+  { "#ff",             '\f',   CTF_SHORT },
+  { "#newline",                '\n',   CTF_PREFER },
+  { "#linefeed",       '\n',   0 },
+  { "#lf",             '\n',   CTF_SHORT },
+  { "#nl",             '\n',   0 },
+  { "#return",         '\r',   CTF_PREFER },
+  { "#carriage-return",        '\r',   0 },
+  { "#cr",             '\r',   CTF_SHORT },
+  { "#tab",            '\t',   CTF_PREFER | CTF_SHORT },
+  { "#horizontal-tab", '\t',   0 },
+  { "#ht",             '\t',   0 },
+  { "#vertical-tab",   '\v',   CTF_PREFER },
+  { "#vt",             '\v',   CTF_SHORT },
+  { "#space",          ' ',    0 },
+  { "#spc",            ' ',    CTF_SHORT },
+  { "#delete",         '\x7f', CTF_PREFER },
+  { "#del",            '\x7f', CTF_SHORT },
+  { 0,                 0,      0 }
+};
+
+/* --- @find_charname@ --- *
+ *
+ * Arguments:  @int ch@ = character to match
+ *             @unsigned f@ = flags (@CTF_...@) to match
+ *
+ * Returns:    The name of the character, or null if no match is found.
+ *
+ * Use:                Looks up a name for a character.  Specifically, it returns
+ *             the first entry in the @chartab@ table which matches @ch@ and
+ *             which has one of the flags @f@ set.
+ */
+
+static const char *find_charname(int ch, unsigned f)
+{
+  const struct chartab *ct;
+
+  for (ct = chartab; ct->name; ct++)
+    if (ct->ch == ch && (ct->f&f)) return (ct->name);
+  return (0);
+}
+
+/* --- @read_charname@ --- *
+ *
+ * Arguments:  @int *ch_out@ = where to put the character
+ *             @const char *p@ = character name
+ *             @unsigned f@ = flags (@TCF_...@)
+ *
+ * Returns:    Zero if a match was found, @-1@ if not.
+ *
+ * Use:                Looks up a character by name.  If @RCF_EOFOK@ is set in @f@,
+ *             then the @EOF@ marker can be matched; otherwise it can't.
+ */
+
+#define RCF_EOFOK 1u
+static int read_charname(int *ch_out, const char *p, unsigned f)
+{
+  const struct chartab *ct;
+
+  for (ct = chartab; ct->name; ct++)
+    if (STRCMP(p, ==, ct->name) && ((f&RCF_EOFOK) || ct->ch >= 0))
+      { *ch_out = ct->ch; return (0); }
+  return (-1);
+}
+
+/* --- @format_charesc@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @int ch@ = character to format
+ *             @unsigned f@ = flags (@FCF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Format a character as an escape sequence, possibly as part of
+ *             a larger string.  If @FCF_BRACE@ is set in @f@, then put
+ *             braces around a `\x...'  code, so that it's suitable for use
+ *             in a longer string.
+ */
+
+#define FCF_BRACE 1u
+static void format_charesc(const struct gprintf_ops *gops, void *go,
+                          int ch, unsigned f)
+{
+  switch (ch) {
+    case '\a': gprintf(gops, go, "\\a"); break;
+    case '\b': gprintf(gops, go, "\\b"); break;
+    case '\x1b': gprintf(gops, go, "\\e"); break;
+    case '\f': gprintf(gops, go, "\\f"); break;
+    case '\r': gprintf(gops, go, "\\r"); break;
+    case '\n': gprintf(gops, go, "\\n"); break;
+    case '\t': gprintf(gops, go, "\\t"); break;
+    case '\v': gprintf(gops, go, "\\v"); break;
+    case '\\': gprintf(gops, go, "\\\\"); break;
+    case '\'': gprintf(gops, go, "\\'"); break;
+    case '\0':
+      if (f&FCF_BRACE) gprintf(gops, go, "\\{0}");
+      else gprintf(gops, go, "\\0");
+      break;
+    default:
+      if (f&FCF_BRACE)
+       gprintf(gops, go, "\\x{%0*x}", hex_width(UCHAR_MAX), ch);
+      else
+       gprintf(gops, go, "\\x%0*x", hex_width(UCHAR_MAX), ch);
+      break;
+  }
+}
+
+/* --- @format_char@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @int ch@ = character to format
+ *
+ * Returns:    ---
+ *
+ * Use:                Format a single character.
+ */
+
+static void format_char(const struct gprintf_ops *gops, void *go, int ch)
+{
+  switch (ch) {
+    case '\\': case '\'': escape:
+      gprintf(gops, go, "'");
+      format_charesc(gops, go, ch, 0);
+      gprintf(gops, go, "'");
+      break;
+    default:
+      if (!isprint(ch)) goto escape;
+      gprintf(gops, go, "'%c'", ch);
+      break;
+  }
+}
+
+/* --- @maybe_format_unsigned_char@, @maybe_format_signed_char@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @unsigned long u@ or @long i@ = an integer
+ *
+ * Returns:    ---
+ *
+ * Use:                Format a (signed or unsigned) integer as a character, if it's
+ *             in range, printing something like `= 'q''.  It's assumed that
+ *             a comment marker has already been output.
+ */
+
+static void maybe_format_unsigned_char
+  (const struct gprintf_ops *gops, void *go, unsigned long u)
+{
+  const char *p;
+
+  p = find_charname(u, CTF_PREFER);
+  if (p) gprintf(gops, go, " = %s", p);
+  if (u < UCHAR_MAX)
+    { gprintf(gops, go, " = "); format_char(gops, go, u); }
+}
+
+static void maybe_format_signed_char
+  (const struct gprintf_ops *gops, void *go, long i)
+{
+  const char *p;
+
+  p = find_charname(i, CTF_PREFER);
+  if (p) gprintf(gops, go, " = %s", p);
+  if (0 <= i && i < UCHAR_MAX)
+    { gprintf(gops, go, " = "); format_char(gops, go, i); }
+}
+
+/* --- @read_charesc@ --- *
+ *
+ * Arguments:  @int *ch_out@ = where to put the result
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Parse and convert an escape sequence from @tv@'s input
+ *             stream, assuming that the initial `\' has already been read.
+ *             Reports errors as appropriate.
+ */
+
+static int read_charesc(int *ch_out, struct tvec_state *tv)
+{
+  int ch, i, esc;
+  unsigned f = 0;
+#define f_brace 1u
+
+  ch = getc(tv->fp);
+  switch (ch) {
+
+    /* Things we shouldn't find. */
+    case EOF: case '\n': return (tvec_syntax(tv, ch, "string escape"));
+
+    /* Single-character escapes. */
+    case '\'': *ch_out = '\''; break;
+    case '\\': *ch_out = '\\'; break;
+    case '"': *ch_out = '"'; break;
+    case 'a': *ch_out = '\a'; break;
+    case 'b': *ch_out = '\b'; break;
+    case 'e': *ch_out = '\x1b'; break;
+    case 'f': *ch_out = '\f'; break;
+    case 'n': *ch_out = '\n'; break;
+    case 'r': *ch_out = '\r'; break;
+    case 't': *ch_out = '\t'; break;
+    case 'v': *ch_out = '\v'; break;
+
+    /* Hex escapes, with and without braces. */
+    case 'x':
+      ch = getc(tv->fp);
+      if (ch == '{') { f |= f_brace; ch = getc(tv->fp); }
+      else f &= ~f_brace;
+      esc = chtodig(ch);
+      if (esc < 0 || esc >= 16) return (tvec_syntax(tv, ch, "hex digit"));
+      for (;;) {
+       ch = getc(tv->fp); i = chtodig(ch); if (i < 0 || i >= 16) break;
+       esc = 16*esc + i;
+       if (esc > UCHAR_MAX)
+         return (tvec_error(tv,
+                            "character code %d out of range", esc));
+      }
+      if (!(f&f_brace)) ungetc(ch, tv->fp);
+      else if (ch != '}') return (tvec_syntax(tv, ch, "`}'"));
+      *ch_out = esc;
+      break;
+
+    /* Other things, primarily octal escapes. */
+    case '{':
+      f |= f_brace; ch = getc(tv->fp);
+      /* fall through */
+    default:
+      if ('0' <= ch && ch < '8') {
+       i = 1; esc = ch - '0';
+       for (;;) {
+         ch = getc(tv->fp);
+         if ('0' > ch || ch >= '8') { ungetc(ch, tv->fp); break; }
+         esc = 8*esc + ch - '0';
+         i++; if (i >= 3) break;
+       }
+       if (f&f_brace) {
+         ch = getc(tv->fp);
+         if (ch != '}') return (tvec_syntax(tv, ch, "`}'"));
+       }
+       if (esc > UCHAR_MAX)
+         return (tvec_error(tv,
+                            "character code %d out of range", esc));
+       *ch_out = esc; break;
+      } else
+       return (tvec_syntax(tv, ch, "string escape"));
+  }
+
+  /* Done. */
+  return (0);
+
+#undef f_brace
+}
+
+/* --- @read_quoted_string@ --- *
+ *
+ * Arguments:  @dstr *d@ = string to write to
+ *             @int quote@ = initial quote, `'' or `"'
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Read the rest of a quoted string into @d@, reporting errors
+ *             as appropriate.
+ *
+ *             A single-quoted string is entirely literal.  A double-quoted
+ *             string may contain C-like escapes.
+ */
+
+static int read_quoted_string(dstr *d, int quote, struct tvec_state *tv)
+{
+  int ch;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    switch (ch) {
+      case EOF: case '\n':
+       return (tvec_syntax(tv, ch, "`%c'", quote));
+      case '\\':
+       if (quote == '\'') goto ordinary;
+       ch = getc(tv->fp); if (ch == '\n') { tv->lno++; break; }
+       ungetc(ch, tv->fp); if (read_charesc(&ch, tv)) return (-1);
+       goto ordinary;
+      default:
+       if (ch == quote) goto end;
+      ordinary:
+       DPUTC(d, ch);
+       break;
+    }
+  }
+
+end:
+  DPUTZ(d);
+  return (0);
+}
+
+/* --- @collect_bare@ --- *
+ *
+ * Arguments:  @dstr *d@ = string to write to
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Read barewords and the whitespace between them.  Stop when we
+ *             encounter something which can't start a bareword.
+ */
+
+static int collect_bare(dstr *d, struct tvec_state *tv)
+{
+  size_t pos = d->len;
+  enum { WORD, SPACE, ESCAPE }; unsigned s = WORD;
+  int ch, rc;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    switch (ch) {
+      case EOF:
+       tvec_syntax(tv, ch, "bareword");
+       rc = -1; goto end;
+      case '\n':
+       if (s == ESCAPE) { tv->lno++; goto addch; }
+       if (s == WORD) pos = d->len;
+       ungetc(ch, tv->fp); if (tvec_nexttoken(tv)) { rc = -1; goto end; }
+       DPUTC(d, ' '); s = SPACE;
+       break;
+      case '"': case '\'': case '!': case '#': case ')': case '}': case ']':
+       if (s == SPACE) { ungetc(ch, tv->fp); goto done; }
+       goto addch;
+      case '\\':
+       s = ESCAPE;
+       break;
+      default:
+       if (s != ESCAPE && isspace(ch)) {
+         if (s == WORD) pos = d->len;
+         DPUTC(d, ch); s = SPACE;
+         break;
+       }
+      addch:
+       DPUTC(d, ch); s = WORD;
+    }
+  }
+
+done:
+  if (s == SPACE) d->len = pos;
+  DPUTZ(d); rc = 0;
+end:
+  return (rc);
+}
+
+/* --- @set_up_encoding@ --- *
+ *
+ * Arguments:  @const codec_class **ccl_out@ = where to put the class
+ *             @unsigned *f_out@ = where to put the flags
+ *             @unsigned code@ = the coding scheme to use (@TVEC_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Helper for @read_compound_string@ below.
+ *
+ *             Return the appropriate codec class and flags for @code@.
+ *             Leaves @*ccl_out@ null if the coding scheme doesn't have a
+ *             backing codec class (e.g., @TVCODE_BARE@).
+ */
+
+enum { TVCODE_BARE, TVCODE_HEX, TVCODE_BASE64, TVCODE_BASE32 };
+static void set_up_encoding(const codec_class **ccl_out, unsigned *f_out,
+                           unsigned code)
+{
+  switch (code) {
+    case TVCODE_BARE:
+      *ccl_out = 0; *f_out = 0;
+      break;
+    case TVCODE_HEX:
+      *ccl_out = &hex_class; *f_out = CDCF_IGNCASE;
+      break;
+    case TVCODE_BASE32:
+      *ccl_out = &base32_class; *f_out = CDCF_IGNCASE | CDCF_IGNEQPAD;
+      break;
+    case TVCODE_BASE64:
+      *ccl_out = &base64_class; *f_out = CDCF_IGNEQPAD;
+      break;
+    default:
+      abort();
+  }
+}
+
+/* --- @flush_codec@ --- *
+ *
+ * Arguments:  @codec *cdc@ = a codec, or null
+ *             @dstr *d@ = output string
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Helper for @read_compound_string@ below.
+ *
+ *             Flush out any final buffered material from @cdc@, and check
+ *             that it's in a good state.  Frees the codec on success.  Does
+ *             nothing if @cdc@ is null.
+ */
+
+static int flush_codec(codec *cdc, dstr *d, struct tvec_state *tv)
+{
+  int err;
+
+  if (cdc) {
+    err = cdc->ops->code(cdc, 0, 0, d);
+    if (err)
+      return (tvec_error(tv, "invalid %s sequence end: %s",
+                        cdc->ops->c->name, codec_strerror(err)));
+    cdc->ops->destroy(cdc);
+  }
+  return (0);
+}
+
+/* --- @read_compound_string@ --- *
+ *
+ * Arguments:  @void **p_inout@ = address of output buffer pointer
+ *             @size_t *sz_inout@ = address of buffer size
+ *             @unsigned code@ = initial interpretation of barewords
+ *             @unsigned f@ = other flags (@RCSF_...@)
+ *             @struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Parse a compound string, i.e., a sequence of stringish pieces
+ *             which might be quoted strings, character names, or barewords
+ *             to be decoded accoding to @code@, interspersed with
+ *             additional directives.
+ *
+ *             If the initial buffer pointer is non-null and sufficiently
+ *             large, then it will be reused; otherwise, it is freed and a
+ *             fresh, sufficiently large buffer is allocated and returned.
+ */
+
+#define RCSF_NESTED 1u
+static int read_compound_string(void **p_inout, size_t *sz_inout,
+                               unsigned code, unsigned f,
+                               struct tvec_state *tv)
+{
+  const codec_class *ccl; unsigned cdf;
+  codec *cdc;
+  dstr d = DSTR_INIT, w = DSTR_INIT;
+  char *p;
+  const char *q;
+  void *pp = 0; size_t sz;
+  unsigned long n;
+  int ch, err, rc;
+
+  set_up_encoding(&ccl, &cdf, code); cdc = 0;
+
+  if (tvec_nexttoken(tv)) return (tvec_syntax(tv, fgetc(tv->fp), "string"));
+  do {
+    ch = getc(tv->fp);
+    switch (ch) {
+
+      case ')': case ']': case '}':
+       /* Close brackets.  Leave these for recursive caller if there is one,
+        * or just complain.
+        */
+
+       if (!(f&RCSF_NESTED))
+         { rc = tvec_syntax(tv, ch, "string"); goto end; }
+       ungetc(ch, tv->fp); goto done;
+
+      case '"': case '\'':
+       /* Quotes.  Read a quoted string. */
+
+       if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; }
+       cdc = 0;
+       if (read_quoted_string(&d, ch, tv)) { rc = -1; goto end; }
+       break;
+
+      case '#':
+       /* A named character. */
+
+       ungetc(ch, tv->fp);
+       if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; }
+       cdc = 0;
+       DRESET(&w); tvec_readword(tv, &w, 0, ";", "character name");
+       if (STRCMP(w.buf, ==, "#empty")) break;
+       if (read_charname(&ch, w.buf, RCF_EOFOK)) {
+         rc = tvec_error(tv, "unknown character name `%s'", d.buf);
+         goto end;
+       }
+       DPUTC(&d, ch); break;
+
+      case '!':
+       /* A magic keyword. */
+
+       if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; }
+       cdc = 0;
+       ungetc(ch, tv->fp);
+       DRESET(&w); tvec_readword(tv, &w, 0, ";", "`!'-keyword");
+
+       /* Change bareword coding system. */
+       if (STRCMP(w.buf, ==, "!bare"))
+         { code = TVCODE_BARE; set_up_encoding(&ccl, &cdf, code); }
+       else if (STRCMP(w.buf, ==, "!hex"))
+         { code = TVCODE_HEX; set_up_encoding(&ccl, &cdf, code); }
+       else if (STRCMP(w.buf, ==, "!base32"))
+         { code = TVCODE_BASE32; set_up_encoding(&ccl, &cdf, code); }
+       else if (STRCMP(w.buf, ==, "!base64"))
+         { code = TVCODE_BASE64; set_up_encoding(&ccl, &cdf, code); }
+
+       /* Repeated substrings. */
+       else if (STRCMP(w.buf, ==, "!repeat")) {
+         if (tvec_nexttoken(tv)) {
+           rc = tvec_syntax(tv, fgetc(tv->fp), "repeat count");
+           goto end;
+         }
+         DRESET(&w);
+         if (tvec_readword(tv, &w, 0, ";{", "repeat count"))
+           { rc = -1; goto end;  }
+         if (parse_unsigned_integer(&n, &q, w.buf)) {
+           rc = tvec_error(tv, "invalid repeat count `%s'", w.buf);
+           goto end;
+         }
+         if (*q) { rc = tvec_syntax(tv, *q, "`{'"); goto end; }
+         if (tvec_nexttoken(tv))
+           { rc = tvec_syntax(tv, fgetc(tv->fp), "`{'"); goto end; }
+         ch = getc(tv->fp); if (ch != '{')
+           { rc = tvec_syntax(tv, ch, "`{'"); goto end; }
+         sz = 0;
+         if (read_compound_string(&pp, &sz, code, f | RCSF_NESTED, tv))
+           { rc = -1; goto end; }
+         ch = getc(tv->fp); if (ch != '}')
+           { rc = tvec_syntax(tv, ch, "`}'"); goto end; }
+         if (sz) {
+           if (n > (size_t)-1/sz)
+             { rc = tvec_error(tv, "repeat size out of range"); goto end; }
+           dstr_ensure(&d, n*sz);
+           if (sz == 1)
+             { memset(d.buf + d.len, *(unsigned char *)pp, n); d.len += n; }
+           else
+             for (; n--; d.len += sz) memcpy(d.buf + d.len, pp, sz);
+         }
+         xfree(pp); pp = 0;
+       }
+
+       /* Anything else is an error. */
+       else {
+         tvec_error(tv, "unknown string keyword `%s'", w.buf);
+         rc = -1; goto end;
+       }
+       break;
+
+      default:
+       /* A bareword.  Process it according to the current coding system. */
+
+       switch (code) {
+         case TVCODE_BARE:
+           ungetc(ch, tv->fp);
+           if (collect_bare(&d, tv)) goto done;
+           break;
+         default:
+           assert(ccl);
+           ungetc(ch, tv->fp); DRESET(&w);
+           if (tvec_readword(tv, &w, 0, ";",
+                             "%s-encoded fragment", ccl->name))
+             { rc = -1; goto end; }
+           if (!cdc) cdc = ccl->decoder(cdf);
+           err = cdc->ops->code(cdc, w.buf, w.len, &d);
+           if (err) {
+             tvec_error(tv, "invalid %s fragment `%s': %s",
+                        ccl->name, w.buf, codec_strerror(err));
+             rc = -1; goto end;
+           }
+           break;
+       }
+       break;
+    }
+  } while (!tvec_nexttoken(tv));
+
+done:
+  /* Wrap things up. */
+  if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; }
+  cdc = 0;
+  if (*sz_inout <= d.len)
+    { xfree(*p_inout); *p_inout = xmalloc(d.len + 1); }
+  p = *p_inout; memcpy(p, d.buf, d.len); p[d.len] = 0; *sz_inout = d.len;
+  rc = 0;
+
+end:
+  /* Clean up any debris. */
+  if (cdc) cdc->ops->destroy(cdc);
+  if (pp) xfree(pp);
+  dstr_destroy(&d); dstr_destroy(&w);
+  return (rc);
+}
+
+/*----- Signed and unsigned integer types ---------------------------------*/
+
+/* --- @init_int@, @init_uint@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a register value.
+ *
+ *             Integer values are initialized to zero.
+ */
+
+static void init_int(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->i = 0; }
+
+static void init_uint(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->u = 0; }
+
+/* --- @eq_int@, @eq_uint@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv0, *rv1@ = register values
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Nonzero if the values are equal, zero if unequal
+ *
+ * Use:                Compare register values for equality.
+ */
+
+static int eq_int(const union tvec_regval *rv0, const union tvec_regval *rv1,
+                 const struct tvec_regdef *rd)
+  { return (rv0->i == rv1->i); }
+
+static int eq_uint(const union tvec_regval *rv0,
+                  const union tvec_regval *rv1,
+                  const struct tvec_regdef *rd)
+  { return (rv0->u == rv1->u); }
+
+/* --- @tobuf_int@, @tobuf_uint@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Serialize a register value to a buffer.
+ *
+ *             Integer values are serialized as little-endian 64-bit signed
+ *             or unsigned integers.
+ */
+
+static int tobuf_int(buf *b, const union tvec_regval *rv,
+                    const struct tvec_regdef *rd)
+  { return (signed_to_buf(b, rv->i)); }
+
+static int tobuf_uint(buf *b, const union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+  { return (unsigned_to_buf(b, rv->u)); }
+
+/* --- @frombuf_int@, @frombuf_uint@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Deserialize a register value from a buffer.
+ *
+ *             Integer values are serialized as 64-bit signed or unsigned
+ *             integers.
+ */
+
+static int frombuf_int(buf *b, union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+  { return (signed_from_buf(b, &rv->i)); }
+
+static int frombuf_uint(buf *b, union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+  { return (unsigned_from_buf(b, &rv->u)); }
+
+/* --- @parse_int@, @parse_uint@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             Integers may be input in decimal, hex, binary, or octal,
+ *             following approximately usual conventions.
+ *
+ *               * Signed integers may be preceded with a `+' or `-' sign.
+ *
+ *               * Decimal integers are just a sequence of decimal digits
+ *                 `0' ... `9'.
+ *
+ *               * Octal integers are a sequence of digits `0' ... `7',
+ *                 preceded by `0o' or `0O'.
+ *
+ *               * Hexadecimal integers are a sequence of digits `0'
+ *                 ... `9', `a' ... `f', or `A' ... `F', preceded by `0x' or
+ *                 `0X'.
+ *
+ *               * Radix-B integers are a sequence of digits `0' ... `9',
+ *                 `a' ... `f', or `A' ... `F', each with value less than B,
+ *                 preceded by `Br' or `BR', where 0 < B < 36 is expressed
+ *                 in decimal without any leading `0' or internal
+ *                 underscores `_'.
+ *
+ *               * A digit sequence may contain internal underscore `_'
+ *                 separators, but not before or after all of the digits;
+ *                 and two consecutive `_' characters are not permitted.
+ */
+
+static int parse_int(union tvec_regval *rv, const struct tvec_regdef *rd,
+                    struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+  int rc;
+
+  if (tvec_readword(tv, &d, 0, ";", "signed integer"))
+    { rc = -1; goto end; }
+  if (parse_signed(&rv->i, d.buf, rd->arg.p, tv)) { rc = -1; goto end; }
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
+}
+
+static int parse_uint(union tvec_regval *rv, const struct tvec_regdef *rd,
+                     struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+  int rc;
+
+  if (tvec_readword(tv, &d, 0, ";", "unsigned integer"))
+    { rc = -1; goto end; }
+  if (parse_unsigned(&rv->u, d.buf, rd->arg.p, tv)) { rc = -1; goto end; }
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
 }
 
+/* --- @dump_int@, @dump_uint@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Integer values are dumped in decimal and, unless compact
+ *             output is requested, hex, and maybe a character, as a
+ *             comment.
+ */
+
 static void dump_int(const union tvec_regval *rv,
                     const struct tvec_regdef *rd,
-                    struct tvec_state *tv, unsigned style)
+                    unsigned style,
+                    const struct gprintf_ops *gops, void *go)
 {
-  unsigned long u;
+  if (style&TVSF_RAW) gprintf(gops, go, "int:");
+  gprintf(gops, go, "%ld", rv->i);
+  if (!(style&(TVSF_COMPACT | TVSF_RAW))) {
+    gprintf(gops, go, " ; = ");
+    format_signed_hex(gops, go, rv->i);
+    maybe_format_signed_char(gops, go, rv->i);
+  }
+}
+
+static void dump_uint(const union tvec_regval *rv,
+                     const struct tvec_regdef *rd,
+                     unsigned style,
+                     const struct gprintf_ops *gops, void *go)
+{
+  if (style&TVSF_RAW) gprintf(gops, go, "uint:");
+  gprintf(gops, go, "%lu", rv->u);
+  if (!(style&(TVSF_COMPACT | TVSF_RAW))) {
+    gprintf(gops, go, " ; = ");
+    format_unsigned_hex(gops, go, rv->u);
+    maybe_format_unsigned_char(gops, go, rv->u);
+  }
+}
+
+/* Integer type definitions. */
+const struct tvec_regty tvty_int = {
+  init_int, trivial_release, eq_int,
+  tobuf_int, frombuf_int,
+  parse_int, dump_int
+};
+const struct tvec_regty tvty_uint = {
+  init_uint, trivial_release, eq_uint,
+  tobuf_uint, frombuf_uint,
+  parse_uint, dump_uint
+};
+
+/* Predefined integer ranges. */
+const struct tvec_irange
+  tvrange_schar = { SCHAR_MIN, SCHAR_MAX },
+  tvrange_short = { SHRT_MIN, SHRT_MAX },
+  tvrange_int = { INT_MIN, INT_MAX },
+  tvrange_long = { LONG_MIN, LONG_MAX },
+  tvrange_sbyte = { -128, 127 },
+  tvrange_i16 = { -32768, +32767 },
+  tvrange_i32 = { -2147483648, 2147483647 };
+const struct tvec_urange
+  tvrange_uchar = { 0, UCHAR_MAX },
+  tvrange_ushort = { 0, USHRT_MAX },
+  tvrange_uint = { 0, UINT_MAX },
+  tvrange_ulong = { 0, ULONG_MAX },
+  tvrange_size = { 0, (size_t)-1 },
+  tvrange_byte = { 0, 255 },
+  tvrange_u16 = { 0, 65535 },
+  tvrange_u32 = { 0, 4294967295 };
+
+/* --- @tvec_claimeq_int@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @long i0, i1@ = two signed integers
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @i0@ and @i1@ are equal, otherwise zero.
+ *
+ * Use:                Check that values of @i0@ and @i1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @i0@ is printed as the output
+ *             value and @i1@ is printed as the input reference.
+ */
+
+int tvec_claimeq_int(struct tvec_state *tv, long i0, long i1,
+                    const char *file, unsigned lno, const char *expr)
+{
+  tv->out[0].v.i = i0; tv->in[0].v.i = i1;
+  return (tvec_claimeq(tv, &tvty_int, 0, file, lno, expr));
+}
+
+/* --- @tvec_claimeq_uint@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned long u0, u1@ = two unsigned integers
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @u0@ and @u1@ are equal, otherwise zero.
+ *
+ * Use:                Check that values of @u0@ and @u1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @u0@ is printed as the output
+ *             value and @u1@ is printed as the input reference.
+ */
+
+int tvec_claimeq_uint(struct tvec_state *tv,
+                     unsigned long u0, unsigned long u1,
+                     const char *file, unsigned lno, const char *expr)
+{
+  tv->out[0].v.u = u0; tv->in[0].v.u = u1;
+  return (tvec_claimeq(tv, &tvty_uint, 0, file, lno, expr));
+}
+
+/*----- Size type ---------------------------------------------------------*/
+
+/* --- @parse_size@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             The input format for a size value consists of an unsigned
+ *             integer followed by an optional unit specifier consisting of
+ *             an SI unit prefix and (optionally) the letter `B'. */
+
+static int parse_size(union tvec_regval *rv, const struct tvec_regdef *rd,
+                     struct tvec_state *tv)
+{
+  unsigned long sz;
+  int rc;
+
+  if (parse_szint(tv, &sz, ";", "size")) { rc = -1; goto end; }
+  if (check_unsigned_range(sz, rd->arg.p, tv, "size")) { rc = -1; goto end; }
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rv->u = sz; rc = 0;
+end:
+  return (rc);
+}
+
+/* --- @dump_size@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Size values are dumped with a unit specifier, with a unit
+ *             prefox only if the size is an exact multiple of the relevant
+ *             power of two.  Unless compact style is requested, the plain
+ *             decimal and hex representations of the value are also
+ *             printed.
+ */
+
+static void dump_size(const union tvec_regval *rv,
+                     const struct tvec_regdef *rd,
+                     unsigned style,
+                     const struct gprintf_ops *gops, void *go)
+{
+  if (style&TVSF_RAW) gprintf(gops, go, "size:");
+  format_size(gops, go, rv->u, style);
+  if (!(style&(TVSF_COMPACT | TVSF_RAW))) {
+    gprintf(gops, go, " ; = %lu", (unsigned long)rv->u);
+    gprintf(gops, go, " = "); format_unsigned_hex(gops, go, rv->u);
+    maybe_format_unsigned_char(gops, go, rv->u);
+  }
+}
+
+/* Size type definitions. */
+const struct tvec_regty tvty_size = {
+  init_uint, trivial_release, eq_uint,
+  tobuf_uint, frombuf_uint,
+  parse_size, dump_size
+};
+
+/* --- @tvec_claimeq_size@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned long sz0, sz1@ = two sizes
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @sz0@ and @sz1@ are equal, otherwise zero.
+ *
+ * Use:                Check that values of @u0@ and @u1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @u0@ is printed as the output
+ *             value and @u1@ is printed as the input reference.
+ */
+
+int tvec_claimeq_size(struct tvec_state *tv,
+                     unsigned long sz0, unsigned long sz1,
+                     const char *file, unsigned lno, const char *expr)
+{
+  tv->out[0].v.u = sz0; tv->in[0].v.u = sz1;
+  return (tvec_claimeq(tv, &tvty_size, 0, file, lno, expr));
+}
+
+/*----- Floating-point type -----------------------------------------------*/
+
+/* --- @int_float@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a register value.
+ *
+ *             Floating-point values are initialized to zero.
+ */
+
+static void init_float(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->f = 0.0; }
+
+/* --- @eq_float@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv0, *rv1@ = register values
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Nonzero if the values are equal, zero if unequal
+ *
+ * Use:                Compare register values for equality.
+ *
+ *             Floating-point values may be considered equal if their
+ *             absolute or relative difference is sufficiently small, as
+ *             described in the register definition.
+ */
+
+static int eq_float(const union tvec_regval *rv0,
+                   const union tvec_regval *rv1,
+                   const struct tvec_regdef *rd)
+  { return (eqish_floating_p(rv0->f, rv1->f, rd->arg.p)); }
+
+/* --- @tobuf_float@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Serialize a register value to a buffer.
+ *
+ *             Floating-point values are serialized as little-endian
+ *             IEEE 754 Binary64.
+ */
+
+static int tobuf_float(buf *b, const union tvec_regval *rv,
+                    const struct tvec_regdef *rd)
+  { return (buf_putf64l(b, rv->f)); }
+
+/* --- @frombuf_float@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Deserialize a register value from a buffer.
+ *
+ *             Floating-point values are serialized as little-endian
+ *             IEEE 754 Binary64.
+ */
+
+static int frombuf_float(buf *b, union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+  { return (buf_getf64l(b, &rv->f)); }
+
+/* --- @parse_float@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             Floating-point values are either NaN (%|#nan|%, if supported
+ *             by the platform); positive or negative infinity (%|#inf|%,
+ *             %|+#inf|%, or %|#+inf|% (preferring the last), and %|-#inf|%
+ *             or %|#-inf|% (preferring the latter), if supported by the
+ *             platform); or a number in strtod(3) syntax.
+ */
+
+static int parse_float(union tvec_regval *rv, const struct tvec_regdef *rd,
+                      struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+  int rc;
+
+  if (tvec_readword(tv, &d, 0, ";", "floating-point number"))
+    { rc = -1; goto end; }
+  if (parse_floating(&rv->f, 0, d.buf, rd->arg.p, tv))
+    { rc = -1; goto end; }
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
+}
+
+/* --- @dump_float@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Floating-point values are dumped in decimal or as a special
+ *             token beginning with `%|#|%'.  Some effort is taken to ensure
+ *             that the output is sufficient to uniquely identify the
+ *             original value, but, honestly, C makes this really hard.
+ */
+
+static void dump_float(const union tvec_regval *rv,
+                      const struct tvec_regdef *rd,
+                      unsigned style,
+                      const struct gprintf_ops *gops, void *go)
+{
+  if (style&TVSF_RAW) gprintf(gops, go, "float:");
+  format_floating(gops, go, rv->f);
+}
+
+/* Floating-point type definition. */
+const struct tvec_regty tvty_float = {
+  init_float, trivial_release, eq_float,
+  tobuf_float, frombuf_float,
+  parse_float, dump_float
+};
+
+/* Predefined floating-point ranges. */
+const struct tvec_floatinfo
+  tvflt_finite = { TVFF_EXACT, -DBL_MAX, DBL_MAX, 0.0 },
+  tvflt_nonneg = { TVFF_EXACT, 0, DBL_MAX, 0.0 };
+
+/* --- @tvec_claimeqish_float@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @double f0, f1@ = two floating-point numbers
+ *             @unsigned f@ = flags (@TVFF_...@)
+ *             @double delta@ = maximum tolerable difference
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @f0@ and @f1@ are sufficiently close, otherwise
+ *             zero.
+ *
+ * Use:                Check that values of @f0@ and @f1@ are sufficiently close.
+ *             As for @tvec_claim@ above, a test case is automatically begun
+ *             and ended if none is already underway.  If the values are
+ *             too far apart, then @tvec_fail@ is called, quoting @expr@,
+ *             and the mismatched values are dumped: @f0@ is printed as the
+ *             output value and @f1@ is printed as the input reference.
+ *
+ *             The details for the comparison are as follows.
+ *
+ *               * A NaN value matches any other NaN, and nothing else.
+ *
+ *               * An infinity matches another infinity of the same sign,
+ *                 and nothing else.
+ *
+ *               * If @f&TVFF_EQMASK@ is @TVFF_EXACT@, then any
+ *                 representable number matches only itself: in particular,
+ *                 positive and negative zero are considered distinct.
+ *                 (This allows tests to check that they land on the correct
+ *                 side of branch cuts, for example.)
+ *
+ *               * If @f&TVFF_EQMASK@ is @TVFF_ABSDELTA@, then %$x$% matches
+ *                 %$y$% when %$|x - y| < \delta$%.
+ *
+ *               * If @f&TVFF_EQMASK@ is @TVFF_RELDELTA@, then %$x$% matches
+ *                 %$y$% when %$|1 - x/y| < \delta$%.  (Note that this
+ *                 criterion is asymmetric.  Write %$x \approx_\delta y$%
+ *                 if and only if %$|1 - x/y < \delta$%.  Then, for example,
+ *                 if %$y/(1 + \delta) < x < y (1 - \delta)$%, then
+ *                 %$x \approx_\delta y$%, but %$y \not\approx_\delta x$%.)
+ */
+
+int tvec_claimeqish_float(struct tvec_state *tv,
+                         double f0, double f1, unsigned f, double delta,
+                         const char *file, unsigned lno,
+                         const char *expr)
+{
+  struct tvec_floatinfo fi;
+  union tvec_misc arg;
+
+  fi.f = f; fi.min = fi.max = 0.0; fi.delta = delta; arg.p = &fi;
+  tv->out[0].v.f = f0; tv->in[0].v.f = f1;
+  return (tvec_claimeq(tv, &tvty_float, &arg, file, lno, expr));
+}
+
+/* --- @tvec_claimeq_float@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @double f0, f1@ = two floating-point numbers
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @f0@ and @f1@ are identical, otherwise zero.
+ *
+ * Use:                Check that values of @f0@ and @f1@ are identical.  The
+ *             function is exactly equivalent to @tvec_claimeqish_float@
+ *             with @f == TVFF_EXACT@.
+ */
+
+int tvec_claimeq_float(struct tvec_state *tv,
+                      double f0, double f1,
+                      const char *file, unsigned lno,
+                      const char *expr)
+{
+  return (tvec_claimeqish_float(tv, f0, f1, TVFF_EXACT, 0.0,
+                               file, lno, expr));
+}
+
+/*----- Durations ---------------------------------------------------------*/
+
+/* A duration is a floating-point number of seconds.  Initialization and
+ * teardown, equality comparison, and serialization are as for floating-point
+ * values.
+ */
+
+static const struct duration_unit {
+  const char *unit;
+  double scale;
+  unsigned f;
+#define DUF_PREFER 1u
+} duration_units[] = {
+  { "Ys",      1e+24,          0 },
+  { "Zs",      1e+21,          0 },
+  { "Es",      1e+18,          0 },
+  { "Ps",      1e+15,          0 },
+  { "Ts",      1e+12,          0 },
+  { "Gs",      1e+9,           0 },
+  { "Ms",      1e+6,           0 },
+  { "ks",      1e+3,           0 },
+  { "hs",      1e+2,           0 },
+  { "das",     1e+1,           0 },
+
+  { "yr",      31557600.0,     DUF_PREFER },
+  { "y",       31557600.0,     0 },
+  { "day",     86400.0,        DUF_PREFER },
+  { "dy",      86400.0,        0 },
+  { "d",       86400.0,        0 },
+  { "hr",      3600.0,         DUF_PREFER },
+  { "hour",    3600.0,         0 },
+  { "h",       3600.0,         0 },
+  { "min",     60.0,           DUF_PREFER },
+  { "m",       60.0,           0 },
+
+  { "s",       1.0,            DUF_PREFER },
+  { "sec",     1.0,            0 },
+
+  { "ds",      1e-1,           0 },
+  { "cs",      1e-2,           0 },
+  { "ms",      1e-3,           DUF_PREFER },
+  { "µs",     1e-6,           DUF_PREFER },
+  { "ns",      1e-9,           DUF_PREFER },
+  { "ps",      1e-12,          DUF_PREFER },
+  { "fs",      1e-15,          DUF_PREFER },
+  { "as",      1e-18,          DUF_PREFER },
+  { "zs",      1e-21,          DUF_PREFER },
+  { "ys",      1e-24,          DUF_PREFER },
+
+  { 0 }
+};
+
+/* --- @tvec_parsedurunit@ --- *
+ *
+ * Arguments:  @double *scale_out@ = where to leave the scale
+ *             @const char **p_inout@ = input unit string, updated
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                If @*p_inout@ begins with a unit string followed by the end
+ *             of the string or some non-alphanumeric character, then store
+ *             the corresponding scale factor in @*scale_out@, advance
+ *             @*p_inout@ past the unit string, and return zero.  Otherwise,
+ *             return %$-1$%.
+ */
+
+int tvec_parsedurunit(double *scale_out, const char **p_inout)
+{
+  const char *p = *p_inout, *q;
+  const struct duration_unit *u;
+  size_t n;
+
+  while (ISSPACE(*p)) p++;
+  for (q = p; *q && ISALNUM(*q); q++);
+  n = q - p; if (!n) { *scale_out = 1.0; return (0); }
+
+  for (u = duration_units; u->unit; u++)
+    if (STRNCMP(p, ==, u->unit, n) && !u->unit[n])
+      { *scale_out = u->scale; *p_inout = q; return (0); }
+  return (-1);
+}
+
+/* --- @parse_duration@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             Duration values are finite nonnegative floating-point
+ *             numbers in @strtod@ syntax, optionally followed by a unit .
+ */
+
+static int parse_duration(union tvec_regval *rv,
+                         const struct tvec_regdef *rd,
+                         struct tvec_state *tv)
+{
+  const struct duration_unit *u;
+  const char *q;
+  dstr d = DSTR_INIT;
+  double t;
+  int rc;
+
+  if (tvec_readword(tv, &d, 0, ";", "duration")) { rc = -1; goto end; }
+  if (parse_floating(&t, &q, d.buf,
+                    rd->arg.p ? rd->arg.p : &tvflt_nonneg, tv))
+    { rc = -1; goto end; }
+
+  if (!*q) tvec_readword(tv, &d, &q, ";", 0);
+  if (*q) {
+    for (u = duration_units; u->unit; u++)
+      if (STRCMP(q, ==, u->unit)) { t *= u->scale; goto found_unit; }
+    rc = tvec_syntax(tv, *q, "end-of-line"); goto end;
+  found_unit:;
+  }
+
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rv->f = t; rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
+}
+
+/* --- @dump_duration@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Durations are dumped as a human-palatable scaled value with
+ *             unit, and, if compact style is not requested, as a raw number
+ *             of seconds at full precision as a comment.
+ */
+
+static void dump_duration(const union tvec_regval *rv,
+                         const struct tvec_regdef *rd,
+                         unsigned style,
+                         const struct gprintf_ops *gops, void *go)
+{
+  const struct duration_unit *u;
+  double t = rv->f;
+
+  if (style&TVSF_RAW) {
+    gprintf(gops, go, "duration:");
+    format_floating(gops, go, rv->f);
+    gprintf(gops, go, "s");
+  } else {
+    if (!t) u = 0;
+    else {
+      for (u = duration_units; u->scale > t && u[1].unit; u++);
+      t /= u->scale;
+    }
+    gprintf(gops, go, "%.4g %s", t, u ? u->unit : "s");
+
+    if (!(style&TVSF_COMPACT)) {
+      gprintf(gops, go, "; = ");
+      format_floating(gops, go, rv->f);
+      gprintf(gops, go, " s");
+    }
+  }
+}
+
+/* Duration type definition. */
+const struct tvec_regty tvty_duration = {
+  init_float, trivial_release, eq_float,
+  tobuf_float, frombuf_float,
+  parse_duration, dump_duration
+};
+
+/* --- @tvec_claimeqish_duration@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @double to, t1@ = two durations
+ *             @unsigned f@ = flags (@TVFF_...@)
+ *             @double delta@ = maximum tolerable difference
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @t0@ and @t1@ are sufficiently close, otherwise
+ *             zero.
+ *
+ * Use:                Check that values of @t0@ and @t1@ are sufficiently close.
+ *             This is essentially the same as @tvec_claimeqish_float@, only
+ *             it dumps the values as durations on a mismatch.
+ */
+
+int tvec_claimeqish_duration(struct tvec_state *tv,
+                            double t0, double t1, unsigned f, double delta,
+                            const char *file, unsigned lno,
+                            const char *expr)
+{
+  struct tvec_floatinfo fi;
+  union tvec_misc arg;
 
-  tvec_write(tv, "%ld", rv->i);
-  if (!(style&TVSF_COMPACT)) {
-    if (rv->i >= 0) u = rv->i;
-    else u = -(unsigned long)rv->i;
-    tvec_write(tv, " ; = %s0x%0*lx", rv->i < 0 ? "-" : "", hex_width(u), u);
-  }
+  fi.f = f; fi.min = fi.max = 0.0; fi.delta = delta; arg.p = &fi;
+  tv->out[0].v.f = t0; tv->in[0].v.f = t1;
+  return (tvec_claimeq(tv, &tvty_duration, &arg, file, lno, expr));
 }
 
-static void dump_uint(const union tvec_regval *rv,
-                     const struct tvec_regdef *rd,
-                     struct tvec_state *tv, unsigned style)
+/* --- @tvec_claimeq_duration@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @double t0, t1@ = two durations
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @t0@ and @t1@ are identical, otherwise zero.
+ *
+ * Use:                Check that values of @t0@ and @t1@ are identical.  The
+ *             function is exactly equivalent to @tvec_claimeqish_duration@
+ *             with @f == TVFF_EXACT@.
+ */
+
+int tvec_claimeq_duration(struct tvec_state *tv,
+                         double t0, double t1,
+                         const char *file, unsigned lno,
+                         const char *expr)
 {
-  tvec_write(tv, "%lu", rv->u);
-  if (!(style&TVSF_COMPACT))
-    tvec_write(tv, " ; = 0x%0*lx", hex_width(rv->u), rv->u);
+  return (tvec_claimeqish_duration(tv, t0, t1, TVFF_EXACT, 0.0,
+                                  file, lno, expr));
 }
 
-const struct tvec_regty tvty_int = {
-  init_int, release_int, eq_int, measure_int,
-  tobuf_int, frombuf_int,
-  parse_int, dump_int
-};
+/*----- Enumerations ------------------------------------------------------*/
 
-const struct tvec_irange
-  tvrange_schar = { SCHAR_MIN, SCHAR_MAX },
-  tvrange_short = { SHRT_MIN, SHRT_MAX },
-  tvrange_int = { INT_MIN, INT_MAX },
-  tvrange_long = { LONG_MIN, LONG_MAX },
-  tvrange_sbyte = { -128, 127 },
-  tvrange_i16 = { -32768, +32767 },
-  tvrange_i32 = { -2147483648, 2147483647 };
+/* --- @init_tenum@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a register value.
+ *
+ *             Integer and floating-point enumeration values are initialized
+ *             as their underlying representations.  Pointer enumerations
+ *             are initialized to %|#nil|%.
+ */
 
-const struct tvec_regty tvty_uint = {
-  init_uint, release_int, eq_uint, measure_int,
-  tobuf_uint, frombuf_uint,
-  parse_uint, dump_uint
-};
+#define init_ienum init_int
+#define init_uenum init_uint
+#define init_fenum init_float
 
-const struct tvec_urange
-  tvrange_uchar = { 0, UCHAR_MAX },
-  tvrange_ushort = { 0, USHRT_MAX },
-  tvrange_uint = { 0, UINT_MAX },
-  tvrange_ulong = { 0, ULONG_MAX },
-  tvrange_size = { 0, (size_t)-1 },
-  tvrange_byte = { 0, 255 },
-  tvrange_u16 = { 0, 65535 },
-  tvrange_u32 = { 0, 4294967296 };
+static void init_penum(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->p = 0; }
 
-int tvec_claimeq_int(struct tvec_state *tv, long i0, long i1,
-                    const char *file, unsigned lno, const char *expr)
-{
-  tv->in[0].v.i = i0; tv->out[0].v.i = i1;
-  return (tvec_claimeq(tv, &tvty_int, 0, file, lno, expr));
-}
+/* --- @eq_tenum@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv0, *rv1@ = register values
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Nonzero if the values are equal, zero if unequal
+ *
+ * Use:                Compare register values for equality.
+ *
+ *             Integer and floating-point enumeration values are compared as
+ *             their underlying representations; in particular, floating-
+ *             point enumerations may compare equal if their absolute or
+ *             relative difference is sufficiently small.  Pointer
+ *             enumerations are compared as pointers.
+ */
 
-int tvec_claimeq_uint(struct tvec_state *tv,
-                     unsigned long u0, unsigned long u1,
-                     const char *file, unsigned lno, const char *expr)
+#define eq_ienum eq_int
+#define eq_uenum eq_uint
+
+static int eq_fenum(const union tvec_regval *rv0,
+                   const union tvec_regval *rv1,
+                   const struct tvec_regdef *rd)
 {
-  tv->in[0].v.u = u0; tv->out[0].v.u = u1;
-  return (tvec_claimeq(tv, &tvty_uint, 0, file, lno, expr));
+  const struct tvec_fenuminfo *ei = rd->arg.p;
+  return (eqish_floating_p(rv0->f, rv1->f, ei->fi));
 }
 
-/*----- Enumerations ------------------------------------------------------*/
+static int eq_penum(const union tvec_regval *rv0,
+                   const union tvec_regval *rv1,
+                   const struct tvec_regdef *rd)
+  { return (rv0->p == rv1->p); }
 
-static void init_enum(union tvec_regval *rv, const struct tvec_regdef *rd)
-{
-  const struct tvec_enuminfo *ei = rd->arg.p;
+/* --- @tobuf_tenum@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Serialize a register value to a buffer.
+ *
+ *             Integer and floating-point enumeration values are serialized
+ *             as their underlying representations.  Pointer enumerations
+ *             are serialized as the signed integer index into the
+ *             association table; %|#nil|% serializes as %$-1$%, and
+ *             unrecognized pointers cause failure.
+ */
 
-  switch (ei->mv) {
-#define CASE(tag, ty, slot)                                            \
-       case TVMISC_##tag: rv->slot = 0; break;
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-    default: abort();
-  }
-}
+#define tobuf_ienum tobuf_int
+#define tobuf_uenum tobuf_uint
+#define tobuf_fenum tobuf_float
 
-static int eq_enum(const union tvec_regval *rv0,
-                  const union tvec_regval *rv1,
-                  const struct tvec_regdef *rd)
+static int tobuf_penum(buf *b, const union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
 {
-  const struct tvec_enuminfo *ei = rd->arg.p;
+  const struct tvec_penuminfo *pei = rd->arg.p;
+  const struct tvec_passoc *pa;
+  long i;
 
-  switch (ei->mv) {
-#define CASE(tag, ty, slot)                                            \
-       case TVMISC_##tag: return (rv0->slot == rv1->slot);
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-    default: abort();
-  }
+  for (pa = pei->av, i = 0; pa->tag; pa++, i++)
+    if (pa->p == rv->p) goto found;
+  if (!rv->p) i = -1;
+  else return (-1);
+found:
+  return (signed_to_buf(b, i));
 }
 
-static int tobuf_enum(buf *b, const union tvec_regval *rv,
-                     const struct tvec_regdef *rd)
-{
-  const struct tvec_enuminfo *ei = rd->arg.p;
-
-  switch (ei->mv) {
-#define CASE(tag, ty, slot)                                            \
-       case TVMISC_##tag: return (HANDLE_##tag);
-#define HANDLE_INT     signed_to_buf(b, rv->i)
-#define HANDLE_UINT    unsigned_to_buf(b, rv->u)
-#define HANDLE_PTR     -1
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-#undef HANDLE_INT
-#undef HANDLE_UINT
-#undef HANDLE_PTR
-    default: abort();
-  }
-  return (0);
-}
+/* --- @frombuf_tenum@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Deserialize a register value from a buffer.
+ *
+ *             Integer and floating-point enumeration values are serialized
+ *             as their underlying representations.  Pointer enumerations
+ *             are serialized as the signed integer index into the
+ *             association table; %|#nil|% serializes as %$-1$%; out-of-
+ *             range indices cause failure.
+ */
 
-static int frombuf_enum(buf *b, union tvec_regval *rv,
+#define frombuf_ienum frombuf_int
+#define frombuf_uenum frombuf_uint
+#define frombuf_fenum frombuf_float
+static int frombuf_penum(buf *b, union tvec_regval *rv,
                        const struct tvec_regdef *rd)
 {
-  const struct tvec_enuminfo *ei = rd->arg.p;
-
-  switch (ei->mv) {
-#define CASE(tag, ty, slot)                                            \
-       case TVMISC_##tag: return (HANDLE_##tag);
-#define HANDLE_INT     signed_from_buf(b, &rv->i)
-#define HANDLE_UINT    unsigned_from_buf(b, &rv->u)
-#define HANDLE_PTR     -1
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-#undef HANDLE_INT
-#undef HANDLE_UINT
-#undef HANDLE_PTR
-    default: abort();
-  }
+  const struct tvec_penuminfo *pei = rd->arg.p;
+  const struct tvec_passoc *pa;
+  long i, n;
+
+  for (pa = pei->av, n = 0; pa->tag; pa++, n++);
+  if (signed_from_buf(b, &i)) return (-1);
+  if (0 <= i && i < n) rv->p = (/*unconst*/ void *)pei->av[i].p;
+  else if (i == -1) rv->p = 0;
+  else { buf_break(b); return (-1); }
+  return (0);
 }
 
-static void parse_enum(union tvec_regval *rv, const struct tvec_regdef *rd,
-                      struct tvec_state *tv)
-{
-  const struct tvec_enuminfo *ei = rd->arg.p;
-#define DECLS(tag, ty, slot)                                           \
-       const struct tvec_##slot##assoc *slot##a;
-  TVEC_MISCSLOTS(DECLS)
-#undef DECLS
-  dstr d = DSTR_INIT;
+/* --- @parse_tenum@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             An enumerated value may be given by name or as a literal
+ *             value.  For enumerations based on numeric types, the literal
+ *             values can be written in the same syntax as the underlying
+ *             values.  For enumerations based on pointers, the only
+ *             permitted literal is %|#nil|%, which denotes a null pointer.
+ */
 
-  tvec_readword(tv, &d, ";", "enumeration tag or literal integer");
-  switch (ei->mv) {
-#define CASE(tag_, ty, slot)                                           \
-       case TVMISC_##tag_:                                             \
-         for (slot##a = ei->u.slot.av; slot##a->tag; slot##a++)        \
-           if (STRCMP(d.buf, ==, slot##a->tag))                        \
-             { rv->slot = FETCH_##tag_; goto end; }
-#define FETCH_INT (ia->i)
-#define FETCH_UINT (ua->u)
-#define FETCH_PTR ((/*unconst*/ void *)(pa->p))
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-#undef FETCH_INT
-#undef FETCH_UINT
-#undef FETCH_PTR
+#define DEFPARSE_ENUM(tag_, ty, slot)                                  \
+  static int parse_##slot##enum(union tvec_regval *rv,                 \
+                               const struct tvec_regdef *rd,           \
+                               struct tvec_state *tv)                  \
+  {                                                                    \
+    const struct tvec_##slot##enuminfo *ei = rd->arg.p;                        \
+    const struct tvec_##slot##assoc *a;                                        \
+    dstr d = DSTR_INIT;                                                        \
+    int rc;                                                            \
+                                                                       \
+    if (tvec_readword(tv, &d, 0,                                       \
+                     ";", "%s tag or " LITSTR_##tag_, ei->name))       \
+      { rc = -1; goto end; }                                           \
+    for (a = ei->av; a->tag; a++)                                      \
+      if (STRCMP(a->tag, ==, d.buf)) { FOUND_##tag_ goto done; }       \
+    MISSING_##tag_                                                     \
+    done:                                                              \
+    if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }                 \
+    rc = 0;                                                            \
+  end:                                                                 \
+    dstr_destroy(&d);                                                  \
+    return (rc);                                                       \
   }
 
-  switch (ei->mv) {
-#define CASE(tag, ty, slot)                                            \
-       case TVMISC_##tag: HANDLE_##tag goto end;
-#define HANDLE_INT     parse_signed(&rv->i, d.buf, ei->u.i.ir, tv);
-#define HANDLE_UINT    parse_unsigned(&rv->u, d.buf, ei->u.u.ur, tv);
-#define HANDLE_PTR     if (STRCMP(d.buf, ==, "#nil")) rv->p = 0;       \
-                       else goto tagonly;
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-#undef HANDLE_INT
-#undef HANDLE_UINT
-#undef HANDLE_PTR
-    default: tagonly:
-      tvec_error(tv, "unknown `%s' value `%s'", ei->name, d.buf);
-  }
+#define LITSTR_INT     "literal signed integer"
+#define FOUND_INT      rv->i = a->i;
+#define MISSING_INT    if (parse_signed(&rv->i, d.buf, ei->ir, tv))    \
+                         { rc = -1; goto end; }
+
+#define LITSTR_UINT    "literal unsigned integer"
+#define FOUND_UINT     rv->u = a->u;
+#define MISSING_UINT   if (parse_unsigned(&rv->u, d.buf, ei->ur, tv))  \
+                         { rc = -1; goto end; }
+
+#define LITSTR_FLT     "literal floating-point number, "               \
+                         "`#-inf', `#+inf', or `#nan'"
+#define FOUND_FLT      rv->f = a->f;
+#define MISSING_FLT    if (parse_floating(&rv->f, 0, d.buf, ei->fi, tv)) \
+                         { rc = -1; goto end; }
+
+#define LITSTR_PTR     "`#nil'"
+#define FOUND_PTR      rv->p = (/*unconst*/ void *)a->p;
+#define MISSING_PTR    if (STRCMP(d.buf, ==, "#nil"))                  \
+                         rv->p = 0;                                    \
+                       else {                                          \
+                         tvec_error(tv, "unknown `%s' value `%s'",     \
+                                    ei->name, d.buf);                  \
+                         rc = -1; goto end;                            \
+                       }
+
+TVEC_MISCSLOTS(DEFPARSE_ENUM)
+
+#undef LITSTR_INT
+#undef FOUND_INT
+#undef MISSING_INT
+
+#undef LITSTR_UINT
+#undef FOUND_UINT
+#undef MISSING_UINT
+
+#undef LITSTR_FLT
+#undef FOUND_FLT
+#undef MISSING_FLT
+
+#undef LITSTR_PTR
+#undef FOUND_PTR
+#undef MISSING_PTR
+
+#undef DEFPARSE_ENUM
+
+/* --- @dump_tenum@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Enumeration values are dumped as their symbolic names, if
+ *             possible, with the underlying values provided as a comment
+ *             unless compact output is requested, as for the underlying
+ *             representation.  A null pointer is printed as %|#nil|%;
+ *             non-null pointers are printed as %|#<TYPE PTR>|%, with the
+ *             enumeration TYPE and the raw pointer PTR printed with the
+ *             system's %|%p|% format specifier.
+ */
 
-end:
-  tvec_flushtoeol(tv, 0);
-  dstr_destroy(&d);
-}
 
-static void dump_enum(const union tvec_regval *rv,
-                     const struct tvec_regdef *rd,
-                     struct tvec_state *tv, unsigned style)
-{
-  const struct tvec_enuminfo *ei = rd->arg.p;
-#define DECLS(tag, ty, slot)                                           \
-       const struct tvec_##slot##assoc *slot##a;
-  TVEC_MISCSLOTS(DECLS)
-#undef DECLS
-  const char *tag;
-  unsigned long u;
-  unsigned f = 0;
-#define f_known 1u
-
-  switch (ei->mv) {
-#define CASE(tag_, ty, slot)                                           \
-       case TVMISC_##tag_:                                             \
-         for (slot##a = ei->u.slot.av; slot##a->tag; slot##a++)        \
-           if (rv->slot == slot##a->slot)                              \
-             { tag = slot##a->tag; goto found; }                       \
-         break;
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-    default: abort();
+#define DEFDUMP_ENUM(tag_, ty, slot)                                   \
+  static void dump_##slot##enum(const union tvec_regval *rv,           \
+                               const struct tvec_regdef *rd,           \
+                               unsigned style,                         \
+                               const struct gprintf_ops *gops, void *go) \
+  {                                                                    \
+    const struct tvec_##slot##enuminfo *ei = rd->arg.p;                        \
+    const struct tvec_##slot##assoc *a;                                        \
+                                                                       \
+    if (style&TVSF_RAW) gprintf(gops, go, #slot "enum/%s:", ei->name); \
+    for (a = ei->av; a->tag; a++)                                      \
+      if (rv->slot == a->slot) {                                       \
+       gprintf(gops, go, "%s", a->tag);                                \
+       if (style&TVSF_COMPACT) return;                                 \
+       gprintf(gops, go, " ; = "); break;                              \
+      }                                                                        \
+                                                                       \
+    PRINTRAW_##tag_                                                    \
   }
-  goto print_int;
 
-found:
-  f |= f_known;
-  tvec_write(tv, "%s", tag);
-  if (style&TVSF_COMPACT) return;
-  tvec_write(tv, " ; = ");
-
-print_int:
-  switch (ei->mv) {
-#define CASE(tag, ty, slot)                                            \
-       case TVMISC_##tag: HANDLE_##tag break;
-#define HANDLE_INT     tvec_write(tv, "%ld", rv->i);
-#define HANDLE_UINT    tvec_write(tv, "%lu", rv->u);
-#define HANDLE_PTR     if (!rv->p) tvec_write(tv, "#nil");             \
-                       else tvec_write(tv, "#<%s %p>", ei->name, rv->p);
-    TVEC_MISCSLOTS(CASE)
-#undef CASE
-#undef HANDLE_INT
-#undef HANDLE_UINT
-#undef HANDLE_PTR
-  }
+#define MAYBE_PRINT_EXTRA                                              \
+       if (style&TVSF_COMPACT) /* nothing to do */;                    \
+       else if (!a->tag) { gprintf(gops, go, " ; = "); goto _extra; }  \
+       else if (1) { gprintf(gops, go, " = "); goto _extra; }          \
+       else _extra:
+
+#define PRINTRAW_INT   gprintf(gops, go, "%ld", rv->i);                \
+                       MAYBE_PRINT_EXTRA {                             \
+                         format_signed_hex(gops, go, rv->i);           \
+                         maybe_format_signed_char(gops, go, rv->i);    \
+                       }
+
+#define PRINTRAW_UINT  gprintf(gops, go, "%lu", rv->u);                \
+                       MAYBE_PRINT_EXTRA {                             \
+                         format_unsigned_hex(gops, go, rv->u);         \
+                         maybe_format_unsigned_char(gops, go, rv->u);  \
+                       }
+
+#define PRINTRAW_FLT   format_floating(gops, go, rv->f);
+
+#define PRINTRAW_PTR   if (!rv->p) gprintf(gops, go, "#nil");          \
+                       else gprintf(gops, go, "#<%s %p>", ei->name, rv->p);
+
+TVEC_MISCSLOTS(DEFDUMP_ENUM)
+
+#undef PRINTRAW_INT
+#undef PRINTRAW_UINT
+#undef PRINTRAW_FLT
+#undef PRINTRAW_PTR
+
+#undef MAYBE_PRINT_EXTRA
+#undef DEFDUMP_ENUM
+
+/* Enumeration type definitions. */
+#define DEFTY_ENUM(tag, ty, slot)                                      \
+  const struct tvec_regty tvty_##slot##enum = {                                \
+    init_##slot##enum, trivial_release, eq_##slot##enum,               \
+    tobuf_##slot##enum, frombuf_##slot##enum,                          \
+    parse_##slot##enum, dump_##slot##enum                              \
+  };
+TVEC_MISCSLOTS(DEFTY_ENUM)
+#undef DEFTY_ENUM
+
+/* Predefined enumeration types. */
+static const struct tvec_iassoc bool_assoc[] = {
+  { "nil",             0 },
+  { "false",           0 },
+  { "f",               0 },
+  { "no",              0 },
+  { "n",               0 },
+  { "off",             0 },
+
+  { "t",               1 },
+  { "true",            1 },
+  { "yes",             1 },
+  { "y",               1 },
+  { "on",              1 },
+
+  TVEC_ENDENUM
+};
 
-  switch (ei->mv) {
-    case TVMISC_INT:
-      if (!(f&f_known)) tvec_write(tv, " ;");
-      if (rv->i >= 0) u = rv->i;
-      else u = -(unsigned long)rv->i;
-      tvec_write(tv, " = %s0x%0*lx", rv->i < 0 ? "-" : "", hex_width(u), u);
-      break;
-    case TVMISC_UINT:
-      if (!(f&f_known)) tvec_write(tv, " ;");
-      tvec_write(tv, " = 0x%0*lx", hex_width(rv->u), rv->u);
-      break;
-  }
-}
+const struct tvec_ienuminfo tvenum_bool =
+  { "bool", bool_assoc, &tvrange_int };
+
+static const struct tvec_iassoc cmp_assoc[] = {
+  { "<",               -1 },
+  { "less",            -1 },
+  { "lt",              -1 },
+
+  { "=",                0 },
+  { "equal",            0 },
+  { "eq",               0 },
+
+  { ">",               +1 },
+  { "greater",         +1 },
+  { "gt",              +1 },
 
-const struct tvec_regty tvty_enum = {
-  init_enum, release_int, eq_enum, measure_int,
-  tobuf_enum, frombuf_enum,
-  parse_enum, dump_enum
+  TVEC_ENDENUM
 };
 
+const struct tvec_ienuminfo tvenum_cmp =
+  { "cmp", cmp_assoc, &tvrange_int };
+
+/* --- @tvec_claimeq_tenum@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const struct tvec_typeenuminfo *ei@ = enumeration type info
+ *             @ty t0, t1@ = two values
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @t0@ and @t1@ are equal, otherwise zero.
+ *
+ * Use:                Check that values of @t0@ and @t1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @t0@ is printed as the output
+ *             value and @t1@ is printed as the input reference.
+ */
+
 #define DEFCLAIM(tag, ty, slot)                                                \
-       int tvec_claimeq_##slot##enum(struct tvec_state *tv,            \
-                                     const struct tvec_enuminfo *ei,   \
-                                     ty e0, ty e1,                     \
-                                     const char *file, unsigned lno,   \
-                                     const char *expr)                 \
+       int tvec_claimeq_##slot##enum                                   \
+         (struct tvec_state *tv,                                       \
+          const struct tvec_##slot##enuminfo *ei, ty e0, ty e1,        \
+          const char *file, unsigned lno, const char *expr)            \
        {                                                               \
          union tvec_misc arg;                                          \
                                                                        \
-         assert(ei->mv == TVMISC_##tag);                               \
          arg.p = ei;                                                   \
-         tv->in[0].v.slot = GET_##tag(e0);                             \
-         tv->out[0].v.slot = GET_##tag(e1);                            \
-         return (tvec_claimeq(tv, &tvty_enum, &arg, file, lno, expr)); \
+         tv->out[0].v.slot = GET_##tag(e0);                            \
+         tv->in[0].v.slot = GET_##tag(e1);                             \
+         return (tvec_claimeq(tv, &tvty_##slot##enum, &arg,            \
+                              file, lno, expr));                       \
        }
 #define GET_INT(e) (e)
 #define GET_UINT(e) (e)
+#define GET_FLT(e) (e)
 #define GET_PTR(e) ((/*unconst*/ void *)(e))
 TVEC_MISCSLOTS(DEFCLAIM)
 #undef DEFCLAIM
 #undef GET_INT
 #undef GET_UINT
+#undef GET_FLT
 #undef GET_PTR
 
 /*----- Flag types --------------------------------------------------------*/
 
-static void parse_flags(union tvec_regval *rv, const struct tvec_regdef *rd,
-                       struct tvec_state *tv)
+/* Flag types are initialized, compared, and serialized as unsigned
+ * integers.
+ */
+
+/* --- @parse_flags@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             The input syntax is a sequence of items separated by `|'
+ *             signs.  Each item may be the symbolic name of a field value,
+ *             or a literal unsigned integer.  The masks associated with the
+ *             given symbolic names must be disjoint.  The resulting
+ *             numerical value is simply the bitwise OR of the given values.
+ */
+
+static int parse_flags(union tvec_regval *rv, const struct tvec_regdef *rd,
+                      struct tvec_state *tv)
 {
   const struct tvec_flaginfo *fi = rd->arg.p;
   const struct tvec_flag *f;
   unsigned long m = 0, v = 0, t;
   dstr d = DSTR_INIT;
-  int ch;
+  int ch, rc;
 
   for (;;) {
-    DRESET(&d); tvec_readword(tv, &d, "|;", "flag name or integer");
 
+    /* Read the next item. */
+    DRESET(&d);
+    if (tvec_readword(tv, &d, 0, "|;", "%s flag name or integer", fi->name))
+      { rc = -1; goto end; }
+
+    /* Try to find a matching entry in the table. */
     for (f = fi->fv; f->tag; f++)
       if (STRCMP(f->tag, ==, d.buf)) {
-       if (m&f->m) tvec_error(tv, "colliding flag setting");
-       else { m |= f->m; v |= f->v; goto next; }
+       if (m&f->m)
+         { tvec_error(tv, "colliding flag setting"); rc = -1; goto end; }
+       else
+         { m |= f->m; v |= f->v; goto next; }
       }
 
-    parse_unsigned(&t, d.buf, fi->range, tv); v |= t;
+    /* Otherwise, try to parse it as a raw integer. */
+    if (parse_unsigned(&t, d.buf, fi->range, tv))
+      { rc = -1; goto end; }
+    v |= t;
+
   next:
+    /* Advance to the next token.  If it's a separator then consume it, and
+     * go round again.  Otherwise we stop here.
+     */
     if (tvec_nexttoken(tv)) break;
-    ch = getc(tv->fp); if (ch != '|') tvec_syntax(tv, ch, "`|'");
-    if (tvec_nexttoken(tv)) tvec_syntax(tv, '\n', "flag name or integer");
+    ch = getc(tv->fp);
+      if (ch != '|') { tvec_syntax(tv, ch, "`|'"); rc = -1; goto end; }
+      if (tvec_nexttoken(tv)) {
+       tvec_syntax(tv, '\n', "%s flag name or integer", fi->name);
+       rc = -1; goto end;
+      }
   }
-  rv->u = v;
+
+  /* Done. */
+  rv->u = v; rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
 }
 
+/* --- @dump_flags@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             The table of symbolic names and their associated values and
+ *             masks is repeatedly scanned, in order, to find disjoint
+ *             matches -- i.e., entries whose value matches the target value
+ *             in the bit positions indicated by the mask, and whose mask
+ *             doesn't overlap with any previously found matches; the names
+ *             are then output, separated by `|'.  Any remaining nonzero
+ *             bits not covered by any of the matching masks are output as a
+ *             single literal integer, in hex.
+ *
+ *             Unless compact output is requested, or no symbolic names were
+ *             found, the raw numeric value is also printed in hex, as a
+ *             comment.
+ */
+
 static void dump_flags(const union tvec_regval *rv,
                       const struct tvec_regdef *rd,
-                      struct tvec_state *tv, unsigned style)
+                      unsigned style,
+                      const struct gprintf_ops *gops, void *go)
 {
   const struct tvec_flaginfo *fi = rd->arg.p;
   const struct tvec_flag *f;
-  unsigned long m = ~(unsigned long)0, v = rv->u;
+  unsigned long m = ~0ul, v = rv->u;
   const char *sep;
 
+  if (style&TVSF_RAW) gprintf(gops, go, "flags/%s:", fi->name);
+
   for (f = fi->fv, sep = ""; f->tag; f++)
     if ((m&f->m) && (v&f->m) == f->v) {
-      tvec_write(tv, "%s%s", sep, f->tag); m &= ~f->m;
+      gprintf(gops, go, "%s%s", sep, f->tag); m &= ~f->m;
       sep = style&TVSF_COMPACT ? "|" : " | ";
     }
 
-  if (v&m) tvec_write(tv, "%s0x%0*lx", sep, hex_width(v), v&m);
+  if (v&m) gprintf(gops, go, "%s0x%0*lx", sep, hex_width(v), v&m);
+
+  if (m != ~0ul && !(style&(TVSF_COMPACT | TVSF_RAW)))
+    gprintf(gops, go, " ; = 0x%0*lx", hex_width(rv->u), rv->u);
+}
+
+/* Flags type definition. */
+const struct tvec_regty tvty_flags = {
+  init_uint, trivial_release, eq_uint,
+  tobuf_uint, frombuf_uint,
+  parse_flags, dump_flags
+};
+
+/* --- @tvec_claimeq_flags@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const struct tvec_flaginfo *fi@ = flags type info
+ *             @unsigned long f0, f1@ = two values
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @f0@ and @f1@ are equal, otherwise zero.
+ *
+ * Use:                Check that values of @f0@ and @f1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @f0@ is printed as the output
+ *             value and @f1@ is printed as the input reference.
+ */
+
+int tvec_claimeq_flags(struct tvec_state *tv,
+                      const struct tvec_flaginfo *fi,
+                      unsigned long f0, unsigned long f1,
+                      const char *file, unsigned lno, const char *expr)
+{
+  union tvec_misc arg;
+
+  arg.p = fi; tv->out[0].v.u = f0; tv->in[0].v.u = f1;
+  return (tvec_claimeq(tv, &tvty_flags, &arg, file, lno, expr));
+}
+
+/*----- Characters --------------------------------------------------------*/
+
+/* Character values are initialized and compared as signed integers. */
+
+/* --- @tobuf_char@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Serialize a register value to a buffer.
+ *
+ *             Character values are serialized as little-endian 32-bit
+ *             unsigned integers, with %|EOF|% serialized as all-bits-set.
+ */
+
+static int tobuf_char(buf *b, const union tvec_regval *rv,
+                     const struct tvec_regdef *rd)
+{
+  uint32 u;
+
+  if (0 <= rv->i && rv->i <= UCHAR_MAX) u = rv->i;
+  else if (rv->i == EOF) u = MASK32;
+  else { buf_break(b); return (-1); }
+  return (buf_putu32l(b, u));
+}
+
+/* --- @frombuf_char@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Deserialize a register value from a buffer.
+ *
+ *             Character values are serialized as little-endian 32-bit
+ *             unsigned integers, with %|EOF|% serialized as all-bits-set.
+ */
+
+static int frombuf_char(buf *b, union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+{
+  uint32 u;
+
+  if (buf_getu32l(b, &u)) return (-1);
+  if (0 <= u && u <= UCHAR_MAX) rv->i = u;
+  else if (u == MASK32) rv->i = EOF;
+  else { buf_break(b); return (-1); }
+  return (0);
+}
+
+/* --- @parse_char@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             A character value can be given by symbolic name, with a
+ *             leading `%|#|%'; or a character or `%|\|%'-escape sequence,
+ *             optionally in single quotes.
+ *
+ *             The following escape sequences and character names are
+ *             recognized.
+ *
+ *             * `%|#eof|%' is the special end-of-file marker.
+ *
+ *             * `%|#nul|%' is the NUL character, sometimes used to
+ *               terminate strings.
+ *
+ *             * `%|bell|%', `%|bel|%', `%|ding|%', or `%|\a|%' is the BEL
+ *               character used to ring the terminal bell (or do some other
+ *               thing to attract the user's attention).
+ *
+ *             * %|#backspace|%, %|#bs|%, or %|\b|% is the backspace
+ *               character, used to move the cursor backwords by one cell.
+ *
+ *             * %|#escape|% %|#esc|%, or%|\e|% is the escape character,
+ *               used to introduce special terminal commands.
+ *
+ *             * %|#formfeed|%, %|#ff|%, or %|\f|% is the formfeed
+ *               character, used to separate pages of text.
+ *
+ *             * %|#newline|%, %|#linefeed|%, %|#lf|%, %|#nl|%, or %|\n|% is
+ *               the newline character, used to terminate lines of text or
+ *               advance the cursor to the next line (perhaps without
+ *               returning it to the start of the line).
+ *
+ *             * %|#return|%, %|#carriage-return|%, %|#cr|%, or %|\r|% is
+ *               the carriage-return character, used to return the cursor to
+ *               the start of the line.
+ *
+ *             * %|#tab|%, %|#horizontal-tab|%, %|#ht|%, or %|\t|% is the
+ *               tab character, used to advance the cursor to the next tab
+ *               stop on the current line.
+ *
+ *             * %|#vertical-tab|%, %|#vt|%, %|\v|% is the vertical tab
+ *               character.
+ *
+ *             * %|#space|%, %|#spc|% is the space character.
+ *
+ *             * %|#delete|%, %|#del|% is the delete character, used to
+ *               erase the most recent character.
+ *
+ *             * %|\'|% is the single-quote character.
+ *
+ *             * %|\\|% is the backslash character.
+ *
+ *             * %|\"|% is the double-quote character.
+ *
+ *             * %|\NNN|% or %|\{NNN}|% is the character with code NNN in
+ *               octal.  The NNN may be up to three digits long.
+ *
+ *             * %|\xNN|% or %|\x{NN}|% is the character with code NNN in
+ *               hexadecimal.
+ */
+
+static int parse_char(union tvec_regval *rv, const struct tvec_regdef *rd,
+                     struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+  int ch, rc;
+  unsigned f = 0;
+#define f_quote 1u
+
+  /* Inspect the character to see what we're up against. */
+  ch = getc(tv->fp);
+
+  if (ch == '#') {
+    /* It looks like a special token.  Push the `%|#|%' back and fetch the
+     * whole word.  If there's just the `%|#|%' after all, then treat it as
+     * literal.
+     */
+
+    ungetc(ch, tv->fp);
+    if (tvec_readword(tv, &d, 0, ";", "character name"))
+      { rc = -1; goto end; }
+    if (STRCMP(d.buf, !=, "#")) {
+      if (read_charname(&ch, d.buf, RCF_EOFOK)) {
+       rc = tvec_error(tv, "unknown character name `%s'", d.buf);
+       goto end;
+      }
+      if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+      rv->i = ch; rc = 0; goto end;
+    }
+  }
+
+  /* If this is a single quote then we expect to see a matching one later,
+   * and we should process backslash escapes.  Get the next character and see
+   * what happens.
+   */
+  if (ch == '\'') { f |= f_quote; ch = getc(tv->fp); }
+
+  /* Main character dispatch. */
+  switch (ch) {
+
+    case ';':
+      /* Unquoted, semicolon begins a comment. */
+      if (!(f&f_quote)) { rc = tvec_syntax(tv, ch, "character"); goto end; }
+      else goto plain;
+
+    case '\n':
+      /* A newline.  If we saw a single quote, then treat that as literal.
+       * Otherwise this is an error.
+       */
+      if (!(f&f_quote)) goto nochar;
+      else { f &= ~f_quote; ungetc(ch, tv->fp); ch = '\''; goto plain; }
+
+    case EOF:
+      /* End-of-file.  Similar to newline, but with slightly different
+       * effects on the parse state.
+       */
+      if (!(f&f_quote)) goto nochar;
+      else { f &= ~f_quote; ch = '\''; goto plain; }
+
+    case '\'': nochar:
+      /* A single quote.  This must be the second of a pair, and there should
+       * have been a character or escape sequence between them.
+       */
+      rc = tvec_syntax(tv, ch, "character"); goto end;
+
+    case '\\':
+      /* A backslash.  Read a character escape. */
+      if (read_charesc(&ch, tv)) return (-1);
+
+    default: plain:
+      /* Anything else.  Treat as literal. */
+      rv->i = ch; break;
+  }
+
+  /* If we saw an opening quote, then expect the closing quote. */
+  if (f&f_quote) {
+    ch = getc(tv->fp);
+    if (ch != '\'') { rc = tvec_syntax(tv, ch, "`''"); goto end; }
+  }
+
+  /* Done. */
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
+
+#undef f_quote
+}
+
+/* --- @dump_char@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Character values are dumped as their symbolic names, if any,
+ *             or as a character or escape sequence within single quotes
+ *             (which may be omitted in compact style).  If compact output
+ *             is not requested, then the single-quoted representation (for
+ *             characters dumped as symbolic names) and integer code in
+ *             decimal and hex are printed as a comment.
+ */
+
+static void dump_char(const union tvec_regval *rv,
+                     const struct tvec_regdef *rd,
+                     unsigned style,
+                     const struct gprintf_ops *gops, void *go)
+{
+  const char *p;
+  unsigned f = 0;
+#define f_semi 1u
+
+  if (style&TVSF_RAW) {
+    /* Print the raw character unconditionally in single quotes. */
+
+    gprintf(gops, go, "char:'");
+    format_char(gops, go, rv->i);
+    gprintf(gops, go, "'");
+  } else {
+    /* Print ina pleasant human-readable way. */
+
+    /* Print a character name if we can find one. */
+    p = find_charname(rv->i, (style&TVSF_COMPACT) ? CTF_SHORT : CTF_PREFER);
+    if (p) {
+      gprintf(gops, go, "%s", p);
+      if (style&TVSF_COMPACT) return;
+      else { gprintf(gops, go, " ;"); f |= f_semi; }
+    }
+
+    /* If the character isn't @EOF@ then print it as a single-quoted thing.
+     * In compact style, see if we can omit the quotes.
+     */
+    if (rv->i >= 0) {
+      if (f&f_semi) gprintf(gops, go, " = ");
+      switch (rv->i) {
+       case ' ': case '\\': case '\'': quote:
+         format_char(gops, go, rv->i);
+         break;
+       default:
+         if (!(style&TVSF_COMPACT) || !isprint(rv->i)) goto quote;
+         gprintf(gops, go, "%c", (int)rv->i);
+         return;
+      }
+    }
+
+    /* And the character code as an integer. */
+    if (!(style&TVSF_COMPACT)) {
+      if (!(f&f_semi)) gprintf(gops, go, " ;");
+      gprintf(gops, go, " = %ld = ", rv->i);
+      format_signed_hex(gops, go, rv->i);
+    }
+  }
 
-  if (!(style&TVSF_COMPACT))
-    tvec_write(tv, " ; = 0x%0*lx", hex_width(rv->u), rv->u);
+#undef f_semi
 }
 
-const struct tvec_regty tvty_flags = {
-  init_uint, release_int, eq_uint, measure_int,
-  tobuf_uint, frombuf_uint,
-  parse_flags, dump_flags
+/* Character type definition. */
+const struct tvec_regty tvty_char = {
+  init_int, trivial_release, eq_int,
+  tobuf_char, frombuf_char,
+  parse_char, dump_char
 };
 
-int tvec_claimeq_flags(struct tvec_state *tv,
-                      const struct tvec_flaginfo *fi,
-                      unsigned long f0, unsigned long f1,
-                      const char *file, unsigned lno, const char *expr)
-{
-  union tvec_misc arg;
+/* --- @tvec_claimeq_char@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @int ch0, ch1@ = two character codes
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if @ch0@ and @ch1@ are equal, otherwise zero.
+ *
+ * Use:                Check that values of @ch0@ and @ch1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @ch0@ is printed as the output
+ *             value and @ch1@ is printed as the input reference.
+ */
 
-  arg.p = fi; tv->in[0].v.u = f0; tv->out[0].v.u = f1;
-  return (tvec_claimeq(tv, &tvty_flags, &arg, file, lno, expr));
+int tvec_claimeq_char(struct tvec_state *tv, int c0, int c1,
+                     const char *file, unsigned lno, const char *expr)
+{
+  tv->out[0].v.i = c0; tv->in[0].v.i = c1;
+  return (tvec_claimeq(tv, &tvty_char, 0, file, lno, expr));
 }
 
 /*----- Text and byte strings ---------------------------------------------*/
 
-void tvec_allocstring(union tvec_regval *rv, size_t sz)
-{
-  if (rv->str.sz < sz) { xfree(rv->str.p); rv->str.p = xmalloc(sz); }
-  rv->str.sz = sz;
-}
-
-void tvec_allocbytes(union tvec_regval *rv, size_t sz)
-{
-  if (rv->bytes.sz < sz) { xfree(rv->bytes.p); rv->bytes.p = xmalloc(sz); }
-  rv->bytes.sz = sz;
-}
+/* --- @init_text@, @init_bytes@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a register value.
+ *
+ *             Text and binary string values are initialized with a null
+ *             pointer and zero length.
+ */
 
-static void init_string(union tvec_regval *rv, const struct tvec_regdef *rd)
-  { rv->str.p = 0; rv->str.sz = 0; }
+static void init_text(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->text.p = 0; rv->text.sz = 0; }
 
 static void init_bytes(union tvec_regval *rv, const struct tvec_regdef *rd)
   { rv->bytes.p = 0; rv->bytes.sz = 0; }
 
-static void release_string(union tvec_regval *rv,
-                         const struct tvec_regdef *rd)
-  { xfree(rv->str.p); }
+/* --- @release_string@, @release_bytes@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Release resources held by a register value.
+ *
+ *             Text and binary string buffers are freed.
+ */
+
+static void release_text(union tvec_regval *rv,
+                        const struct tvec_regdef *rd)
+  { xfree(rv->text.p); }
 
 static void release_bytes(union tvec_regval *rv,
                          const struct tvec_regdef *rd)
   { xfree(rv->bytes.p); }
 
-static int eq_string(const union tvec_regval *rv0,
-                    const union tvec_regval *rv1,
-                    const struct tvec_regdef *rd)
+/* --- @eq_text@, @eq_bytes@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv0, *rv1@ = register values
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Nonzero if the values are equal, zero if unequal
+ *
+ * Use:                Compare register values for equality.
+ */
+
+static int eq_text(const union tvec_regval *rv0,
+                  const union tvec_regval *rv1,
+                  const struct tvec_regdef *rd)
 {
-  return (rv0->str.sz == rv1->str.sz &&
-         (!rv0->bytes.sz ||
-          MEMCMP(rv0->str.p, ==, rv1->str.p, rv1->str.sz)));
+  return (rv0->text.sz == rv1->text.sz &&
+         (!rv0->text.sz ||
+          MEMCMP(rv0->text.p, ==, rv1->text.p, rv1->text.sz)));
 }
 
 static int eq_bytes(const union tvec_regval *rv0,
@@ -831,30 +3078,52 @@ static int eq_bytes(const union tvec_regval *rv0,
           MEMCMP(rv0->bytes.p, ==, rv1->bytes.p, rv1->bytes.sz)));
 }
 
-static size_t measure_string(const union tvec_regval *rv,
-                            const struct tvec_regdef *rd)
-  { return (rv->str.sz + 4); }
-
-static size_t measure_bytes(const union tvec_regval *rv,
-                           const struct tvec_regdef *rd)
-  { return (rv->bytes.sz + 4); }
+/* --- @tobuf_text@, @tobuf_bytes@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Serialize a register value to a buffer.
+ *
+ *             Text and binary string values are serialized as a little-
+ *             endian 64-bit length %$n$% in bytes followed by %$n$% bytes
+ *             of string data.
+ */
 
-static int tobuf_string(buf *b, const union tvec_regval *rv,
-                       const struct tvec_regdef *rd)
-  { return (buf_putmem32l(b, rv->str.p, rv->str.sz)); }
+static int tobuf_text(buf *b, const union tvec_regval *rv,
+                     const struct tvec_regdef *rd)
+  { return (buf_putmem64l(b, rv->text.p, rv->text.sz)); }
 
 static int tobuf_bytes(buf *b, const union tvec_regval *rv,
                       const struct tvec_regdef *rd)
-  { return (buf_putmem32l(b, rv->bytes.p, rv->bytes.sz)); }
+  { return (buf_putmem64l(b, rv->bytes.p, rv->bytes.sz)); }
 
-static int frombuf_string(buf *b, union tvec_regval *rv,
-                         const struct tvec_regdef *rd)
+/* --- @frombuf_text@, @frombuf_bytes@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Deserialize a register value from a buffer.
+ *
+ *             Text and binary string values are serialized as a little-
+ *             endian 64-bit length %$n$% in bytes followed by %$n$% bytes
+ *             of string data.
+ */
+
+static int frombuf_text(buf *b, union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
 {
   const void *p;
   size_t sz;
 
-  p = buf_getmem32l(b, &sz); if (!p) return (-1);
-  tvec_allocstring(rv, sz); memcpy(rv->str.p, p, sz);
+  p = buf_getmem64l(b, &sz); if (!p) return (-1);
+  tvec_alloctext(rv, sz); memcpy(rv->text.p, p, sz); rv->text.p[sz] = 0;
   return (0);
 }
 
@@ -864,92 +3133,181 @@ static int frombuf_bytes(buf *b, union tvec_regval *rv,
   const void *p;
   size_t sz;
 
-  p = buf_getmem32l(b, &sz); if (!p) return (-1);
+  p = buf_getmem64l(b, &sz); if (!p) return (-1);
   tvec_allocbytes(rv, sz); memcpy(rv->bytes.p, p, sz);
   return (0);
 }
 
-static void check_string_length(size_t sz, const struct tvec_urange *ur,
-                               struct tvec_state *tv)
+/* --- @check_string_length@ --- *
+ *
+ * Arguments:  @size_t sz@ = found string length
+ *             @const struct tvec_urange *ur@ = acceptable range
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Checks that @sz@ is within the bounds described by @ur@,
+ *             reporting an error if not.
+ */
+
+static int check_string_length(size_t sz, const struct tvec_urange *ur,
+                              struct tvec_state *tv)
 {
   if (ur && (ur->min > sz || sz > ur->max))
-    tvec_error(tv, "invalid string length %lu; must be in [%lu..%lu]",
-              (unsigned long)sz, ur->min, ur->max);
+    return (tvec_error(tv,
+                      "invalid string length %lu; must be in [%lu .. %lu]",
+                      (unsigned long)sz, ur->min, ur->max));
+  return (0);
 }
 
-static void parse_string(union tvec_regval *rv, const struct tvec_regdef *rd,
-                        struct tvec_state *tv)
+/* --- @parse_text@, @parse_bytes@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             The input format for both kinds of strings is basically the
+ *             same: a `compound string', consisting of
+ *
+ *               * single-quoted strings, which are interpreted entirely
+ *                 literally, but can't contain single quotes or newlines;
+ *
+ *               * double-quoted strings, in which `%|\|%'-escapes are
+ *                 interpreted as for characters;
+ *
+ *               * character names, marked by an initial `%|#|%' sign;
+ *
+ *               * special tokens marked by an initial `%|!|%' sign; or
+ *
+ *               * barewords interpreted according to the current coding
+ *                 scheme.
+ *
+ *             The special tokens are
+ *
+ *               * `%|!bare|%', which causes subsequent sequences of
+ *                 barewords to be treated as plain text;
+ *
+ *               * `%|!hex|%', `%|!base32|%', `%|!base64|%', which cause
+ *                 subsequent barewords to be decoded in the requested
+ *                 manner.
+ *
+ *               * `%|!repeat|% %$n$% %|{|% %%\textit{string}%% %|}|%',
+ *                 which includes %$n$% copies of the (compound) string.
+ *
+ *             The only difference between text and binary strings is that
+ *             the initial coding scheme is %|bare|% for text strings and
+ *             %|hex|% for binary strings.
+ */
+
+static int parse_text(union tvec_regval *rv, const struct tvec_regdef *rd,
+                     struct tvec_state *tv)
 {
-  void *p = rv->str.p;
+  void *p = rv->text.p;
 
-  read_compound_string(&p, &rv->str.sz, TVCODE_BARE, tv); rv->str.p = p;
-  check_string_length(rv->str.sz, rd->arg.p, tv);
+  if (read_compound_string(&p, &rv->text.sz, TVCODE_BARE, 0, tv))
+    return (-1);
+  rv->text.p = p;
+  if (check_string_length(rv->text.sz, rd->arg.p, tv)) return (-1);
+  return (0);
 }
 
-static void parse_bytes(union tvec_regval *rv, const struct tvec_regdef *rd,
-                       struct tvec_state *tv)
+static int parse_bytes(union tvec_regval *rv, const struct tvec_regdef *rd,
+                      struct tvec_state *tv)
 {
   void *p = rv->bytes.p;
 
-  read_compound_string(&p, &rv->bytes.sz, TVCODE_HEX, tv); rv->bytes.p = p;
-  check_string_length(rv->bytes.sz, rd->arg.p, tv);
+  if (read_compound_string(&p, &rv->bytes.sz, TVCODE_HEX, 0, tv))
+    return (-1);
+  rv->bytes.p = p;
+  if (check_string_length(rv->bytes.sz, rd->arg.p, tv)) return (-1);
+  return (0);
 }
 
-static void dump_string(const union tvec_regval *rv,
-                       const struct tvec_regdef *rd,
-                       struct tvec_state *tv, unsigned style)
+/* --- @dump_text@, @dump_bytes@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Text string values are dumped as plain text, in double quotes
+ *             if necessary, and using backslash escape sequences for
+ *             nonprintable characters.  Unless compact output is requested,
+ *             strings consisting of multiple lines are dumped with each
+ *             line of the string on a separate output line.
+ *
+ *             Binary string values are dumped in hexadecimal.  In compact
+ *             style, the output simply consists of a single block of hex
+ *             digits.  Otherwise, the dump is a display consisting of
+ *             groups of hex digits, with comments showing the offset (if
+ *             the string is long enough) and the corresponding plain text.
+ *
+ *             Empty strings are dumped as %|#empty|%.
+ */
+
+static void dump_empty(const char *ty, unsigned style,
+                      const struct gprintf_ops *gops, void *go)
+{
+  if (style&TVSF_RAW) gprintf(gops, go, "%s:", ty);
+  if (!(style&TVSF_COMPACT)) gprintf(gops, go, "#empty");
+  if (!(style&(TVSF_COMPACT | TVSF_RAW))) gprintf(gops, go, " ; = ");
+  if (!(style&TVSF_RAW)) gprintf(gops, go, "\"\"");
+}
+
+
+static void dump_text(const union tvec_regval *rv,
+                     const struct tvec_regdef *rd,
+                     unsigned style,
+                     const struct gprintf_ops *gops, void *go)
 {
   const unsigned char *p, *q, *l;
-  int ch;
   unsigned f = 0;
 #define f_nonword 1u
 #define f_newline 2u
 
-  if (!rv->str.sz) { tvec_write(tv, "\"\""); return; }
+  if (!rv->text.sz) { dump_empty("text", style, gops, go); return; }
+
+  p = (const unsigned char *)rv->text.p; l = p + rv->text.sz;
+  if (style&TVSF_RAW) { gprintf(gops, go, "text:"); goto quote; }
+  else if (style&TVSF_COMPACT) goto quote;
 
-  p = (const unsigned char *)rv->str.p; l = p + rv->str.sz;
-  if (*p == '!' || *p == ';' || *p == '"' || *p == '\'') goto quote;
+  switch (*p) {
+    case '!': case '#': case ';': case '"': case '\'':
+    case '(': case '{': case '[': case ']': case '}': case ')':
+      f |= f_nonword; break;
+  }
   for (q = p; q < l; q++)
     if (*q == '\n' && q != l - 1) f |= f_newline;
-    else if (!*q || !isgraph(*q) || *q == '\\') f |= f_nonword;
-  if (f&f_newline) { tvec_write(tv, "\n\t"); goto quote; }
+    else if (!*q || !ISGRAPH(*q) || *q == '\\') f |= f_nonword;
+  if (f&f_newline) { gprintf(gops, go, "\n\t"); goto quote; }
   else if (f&f_nonword) goto quote;
-  tv->output->ops->write(tv->output, (const char *)p, rv->str.sz); return;
+
+  gops->putm(go, (const char *)p, rv->text.sz);
+  return;
 
 quote:
-  tvec_write(tv, "\"");
+  gprintf(gops, go, "\"");
   for (q = p; q < l; q++)
-    switch (*q) {
-      case '"': case '\\': ch = *q; goto escape;
-      case '\a': ch = 'a'; goto escape;
-      case '\b': ch = 'b'; goto escape;
-      case '\x1b': ch = 'e'; goto escape;
-      case '\f': ch = 'f'; goto escape;
-      case '\r': ch = 'r'; goto escape;
-      case '\t': ch = 't'; goto escape;
-      case '\v': ch = 'v'; goto escape;
-      escape:
-       if (p < q)
-         tv->output->ops->write(tv->output, (const char *)p, q - p);
-       tvec_write(tv, "\\%c", ch); p = q + 1;
-       break;
-
-      case '\n':
-       if (p < q)
-         tv->output->ops->write(tv->output, (const char *)p, q - p);
-       tvec_write(tv, "\\n"); p = q + 1;
-       if (!(style&TVSF_COMPACT) && q < l) tvec_write(tv, "\"\t\"");
-       break;
-
-      default:
-       if (isprint(*q)) break;
-       if (p < q)
-         tv->output->ops->write(tv->output, (const char *)p, q - p);
-       tvec_write(tv, "\\x{%0*x}", hex_width(UCHAR_MAX), *q); p = q + 1;
-       break;
+    if (!ISPRINT(*q) || *q == '"') {
+      if (p < q) gops->putm(go, (const char *)p, q - p);
+      if (*q != '\n' || (style&TVSF_COMPACT))
+       format_charesc(gops, go, *q, FCF_BRACE);
+      else {
+       if (q + 1 == l) { gprintf(gops, go, "\\n\""); return; }
+       else gprintf(gops, go, "\\n\"\n\t\"");
+      }
+      p = q + 1;
     }
-  if (p < q) tv->output->ops->write(tv->output, (const char *)p, q - p);
-  tvec_write(tv, "\"");
+  if (p < q) gops->putm(go, (const char *)p, q - p);
+  gprintf(gops, go, "\"");
 
 #undef f_nonword
 #undef f_newline
@@ -957,180 +3315,438 @@ quote:
 
 static void dump_bytes(const union tvec_regval *rv,
                       const struct tvec_regdef *rd,
-                      struct tvec_state *tv, unsigned style)
+                      unsigned style,
+                      const struct gprintf_ops *gops, void *go)
 {
   const unsigned char *p = rv->bytes.p, *l = p + rv->bytes.sz;
   size_t off, sz = rv->bytes.sz;
   unsigned i, n;
   int wd;
 
-  if (!sz) {
-    tvec_write(tv, style&TVSF_COMPACT ? "\"\"" : "\"\" ; empty");
-    return;
-  }
+  if (!rv->text.sz) { dump_empty("bytes", style, gops, go); return; }
 
-  if (style&TVSF_COMPACT) {
-    while (p < l) tvec_write(tv, "%02x", *p++);
+  if (style&(TVSF_COMPACT | TVSF_RAW)) {
+    while (p < l) gprintf(gops, go, "%02x", *p++);
     return;
   }
 
-  if (sz > 16) tvec_write(tv, "\n\t");
+  if (sz > 16) gprintf(gops, go, "\n\t");
 
   off = 0; wd = hex_width(sz);
   while (p < l) {
     if (l - p < 16) n = l - p;
     else n = 16;
 
-    for (i = 0; i < 16; i++) {
-      if (i < n) tvec_write(tv, "%02x", p[i]);
-      else tvec_write(tv, "  ");
-      if (i%4 == 3) tvec_write(tv, " ");
+    for (i = 0; i < n; i++) {
+      if (i < n) gprintf(gops, go, "%02x", p[i]);
+      else gprintf(gops, go, "  ");
+      if (i < n - 1 && i%4 == 3) gprintf(gops, go, " ");
     }
-    tvec_write(tv, " ; ");
-    if (sz > 16) tvec_write(tv, "[%0*lx] ", wd, (unsigned long)off);
+    gprintf(gops, go, " ; ");
+    if (sz > 16) gprintf(gops, go, "[%0*lx] ", wd, (unsigned long)off);
     for (i = 0; i < n; i++)
-      tvec_write(tv, "%c", isprint(p[i]) ? p[i] : '.');
+      gprintf(gops, go, "%c", isprint(p[i]) ? p[i] : '.');
     p += n; off += n;
-    if (p < l) tvec_write(tv, "\n\t");
+    if (p < l) gprintf(gops, go, "\n\t");
   }
 }
 
-const struct tvec_regty tvty_string = {
-  init_string, release_string, eq_string, measure_string,
-  tobuf_string, frombuf_string,
-  parse_string, dump_string
+/* Text and byte string type definitions. */
+const struct tvec_regty tvty_text = {
+  init_text, release_text, eq_text,
+  tobuf_text, frombuf_text,
+  parse_text, dump_text
 };
-
 const struct tvec_regty tvty_bytes = {
-  init_bytes, release_bytes, eq_bytes, measure_bytes,
+  init_bytes, release_bytes, eq_bytes,
   tobuf_bytes, frombuf_bytes,
   parse_bytes, dump_bytes
 };
 
-int tvec_claimeq_string(struct tvec_state *tv,
-                       const char *p0, size_t sz0,
-                       const char *p1, size_t sz1,
-                       const char *file, unsigned lno, const char *expr)
+/* --- @tvec_claimeq_text@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *p0@, @size_t sz0@ = first string with length
+ *             @const char *p1@, @size_t sz1@ = second string with length
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if the strings at @p0@ and @p1@ are equal, otherwise
+ *             zero.
+ *
+ * Use:                Check that strings at @p0@ and @p1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @p0@ is printed as the output
+ *             value and @p1@ is printed as the input reference.
+ */
+
+int tvec_claimeq_text(struct tvec_state *tv,
+                     const char *p0, size_t sz0,
+                     const char *p1, size_t sz1,
+                     const char *file, unsigned lno, const char *expr)
 {
-  tv->in[0].v.str.p = (/*unconst*/ char *)p0; tv->in[0].v.str.sz = sz0;
-  tv->out[0].v.str.p =(/*unconst*/ char *) p1; tv->out[0].v.str.sz = sz1;
-  return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr));
+  tv->out[0].v.text.p = (/*unconst*/ char *)p0; tv->out[0].v.text.sz = sz0;
+  tv->in[0].v.text.p =(/*unconst*/ char *) p1; tv->in[0].v.text.sz = sz1;
+  return (tvec_claimeq(tv, &tvty_text, 0, file, lno, expr));
 }
 
-int tvec_claimeq_strz(struct tvec_state *tv,
-                     const char *p0, const char *p1,
-                     const char *file, unsigned lno, const char *expr)
+/* --- @tvec_claimeq_textz@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *p0, *p1@ = two strings to compare
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if the strings at @p0@ and @p1@ are equal, otherwise
+ *             zero.
+ *
+ * Use:                Check that strings at @p0@ and @p1@ are equal, as for
+ *             @tvec_claimeq_string@, except that the strings are assumed
+ *             null-terminated, so their lengths don't need to be supplied
+ *             explicitly.
+ */
+
+int tvec_claimeq_textz(struct tvec_state *tv,
+                      const char *p0, const char *p1,
+                      const char *file, unsigned lno, const char *expr)
 {
-  tv->in[0].v.str.p = (/*unconst*/ char *)p0;
-    tv->in[0].v.str.sz = strlen(p0);
-  tv->out[0].v.str.p = (/*unconst*/ char *)p1;
-    tv->out[0].v.str.sz = strlen(p1);
-  return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr));
+  tv->out[0].v.text.p = (/*unconst*/ char *)p0;
+    tv->out[0].v.text.sz = strlen(p0);
+  tv->in[0].v.text.p = (/*unconst*/ char *)p1;
+    tv->in[0].v.text.sz = strlen(p1);
+  return (tvec_claimeq(tv, &tvty_text, 0, file, lno, expr));
 }
 
+/* --- @tvec_claimeq_bytes@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const void *p0@, @size_t sz0@ = first string with length
+ *             @const void *p1@, @size_t sz1@ = second string with length
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *expr@ = the expression to quote on failure
+ *
+ * Returns:    Nonzero if the strings at @p0@ and @p1@ are equal, otherwise
+ *             zero.
+ *
+ * Use:                Check that binary strings at @p0@ and @p1@ are equal.  As for
+ *             @tvec_claim@ above, a test case is automatically begun and
+ *             ended if none is already underway.  If the values are
+ *             unequal, then @tvec_fail@ is called, quoting @expr@, and the
+ *             mismatched values are dumped: @p0@ is printed as the output
+ *             value and @p1@ is printed as the input reference.
+ */
+
 int tvec_claimeq_bytes(struct tvec_state *tv,
                       const void *p0, size_t sz0,
                       const void *p1, size_t sz1,
                       const char *file, unsigned lno, const char *expr)
 {
-  tv->in[0].v.bytes.p = (/*unconst*/ void *)p0;
-    tv->in[0].v.bytes.sz = sz0;
-  tv->out[0].v.bytes.p = (/*unconst*/ void *)p1;
-    tv->out[0].v.bytes.sz = sz1;
+  tv->out[0].v.bytes.p = (/*unconst*/ void *)p0;
+    tv->out[0].v.bytes.sz = sz0;
+  tv->in[0].v.bytes.p = (/*unconst*/ void *)p1;
+    tv->in[0].v.bytes.sz = sz1;
   return (tvec_claimeq(tv, &tvty_bytes, 0, file, lno, expr));
 }
 
+/* --- @tvec_alloctext@, @tvec_allocbytes@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @size_t sz@ = required size
+ *
+ * Returns:    ---
+ *
+ * Use:                Allocated space in a text or binary string register.  If the
+ *             current register size is sufficient, its buffer is left
+ *             alone; otherwise, the old buffer, if any, is freed and a
+ *             fresh buffer allocated.  These functions are not intended to
+ *             be used to adjust a buffer repeatedly, e.g., while building
+ *             output incrementally: (a) they will perform badly, and (b)
+ *             the old buffer contents are simply discarded if reallocation
+ *             is necessary.  Instead, use a @dbuf@ or @dstr@.
+ *
+ *             The @tvec_alloctext@ function sneakily allocates an extra
+ *             byte for a terminating zero.  The @tvec_allocbytes@ function
+ *             doesn't do this.
+ */
+
+void tvec_alloctext(union tvec_regval *rv, size_t sz)
+{
+  if (rv->text.sz <= sz) { xfree(rv->text.p); rv->text.p = xmalloc(sz + 1); }
+  rv->text.sz = sz;
+}
+
+void tvec_allocbytes(union tvec_regval *rv, size_t sz)
+{
+  if (rv->bytes.sz < sz) { xfree(rv->bytes.p); rv->bytes.p = xmalloc(sz); }
+  rv->bytes.sz = sz;
+}
+
 /*----- Buffer type -------------------------------------------------------*/
 
+/* --- @init_buffer@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a register value.
+ *
+ *             Buffer values values are initialized with a null pointer,
+ *             zero length, and zero residue, modulus, and offset.
+ */
+
+static void init_buffer(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->buf.p = 0; rv->buf.sz = rv->buf.a = rv->buf.m = rv->buf.off = 0; }
+
+/* --- @release_buffer@, @release_bytes@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Release resources held by a register value.
+ *
+ *             Buffers are freed.
+ */
+
+static void release_buffer(union tvec_regval *rv,
+                          const struct tvec_regdef *rd)
+  { if (rv->buf.p) xfree(rv->buf.p - rv->buf.off); }
+
+/* --- @eq_buffer@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv0, *rv1@ = register values
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Nonzero if the values are equal, zero if unequal
+ *
+ * Use:                Compare register values for equality.
+ *
+ *             Buffer values are equal if and only if their sizes and
+ *             alignment parameters are equal; their contents are
+ *             %%\emph{not}%% compared.
+ */
+
 static int eq_buffer(const union tvec_regval *rv0,
                     const union tvec_regval *rv1,
                     const struct tvec_regdef *rd)
-  { return (rv0->bytes.sz == rv1->bytes.sz); }
+{
+  return (rv0->buf.sz == rv1->buf.sz &&
+         rv0->buf.a == rv1->buf.a &&
+         rv0->buf.m == rv1->buf.m);
+}
+
+/* --- @tobuf_buffer@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Serialize a register value to a buffer.
+ *
+ *             Buffer values are serialized as their lengths, residues, and
+ *             moduli, as unsigned integers.
+ */
 
 static int tobuf_buffer(buf *b, const union tvec_regval *rv,
                         const struct tvec_regdef *rd)
-  { return (unsigned_to_buf(b, rv->bytes.sz)); }
+{
+  return (unsigned_to_buf(b, rv->buf.sz) ||
+         unsigned_to_buf(b, rv->buf.a) ||
+         unsigned_to_buf(b, rv->buf.m));
+}
+
+/* --- @frombuf_buffer@ --- *
+ *
+ * Arguments:  @buf *b@ = buffer
+ *             @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    Zero on success, %$-1$% on failure.
+ *
+ * Use:                Deserialize a register value from a buffer.
+ *
+ *             Buffer values are serialized as just their lengths, as
+ *             unsigned integers.  The buffer is allocated on
+ *             deserialization and filled with a distinctive pattern.
+ */
 
 static int frombuf_buffer(buf *b, union tvec_regval *rv,
                          const struct tvec_regdef *rd)
 {
-  unsigned long u;
-
-  if (unsigned_from_buf(b, &u)) return (-1);
-  if (u > (size_t)-1) return (-1);
-  tvec_allocbytes(rv, u); memset(rv->bytes.p, '!', u);
+  unsigned long sz, a, m;
+
+  if (unsigned_from_buf(b, &sz)) return (-1);
+  if (unsigned_from_buf(b, &a)) return (-1);
+  if (unsigned_from_buf(b, &m)) return (-1);
+  if (sz > (size_t)-1 || a > (size_t)-1 || m > (size_t)-1)
+    { buf_break(b); return (-1); }
+  rv->buf.sz = sz; rv->buf.a = a; rv->buf.m = m;
   return (0);
 }
 
-static const char units[] = "kMGTPEZY";
+/* --- @parse_buffer@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, %$-1$% on error.
+ *
+ * Use:                Parse a register value from an input file.
+ *
+ *             The input format for a buffer value is a size, followed by an
+ *             optional `%|@$%' and an alignment quantum and a further
+ *             optional `%|+|%' and an alignment offset.  The size, quantum,
+ *             and offset are syntactically sizes.
+ *
+ *             The buffer is not allocated.
+ */
 
-static void parse_buffer(union tvec_regval *rv,
-                        const struct tvec_regdef *rd,
-                        struct tvec_state *tv)
+static int parse_buffer(union tvec_regval *rv,
+                       const struct tvec_regdef *rd,
+                       struct tvec_state *tv)
 {
-  dstr d = DSTR_INIT;
-  char *q; const char *unit;
-  int olderr;
-  size_t pos;
-  unsigned long u, t;
-  unsigned f = 0;
-#define f_range 1u
-
-  tvec_readword(tv, &d, ";", "buffer length");
-  olderr = errno; errno = 0;
-  u = strtoul(d.buf, &q, 0);
-  if (errno) goto bad;
-  errno = olderr;
-  if (!*q) {
-    tvec_skipspc(tv); pos = d.len;
-    if (!tvec_readword(tv, &d, ";", 0)) pos++;
-    q = d.buf + pos;
-  }
+  unsigned long sz, a = 0, m = 0;
+  int ch, rc;
 
-  if (u > (size_t)-1) goto rangerr;
-  for (t = u, unit = units; *unit; unit++) {
-    if (t > (size_t)-1/1024) f |= f_range;
-    else t *= 1024;
-    if (*q == *unit && (!q[1] || q[1] == 'B')) {
-      if (f&f_range) goto rangerr;
-      u = t; q += 2; break;
-    }
+  if (parse_szint(tv, &sz, "@;", "buffer length")) { rc = -1; goto end; }
+  if (check_unsigned_range(sz, &tvrange_size, tv, "buffer length"))
+    { rc = -1; goto end; }
+  if (check_string_length(sz, rd->arg.p, tv)) { rc = -1; goto end; }
+
+  tvec_skipspc(tv);
+  ch = getc(tv->fp);
+  if (ch == ';' || ch == '\n') { ungetc(ch, tv->fp); goto done; }
+  else if (ch != '@') { rc = tvec_syntax(tv, ch, "`@'"); goto end; }
+
+  if (parse_szint(tv, &m, "+;", "alignment quantum")) { rc = -1; goto end; }
+  if (check_unsigned_range(a, &tvrange_size, tv, "alignment quantum"))
+    { rc = -1; goto end; }
+  if (m == 1) m = 0;
+
+  tvec_skipspc(tv);
+  ch = getc(tv->fp);
+  if (ch == ';' || ch == '\n') { ungetc(ch, tv->fp); goto done; }
+  else if (ch != '+') { rc = tvec_syntax(tv, ch, "`+'"); goto end; }
+
+  if (parse_szint(tv, &a, ";", "alignment offset")) { rc = -1; goto end; }
+  if (check_unsigned_range(m, &tvrange_size, tv, "alignment offset"))
+    { rc = -1; goto end; }
+  if (a >= m) {
+    rc = tvec_error(tv, "alignment offset %lu >= quantum %lu",
+                   (unsigned long)a, (unsigned long)m);
+    goto end;
   }
-  if (*q && *q != ';') goto bad;
-  check_string_length(u, rd->arg.p, tv);
 
-  tvec_flushtoeol(tv, 0);
-  tvec_allocbytes(rv, u); memset(rv->bytes.p, 0, u);
-  DDESTROY(&d); return;
-
-bad:
-  tvec_error(tv, "invalid buffer length `%s'", d.buf);
-
-rangerr:
-  tvec_error(tv, "buffer length `%s' out of range", d.buf);
-
-#undef f_range
+done:
+  if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+  rv->buf.sz = sz; rv->buf.a = a; rv->buf.m = m;
+  rc = 0;
+end:
+  return (rc);
 }
 
+/* --- @dump_buffer@ --- *
+ *
+ * Arguments:  @const union tvec_regval *rv@ = register value
+ *             @const struct tvec_regdef *rd@ = register definition
+ *             @unsigned style@ = output style (@TVSF_...@)
+ *             @const struct gprintf_ops *gops@, @void *gp@ = format output
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the format output.
+ *
+ *             Buffer values are dumped as their size, with the alignment
+ *             quantum and alignment offset if these are non-default.
+ */
+
 static void dump_buffer(const union tvec_regval *rv,
                        const struct tvec_regdef *rd,
-                       struct tvec_state *tv, unsigned style)
+                       unsigned style,
+                       const struct gprintf_ops *gops, void *go)
 {
-  const char *unit;
-  unsigned long u = rv->bytes.sz;
-
-  if (!u || u%1024)
-    tvec_write(tv, "%lu B", u);
-  else {
-    for (unit = units, u /= 1024; !(u%1024) && unit[1]; u /= 1024, unit++);
-    tvec_write(tv, "%lu %cB", u, *unit);
+  format_size(gops, go, rv->buf.sz, style);
+  if (rv->buf.m) {
+    gprintf(gops, go, style&(TVSF_COMPACT | TVSF_RAW) ? "@" : " @ ");
+    format_size(gops, go, rv->buf.m, style);
+    if (rv->buf.a) {
+      gprintf(gops, go, style&(TVSF_COMPACT | TVSF_RAW) ? "+" : " + ");
+      format_size(gops, go, rv->buf.a, style);
+    }
+  }
+  if (!(style&TVSF_COMPACT)) {
+    gprintf(gops, go, " ; = %lu", (unsigned long)rv->buf.sz);
+    if (rv->buf.m) {
+      gprintf(gops, go, " @ %lu", (unsigned long)rv->buf.m);
+      if (rv->buf.a) gprintf(gops, go, " + %lu", (unsigned long)rv->buf.a);
+    }
+    gprintf(gops, go, " = "); format_unsigned_hex(gops, go, rv->buf.sz);
+    if (rv->buf.m) {
+      gprintf(gops, go, " @ "); format_unsigned_hex(gops, go, rv->buf.m);
+      if (rv->buf.a) {
+       gprintf(gops, go, " + ");
+       format_unsigned_hex(gops, go, rv->buf.a);
+      }
+    }
   }
 }
 
+/* Buffer type definition. */
 const struct tvec_regty tvty_buffer = {
-  init_bytes, release_bytes, eq_buffer, measure_int,
+  init_buffer, release_buffer, eq_buffer,
   tobuf_buffer, frombuf_buffer,
   parse_buffer, dump_buffer
 };
 
+/* --- @tvec_initbuffer@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *             @const union tvec_regval *ref@ = source buffer
+ *             @size_t sz@ = size to allocate
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize the alignment parameters in @rv@ to match @ref@,
+ *             and the size to @sz@.
+ */
+
+void tvec_initbuffer(union tvec_regval *rv,
+                    const union tvec_regval *ref, size_t sz)
+  { rv->buf.sz = sz; rv->buf.a = ref->buf.a; rv->buf.m = ref->buf.m; }
+
+/* --- @tvec_allocbuffer@ --- *
+ *
+ * Arguments:  @union tvec_regval *rv@ = register value
+ *
+ * Returns:    ---
+ *
+ * Use:                Allocate @sz@ bytes to the buffer and fill the space with a
+ *             distinctive pattern.
+ */
+
+void tvec_allocbuffer(union tvec_regval *rv)
+{
+  unsigned char *p; size_t n;
+
+  if (rv->buf.p) xfree(rv->buf.p - rv->buf.off);
+
+  if (rv->buf.m < 2) {
+    rv->buf.p = xmalloc(rv->buf.sz); rv->buf.off = 0;
+  } else {
+    p = xmalloc(rv->buf.sz + rv->buf.m - 1);
+    n = (size_t)p%rv->buf.m;
+    rv->buf.off = (rv->buf.a - n + rv->buf.m)%rv->buf.m;
+    rv->buf.p = p + rv->buf.off;
+  }
+  memset(rv->buf.p, '?', rv->buf.sz);
+}
+
 /*----- That's all, folks -------------------------------------------------*/