@@@ man wip
[mLib] / test / tvec-types.c
index 695d66d..87748eb 100644 (file)
@@ -107,7 +107,7 @@ 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);
 }
 
@@ -161,7 +161,7 @@ static int signed_from_buf(buf *b, long *i_out)
   else {
     CPL64(k, k);
     if (CMP64(k, <=, not_lmin)) *i_out = -(long)GET64(unsigned long, k) - 1;
-    else return (-1);
+    else { buf_break(b); return (-1); }
   }
   return (0);
 }
@@ -387,6 +387,90 @@ static int parse_signed(long *i_out, const char *p,
   if (check_signed_range(i, ir, tv)) return (-1);
   *i_out = i; return (0);
 }
+static const char size_units[] = "kMGTPEZY";
+
+/* --- @parse_size@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @size_t *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 int parse_size(struct tvec_state *tv, size_t *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
+
+  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);
+
+  if (u > (size_t)-1) goto rangerr;
+  for (t = u, unit = size_units; *unit; unit++) {
+    if (t > (size_t)-1/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;
+
+  *u_out = u; rc = 0;
+end:
+  dstr_destroy(&d);
+  return (rc);
+
+bad:
+  tvec_error(tv, "invalid %s `%s'", what, d.buf);
+  rc = -1; goto end;
+
+rangerr:
+  tvec_error(tv, "%s `%s' out of range", what, d.buf);
+  rc = -1; goto end;
+
+#undef f_range
+}
+
+/* --- @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 format_size(const struct gprintf_ops *gops, void *go,
+                       unsigned long u, unsigned style)
+{
+  const char *unit;
+
+  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);
+  }
+}
 
 /*----- Floating-point utilities ------------------------------------------*/
 
@@ -525,6 +609,7 @@ static void format_floating(const struct gprintf_ops *gops, void *go,
 /* --- @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
@@ -532,10 +617,12 @@ static void format_floating(const struct gprintf_ops *gops, void *go,
  * Returns:    Zero on success, @-1@ on error.
  *
  * Use:                Parse a floating-point number from a string.  Reports any
- *             necessary errors.
+ *             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 *p,
+static int parse_floating(double *x_out, const char **q_out, const char *p,
                          const struct tvec_floatinfo *fi,
                          struct tvec_state *tv)
 {
@@ -547,6 +634,7 @@ static int parse_floating(double *x_out, const char *p,
   /* 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");
@@ -557,6 +645,7 @@ static int parse_floating(double *x_out, const char *p,
   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");
@@ -566,6 +655,7 @@ static int parse_floating(double *x_out, const char *p,
 
   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");
@@ -588,13 +678,11 @@ static int parse_floating(double *x_out, const char *p,
     /* Parse the number using the system parser. */
     olderr = errno; errno = 0;
     x = strtod(p, &q);
-    if (*q) {
-      tvec_syntax(tv, *q, "end-of-line");
-      rc = -1; goto end;
-    }
+    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",
-                p, strerror(errno));
+      tvec_error(tv, "invalid floating-point number `%.*s': %s",
+                (int)(q - p), p, strerror(errno));
       rc = -1; goto end;
     }
     errno = olderr;
@@ -1131,7 +1219,7 @@ static int read_compound_string(void **p_inout, size_t *sz_inout,
        ungetc(ch, tv->fp);
        if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; }
        cdc = 0;
-       DRESET(&w); tvec_readword(tv, &w, ";", "character name");
+       DRESET(&w); tvec_readword(tv, &w, 0, ";", "character name");
        if (read_charname(&ch, w.buf, RCF_EOFOK)) {
          rc = tvec_error(tv, "unknown character name `%s'", d.buf);
          goto end;
@@ -1144,7 +1232,7 @@ static int read_compound_string(void **p_inout, size_t *sz_inout,
        if (cdc && flush_codec(cdc, &d, tv)) { rc = -1; goto end; }
        cdc = 0;
        ungetc(ch, tv->fp);
-       DRESET(&w); tvec_readword(tv, &w, ";", "`!'-keyword");
+       DRESET(&w); tvec_readword(tv, &w, 0, ";", "`!'-keyword");
 
        /* Change bareword coding system. */
        if (STRCMP(w.buf, ==, "!bare"))
@@ -1163,7 +1251,7 @@ static int read_compound_string(void **p_inout, size_t *sz_inout,
            goto end;
          }
          DRESET(&w);
-         if (tvec_readword(tv, &w, ";{", "repeat count"))
+         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);
@@ -1209,7 +1297,8 @@ static int read_compound_string(void **p_inout, size_t *sz_inout,
          default:
            assert(ccl);
            ungetc(ch, tv->fp); DRESET(&w);
-           if (tvec_readword(tv, &w, ";", "%s-encoded fragment", ccl->name))
+           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);
@@ -1241,36 +1330,36 @@ end:
   return (rc);
 }
 
