@@@ remote works?
[mLib] / test / tvec-types.c
index 1984a1e..695d66d 100644 (file)
 #  include "base64.h"
 #  include "hex.h"
 #include "dstr.h"
+#include "maths.h"
 #include "tvec.h"
 
 /*----- Preliminary utilities ---------------------------------------------*/
 
-#ifdef isnan
-#  define NANP(x) isnan(x)
-#else
-#  define NANP(x) (!((x) == (x)))
-#endif
-
-#ifdef isinf
-#  define INFP(x) isinf(x)
-#else
-#  define INFP(x) ((x) > DBL_MAX || (x) < DBL_MIN)
-#endif
-
-#ifdef signbit
-#  define NEGP(x) signbit(x)
-#else
-#  define NEGP(x) ((x) < 0.0)
-#endif
+/* --- @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;
@@ -79,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)
 {
@@ -107,6 +111,15 @@ static int unsigned_from_buf(buf *b, unsigned long *u_out)
   *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;
@@ -116,6 +129,57 @@ static int hex_width(unsigned long u)
   return (wd/4);
 }
 
+/* --- @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 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)
+{
+  unsigned long u = i >= 0 ? i : -(unsigned long)i;
+  gprintf(gops, go, "%s0x%0*lx", i < 0 ? "-" : "", hex_width(u), u);
+}
+
+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);
+}
+
+/* --- @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
+ *
+ * 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)
@@ -140,6 +204,15 @@ static int check_unsigned_range(unsigned long u,
   return (0);
 }
 
+/* --- @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') return (ch - '0');
@@ -148,6 +221,29 @@ static int chtodig(int ch)
   else return (-1);
 }
 
+/* --- @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)
 {
@@ -155,10 +251,15 @@ static int parse_unsigned_integer(unsigned long *u_out, const char **q_out,
   int ch, d, r;
   const char *q;
   unsigned f = 0;
-#define f_implicit 1u
-#define f_digit 2u
-#define f_uscore 4u
-
+#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;
@@ -173,24 +274,38 @@ static int parse_unsigned_integer(unsigned long *u_out, const char **q_out,
 
   q = p;
   for (;;) {
-    ch = *p;
-    if (ch == '_') {
-      if (f&f_uscore) break;
-      p++; f = (f&~f_implicit) | f_uscore;
-    }
-    else if (ch == 'r' || ch == 'R') {
-      if (!(f&f_implicit) || !u || u >= 36) break;
-      d = chtodig(p[1]); if (0 > d || d >= u) break;
-      r = u; u = d; f = (f&~f_implicit) | f_digit; p += 2; q = p;
-    } else {
-      d = chtodig(ch);
-      if (d < 0 || d >= r) break;
-      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;
+    /* Work through the string a character at a time. */
+
+    ch = *p; switch (ch) {
+
+      case '_':
+       /* An underscore is OK if we haven't just seen one. */
+
+       if (f&f_uscore) goto done;
+       p++; f = (f&~f_implicit) | f_uscore;
+       break;
+
+      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.
+        */
+
+       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:
+       /* 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 (!(f&f_digit)) return (-1);
   *u_out = u; *q_out = q; return (0);
 
@@ -206,11 +321,14 @@ static int parse_signed_integer(long *i_out, const char **q_out,
   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;
@@ -224,6 +342,38 @@ static int parse_signed_integer(long *i_out, const char **q_out,
 #undef f_neg
 }
 
+/* --- @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.
+ */
+
+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;
+
+  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)) return (-1);
+  *u_out = u; return (0);
+}
+
 static int parse_signed(long *i_out, const char *p,
                        const struct tvec_irange *ir,
                        struct tvec_state *tv)
@@ -238,30 +388,47 @@ static int parse_signed(long *i_out, const char *p,
   *i_out = i; return (0);
 }
 
-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;
+/*----- Floating-point utilities ------------------------------------------*/
 
-  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)) return (-1);
-  *u_out = u; return (0);
-}
+/* --- @eqish_floating_p@ --- *
+ *
+ * Arguments:  @double x, y@ = two numbers to compare
+ *             @const struct tvec_floatinfo *fi@ = floating-point info
+ *
+ * Returns:    Nonzero if  the comparand @y@ is sufficiently close to the
+ *             reference @x@, or zero if it's definitely different.
+ */
 