-/*----- 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 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 int 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_...,
-  tobuf_..., frombuf_...,
-  parse_..., dump_...
-};
-*/
 /*----- 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); }
@@ -1280,6 +1369,20 @@ static int eq_uint(const union tvec_regval *rv0,
                   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)); }
@@ -1288,6 +1391,20 @@ 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)); }
@@ -1296,18 +1413,52 @@ 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, ";", "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))
+  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);
@@ -1320,18 +1471,32 @@ static int parse_uint(union tvec_regval *rv, const struct tvec_regdef *rd,
   dstr d = DSTR_INIT;
   int rc;
 
-  if (tvec_readword(tv, &d, ";", "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))
+  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,
                     unsigned style,
@@ -1359,12 +1524,19 @@ static void dump_uint(const union tvec_regval *rv,
   }
 }
 
+/* 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 },
@@ -1373,13 +1545,6 @@ const struct tvec_irange
   tvrange_sbyte = { -128, 127 },
   tvrange_i16 = { -32768, +32767 },
   tvrange_i32 = { -2147483648, 2147483647 };
-
-const struct tvec_regty tvty_uint = {
-  init_uint, trivial_release, eq_uint,
-  tobuf_uint, frombuf_uint,
-  parse_uint, dump_uint
-};
-
 const struct tvec_urange
   tvrange_uchar = { 0, UCHAR_MAX },
   tvrange_ushort = { 0, USHRT_MAX },
@@ -1388,7 +1553,7 @@ const struct tvec_urange
   tvrange_size = { 0, (size_t)-1 },
   tvrange_byte = { 0, 255 },
   tvrange_u16 = { 0, 65535 },
-  tvrange_u32 = { 0, 4294967296 };
+  tvrange_u32 = { 0, 4294967295 };
 
 /* --- @tvec_claimeq_int@ --- *
  *
@@ -1441,51 +1606,145 @@ int tvec_claimeq_uint(struct tvec_state *tv,
 
 /*----- 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, ";", "floating-point number"))
+  if (tvec_readword(tv, &d, 0, ";", "floating-point number"))
     { rc = -1; goto end; }
-  if (parse_floating(&rv->f, d.buf, rd->arg.p, tv))
-    { rc = -1; goto end; }
-  if (tvec_flushtoeol(tv, 0))
+  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)
   { 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
@@ -1562,16 +1821,219 @@ int tvec_claimeq_float(struct tvec_state *tv,
                                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 (!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
+};
+
 /*----- Enumerations ------------------------------------------------------*/
 
+/* --- @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|%.
+ */
+
 #define init_ienum init_int
 #define init_uenum init_uint
 #define init_fenum init_float
+
 static void init_penum(union tvec_regval *rv, const struct tvec_regdef *rd)
   { rv->p = 0; }
 
+/* --- @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.
+ */
+
 #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)
@@ -1579,14 +2041,33 @@ static int eq_fenum(const union tvec_regval *rv0,
   const struct tvec_fenuminfo *ei = rd->arg.p;
   return (eqish_floating_p(rv0->f, rv1->f, ei->fi));
 }
+
 static int eq_penum(const union tvec_regval *rv0,
                    const union tvec_regval *rv1,
                    const struct tvec_regdef *rd)
   { return (rv0->p == rv1->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.
+ */
+
 #define tobuf_ienum tobuf_int
 #define tobuf_uenum tobuf_uint
 #define tobuf_fenum tobuf_float
+
 static int tobuf_penum(buf *b, const union tvec_regval *rv,
                       const struct tvec_regdef *rd)
 {
@@ -1602,11 +2083,28 @@ found:
   return (signed_to_buf(b, i));
 }
 
-#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)
+/* --- @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.
+ */
+
+#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_penuminfo *pei = rd->arg.p;
   const struct tvec_passoc *pa;
@@ -1616,10 +2114,27 @@ static int frombuf_penum(buf *b, union tvec_regval *rv,
   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 return (-1);
+  else { buf_break(b); return (-1); }
   return (0);
 }
 
+/* --- @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.
+ */
+
 #define DEFPARSE_ENUM(tag_, ty, slot)                                  \
   static int parse_##slot##enum(union tvec_regval *rv,                 \
                                const struct tvec_regdef *rd,           \
@@ -1630,7 +2145,7 @@ static int frombuf_penum(buf *b, union tvec_regval *rv,
     dstr d = DSTR_INIT;                                                        \
     int rc;                                                            \
                                                                        \
-    if (tvec_readword(tv, &d, ";", "enumeration tag or " LITSTR_##tag_)) \
+    if (tvec_readword(tv, &d, 0, ";", "enumeration tag or " LITSTR_##tag_)) \
       { rc = -1; goto end; }                                           \
     for (a = ei->av; a->tag; a++)                                      \
       if (STRCMP(a->tag, ==, d.buf)) { FOUND_##tag_ goto done; }       \
@@ -1656,7 +2171,7 @@ static int frombuf_penum(buf *b, union tvec_regval *rv,
 #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, d.buf, ei->fi, tv))  \
+#define MISSING_FLT    if (parse_floating(&rv->f, 0, d.buf, ei->fi, tv)) \
                          { rc = -1; goto end; }
 
 #define LITSTR_PTR     "`#nil'"
@@ -1689,6 +2204,27 @@ TVEC_MISCSLOTS(DEFPARSE_ENUM)
 
 #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.
+ */
+
+
 #define DEFDUMP_ENUM(tag_, ty, slot)                                   \
   static void dump_##slot##enum(const union tvec_regval *rv,           \
                                const struct tvec_regdef *rd,           \
@@ -1709,7 +2245,7 @@ TVEC_MISCSLOTS(DEFPARSE_ENUM)
   }
 
 #define MAYBE_PRINT_EXTRA                                              \
-       if (style&TVSF_COMPACT) ;                                       \
+       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:
@@ -1741,6 +2277,7 @@ TVEC_MISCSLOTS(DEFDUMP_ENUM)
 #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,               \
@@ -1750,6 +2287,7 @@ TVEC_MISCSLOTS(DEFDUMP_ENUM)
 TVEC_MISCSLOTS(DEFTY_ENUM)
 #undef DEFTY_ENUM
 
+/* Predefined enumeration types. */
 static const struct tvec_iassoc bool_assoc[] = {
   { "nil",             0 },
   { "false",           0 },
@@ -1764,12 +2302,31 @@ static const struct tvec_iassoc bool_assoc[] = {
   { "y",               1 },
   { "on",              1 },
 
-  { 0,                 0 }
+  TVEC_ENDENUM
 };
 
 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 },
+
+  TVEC_ENDENUM
+};
+
+const struct tvec_ienuminfo tvenum_cmp =
+  { "cmp", cmp_assoc, &tvrange_int };
+
 /* --- @tvec_claimeq_tenum@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
@@ -1815,6 +2372,27 @@ TVEC_MISCSLOTS(DEFCLAIM)
 
 /*----- Flag types --------------------------------------------------------*/
 
+/* 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)
 {
@@ -1825,10 +2403,13 @@ static int parse_flags(union tvec_regval *rv, const struct tvec_regdef *rd,
   int ch, rc;
 
   for (;;) {
+
+    /* Read the next item. */
     DRESET(&d);
-    if (tvec_readword(tv, &d, "|;", "flag name or integer"))
+    if (tvec_readword(tv, &d, 0, "|;", "flag name or integer"))
       { 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)
@@ -1837,23 +2418,54 @@ static int parse_flags(union tvec_regval *rv, const struct tvec_regdef *rd,
          { m |= f->m; v |= f->v; goto next; }
       }
 
+    /* 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, "`|'"); rc = -1; goto end; }
-    if (tvec_nexttoken(tv))
+      if (tvec_nexttoken(tv))
       { tvec_syntax(tv, '\n', "flag name or integer"); rc = -1; goto end; }
   }
-  rv->u = v;
-  rc = 0;
+
+  /* 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,
                       unsigned style,
@@ -1861,7 +2473,7 @@ static void dump_flags(const union tvec_regval *rv,
 {
   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;
 
   for (f = fi->fv, sep = ""; f->tag; f++)
@@ -1872,10 +2484,11 @@ static void dump_flags(const union tvec_regval *rv,
 
   if (v&m) gprintf(gops, go, "%s0x%0*lx", sep, hex_width(v), v&m);
 
-  if (!(style&TVSF_COMPACT))
+  if (m != ~0ul && !(style&TVSF_COMPACT))
     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,
@@ -1913,16 +2526,47 @@ int tvec_claimeq_flags(struct tvec_state *tv,
 
 /*----- 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 return (-1);
+  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)
 {
@@ -1931,10 +2575,79 @@ static int frombuf_char(buf *b, union tvec_regval *rv,
   if (buf_getu32l(b, &u)) return (-1);
   if (0 <= u && u <= UCHAR_MAX) rv->i = u;
   else if (u == MASK32) rv->i = EOF;
-  else return (-1);
+  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)
 {
@@ -1943,40 +2656,78 @@ static int parse_char(union tvec_regval *rv, const struct tvec_regdef *rd,
   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, ";", "character name")) { rc = -1; goto end; }
-    if (read_charname(&ch, d.buf, RCF_EOFOK)) {
-      rc = tvec_error(tv, "unknown character name `%s'", d.buf);
-      goto end;
+    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 (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; }
-      goto plain;
+      else goto plain;
+
     case '\n':
-      if (f&f_quote)
-       { f &= ~f_quote; ungetc(ch, tv->fp); ch = '\''; goto plain; }
+      /* 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:
-      if (f&f_quote) { f &= ~f_quote; ch = '\''; goto plain; }
-      /* fall through */
-    case '\'':
+      /* 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:
@@ -1986,6 +2737,25 @@ end:
 #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,
@@ -1995,6 +2765,7 @@ static void dump_char(const union tvec_regval *rv,
   unsigned f = 0;
 #define f_semi 1u
 
+  /* 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);
@@ -2002,6 +2773,9 @@ static void dump_char(const union tvec_regval *rv,
     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) {
@@ -2015,6 +2789,7 @@ static void dump_char(const union tvec_regval *rv,
     }
   }
 
+  /* And the character code as an integer. */
   if (!(style&TVSF_COMPACT)) {
     if (!(f&f_semi)) gprintf(gops, go, " ;");
     gprintf(gops, go, " = %ld = ", rv->i);
@@ -2024,6 +2799,7 @@ static void dump_char(const union tvec_regval *rv,
 #undef f_semi
 }
 
+/* Character type definition. */
 const struct tvec_regty tvty_char = {
   init_int, trivial_release, eq_int,
   tobuf_char, frombuf_char,
@@ -2056,27 +2832,62 @@ int tvec_claimeq_char(struct tvec_state *tv, int c0, int c1,
 
 /*----- Text and byte strings ---------------------------------------------*/
 
-static void init_string(union tvec_regval *rv, const struct tvec_regdef *rd)
-  { rv->str.p = 0; rv->str.sz = 0; }
+/* --- @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_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,
@@ -2088,22 +2899,52 @@ static int eq_bytes(const union tvec_regval *rv0,
           MEMCMP(rv0->bytes.p, ==, rv1->bytes.p, rv1->bytes.sz)));
 }
 
-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)); }
+/* --- @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_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); rv->str.p[sz] = 0;
+  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);
 }
 
@@ -2113,11 +2954,23 @@ 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);
 }
 
+/* --- @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)
 {
@@ -2128,15 +2981,58 @@ static int check_string_length(size_t sz, const struct tvec_urange *ur,
   return (0);
 }
 
-static int 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;
 
-  if (read_compound_string(&p, &rv->str.sz, TVCODE_BARE, 0, tv))
+  if (read_compound_string(&p, &rv->text.sz, TVCODE_BARE, 0, tv))
     return (-1);
-  rv->str.p = p;
-  if (check_string_length(rv->str.sz, rd->arg.p, tv)) return (-1);
+  rv->text.p = p;
+  if (check_string_length(rv->text.sz, rd->arg.p, tv)) return (-1);
   return (0);
 }
 
@@ -2152,19 +3048,45 @@ static int parse_bytes(union tvec_regval *rv, const struct tvec_regdef *rd,
   return (0);
 }
 
-static void dump_string(const union tvec_regval *rv,
-                       const struct tvec_regdef *rd,
-                       unsigned style,
-                       const struct gprintf_ops *gops, void *go)
+/* --- @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 %|""|%.
+ */
+
+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;
   unsigned f = 0;
 #define f_nonword 1u
 #define f_newline 2u
 
-  if (!rv->str.sz) { gprintf(gops, go, "\"\""); return; }
+  if (!rv->text.sz) { gprintf(gops, go, "\"\""); return; }
 
-  p = (const unsigned char *)rv->str.p; l = p + rv->str.sz;
+  p = (const unsigned char *)rv->text.p; l = p + rv->text.sz;
   switch (*p) {
     case '!': case '#': case ';': case '"': case '\'':
     case '(': case '{': case '[': case ']': case '}': case ')':
@@ -2176,7 +3098,7 @@ static void dump_string(const union tvec_regval *rv,
   if (f&f_newline) { gprintf(gops, go, "\n\t"); goto quote; }
   else if (f&f_nonword) goto quote;
 
-  gops->putm(go, (const char *)p, rv->str.sz);
+  gops->putm(go, (const char *)p, rv->text.sz);
   return;
 
 quote:
@@ -2240,19 +3162,19 @@ static void dump_bytes(const union tvec_regval *rv,
   }
 }
 
-const struct tvec_regty tvty_string = {
-  init_string, release_string, eq_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,
   tobuf_bytes, frombuf_bytes,
   parse_bytes, dump_bytes
 };
 
-/* --- @tvec_claimeq_string@ --- *
+/* --- @tvec_claimeq_text@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
  *             @const char *p0@, @size_t sz0@ = first string with length
@@ -2271,17 +3193,17 @@ const struct tvec_regty tvty_bytes = {
  *             value and @p1@ is printed as the input reference.
  */
 
-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)
+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->out[0].v.str.p = (/*unconst*/ char *)p0; tv->out[0].v.str.sz = sz0;
-  tv->in[0].v.str.p =(/*unconst*/ char *) p1; tv->in[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));
 }
 
-/* --- @tvec_claimeq_strz@ --- *
+/* --- @tvec_claimeq_textz@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
  *             @const char *p0, *p1@ = two strings to compare
@@ -2297,15 +3219,15 @@ int tvec_claimeq_string(struct tvec_state *tv,
  *             explicitly.
  */
 
-int tvec_claimeq_strz(struct tvec_state *tv,
-                     const char *p0, const char *p1,
-                     const char *file, unsigned lno, const char *expr)
+int tvec_claimeq_textz(struct tvec_state *tv,
+                      const char *p0, const char *p1,
+                      const char *file, unsigned lno, const char *expr)
 {
-  tv->out[0].v.str.p = (/*unconst*/ char *)p0;
-    tv->out[0].v.str.sz = strlen(p0);
-  tv->in[0].v.str.p = (/*unconst*/ char *)p1;
-    tv->in[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@ --- *
@@ -2339,7 +3261,7 @@ int tvec_claimeq_bytes(struct tvec_state *tv,
   return (tvec_claimeq(tv, &tvty_bytes, 0, file, lno, expr));
 }
 
-/* --- @tvec_allocstring@, @tvec_allocbytes@ --- *
+/* --- @tvec_alloctext@, @tvec_allocbytes@ --- *
  *
  * Arguments:  @union tvec_regval *rv@ = register value
  *             @size_t sz@ = required size
@@ -2355,15 +3277,15 @@ int tvec_claimeq_bytes(struct tvec_state *tv,
  *             the old buffer contents are simply discarded if reallocation
  *             is necessary.  Instead, use a @dbuf@ or @dstr@.
  *
- *             The @tvec_allocstring@ function sneakily allocates an extra
+ *             The @tvec_alloctext@ function sneakily allocates an extra
  *             byte for a terminating zero.  The @tvec_allocbytes@ function
  *             doesn't do this.
  */
 
-void tvec_allocstring(union tvec_regval *rv, size_t sz)
+void tvec_alloctext(union tvec_regval *rv, size_t sz)
 {
-  if (rv->str.sz <= sz) { xfree(rv->str.p); rv->str.p = xmalloc(sz + 1); }
-  rv->str.sz = 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)
@@ -2374,98 +3296,264 @@ void tvec_allocbytes(union tvec_regval *rv, size_t 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;
+  unsigned long sz, a, m;
 
-  if (unsigned_from_buf(b, &u)) return (-1);
-  if (u > (size_t)-1) return (-1);
-  tvec_allocbytes(rv, u); memset(rv->bytes.p, '!', u);
+  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 consists of an unsigned
+ *             integer followed by an optional unit specifier consisting of
+ *             an SI unit prefix and (optionally) the letter `B'.  Unit
+ *             prefixes denote %%\emph{binary}%% multipliers, not decimal.
+ *
+ *             The buffer is allocated and filled with a distinctive
+ *             pattern.
+ */
 
 static int parse_buffer(union tvec_regval *rv,
                        const struct tvec_regdef *rd,
                        struct tvec_state *tv)
 {
-  dstr d = DSTR_INIT;
-  const char *q, *unit;
-  size_t pos;
-  unsigned long u, t;
-  int rc;
-  unsigned f = 0;
-#define f_range 1u
+  size_t sz, a = 0, m = 0;
+  int ch, rc;
 
-  if (tvec_readword(tv, &d, ";", "buffer length")) { rc = -1; goto end; }
-  if (parse_unsigned_integer(&u, &q, d.buf)) goto bad;
-  if (!*q) {
-    tvec_skipspc(tv); pos = d.len;
-    if (!tvec_readword(tv, &d, ";", 0)) pos++;
-    q = d.buf + pos;
-  }
+  if (parse_size(tv, &sz, "@;", "buffer length")) { rc = -1; goto end; }
+  if (check_string_length(sz, rd->arg.p, tv)) { rc = -1; goto end; }
 
-  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) {
-      if (f&f_range) goto rangerr;
-      u = t; q++; break;
-    }
+  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_size(tv, &m, "+;", "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_size(tv, &a, ";", "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 == 'B') q++;
-  if (*q) goto bad;
-  if (check_string_length(u, rd->arg.p, tv)) { rc = -1; goto end; }
 
+done:
   if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
-  tvec_allocbytes(rv, u); memset(rv->bytes.p, '?', u);
+  rv->buf.sz = sz; rv->buf.a = a; rv->buf.m = m;
   rc = 0;
 end:
-  DDESTROY(&d); return (rc);
-
-bad:
-  tvec_error(tv, "invalid buffer length `%s'", d.buf);
-  rc = -1; goto end;
-
-rangerr:
-  tvec_error(tv, "buffer length `%s' out of range", d.buf);
-  rc = -1; goto end;
-
-#undef f_range
+  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 an appropriate
+ *             unit specifier.  A unit prefix is only used if the size is an
+ *             exact multiple of the relevant power of two.
+ */
+
 static void dump_buffer(const union tvec_regval *rv,
                        const struct tvec_regdef *rd,
                        unsigned style,
                        const struct gprintf_ops *gops, void *go)
 {
-  const char *unit;
-  unsigned long u = rv->bytes.sz;
-
-  if (!u || u%1024)
-    gprintf(gops, go, "%lu B", u);
-  else {
-    for (unit = units, u /= 1024; !(u%1024) && unit[1]; u /= 1024, unit++);
-    gprintf(gops, go, "%lu %cB", u, *unit);
+  format_size(gops, go, rv->buf.sz, style);
+  if (rv->buf.m) {
+    gprintf(gops, go, style&TVSF_COMPACT ? "@" : " @ ");
+    format_size(gops, go, rv->buf.m, style);
+    if (rv->buf.a) {
+      gprintf(gops, go, style&TVSF_COMPACT ? "+" : " + ");
+      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,
+  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 -------------------------------------------------*/