-static void format_signed_hex(const struct gprintf_ops *gops, void *go,
-                             long i)
+static int eqish_floating_p(double x, double y,
+                           const struct tvec_floatinfo *fi)
 {
-  unsigned long u = i >= 0 ? i : -(unsigned long)i;
-  gprintf(gops, go, "%s0x%0*lx", i < 0 ? "-" : "", hex_width(u), u);
+  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 - y/x; if (t < 0) t = -t; return (t < fi->delta);
+    default:
+      abort();
+  }
 }
 
-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); }
+/* --- @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)
@@ -355,25 +522,18 @@ static void format_floating(const struct gprintf_ops *gops, void *go,
   }
 }
 
-static int eqish_floating_p(double x, double y,
-                           const struct tvec_floatinfo *fi)
-{
-  double xx, yy, 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 - y/x; if (t < 0) t = -t; return (t < fi->delta);
-    default:
-      abort();
-  }
-}
+/* --- @parse_floating@ --- *
+ *
+ * Arguments:  @double *x_out@ = where to put the result
+ *             @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.
+ */
 
 static int parse_floating(double *x_out, const char *p,
                          const struct tvec_floatinfo *fi,
@@ -384,6 +544,7 @@ static int parse_floating(double *x_out, const char *p,
   double x;
   int olderr, rc;
 
+  /* Check for special tokens. */
   if (STRCMP(p, ==, "#nan")) {
 #ifdef NAN
     x = NAN; rc = 0;
@@ -391,24 +552,31 @@ static int parse_floating(double *x_out, const char *p,
     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")) {
+  }
+
+  else if (STRCMP(p, ==, "#inf") ||
+          STRCMP(p, ==, "#+inf") || STRCMP(p, ==, "+#inf")) {
 #ifdef INFINITY
     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")) {
+  }
+
+  else if (STRCMP(p, ==, "#-inf") || STRCMP(p, ==, "-#inf")) {
 #ifdef INFINITY
     x = -INFINITY; rc = 0;
 #else
     tvec_error(tv, "infinity not supported on this system");
     rc = -1; goto end;
 #endif
-  } else {
+  }
+
+  /* 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++;
@@ -416,6 +584,8 @@ static int parse_floating(double *x_out, const char *p,
       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) {
@@ -430,10 +600,12 @@ static int parse_floating(double *x_out, const char *p,
     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 ");
@@ -452,21 +624,214 @@ static int parse_floating(double *x_out, const char *p,
     tvec_error(tv, "%s", d.buf); rc = -1; goto end;
   }
 
-  *x_out = x; rc = 0;
-end:
-  dstr_destroy(&d);
-  return (rc);
+  /* 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 int convert_hex(char ch, int *v_out)
+static void maybe_format_signed_char
+  (const struct gprintf_ops *gops, void *go, long i)
 {
-  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); }
-  else return (-1);
+  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); }
 }
 
-static int read_escape(int *ch_out, struct tvec_state *tv)
+/* --- @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;
@@ -474,7 +839,11 @@ static int read_escape(int *ch_out, struct tvec_state *tv)
 
   ch = getc(tv->fp);
   switch (ch) {
-    case EOF: case '\n': tvec_syntax(tv, ch, "string escape");
+
+    /* 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;
@@ -487,15 +856,16 @@ static int read_escape(int *ch_out, struct tvec_state *tv)
     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;
-      if (convert_hex(ch, &esc))
-       return (tvec_syntax(tv, ch, "hex digit"));
+      esc = chtodig(ch);
+      if (esc < 0 || esc >= 16) return (tvec_syntax(tv, ch, "hex digit"));
       for (;;) {
-       ch = getc(tv->fp); if (convert_hex(ch, &i)) break;
-       esc = 8*esc + i;
+       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));
@@ -505,6 +875,10 @@ static int read_escape(int *ch_out, struct tvec_state *tv)
       *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';
@@ -514,19 +888,39 @@ static int read_escape(int *ch_out, struct tvec_state *tv)
          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;
+       *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;
@@ -539,7 +933,7 @@ static int read_quoted_string(dstr *d, int quote, struct tvec_state *tv)
       case '\\':
        if (quote == '\'') goto ordinary;
        ch = getc(tv->fp); if (ch == '\n') { tv->lno++; break; }
-       ungetc(ch, tv->fp); if (read_escape(&ch, tv)) return (-1);
+       ungetc(ch, tv->fp); if (read_charesc(&ch, tv)) return (-1);
        goto ordinary;
       default:
        if (ch == quote) goto end;
@@ -554,58 +948,16 @@ end:
   return (0);
 }
 
-#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;
-    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;
-  }
-}
-
-static void format_char(const struct gprintf_ops *gops, void *go, int ch)
-{
-  if (ch == EOF)
-    gprintf(gops, go, "#eof");
-  else if (isprint(ch) && ch != '\'')
-    gprintf(gops, go, "'%c'", ch);
-  else {
-    gprintf(gops, go, "'");
-    format_charesc(gops, go, ch, 0);
-    gprintf(gops, go, "'");
-  }
-}
-
-static void maybe_format_signed_char
-  (const struct gprintf_ops *gops, void *go, long i)
-{
-  if (i == EOF || (0 <= i && i < UCHAR_MAX))
-    { gprintf(gops, go, " = "); format_char(gops, go, i); }
-}
-
-static void maybe_format_unsigned_char
-  (const struct gprintf_ops *gops, void *go, unsigned long u)
-{
-  if (u < UCHAR_MAX)
-    { gprintf(gops, go, " = "); format_char(gops, go, u); }
-}
-
-enum { TVCODE_BARE, TVCODE_HEX, TVCODE_BASE64, TVCODE_BASE32 };
+/* --- @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)
 {
@@ -625,7 +977,7 @@ static int collect_bare(dstr *d, struct tvec_state *tv)
        ungetc(ch, tv->fp); if (tvec_nexttoken(tv)) { rc = -1; goto end; }
        DPUTC(d, ' '); s = SPACE;
        break;
-      case '"': case '\'': case '!':
+      case '"': case '\'': case '!': case '#': case ')': case '}': case ']':
        if (s == SPACE) { ungetc(ch, tv->fp); goto done; }
        goto addch;
       case '\\':
@@ -649,6 +1001,22 @@ 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)
 {
@@ -670,63 +1038,205 @@ static void set_up_encoding(const codec_class **ccl_out, unsigned *f_out,
   }
 }
 
+/* --- @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, struct tvec_state *tv)
+                               unsigned code, unsigned f,
+                               struct tvec_state *tv)
 {
-  const codec_class *ccl; unsigned f;
+  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, &f, code);
-  if (tvec_nexttoken(tv)) tvec_syntax(tv, fgetc(tv->fp), "string");
+  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);
-    if (ch == '"' || ch == '\'')
-      { if (read_quoted_string(&d, ch, tv)) { rc = -1; goto end; } }
-    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);
-       rc = -1; goto end;
-      }
-      set_up_encoding(&ccl, &f, code);
-    } else if (ccl) {
-      ungetc(ch, tv->fp);
-      DRESET(&w);
-      if (tvec_readword(tv, &w, ";", "%s-encoded fragment", ccl->name))
-       { rc = -1; goto end; }
-      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));
-       rc = -1; goto end;
-      }
-      cdc->ops->destroy(cdc);
-    } else switch (code) {
-      case TVCODE_BARE:
+    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, ";", "character name");
+       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);
-       if (collect_bare(&d, tv)) goto done;
+       DRESET(&w); tvec_readword(tv, &w, ";", "`!'-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, ";{", "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:
-       abort();
+       /* 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, ";", "%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);
 }
@@ -880,6 +1390,23 @@ const struct tvec_urange
   tvrange_u16 = { 0, 65535 },
   tvrange_u32 = { 0, 4294967296 };
 
+/* --- @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)
 {
@@ -887,6 +1414,23 @@ int tvec_claimeq_int(struct tvec_state *tv, long i0, long 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)
@@ -942,14 +1486,46 @@ const struct tvec_regty tvty_float = {
   parse_float, dump_float
 };
 
-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));
-}
+/* --- @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 @u1@ 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 - y/x| < \delta$%.  (Note that this
+ *                 criterion is asymmetric FIXME
+ */
+
 int tvec_claimeqish_float(struct tvec_state *tv,
                          double f0, double f1, unsigned f, double delta,
                          const char *file, unsigned lno,
@@ -963,6 +1539,29 @@ int tvec_claimeqish_float(struct tvec_state *tv,
   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 @u1@ 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));
+}
+
 /*----- Enumerations ------------------------------------------------------*/
 
 #define init_ienum init_int
@@ -1171,6 +1770,24 @@ static const struct tvec_iassoc bool_assoc[] = {
 const struct tvec_ienuminfo tvenum_bool =
   { "bool", bool_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,                                       \
@@ -1265,6 +1882,24 @@ const struct tvec_regty tvty_flags = {
   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,
@@ -1279,7 +1914,7 @@ int tvec_claimeq_flags(struct tvec_state *tv,
 /*----- Characters --------------------------------------------------------*/
 
 static int tobuf_char(buf *b, const union tvec_regval *rv,
-                    const struct tvec_regdef *rd)
+                     const struct tvec_regdef *rd)
 {
   uint32 u;
   if (0 <= rv->i && rv->i <= UCHAR_MAX) u = rv->i;
@@ -1289,7 +1924,7 @@ static int tobuf_char(buf *b, const union tvec_regval *rv,
 }
 
 static int frombuf_char(buf *b, union tvec_regval *rv,
-                      const struct tvec_regdef *rd)
+                       const struct tvec_regdef *rd)
 {
   uint32 u;
 
@@ -1312,23 +1947,29 @@ static int parse_char(union tvec_regval *rv, const struct tvec_regdef *rd,
   if (ch == '#') {
     ungetc(ch, tv->fp);
     if (tvec_readword(tv, &d, ";", "character name")) { rc = -1; goto end; }
-    if (STRCMP(d.buf, ==, "#eof"))
-      rv->i = EOF;
-    else {
+    if (read_charname(&ch, d.buf, RCF_EOFOK)) {
       rc = tvec_error(tv, "unknown character name `%s'", d.buf);
       goto end;
     }
-    rc = 0; goto end;
+    if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }
+    rv->i = ch; rc = 0; goto end;
   }
 
   if (ch == '\'') { f |= f_quote; ch = getc(tv->fp); }
   switch (ch) {
+    case ';':
+      if (!(f&f_quote)) { rc = tvec_syntax(tv, ch, "character"); goto end; }
+      goto plain;
+    case '\n':
+      if (f&f_quote)
+       { 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 '\'':
-      if (!(f&f_quote)) goto plain;
-    case EOF: case '\n':
       rc = tvec_syntax(tv, ch, "character"); goto end;
     case '\\':
-      if (read_escape(&ch, tv)) return (-1);
+      if (read_charesc(&ch, tv)) return (-1);
     default: plain:
       rv->i = ch; break;
   }
@@ -1350,15 +1991,37 @@ static void dump_char(const union tvec_regval *rv,
                      unsigned style,
                      const struct gprintf_ops *gops, void *go)
 {
-  if ((style&TVSF_COMPACT) && isprint(rv->i) && rv->i != '\'')
-    gprintf(gops, go, "%c", (int)rv->i);
-  else
-    format_char(gops, go, rv->i);
+  const char *p;
+  unsigned f = 0;
+#define f_semi 1u
+
+  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 (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;
+    }
+  }
 
   if (!(style&TVSF_COMPACT)) {
-    gprintf(gops, go, " ; = %ld = ", rv->i);
+    if (!(f&f_semi)) gprintf(gops, go, " ;");
+    gprintf(gops, go, " = %ld = ", rv->i);
     format_signed_hex(gops, go, rv->i);
   }
+
+#undef f_semi
 }
 
 const struct tvec_regty tvty_char = {
@@ -1367,6 +2030,23 @@ const struct tvec_regty tvty_char = {
   parse_char, dump_char
 };
 
+/* --- @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.
+ */
+
 int tvec_claimeq_char(struct tvec_state *tv, int c0, int c1,
                      const char *file, unsigned lno, const char *expr)
 {
@@ -1376,18 +2056,6 @@ int tvec_claimeq_char(struct tvec_state *tv, int c0, int c1,
 
 /*----- 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;
-}
-
 static void init_string(union tvec_regval *rv, const struct tvec_regdef *rd)
   { rv->str.p = 0; rv->str.sz = 0; }
 
@@ -1435,7 +2103,7 @@ static int frombuf_string(buf *b, union tvec_regval *rv,
   size_t sz;
 
   p = buf_getmem32l(b, &sz); if (!p) return (-1);
-  tvec_allocstring(rv, sz); memcpy(rv->str.p, p, sz);
+  tvec_allocstring(rv, sz); memcpy(rv->str.p, p, sz); rv->str.p[sz] = 0;
   return (0);
 }
 
@@ -1455,7 +2123,7 @@ static int check_string_length(size_t sz, const struct tvec_urange *ur,
 {
   if (ur && (ur->min > sz || sz > ur->max))
     return (tvec_error(tv,
-                      "invalid string length %lu; must be in [%lu..%lu]",
+                      "invalid string length %lu; must be in [%lu .. %lu]",
                       (unsigned long)sz, ur->min, ur->max));
   return (0);
 }
@@ -1465,7 +2133,8 @@ static int parse_string(union tvec_regval *rv, const struct tvec_regdef *rd,
 {
   void *p = rv->str.p;
 
-  if (read_compound_string(&p, &rv->str.sz, TVCODE_BARE, tv)) return (-1);
+  if (read_compound_string(&p, &rv->str.sz, TVCODE_BARE, 0, tv))
+    return (-1);
   rv->str.p = p;
   if (check_string_length(rv->str.sz, rd->arg.p, tv)) return (-1);
   return (0);
@@ -1476,7 +2145,8 @@ static int parse_bytes(union tvec_regval *rv, const struct tvec_regdef *rd,
 {
   void *p = rv->bytes.p;
 
-  if (read_compound_string(&p, &rv->bytes.sz, TVCODE_HEX, tv)) return (-1);
+  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);
@@ -1495,23 +2165,32 @@ static void dump_string(const union tvec_regval *rv,
   if (!rv->str.sz) { gprintf(gops, go, "\"\""); return; }
 
   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) { gprintf(gops, go, "\n\t"); goto quote; }
   else if (f&f_nonword) goto quote;
-  gops->putm(go, (const char *)p, rv->str.sz); return;
+
+  gops->putm(go, (const char *)p, rv->str.sz);
+  return;
 
 quote:
   gprintf(gops, go, "\"");
   for (q = p; q < l; q++)
     if (!isprint(*q) || *q == '"') {
       if (p < q) gops->putm(go, (const char *)p, q - p);
-      if (*q == '\n' && !(style&TVSF_COMPACT))
-       gprintf(gops, go, "\\n\"\t\"");
-      else
+      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) gops->putm(go, (const char *)p, q - p);
   gprintf(gops, go, "\"");
@@ -1547,10 +2226,10 @@ static void dump_bytes(const union tvec_regval *rv,
     if (l - p < 16) n = l - p;
     else n = 16;
 
-    for (i = 0; i < 16; i++) {
+    for (i = 0; i < n; i++) {
       if (i < n) gprintf(gops, go, "%02x", p[i]);
       else gprintf(gops, go, "  ");
-      if (i%4 == 3) gprintf(gops, go, " ");
+      if (i < n - 1 && i%4 == 3) gprintf(gops, go, " ");
     }
     gprintf(gops, go, " ; ");
     if (sz > 16) gprintf(gops, go, "[%0*lx] ", wd, (unsigned long)off);
@@ -1573,6 +2252,25 @@ const struct tvec_regty tvty_bytes = {
   parse_bytes, dump_bytes
 };
 
+/* --- @tvec_claimeq_string@ --- *
+ *
+ * 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_string(struct tvec_state *tv,
                        const char *p0, size_t sz0,
                        const char *p1, size_t sz1,
@@ -1583,6 +2281,22 @@ int tvec_claimeq_string(struct tvec_state *tv,
   return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr));
 }
 
+/* --- @tvec_claimeq_strz@ --- *
+ *
+ * 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_strz(struct tvec_state *tv,
                      const char *p0, const char *p1,
                      const char *file, unsigned lno, const char *expr)
@@ -1594,6 +2308,25 @@ int tvec_claimeq_strz(struct tvec_state *tv,
   return (tvec_claimeq(tv, &tvty_string, 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,
@@ -1606,6 +2339,39 @@ int tvec_claimeq_bytes(struct tvec_state *tv,
   return (tvec_claimeq(tv, &tvty_bytes, 0, file, lno, expr));
 }
 
+/* --- @tvec_allocstring@, @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_allocstring@ 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)
+{
+  if (rv->str.sz <= sz) { xfree(rv->str.p); rv->str.p = xmalloc(sz + 1); }
+  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;
+}
+
 /*----- Buffer type -------------------------------------------------------*/
 
 static int eq_buffer(const union tvec_regval *rv0,
@@ -1654,12 +2420,13 @@ static int parse_buffer(union tvec_regval *rv,
   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 (*q == *unit) {
       if (f&f_range) goto rangerr;
-      u = t; q += 2; break;
+      u = t; q++; break;
     }
   }
-  if (*q && *q != ';') goto bad;
+  if (*q == 'B') q++;
+  if (*q) goto bad;
   if (check_string_length(u, rd->arg.p, tv)) { rc = -1; goto end; }
 
   if (tvec_flushtoeol(tv, 0)) { rc = -1; goto end; }