chiark / gitweb /
@@@ wip
authorMark Wooding <mdw@distorted.org.uk>
Wed, 27 Dec 2023 17:58:04 +0000 (17:58 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Wed, 27 Dec 2023 17:58:04 +0000 (17:58 +0000)
21 files changed:
codec/baseconv.c
codec/codec.h
codec/tests.at
struct/buf-float.c
struct/buf-putf.c
struct/dstr-putf.c
test/bench.c
test/bench.h
test/t/tvec-test.c
test/tests.at
test/tvec-bench.c
test/tvec-core.c
test/tvec-main.c
test/tvec-output.c
test/tvec-remote.c
test/tvec-types.c
test/tvec.h
utils/Makefile.am
utils/gprintf.c
utils/gprintf.h
utils/maths.h [new file with mode: 0644]

index b7cb0a0618ebe0a893c2721c336429a9eb8cdb88..53fceb2818eb4039f83ec24659782d5c168da793 100644 (file)
@@ -364,8 +364,8 @@ static int ctxn##_dodecode(ctxn##_ctx *ctx,                         \
        case PC:                                                        \
          if (f & CDCF_IGNEQMID) break;                                 \
          if (f & CDCF_NOEQPAD) goto badch;                             \
-         if (st == ST_MAIN &&                                          \
-             !(f & CDCF_IGNZPAD) && (a & ((1 << nb) - 1)))             \
+         if (st == ST_MAIN && !(f & CDCF_IGNZPAD) &&                   \
+             ((nb && !(nb%wd)) || (a & ((1 << nb) - 1))))              \
            return (CDCERR_INVZPAD);                                    \
          st = ST_PAD;                                                  \
          if (!(f & CDCF_IGNEQPAD)) {                                   \
@@ -387,8 +387,8 @@ static int ctxn##_dodecode(ctxn##_ctx *ctx,                         \
       }                                                                        \
     }                                                                  \
   } else {                                                             \
-    if (st == ST_MAIN &&                                               \
-       !(f & CDCF_IGNZPAD) && (a & ((1 << nb) - 1)))                   \
+    if (st == ST_MAIN && !(f & CDCF_IGNZPAD) &&                                \
+       ((nb && !(nb%wd)) || (a & ((1 << nb) - 1))))                    \
       return (CDCERR_INVZPAD);                                         \
     if (!(f & (CDCF_IGNEQPAD | CDCF_IGNEQMID | CDCF_NOEQPAD)) && nb)   \
       return (CDCERR_INVEQPAD);                                                \
index 6155d5a5e640fbeec566bb1473a98056bc1a7ae4..f006da5e07c84852a49398aec7da85b1446aa760 100644 (file)
@@ -70,7 +70,7 @@ typedef struct codec_class {
   _(OK,                "No error")                                             \
   _(INVCH,     "Invalid character")                                    \
   _(INVEQPAD,  "Invalid padding character")                            \
-  _(INVZPAD,   "Nonzero padding bits")
+  _(INVZPAD,   "Excess or nonzero padding bits")
 enum {
 #define DECLERR(name, text) CDCERR_##name,
   CODEC_ERRORS(DECLERR)
index bc93d3a794becbd56d088f4524b9c768a617774a..5462dbb75826cb8bae2c595dd191903e09c0395a 100644 (file)
@@ -103,8 +103,10 @@ CODEC_TESTDECODE([base64], [Z:g=:==],      [],             [-figninvch],
                 [1], [Invalid padding character])
 
 ## Test for incorrect padding bits.
+CODEC_TESTDECODE([base64],     [A===],         [],             [],
+                [1], [Excess or nonzero padding bits])
 CODEC_TESTDECODE([base64],     [Zh==],         [],             [],
-                [1], [Nonzero padding bits])
+                [1], [Excess or nonzero padding bits])
 CODEC_TESTDECODE([base64],     [Zh==],         [f],            [-fignzpad])
 
 ## Make sure the case flags are suppressed.
index 502b96b8ad991b2e4750259f60bac9dcfe4afa45..88d6c440a1622a21389893dcb0bb7454af6eceec 100644 (file)
@@ -32,6 +32,7 @@
 
 #include "bits.h"
 #include "buf.h"
+#include "maths.h"
 
 /*----- Formatting primitives ---------------------------------------------*/
 
@@ -80,24 +81,6 @@ static kludge64 f64_to_k64(double x)
 
   /* Some machinery before we start. */
 
-#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_MAX)
-#endif
-
-#ifdef signbit
-#  define NEGP(x) signbit(x)
-#else
-#  define NEGP(x) ((x) < 0)            /* incorrect for negative zero! */
-#endif
-
   if (NANP(x)) {
     /* A NaN. */
     hi = 0x7ff80000; lo = 0;
@@ -168,10 +151,6 @@ static kludge64 f64_to_k64(double x)
 
   /* Convert to external format and go home. */
   SET64(k, hi, lo); return (k);
-
-#undef NANP
-#undef INFP
-#undef NEGP
 }
 
 /* --- @k64_to_f64@ --- *
index 5b465f275b72f14bfa63071ff934263cbcef2aa4..3b4261dbd9d6dde95a29648325b43e4541e344af 100644 (file)
@@ -27,6 +27,8 @@
 
 /*----- Header files ------------------------------------------------------*/
 
+#include "config.h"
+
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdio.h>
@@ -72,9 +74,6 @@ static int nputf(void *out, size_t maxsz, const char *p, ...)
   va_end(ap); b->p += n; return (n);
 }
 
-static int putbuf(void *out, const char *p, size_t sz)
-  { buf *b = out; b->p += sz; return (0); }
-
 const struct gprintf_ops buf_printops =
   { putch, putm, nputf };
 
index aa492e00efad4a46cbf36114dfa7eeae8320e0cc..48c2f6a9ad3facb4c1257bfde99aa8e5acd970d9 100644 (file)
@@ -27,6 +27,8 @@
 
 /*----- Header files ------------------------------------------------------*/
 
+#include "config.h"
+
 #include <stdarg.h>
 #include <stddef.h>
 #include <stdio.h>
index d1a00ea4da9e6f119d95de7f019a079cab9d0c66..9e65a6bcf9a9ce3c9966b291eba191c8b3ef002d 100644 (file)
 
 struct timer {
   struct bench_timer _t;
-  const struct timer_ops *clkops, *cyops;
-  union { int fd; } u_cy;
+  const struct timer_ops *clkops, *cyops; /* time and cycle measurements */
+  union { int fd; } u_cy;              /* state for cycle measurement */
 };
 
 struct timer_ops {
-  void (*now)(struct bench_time *t_out, struct timer *t);
-  void (*teardown)(struct timer *t);
+  void (*now)(struct bench_time *t_out, struct timer *t); /* read current */
+  void (*teardown)(struct timer *t);   /* release held resources */
 };
 
 /*----- Preliminaries -----------------------------------------------------*/
 
 #define NS_PER_S 1000000000
 
+/* --- @debug@ --- *
+ *
+ * Arguments:  @const char *fmt@ = format control string
+ *             @...@ = format arguemnts
+ *
+ * Returns:    ---
+ *
+ * Use:                Maybe report a debugging message to standard error.
+ */
+
 static void PRINTF_LIKE(1, 2) debug(const char *fmt, ...)
 {
   const char *p;
@@ -74,8 +84,58 @@ static void PRINTF_LIKE(1, 2) debug(const char *fmt, ...)
   }
 }
 
+/* --- @timer_diff@ --- *
+ *
+ * Arguments:  @struct bench_timing *delta_out@ = where to putt the result
+ *             @const struct bench_time *t0, *t1@ = two times captured by a
+ *                     timer's @now@ function
+ *
+ * Returns:    ---
+ *
+ * Use:                Calculates the difference between two captured times.  The
+ *             flags are set according to whether the differences are
+ *             meaningful; @delta_out->n@ is left unset.
+ */
+
+static void timer_diff(struct bench_timing *delta_out,
+                      const struct bench_time *t0,
+                      const struct bench_time *t1)
+{
+  unsigned f = t0->f&t1->f;
+  kludge64 k;
+
+#ifdef HAVE_UINT64
+#  define FLOATK64(k) ((double)(k).i)
+#else
+#  define FLOATK64(k) ((double)(k).lo + 4275123318.0*(double)(k).hi)
+#endif
+
+  if (!(f&BTF_TIMEOK))
+    delta_out->t = 0.0;
+  else {
+    SUB64(k, t1->s, t0->s);
+    delta_out->t = FLOATK64(k) - 1 +
+      (t1->ns + NS_PER_S - t0->ns)/(double)NS_PER_S;
+  }
+
+  if (!(f&BTF_CYOK))
+    delta_out->cy = 0.0;
+  else {
+    SUB64(k, t1->cy, t0->cy);
+    delta_out->cy = FLOATK64(k);
+  }
+
+  delta_out->f = f;
+
+#undef FLOATK64
+}
+
 /*----- The null clock ----------------------------------------------------*/
 
+/* This is a cycle counter which does nothing, in case we don't have any
+ * better ideas.
+ */
+
 static void null_now(struct bench_time *t_out, struct timer *t) { ; }
 static void null_teardown(struct timer *t) { ; }
 static const struct timer_ops null_ops = { null_now, null_teardown };
@@ -87,6 +147,10 @@ static int null_cyinit(struct timer *t)
 
 /*----- Linux performance counters ----------------------------------------*/
 
+/* This is a cycle counter which uses the Linux performance event system,
+ * which is probably the best choice if it's available.
+ */
+
 #if defined(HAVE_LINUX_PERF_EVENT_H) && defined(HAVE_UINT64)
 
 #include <sys/types.h>
@@ -143,6 +207,11 @@ static int perfevent_init(struct timer *t)
 
 /*----- Intel time-stamp counter ------------------------------------------*/
 
+/* This is a cycle counter based on the Intel `rdtsc' instruction.  It's not
+ * really suitable for performance measurement because it gets confused by
+ * CPU frequency adjustments.
+ */
+
 #if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
 
 #define EFLAGS_ID (1u << 21)
@@ -224,6 +293,10 @@ static int x86rdtsc_init(struct timer *t)
 
 /*----- POSIX `clock_gettime' ---------------------------------------------*/
 
+/* This is a real-time clock based on the POSIX time interface, with up to
+ * nanosecond precision.
+ */
+
 #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_THREAD_CPUTIME_ID)
 
 static void gettime_now(struct bench_time *t_out, struct timer *t)
@@ -253,6 +326,10 @@ static int gettime_init(struct timer *t)
 
 /*----- Standard C `clock' ------------------------------------------------*/
 
+/* This is a real-time clock based on the C `clock' function which is
+ * guaranteed to be available, though it's not likely to be very good.
+ */
+
 static void clock_now(struct bench_time *t_out, struct timer *t)
 {
   clock_t now, x;
@@ -290,6 +367,7 @@ static int clock_init(struct timer *t)
 
 /*----- Timing setup ------------------------------------------------------*/
 
+/* Tables of timing sources. */
 static const struct timerent {
   const char *name;
   int (*init)(struct timer */*t*/);
@@ -297,6 +375,17 @@ static const struct timerent {
   clktab[] = { GETTIME_CLKENT CLOCK_CLKENT { 0, 0 } },
   cytab[] = { PERFEVENT_CYENT X86RDTSC_CYENT NULL_CYENT { 0, 0 } };
 
+/* --- @find_timer@ --- *
+ *
+ * Arguments:  @const char *name@ = timer name
+ *             @size_t sz@ = length of name
+ *             @const struct timerent *timers@ = table to search
+ *             @const char *what@ = adjective describing table
+ *
+ * Returns:    The table entry matching the given name, or null if there
+ *             isn't one.
+ */
+
 static const struct timerent *find_timer_n(const char *name, size_t sz,
                                           const struct timerent *timers,
                                           const char *what)
@@ -309,6 +398,18 @@ static const struct timerent *find_timer_n(const char *name, size_t sz,
   debug("%s timer `%.*s' not found", what, (int)sz, name); return (0);
 }
 
+/* --- @try_timer@ --- *
+ *
+ * Arguments:  @struct timer *t@ = timer structure
+ *             @const struct timerent *timer@ = timer table entry
+ *             @const char *what@ = adjective describing table
+ *
+ * Returns:    Zero on success, @-1@ if timer failed.
+ *
+ * Use:                Tries to initialize the timer @t@, reporting a debug message
+ *             if it worked.
+ */
+
 static int try_timer(struct timer *t,
                     const struct timerent *timer, const char *what)
 {
@@ -316,6 +417,21 @@ static int try_timer(struct timer *t,
   debug("selected %s timer `%s'", what, timer->name); return (0);
 }
 
+/* --- @select_timer@ --- *
+ *
+ * Arguments:  @struct timer *t@ = timer structure
+ *             @const struct timerent *timer@ = timer table
+ *             @const char *varname@ = environment variable to consult
+ *             @const char *what@ = adjective describing table
+ *
+ * Returns:    Zero on success, @-1@ if timer failed.
+ *
+ * Use:                Select a timer from the table.  If the environment variable
+ *             is set, then parse a comma-separated list of timer names and
+ *             use the first one listed that seems to work; otherwise, try
+ *             the timers in the table in order.
+ */
+
 static int select_timer(struct timer *t, const struct timerent *timers,
                        const char *varname, const char *what)
 {
@@ -338,6 +454,7 @@ static int select_timer(struct timer *t, const struct timerent *timers,
   debug("no suitable %s timer found", what); return (-1);
 }
 
+/* Bench timer operations. */
 static void timer_now(struct bench_timer *tm, struct bench_time *t_out)
 {
   struct timer *t = (struct timer *)tm;
@@ -345,7 +462,6 @@ static void timer_now(struct bench_timer *tm, struct bench_time *t_out)
   t->clkops->now(t_out, t);
   t->cyops->now(t_out, t);
 }
-
 static void timer_destroy(struct bench_timer *tm)
 {
   struct timer *t = (struct timer *)tm;
@@ -358,6 +474,16 @@ static void timer_destroy(struct bench_timer *tm)
 
 static const struct bench_timerops timer_ops = { timer_now, timer_destroy };
 
+/* --- @bench_createtimer@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    A freshly constructed standard timer object.
+ *
+ * Use:                Allocate a timer.  Dispose of it by calling
+ *             @tm->ops->destroy(tm)@ when you're done.
+ */
+
 struct bench_timer *bench_createtimer(void)
 {
   struct timer *t = 0;
@@ -372,46 +498,62 @@ end:
   return (ret);
 }
 
-#ifdef HAVE_UINT64
-#  define FLOATK64(k) ((double)(k).i)
-#else
-#  define FLOATK64(k) ((double)(k).lo + 4275123318.0*(double)(k).hi)
-#endif
-
-static void timer_diff(struct bench_timing *delta_out,
-                      const struct bench_time *t0,
-                      const struct bench_time *t1)
-{
-  delta_out->f = t0->f&t1->f;
-  kludge64 k;
-
-  if (!(delta_out->f&BTF_TIMEOK))
-    delta_out->t = 0.0;
-  else {
-    SUB64(k, t1->s, t0->s);
-    delta_out->t = FLOATK64(k) - 1 +
-      (t1->ns + NS_PER_S - t0->ns)/(double)NS_PER_S;
-  }
-
-  if (!(delta_out->f&BTF_CYOK))
-    delta_out->cy = 0.0;
-  else {
-    SUB64(k, t1->cy, t0->cy);
-    delta_out->cy = FLOATK64(k);
-  }
-}
+/*----- Benchmarking ------------------------------------------------------*/
 
-/*----- Calibration -------------------------------------------------------*/
+/* --- @bench_init@ --- *
+ *
+ * Arguments:  @struct bench_state *b@ = bench state to initialize
+ *             @struct bench_timer *tm@ = timer to attach
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize the benchmark state.  It still needs to be
+ *             calibrated (use @bench_calibrate@) before it can be used, but
+ *             this will be done automatically by @bench_measure@ if it's
+ *             not done by hand earlier.  The timer is now owned by the
+ *             benchmark state and will be destroyed by @bench_destroy@.
+ */
 
 void bench_init(struct bench_state *b, struct bench_timer *tm)
   { b->tm = tm; b->target_s = 1.0; b->f = 0; }
 
+/* --- @bench_destroy@ --- *
+ *
+ * Arguments:  @struct bench_state *b@ = bench state
+ *
+ * Returns:    ---
+ *
+ * Use:                Destroy the benchmark state, releasing the resources that it
+ *             holds.
+ */
+
 void bench_destroy(struct bench_state *b)
   { b->tm->ops->destroy(b->tm); }
 
-static void do_nothing(unsigned long n, void *p)
+/* --- @do_nothing@ --- *
+ *
+ * Arguments:  @unsigned long n@ = iteration count
+ *             @void *ctx@ = context pointer (ignored)
+ *
+ * Returns:    ---
+ *
+ * Use:                Does nothing at all for @n@ iterations.  Used to calibrate
+ *             the benchmarking state.
+ */
+
+static void do_nothing(unsigned long n, void *ctx)
   { while (n--) RELAX; }
 
+/* --- @bench_calibrate@ --- *
+ *
+ * Arguments:  @struct bench_state *b@ = bench state
+ *
+ * Returns:    Zero on success, @-1@ if calibration failed.
+ *
+ * Use:                Calibrate the benchmark state, so that it can be used to
+ *             measure performance reasonably accurately.
+ */
+
 int bench_calibrate(struct bench_state *b)
 {
   struct linreg lr_clk = LINREG_INIT, lr_cy = LINREG_INIT;
@@ -424,17 +566,32 @@ int bench_calibrate(struct bench_state *b)
   unsigned f = BTF_ANY;
   int rc;
 
+  /* The model here is that a timing loop has a fixed overhead as we enter
+   * and leave (e.g., to do with the indirect branch into the code), and
+   * per-iteration overheads as we check the counter and loop back.  We aim
+   * to split these apart using linear regression.
+   */
+
+  /* If we've already calibrated then there's nothing to do. */
   if (b->f&BTF_ANY) return (0);
 
+  /* Exercise the inner loop a few times to educate the branch predictor. */
   for (i = 0; i < 10; i++)
-    { tm->ops->now(tm, &t0); fn(1, 0); tm->ops->now(tm, &t1); }
+    { tm->ops->now(tm, &t0); fn(50, 0); tm->ops->now(tm, &t1); }
 
+  /* Now we measure idle loops until they take sufficiently long -- or we run
+   * out of counter.
+   */
   debug("calibrating...");
   n = 1;
   for (;;) {
+
+    /* Measure @n@ iterations of the idle loop. */
     tm->ops->now(tm, &t0); fn(n, 0); tm->ops->now(tm, &t1);
     timer_diff(&delta, &t0, &t1); f &= delta.f;
     if (!(f&BTF_TIMEOK)) { rc = -1; goto end; }
+
+    /* Register the timings with the regression machinery. */
     linreg_update(&lr_clk, n, delta.t);
     if (!(f&BTF_CYOK))
       debug("  n = %10lu; t = %12g s", n, delta.t);
@@ -442,33 +599,64 @@ int bench_calibrate(struct bench_state *b)
       linreg_update(&lr_cy, n, delta.cy);
       debug("  n = %10lu; t = %12g s, cy = %10.0f", n, delta.t, delta.cy);
     }
+
+    /* If we're done then stop. */
     if (delta.t >= b->target_s/20.0) break;
     if (n >= ULONG_MAX - n/3) break;
+
+    /* Update the counter and continue. */
     n += n/3 + 1;
   }
 
+  /* Now run the linear regression to extract the constant and per-iteration
+   * overheads.
+   */
   linreg_fit(&lr_clk, &b->clk.m, &b->clk.c, 0);
   debug("clock overhead = (%g n + %g) s", b->clk.m, b->clk.c);
   if (f&BTF_CYOK) {
     linreg_fit(&lr_clk, &b->clk.m, &b->clk.c, 0);
     debug("cycle overhead = (%g n + %g) cy", b->cy.m, b->cy.c);
   }
+
+  /* We're done. */
   b->f |= f; rc = 0;
 end:
   return (rc);
 }
 
+/* --- @bench_measure@ --- *
+ *
+ * Arguments:  @struct bench_timing *t_out@ = where to leave the timing
+ *             @struct bench_state *b@ = benchmark state
+ *             @double base@ = number of internal units per call
+ *             @bench_fn *fn@, @void *ctx@ = benchmark function to run
+ *
+ * Returns:    Zero on success, @-1@ if timing failed.
+ *
+ * Use:                Measure a function.  The function @fn@ is called adaptively
+ *             with an iteration count @n@ set so as to run for
+ *             approximately @b->target_s@ seconds.
+ *
+ *             The result is left in @*t_out@, with @t_out->n@ counting the
+ *             final product of the iteration count and @base@ (which might,
+ *             e.g., reflect the number of inner iterations the function
+ *             performs, or the number of bytes it processes per iteration).
+ */
+
 int bench_measure(struct bench_timing *t_out, struct bench_state *b,
-                 double base, bench_fn *fn, void *p)
+                 double base, bench_fn *fn, void *ctx)
 {
   struct bench_timer *tm = b->tm;
   struct bench_time t0, t1;
   unsigned long n;
 
+  /* Make sure the state is calibrated. */
   if (bench_calibrate(b)) return (-1);
+
+  /* Main adaptive measurement loop. */
   debug("measuring..."); n = 1;
   for (;;) {
-    tm->ops->now(tm, &t0); fn(n, p); tm->ops->now(tm, &t1);
+    tm->ops->now(tm, &t0); fn(n, ctx); tm->ops->now(tm, &t1);
     timer_diff(t_out, &t0, &t1);
     if (!(t_out->f&BTF_TIMEOK)) return (-1);
     if (!(t_out->f&BTF_CYOK)) debug("  n = %10lu; t = %12g", n, t_out->t);
@@ -476,8 +664,12 @@ int bench_measure(struct bench_timing *t_out, struct bench_state *b,
     if (t_out->t >= 0.72*b->target_s) break;
     n *= 1.44*b->target_s/t_out->t;
   }
+
+  /* Adjust according to the calibration. */
   t_out->t -= n*b->clk.m + b->clk.c;
   if (t_out->f&BTF_CYOK) t_out->cy -= n*b->cy.m + b->cy.c;
+
+  /* Report the results, if debugging. */
   if (!(t_out->f&BTF_CYOK)) debug("  adjusted t' = %12g", t_out->t);
   else debug("  adjusted t = %12g, cy = %10.0f", t_out->t, t_out->cy);
   if (!(t_out->f&BTF_CYOK))
@@ -485,6 +677,8 @@ int bench_measure(struct bench_timing *t_out, struct bench_state *b,
   else
     debug("  %g s (%g cy) per op; %g ops/s",
          t_out->t/n, t_out->cy/n, n/t_out->t);
+
+  /* All done. */
   t_out->n = n*base; return (0);
 }
 
index 2dbce0f004e755e7cf9be5d372e3c4acd245459d..e7736c4d84e6f73ef046bbae9fdf101242c3b6d2 100644 (file)
 /*----- Data structures ---------------------------------------------------*/
 
 struct bench_time {
-  unsigned f;
-#define BTF_TIMEOK 1u
-#define BTF_CYOK 2u
-#define BTF_ANY (BTF_TIMEOK | BTF_CYOK)
-  kludge64 s; uint32 ns;
-  kludge64 cy;
+  unsigned f;                          /* flags */
+#define BTF_TIMEOK 1u                  /*   @s@ ad @ns@ slots are value */
+#define BTF_CYOK 2u                    /*   @cy@ slot is valid */
+#define BTF_ANY (BTF_TIMEOK | BTF_CYOK)        /*   some part is useful */
+  kludge64 s; uint32 ns;              /*   real time in seconds and nanos */
+  kludge64 cy;                         /*   count of CPU cycles */
 };
 
 struct bench_timing {
-  unsigned f;
-  double n, t, cy;
+  unsigned f;                        /* flags (as in @struct bench_time@) */
+  double n, t, cy;                     /* count, time, and cycles */
 };
 
 struct bench_timer { const struct bench_timerops *ops; };
 
 struct bench_timerops {
   void (*now)(struct bench_timer */*bt*/, struct bench_time */*t_out*/);
+    /* Fill in @*t_out@ with the current time. v*/
+
   void (*destroy)(struct bench_timer */*bt*/);
+    /* Release the timer and any resources it holds. */
 };
 
 struct bench_state {
-  struct bench_timer *tm;
-  double target_s;
-  unsigned f;
-  struct { double m, c; } clk, cy;
+  struct bench_timer *tm;              /* a timer */
+  double target_s;                     /* target time to run benchmarks */
+  unsigned f;                       /* flags (@BTF_...@) for calibrations */
+  struct { double m, c; } clk, cy;     /* calculated overheads */
 };
 
-typedef void bench_fn(unsigned long /*n*/, void */*p*/);
+typedef void bench_fn(unsigned long /*n*/, void */*ctx*/);
+/* Run the benchmark @n@ times, given a context pointer @ctx@. */
 
 /*----- Functions provided ------------------------------------------------*/
 
+/* --- @bench_createtimer@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    A freshly constructed standard timer object.
+ *
+ * Use:                Allocate a timer.  Dispose of it by calling
+ *             @tm->ops->destroy(tm)@ when you're done.
+ */
+
 extern struct bench_timer *bench_createtimer(void);
 
+/* --- @bench_init@ --- *
+ *
+ * Arguments:  @struct bench_state *b@ = bench state to initialize
+ *             @struct bench_timer *tm@ = timer to attach
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize the benchmark state.  It still needs to be
+ *             calibrated (use @bench_calibrate@) before it can be used, but
+ *             this will be done automatically by @bench_measure@ if it's
+ *             not done by hand earlier.  The timer is now owned by the
+ *             benchmark state and will be destroyed by @bench_destroy@.
+ */
+
 extern void bench_init(struct bench_state */*b*/,
                       struct bench_timer */*tm*/);
 
+/* --- @bench_destroy@ --- *
+ *
+ * Arguments:  @struct bench_state *b@ = bench state
+ *
+ * Returns:    ---
+ *
+ * Use:                Destroy the benchmark state, releasing the resources that it
+ *             holds.
+ */
+
 extern void bench_destroy(struct bench_state */*b*/);
 
+/* --- @bench_calibrate@ --- *
+ *
+ * Arguments:  @struct bench_state *b@ = bench state
+ *
+ * Returns:    Zero on success, @-1@ if calibration failed.
+ *
+ * Use:                Calibrate the benchmark state, so that it can be used to
+ *             measure performance reasonably accurately.
+ */
+
 extern int bench_calibrate(struct bench_state */*b*/);
 
+/* --- @bench_measure@ --- *
+ *
+ * Arguments:  @struct bench_timing *t_out@ = where to leave the timing
+ *             @struct bench_state *b@ = benchmark state
+ *             @double base@ = number of internal units per call
+ *             @bench_fn *fn@, @void *ctx@ = benchmark function to run
+ *
+ * Returns:    Zero on success, @-1@ if timing failed.
+ *
+ * Use:                Measure a function.  The function @fn@ is called adaptively
+ *             with an iteration count @n@ set so as to run for
+ *             approximately @b->target_s@ seconds.
+ *
+ *             The result is left in @*t_out@, with @t_out->n@ counting the
+ *             final product of the iteration count and @base@ (which might,
+ *             e.g., reflect the number of inner iterations the function
+ *             performs, or the number of bytes it processes per iteration).
+ */
+
 extern int bench_measure(struct bench_timing */*t_out*/,
                         struct bench_state */*b*/,
-                        double /*base*/, bench_fn */*fn*/, void */*p*/);
+                        double /*base*/, bench_fn */*fn*/, void */*ctx*/);
 
 /*----- That's all, folks -------------------------------------------------*/
 
index 91c4529998f7f0dbc11574bc01e609881dbfba2f..6dd0ef0cca4bda9547fc72fafc7cf01558d5c634 100644 (file)
@@ -129,7 +129,7 @@ static const struct tvec_urange range_32 = { 0, 31 };
   _(flags,     RF,     flags,                  p, &attr_info)          \
   _(string,    RSTR,   string,                 p, &range_32)           \
   _(bytes,     RBY,    bytes,                  p, &tvrange_byte)       \
-  _(buffer,    RBUF,   buffer,                 p, &tvrange_u16)
+  _(buffer,    RBUF,   buffer,                 p, 0)
 
 enum {
   /* Output registers, one for each register type. */
index 9a890da37ac5b5ae6bf5a2f6506f8dfa199f850d..74f40faf94631817a2ee6ca62d6c1d92f41348b6 100644 (file)
@@ -85,7 +85,7 @@ test_parse([int], [4], [4 ; = 0x04 = '\x04'])
 test_parse([int], [ 17; comment], [17 ; = 0x11 = '\x11'])
 
 test_parse([int], [0x234], [564 ; = 0x0234])
-test_parse([int], [0o33], [27 ; = 0x1b = '\e'])
+test_parse([int], [0o33], [27 ; = 0x1b = @%:@escape = '\e'])
 
 test_parse([int], [ +192], [192 ; = 0xc0 = '\xc0'])
 test_parse([int], [ -192], [-192 ; = -0xc0])
@@ -123,7 +123,7 @@ test_parserr([int], [0x],
        [3], [syntax error: expected end-of-line but found `x'])
 
 test_parserr([int], [],
-       [3], [syntax error: expected signed integer but found @%:@<eol>])
+       [3], [syntax error: expected signed integer but found @%:@eol])
 
 test_parserr([int], [123456],
        [3], [integer 123456 out of range (must be in @<:@-32768 .. 32767@:>@)])
@@ -136,9 +136,11 @@ AT_SETUP(tvec type-uint)
 test_parse([uint], [4], [4 ; = 0x04 = '\x04'])
 test_parse([uint], [ 17; comment], [17 ; = 0x11 = '\x11'])
 
+test_parse([uint], [0], [0 ; = 0x00 = @%:@nul = '\0'])
+
 test_parse([uint], [012345], [12345 ; = 0x3039])
 test_parse([uint], [0x234], [564 ; = 0x0234])
-test_parse([uint], [0o33], [27 ; = 0x1b = '\e'])
+test_parse([uint], [0o33], [27 ; = 0x1b = @%:@escape = '\e'])
 test_parse([uint], [0b1011_1101], [189 ; = 0xbd = '\xbd'])
 test_parse([uint], [12r123], [171 ; = 0xab = '\xab'])
 
@@ -176,7 +178,7 @@ test_parserr([uint], [0x],
        [3], [syntax error: expected end-of-line but found `x'])
 
 test_parserr([uint], [],
-       [3], [syntax error: expected unsigned integer but found @%:@<eol>])
+       [3], [syntax error: expected unsigned integer but found @%:@eol])
 
 test_parserr([uint], [123456],
        [3], [integer 123456 out of range (must be in @<:@0 .. 65535@:>@)])
@@ -191,6 +193,13 @@ test_parse([float], [-0.0], [-0])
 
 test_parse([float], [1.234], [1.234])
 
+test_parse([float], [@%:@nan], [@%:@nan])
+test_parse([float], [@%:@+inf], [@%:@+inf])
+test_parse([float], [@%:@inf], [@%:@+inf])
+test_parse([float], [+@%:@inf], [@%:@+inf])
+test_parse([float], [@%:@-inf], [@%:@-inf])
+test_parse([float], [-@%:@inf], [@%:@-inf])
+
 AT_CLEANUP
 
 ###--------------------------------------------------------------------------
@@ -210,6 +219,175 @@ test_parse([penum], [@%:@nil], [@%:@nil])
 
 AT_CLEANUP
 
+###--------------------------------------------------------------------------
+AT_SETUP([tvec type-char])
+
+test_parse([char], [a], ['a' ; = 97 = 0x61])
+test_parse([char], [a;?], ['a' ; = 97 = 0x61])
+test_parse([char], [a ;?], ['a' ; = 97 = 0x61])
+test_parse([char], [\\], ['\\' ; = 92 = 0x5c])
+test_parse([char], ['], ['\'' ; = 39 = 0x27])
+test_parse([char], [\'], ['\'' ; = 39 = 0x27])
+test_parse([char], ["], ['"' ; = 34 = 0x22])
+test_parse([char], [';'], [';' ; = 59 = 0x3b])
+test_parse([char], [';';?], [';' ; = 59 = 0x3b])
+test_parse([char], [';' ;?], [';' ; = 59 = 0x3b])
+test_parse([char], [\"], ['"' ; = 34 = 0x22]) # "
+test_parse([char], ['@%:@'], ['@%:@' ; = 35 = 0x23])
+
+test_parse([char], [\n], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], [\x0a], [@%:@newline ; = '\n' = 10 = 0x0a])
+
+test_parse([char], [@%:@newline], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], [@%:@lf], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], [@%:@del;?], [@%:@delete ; = '\x7f' = 127 = 0x7f])
+test_parse([char], [@%:@space ;?], [' ' ; = 32 = 0x20])
+
+test_parse([char], [' '], [' ' ; = 32 = 0x20])
+test_parse([char], ['a'], ['a' ; = 97 = 0x61])
+test_parse([char], ['\n'], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], ['\12' ;?], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], ['\{12}' ;?], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], ['\x0a'], [@%:@newline ; = '\n' = 10 = 0x0a])
+test_parse([char], ['\x{0a}'], [@%:@newline ; = '\n' = 10 = 0x0a])
+
+test_parse([char], [@%:@eof], [@%:@eof ; = -1 = -0x01])
+test_parse([char], [@%:@eof], [@%:@eof ; = -1 = -0x01])
+
+test_parserr([char], [ ],
+       [3], [syntax error: expected character but found @%:@eol])
+test_parserr([char], [''],
+       [3], [syntax error: expected character but found `''])
+test_parserr([char], ['''],
+       [3], [syntax error: expected character but found `''])
+test_parserr([char], [;],
+       [3], [syntax error: expected character but found `;'])
+test_parserr([char], [@%:@],
+       [3], [unknown character name `@%:@'])
+test_parserr([char], [';],
+       [3], [syntax error: expected `'' but found @%:@eol])
+test_parserr([char], [\],
+       [3], [syntax error: expected string escape but found @%:@eol])
+test_parserr([char], [\q],
+       [3], [syntax error: expected string escape but found `q'])
+test_parserr([char], ['\],
+       [3], [syntax error: expected string escape but found @%:@eol])
+test_parserr([char], [ab],
+       [3], [syntax error: expected end-of-line but found `b'])
+test_parserr([char], [\x{0a],
+       [3], [syntax error: expected `}' but found @%:@eol])
+test_parserr([char], [\{012],
+       [3], [syntax error: expected `}' but found @%:@eol])
+
+AT_CLEANUP
+
+###--------------------------------------------------------------------------
+AT_SETUP([tvec type-string])
+
+test_parse([string], [foo], [foo])
+test_parse([string], [foo bar], ["foo bar"])
+test_parse([string], [foo  bar], ["foo  bar"])
+test_parse([string], [foo  ""  bar], [foobar])
+test_parse([string], [ foo  bar ], ["foo  bar"])
+test_parse([string], [ foo @&t@
+       bar ], ["foo  bar"])
+
+test_parse([string], [foo @%:@nul bar], ["foo\{0}bar"])
+
+test_parse([string], ["f" !repeat 2 { o } "bar"], [foobar])
+test_parse([string], ["{"!repeat 5{"abc"}"}"], ["{abcabcabcabcabc}"])
+
+test_parse([string], [!hex "f" 6f "o"], [foo])
+
+test_parse([string], ["foo\n"], ["foo\n"])
+
+test_parse([string], [foo\
+bar], [
+       "foo\n"
+       "bar"])
+test_parse([string], ["foo\
+bar"], [foobar])
+test_parse([string], ["foo" @%:@newline "bar" @%:@newline], [
+       "foo\n"
+       "bar\n"])
+
+test_parserr([string], [],
+       [4], [syntax error: expected string but found @%:@eof])
+test_parse([string], [""], [""])
+test_parse([string], [''], [""])
+
+test_parse([string], ["f\x{6f}o"], [foo])
+
+AT_CLEANUP
+
+###--------------------------------------------------------------------------
+AT_SETUP([tvec type-bytes])
+
+test_parse([bytes], [""], ["" ; empty])
+test_parse([bytes], [61], [61 ; a])
+test_parse([bytes], ["abc"], [616263 ; abc])
+test_parse([bytes], ["abcd"], [61626364 ; abcd])
+test_parse([bytes], ["abcde"], [61626364 65 ; abcde])
+
+test_parse([bytes], [!base64 YWJjZGVmZ2hpamtsbW5vcA==],
+       [61626364 65666768 696a6b6c 6d6e6f70 ; abcdefghijklmnop])
+test_parse([bytes],
+       [!base64 QUJDREVGR0hJSktMTU5PUGFiY2RlZmdo
+                a Wp rbG 1ub3A=],
+       [
+       41424344 45464748 494a4b4c 4d4e4f50 ; @<:@00@:>@ ABCDEFGHIJKLMNOP
+       61626364 65666768 696a6b6c 6d6e6f70 ; @<:@10@:>@ abcdefghijklmnop])
+
+test_parse([bytes], [6 1], [61 ; a])
+test_parserr([bytes], [6 "" 1],
+       [3], [invalid hex sequence end: Excess or nonzero padding bits])
+
+test_parse([bytes], [!base64 AA==], [00 ; .])
+test_parse([bytes], [!base64 AAA=], [0000 ; ..])
+test_parse([bytes], [!base64 AAAA], [000000 ; ...])
+test_parse([bytes], [!base64 AA], [00 ; .])
+test_parse([bytes], [!base64 AAA], [0000 ; ..])
+
+test_parserr([bytes], [0],
+       [4], [invalid hex sequence end: Excess or nonzero padding bits])
+test_parserr([bytes], [!base64 A],
+       [4], [invalid base64 sequence end: Excess or nonzero padding bits])
+test_parserr([bytes], [!base64 A=],
+       [3], [invalid base64 fragment `A=': Excess or nonzero padding bits])
+
+AT_CLEANUP
+
+###--------------------------------------------------------------------------
+AT_SETUP([tvec type-buffer])
+
+test_parse([buffer], [16], [16 B])
+test_parse([buffer], [16;?], [16 B])
+test_parse([buffer], [16 ;?], [16 B])
+test_parse([buffer], [16384], [16 kB])
+test_parse([buffer], [16777216], [16 MB])
+test_parse([buffer], [16k], [16 kB])
+test_parse([buffer], [16k;?], [16 kB])
+test_parse([buffer], [16k ;?], [16 kB])
+test_parse([buffer], [16 k], [16 kB])
+test_parse([buffer], [16 k;?], [16 kB])
+test_parse([buffer], [16 k ;?], [16 kB])
+test_parse([buffer], [16kB], [16 kB])
+test_parse([buffer], [16kB;?], [16 kB])
+test_parse([buffer], [16kB ;?], [16 kB])
+test_parse([buffer], [16 kB], [16 kB])
+test_parse([buffer], [16 kB;?], [16 kB])
+test_parse([buffer], [16 kB ;?], [16 kB])
+
+test_parserr([buffer], [16!], [3], [invalid buffer length `16!'])
+test_parserr([buffer], [16   !], [3], [invalid buffer length `16 !'])
+test_parserr([buffer], [16 k!], [3], [invalid buffer length `16 k!'])
+test_parserr([buffer], [16 kB!], [3], [invalid buffer length `16 kB!'])
+test_parserr([buffer], [16 kB !],
+       [3], [syntax error: expected end-of-line but found `!'])
+test_parserr([buffer], [16 EB], [3], [buffer length `16 EB' out of range])
+
+AT_CLEANUP
+
 ###--------------------------------------------------------------------------
 AT_SETUP([tvec serialize])
 
index ab21b9c654f72060d67d982dd8f1cc6770c4aa51..b56fcbe4a904e22a1cdb9103655d4278a2eb32a2 100644 (file)
 /*----- Data structures ---------------------------------------------------*/
 
 struct benchrun {
-  struct tvec_state *tv;
-  const struct tvec_env *env;
-  void *ctx;
-  unsigned long *n;
-  const struct tvec_reg *in; struct tvec_reg *out;
-  tvec_testfn *fn;
+  struct tvec_state *tv;               /* test vector state */
+  const struct tvec_env *env;          /* subordinate environment */
+  void *ctx;                           /* subordinate env's context */
+  unsigned long *n;                    /* iteration count address */
+  const struct tvec_reg *in; struct tvec_reg *out; /* register vectors */
+  tvec_testfn *fn;                     /* test function to run */
 };
 
 /*----- Global variables --------------------------------------------------*/
 
-struct bench_state *tvec_benchstate;
+struct bench_state *tvec_benchstate;   /* common benchmarking state */
 
-/*----- Benchmarking ------------------------------------------------------*/
+/*----- Utilities ---------------------------------------------------------*/
+
+/* --- @normalize@ --- *
+ *
+ * Arguments:  @double *x_inout@ = address of a value to normalize
+ *             @const char **unit_out@ = address to store unit prefix
+ *             @double scale@ = scale factor for unit steps
+ *
+ * Returns:    ---
+ *
+ * Use:                Adjust @*x_inout@ by a power of @scale@, and set @*unit_out@
+ *             so that printing the two reflects the original value with an
+ *             appropriate SI unit scaling.  The @scale@ should be 1024 for
+ *             binary quantities, most notably memory sizes, or 1000 for
+ *             other quantities.
+ */
 
 static void normalize(double *x_inout, const char **unit_out, double scale)
 {
@@ -66,6 +81,29 @@ static void normalize(double *x_inout, const char **unit_out, double scale)
   *x_inout = x; *unit_out = *u;
 }
 
+/*----- Benchmark environment scaffolding ---------------------------------*/
+
+/* --- @tvec_benchsetup@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const struct tvec_env *env@ = environment description
+ *             @void *pctx@ = parent context (ignored)
+ *             @void *ctx@ = context pointer to initialize
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Initialize a benchmarking environment context.
+ *
+ *             The environment description must really be a @struct
+ *             tvec_bench@.  If the @bst@ slot is null, then a temporary
+ *             benchmark state is allocated for the current test group and
+ *             released at the end.  Otherwise, it must be the address of a
+ *             pointer to a benchmark state: if the pointer is null, then a
+ *             fresh state is allocated and initialized and the pointer is
+ *             updated; otherwise, the pointer is assumed to refer to an
+ *             existing valid benchmark state.
+ */
+
 int tvec_benchsetup(struct tvec_state *tv, const struct tvec_env *env,
                    void *pctx, void *ctx)
 {
@@ -74,8 +112,10 @@ int tvec_benchsetup(struct tvec_state *tv, const struct tvec_env *env,
   const struct tvec_env *subenv = b->env;
   struct bench_timer *bt;
 
+  /* Basic initialization. */
   bc->b = b; bc->bst = 0; bc->subctx = 0;
 
+  /* Set up the benchmarking state if it hasn't been done before. */
   if (!b->bst || !*b->bst) {
     bt = bench_createtimer(); if (!bt) goto fail_timer;
     bc->bst = xmalloc(sizeof(*bc->bst)); bench_init(bc->bst, bt);
@@ -84,18 +124,41 @@ int tvec_benchsetup(struct tvec_state *tv, const struct tvec_env *env,
     goto fail_timer;
   else
     bc->bst = *b->bst;
+
+  /* Set the default target time. */
   bc->dflt_target = bc->bst->target_s;
 
+  /* Initialize the subordinate environment. */
   if (subenv && subenv->ctxsz) bc->subctx = xmalloc(subenv->ctxsz);
   if (subenv && subenv->setup && subenv->setup(tv, subenv, bc, bc->subctx))
     { xfree(bc->subctx); bc->subctx = 0; return (-1); }
 
+  /* All done. */
 end:
   return (0);
 fail_timer:
   tvec_skipgroup(tv, "failed to create timer"); goto end;
 }
 
+/* --- @tvec_benchset@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *var@ = variable name to set
+ *             @const struct tvec_env *env@ = environment description
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Set a special variable.  The following special variables are
+ *             supported.
+ *
+ *               * %|@target|% is the (approximate) number of seconds to run
+ *                 the benchmark.
+ *
+ *             Unrecognized variables are passed to the subordinate
+ *             environment, if there is one.
+ */
+
 int tvec_benchset(struct tvec_state *tv, const char *var,
                  const struct tvec_env *env, void *ctx)
 {
@@ -117,26 +180,62 @@ int tvec_benchset(struct tvec_state *tv, const char *var,
     return (0);
 }
 
+/* --- @tvec_benchbefore@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Invoke the subordinate environment's @before@ function to
+ *             prepare for the benchmark.
+ */
+
 int tvec_benchbefore(struct tvec_state *tv, void *ctx)
 {
   struct tvec_benchctx *bc = ctx;
   const struct tvec_bench *b = bc->b;
   const struct tvec_env *subenv = b->env;
 
+  /* Just call the subsidiary environment. */
   if (subenv && subenv->before) return (subenv->before(tv, bc->subctx));
   else return (0);
 }
 
+/* --- @tvec_benchafter@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Invoke the subordinate environment's @after@ function to
+ *             clean up after the benchmark.
+ */
+
 void tvec_benchafter(struct tvec_state *tv, void *ctx)
 {
   struct tvec_benchctx *bc = ctx;
   const struct tvec_bench *b = bc->b;
   const struct tvec_env *subenv = b->env;
 
+  /* Restore the benchmark state's old target. */
   bc->bst->target_s = bc->dflt_target;
+
+  /* Pass the call on to the subsidiary environment. */
   if (subenv && subenv->after) subenv->after(tv, bc->subctx);
 }
 
+/* --- @tvec_benchteardown@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Tear down the benchmark environment.
+ */
+
 void tvec_benchteardown(struct tvec_state *tv, void *ctx)
 {
   struct tvec_benchctx *bc = ctx;
@@ -145,38 +244,79 @@ void tvec_benchteardown(struct tvec_state *tv, void *ctx)
 
   if (!bc) return;
   b = bc->b; subenv = b->env;
+
+  /* Tear down any subsidiary environment. */
   if (subenv && subenv->teardown && bc->subctx)
     subenv->teardown(tv, bc->subctx);
+
+  /* If the benchmark state was temporary, then dispose of it. */
   if (bc->bst) {
     if (b->bst) bc->bst->target_s = bc->dflt_target;
     else { bench_destroy(bc->bst); xfree(bc->bst); }
   }
 }
 
-static void benchloop_outer_direct(unsigned long n, void *p)
+/*----- Measurement machinery ---------------------------------------------*/
+
+/* --- @benchloop_...@ --- *
+ *
+ * Arguments:  @unsigned long n@ = iteration count
+ *             @void *ctx@ = benchmark running context
+ *
+ * Returns:    ---
+ *
+ * Use:                Run various kinds of benchmarking loops.
+ *
+ *               * The @..._outer_...@ functions call the underlying
+ *                 function @n@ times in a loop; by contrast, the
+ *                 @..._inner_...@ functions set a register value to the
+ *                 chosen iteration count and expect the underlying function
+ *                 to perform the loop itself.
+ *
+ *               * The @..._direct@ functions just call the underlying test
+ *                 function directly (though still through an `indirect
+ *                 jump' instruction); by contrast, the @..._indirect@
+ *                 functions invoke a subsidiary environment's @run@
+ *                 function, which adds additional overhead.
+ */
+
+static void benchloop_outer_direct(unsigned long n, void *ctx)
 {
-  struct benchrun *r = p;
-  tvec_testfn *fn = r->fn; void *ctx = r->ctx;
+  struct benchrun *r = ctx;
+  tvec_testfn *fn = r->fn; void *tctx = r->ctx;
   const struct tvec_reg *in = r->in; struct tvec_reg *out = r->out;
 
-  while (n--) fn(in, out, ctx);
+  while (n--) fn(in, out, tctx);
 }
 
-static void benchloop_inner_direct(unsigned long n, void *p)
-  { struct benchrun *r = p; *r->n = n; r->fn(r->in, r->out, r->ctx); }
+static void benchloop_inner_direct(unsigned long n, void *ctx)
+  { struct benchrun *r = ctx; *r->n = n; r->fn(r->in, r->out, r->ctx); }
 
-static void benchloop_outer_indirect(unsigned long n, void *p)
+static void benchloop_outer_indirect(unsigned long n, void *ctx)
 {
-  struct benchrun *r = p;
+  struct benchrun *r = ctx;
   struct tvec_state *tv = r->tv;
   void (*run)(struct tvec_state *, tvec_testfn, void *) = r->env->run;
-  tvec_testfn *fn = r->fn; void *ctx = r->ctx;
+  tvec_testfn *fn = r->fn; void *tctx = r->ctx;
 
-  while (n--) run(tv, fn, ctx);
+  while (n--) run(tv, fn, tctx);
 }
 
-static void benchloop_inner_indirect(unsigned long n, void *p)
-  { struct benchrun *r = p; *r->n = n; r->env->run(r->tv, r->fn, r->ctx); }
+static void benchloop_inner_indirect(unsigned long n, void *ctx)
+  { struct benchrun *r = ctx; *r->n = n; r->env->run(r->tv, r->fn, r->ctx); }
+
+/* --- @tvec_benchrun@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @tvec_testfn *fn@ = test function to run
+ *             @void *ctx@ = context pointer for the test function
+ *
+ * Returns:    ---
+ *
+ * Use:                Measures and reports the performance of a test function.
+ *
+ *
+ */
 
 void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
 {
@@ -194,9 +334,11 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
   unsigned f = 0;
 #define f_any 1u
 
+  /* Fill in the easy parts of the run context. */
   r.tv = tv; r.env = subenv; r.ctx = bc->subctx;
   r.in = tv->in; r.out = tv->out; r.fn = fn;
 
+  /* Decide on the run function to select. */
   if (b->riter >= 0) {
     r.n = &TVEC_REG(tv, in, b->riter)->v.u;
     loopfn = subenv && subenv->run ?
@@ -207,10 +349,12 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
       benchloop_outer_indirect : benchloop_outer_direct;
   }
 
+  /* Decide on the kind of unit and the base count. */
   base = b->niter;
   if (b->rbuf < 0) unit = TVBU_OP;
   else { unit = TVBU_BYTE; base *= TVEC_REG(tv, in, b->rbuf)->v.bytes.sz; }
 
+  /* Construct a description of the test using the identifier registers. */
   for (rd = tv->test->regs; rd->name; rd++)
     if (rd->f&TVRF_ID) {
       if (f&f_any) dstr_puts(&d, ", ");
@@ -220,6 +364,7 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
                   TVSF_COMPACT, &dstr_printops, &d);
     }
 
+  /* Run the benchmark. */
   o->ops->bbench(o, d.buf, unit);
   if (bench_measure(&tm, bc->bst, base, loopfn, &r))
     o->ops->ebench(o, d.buf, unit, 0);
@@ -231,6 +376,22 @@ void tvec_benchrun(struct tvec_state *tv, tvec_testfn *fn, void *ctx)
 #undef f_any
 }
 
+/*----- Output utilities --------------------------------------------------*/
+
+/* --- @tvec_benchreport@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @unsigned unit@ = the unit being measured (~TVBU_...@)
+ *             @const struct bench_timing *tm@ = the benchmark result
+ *
+ * Returns:    ---
+ *
+ * Use:                Formats a report about the benchmark performance.  This
+ *             function is intended to be called on by an output
+ *             @ebench@ function.
+ */
+
 void tvec_benchreport(const struct gprintf_ops *gops, void *go,
                      unsigned unit, const struct bench_timing *tm)
 {
index 03873dc61e167505a8fbd3446e9e1ee61f01dcbd..92f31d38a8f8eb73c08131b108a91803b10684cf 100644 (file)
 
 /*----- Output ------------------------------------------------------------*/
 
+/* --- @tvec_error@, @tvec_error_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *msg@, @va_list ap@ = error message
+ *
+ * Returns:    @-1@.
+ *
+ * Use:                Report an error.  Errors are distinct from test failures,
+ *             and indicate that a problem was encountered which compromised
+ *             the activity of testing.
+ */
+
 int tvec_error(struct tvec_state *tv, const char *msg, ...)
 {
   va_list ap;
 
   va_start(ap, msg); tvec_error_v(tv, msg, &ap); va_end(ap);
-  tv->f |= TVSF_ERROR; return (-1);
+  return (-1);
 }
 int tvec_error_v(struct tvec_state *tv, const char *msg, va_list *ap)
-  { tv->output->ops->error(tv->output, msg, ap); return (-1); }
+{
+  tv->output->ops->error(tv->output, msg, ap);
+  tv->f |= TVSF_ERROR; return (-1);
+}
+
+/* --- @tvec_notice@, @tvec_notice_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *msg@, @va_list ap@ = message
+ *
+ * Returns:    ---
+ *
+ * Use:                Output a notice: essentially, some important information
+ *             which doesn't fit into any of the existing categories.
+ */
 
 void tvec_notice(struct tvec_state *tv, const char *msg, ...)
 {
@@ -55,35 +81,12 @@ void tvec_notice(struct tvec_state *tv, const char *msg, ...)
 void tvec_notice_v(struct tvec_state *tv, const char *msg, va_list *ap)
   { tv->output->ops->notice(tv->output, msg, ap); }
 
-int tvec_syntax(struct tvec_state *tv, int ch, const char *expect, ...)
-{
-  va_list ap;
-
-  va_start(ap, expect); tvec_syntax_v(tv, ch, expect, &ap); va_end(ap);
-  return (-1);
-}
-int tvec_syntax_v(struct tvec_state *tv, int ch,
-                  const char *expect, va_list *ap)
-{
-  dstr d = DSTR_INIT;
-  char found[8];
-
-  switch (ch) {
-    case EOF: strcpy(found, "#<eof>"); break;
-    case '\n': strcpy(found, "#<eol>"); ungetc(ch, tv->fp); break;
-    default:
-      if (isprint(ch)) sprintf(found, "`%c'", ch);
-      else sprintf(found, "#<\\x%02x>", ch);
-      break;
-  }
-  dstr_vputf(&d, expect, ap);
-  tvec_error(tv, "syntax error: expected %s but found %s", expect, found);
-  return (-1);
-}
+/*----- Test processing ---------------------------------------------------*/
 
 void tvec_skipgroup(struct tvec_state *tv, const char *excuse, ...)
 {
   va_list ap;
+
   va_start(ap, excuse); tvec_skipgroup_v(tv, excuse, &ap); va_end(ap);
 }
 void tvec_skipgroup_v(struct tvec_state *tv, const char *excuse, va_list *ap)
@@ -152,12 +155,33 @@ void tvec_mismatch(struct tvec_state *tv, unsigned f)
   }
 }
 
-/*----- Main machinery ----------------------------------------------------*/
+/*----- Parsing -----------------------------------------------------------*/
 
-struct groupstate {
-  void *ctx;
-};
-#define GROUPSTATE_INIT { 0 }
+int tvec_syntax(struct tvec_state *tv, int ch, const char *expect, ...)
+{
+  va_list ap;
+
+  va_start(ap, expect); tvec_syntax_v(tv, ch, expect, &ap); va_end(ap);
+  return (-1);
+}
+int tvec_syntax_v(struct tvec_state *tv, int ch,
+                 const char *expect, va_list *ap)
+{
+  dstr d = DSTR_INIT;
+  char found[8];
+
+  switch (ch) {
+    case EOF: strcpy(found, "#eof"); break;
+    case '\n': strcpy(found, "#eol"); ungetc(ch, tv->fp); break;
+    default:
+      if (isprint(ch)) sprintf(found, "`%c'", ch);
+      else sprintf(found, "#\\x%02x", ch);
+      break;
+  }
+  dstr_vputf(&d, expect, ap);
+  tvec_error(tv, "syntax error: expected %s but found %s", d.buf, found);
+  dstr_destroy(&d); return (-1);
+}
 
 void tvec_skipspc(struct tvec_state *tv)
 {
@@ -256,6 +280,13 @@ int tvec_readword_v(struct tvec_state *tv, dstr *d, const char *delims,
   return (0);
 }
 
+/*----- Main machinery ----------------------------------------------------*/
+
+struct groupstate {
+  void *ctx;
+};
+#define GROUPSTATE_INIT { 0 }
+
 void tvec_resetoutputs(struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
index a9cff8f19ea6ee3cb6066acb46e9799249b02c7f..fc1a2ae558eed007df0c87dd0c7bc1d23e32ced7 100644 (file)
@@ -47,6 +47,7 @@
 
 /*----- Main code ---------------------------------------------------------*/
 
+/* Table of output formats. */
 static const struct outform {
   const char *name;
   struct tvec_output *(*makefn)(FILE *fp);
@@ -56,9 +57,20 @@ static const struct outform {
   { 0,                 0 }
 };
 
+/* Configuration for ad-hoc testing. */
 const struct tvec_config tvec_adhocconfig =
   { 0, 1, 1, sizeof(struct tvec_reg) };
 
+/* --- @find_outform@ ---
+ *
+ * Arguments:  @const char *p@ = output name
+ *
+ * Returns:    Pointer to output format record.
+ *
+ * Use:                Looks up an output format by name.  Reports a fatal error if
+ *             no matching record is found.
+ */
+
 static const struct outform *find_outform(const char *p)
 {
   const struct outform *best = 0, *of;
@@ -78,6 +90,15 @@ static const struct outform *find_outform(const char *p)
   else die(2, "unknown output format `%s'", optarg);
 }
 
+/* --- @version@, @usage@, @help@ --- *
+ *
+ * Arguments:  @FILE *fp@ = stream to write on
+ *
+ * Returns:    ---
+ *
+ * Use:                Output information about the program.
+ */
+
 static void version(FILE *fp)
   { pquis(fp, "$, mLib test-vector framework version " VERSION "\n"); }
 
@@ -102,6 +123,21 @@ Options:\n\
 ", fp);
 }
 
+/* --- @tvec_parseargs@ --- *
+ *
+ * Arguments:  @int argc@ = number of command-line arguments
+ *             @char *argv[]@ = vector of argument strings
+ *             @struct tvec_state *tv_out@ = test vector state to initialize
+ *             @int *argpos_out@ = where to leave unread argument index
+ *             @const struct tvec_config *cofig@ = test vector configuration
+ *
+ * Returns:    ---
+ *
+ * Use:                Parse arguments and set up the test vector state @*tv_out@.
+ *             If errors occur, print messages to standard error and exit
+ *             with status 2.
+ */
+
 void tvec_parseargs(int argc, char *argv[], struct tvec_state *tv_out,
                    int *argpos_out, const struct tvec_config *config)
 {
@@ -158,6 +194,19 @@ void tvec_parseargs(int argc, char *argv[], struct tvec_state *tv_out,
   tvec_begin(tv_out, config, o); *argpos_out = optind;
 }
 
+/* --- @tvec_readstdin@, @tvec_readfile@, @tvec_readarg@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *file@ = pathname of file to read
+ *             @const char *arg@ = argument to interpret
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Read test vector data from stdin or a named file.  The
+ *             @tvec_readarg@ function reads from stdin if @arg@ is `%|-|%',
+ *             and from the named file otherwise.
+ */
+
 int tvec_readstdin(struct tvec_state *tv)
   { return (tvec_read(tv, "<stdin>", stdin)); }
 
@@ -179,16 +228,39 @@ end:
   return (rc);
 }
 
-int tvec_readdflt(struct tvec_state *tv, const char *file)
+int tvec_readarg(struct tvec_state *tv, const char *arg)
+{
+  int rc;
+
+  if (STRCMP(arg, ==, "-")) rc = tvec_readstdin(tv);
+  else rc = tvec_readfile(tv, arg);
+  return (rc);
+}
+
+/* --- @tvec_readdflt@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *dflt@ = defsault filename or null
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Reads from the default test vector data.  If @file@ is null,
+ *             then read from standard input, unless that's a terminal; if
+ *             @file@ is not null, then read the named file, looking in the
+ *             directory named by the `%|srcdir|%' environment variable if
+ *             that's set, or otherwise in the current directory.
+ */
+
+int tvec_readdflt(struct tvec_state *tv, const char *dflt)
 {
   dstr d = DSTR_INIT;
   const char *p;
   int rc;
 
-  if (file) {
+  if (dflt) {
     p = getenv("srcdir");
-    if (p) { dstr_putf(&d, "%s/%s", p, file); file = d.buf; }
-    rc = tvec_readfile(tv, file);
+    if (p) { dstr_putf(&d, "%s/%s", p, dflt); dflt = d.buf; }
+    rc = tvec_readfile(tv, dflt);
   } else if (isatty(0))
     rc = tvec_error(tv, "use `-' to force reading from interactive stdin");
   else
@@ -197,14 +269,21 @@ int tvec_readdflt(struct tvec_state *tv, const char *file)
   return (rc);
 }
 
-int tvec_readarg(struct tvec_state *tv, const char *arg)
-{
-  int rc;
-
-  if (STRCMP(arg, ==, "-")) rc = tvec_readstdin(tv);
-  else rc = tvec_readfile(tv, arg);
-  return (rc);
-}
+/* --- @tvec_readargs@ --- *
+ *
+ * Arguments:  @int argc@ = number of command-line arguments
+ *             @char *argv[]@ = vector of argument strings
+ *             @struct tvec_state *tv@ = test vector state
+ *             @int *argpos_inout@ = current argument position (updated)
+ *             @const char *dflt@ = default filename or null
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Reads from the sources indicated by the command-line
+ *             arguments, in order, interpreting each as for @tvec_readarg@;
+ *             if no arguments are given then read from @dflt@ as for
+ *             @tvec_readdflt@.
+ */
 
 int tvec_readargs(int argc, char *argv[], struct tvec_state *tv,
                   int *argpos_inout, const char *dflt)
@@ -212,7 +291,8 @@ int tvec_readargs(int argc, char *argv[], struct tvec_state *tv,
   int i = *argpos_inout;
   int rc;
 
-  if (i == argc) rc = tvec_readdflt(tv, dflt);
+  if (i == argc)
+    rc = tvec_readdflt(tv, dflt);
   else {
     rc = 0;
     while (i < argc)
@@ -222,6 +302,21 @@ int tvec_readargs(int argc, char *argv[], struct tvec_state *tv,
   return (rc);
 }
 
+/* --- @tvec_main@ --- *
+ *
+ * Arguments:  @int argc@ = number of command-line arguments
+ *             @char *argv[]@ = vector of argument strings
+ *             @const struct tvec_config *cofig@ = test vector configuration
+ *             @const char *dflt@ = default filename or null
+ *
+ * Returns:    Exit code.
+ *
+ * Use:                All-in-one test vector front-end.  Parse options from the
+ *             command-line as for @tvec_parseargs@, and then process the
+ *             remaining positional arguments as for @tvec_readargs@.  The
+ *             function constructs and disposes of a test vector state.
+ */
+
 int tvec_main(int argc, char *argv[],
              const struct tvec_config *config, const char *dflt)
 {
index c4809bb389ce337a2ab76edd6116abe5fd797756..9ca4f29ef7da1fd8010a9caca486b1bb8516319e 100644 (file)
@@ -30,6 +30,7 @@
 #include "config.h"
 
 #include <assert.h>
+#include <ctype.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <string.h>
 #include "alloc.h"
 #include "bench.h"
 #include "dstr.h"
+#include "macros.h"
 #include "quis.h"
 #include "report.h"
 #include "tvec.h"
 
 /*----- Common machinery --------------------------------------------------*/
 
+/* --- @regdisp@ --- *
+ *
+ * Arguments:  @unsigned disp@ = a @TVRD_...@ disposition code
+ *
+ * Returns:    A human-readable adjective describing the register
+ *             disposition.
+ */
+
 static const char *regdisp(unsigned disp)
 {
   switch (disp) {
@@ -57,6 +67,15 @@ static const char *regdisp(unsigned disp)
   }
 }
 
+/* --- @getenv_boolean@ --- *
+ *
+ * Arguments:  @const char *var@ = environment variable name
+ *             @int dflt@ = default value
+ *
+ * Returns:    @0@ if the variable is set to something falseish, @1@ if it's
+ *             set to something truish, or @dflt@ otherwise.
+ */
+
 static int getenv_boolean(const char *var, int dflt)
 {
   const char *p;
@@ -75,12 +94,19 @@ static int getenv_boolean(const char *var, int dflt)
           STRCMP(p, ==, "0"))
     return (0);
   else {
-    moan("unexpected value `%s' for boolean environment variable `%s'",
+    moan("ignoring unexpected value `%s' for environment variable `%s'",
         var, p);
     return (dflt);
   }
 }
 
+/* --- @register_maxnamelen@ --- *
+ *
+ * Arguments:  @const struct tvec_state *tv@ = test vector state
+ *
+ * Returns:    The maximum length of a register name in the current test.
+ */
+
 static int register_maxnamelen(const struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
@@ -91,6 +117,285 @@ static int register_maxnamelen(const struct tvec_state *tv)
   return (maxlen);
 }
 
+/*----- Output formatting -------------------------------------------------*/
+
+/* We have two main jobs in output formatting: trimming trailing blanks; and
+ * adding a prefix to each line.
+ *
+ * This is somehow much more complicated than it ought to be.
+ */
+
+struct format {
+  FILE *fp;                             /* output file */
+  const char *prefix, *pfxtail, *pfxlim; /* prefix pointers */
+  dstr w;                              /* trailing whitespace */
+  unsigned f;                          /* flags */
+#define FMTF_NEWL 1u                   /*   start of output line */
+};
+
+/* Support macros.  These assume `fmt' is defined as a pointer to the `struct
+ * format' state.
+ */
+
+#define SPLIT_RANGE(tail, base, limit) do {                            \
+  /* Set TAIL to point just after the last nonspace character between  \
+   * BASE and LIMIT.  If there are no nonspace characters, then set    \
+   * TAIL to equal BASE.                                               \
+   */                                                                  \
+                                                                       \
+  for (tail = limit; tail > base && ISSPACE(tail[-1]); tail--);                \
+} while (0)
+
+#define PUT_RANGE(base, limit) do {                                    \
+  /* Write the range of characters between BASE and LIMIT to the output \
+   * file.  Return immediately on error.                               \
+   */                                                                  \
+                                                                       \
+  size_t n = limit - base;                                             \
+  if (fwrite(base, 1, n, fmt->fp) < n) return (-1);                    \
+} while (0)
+
+#define PUT_CHAR(ch) do {                                              \
+  /* Write CH to the output. Return immediately on error. */           \
+                                                                       \
+  if (putc(ch, fmt->fp) == EOF) return (-1);                           \
+} while (0)
+
+#define PUT_PREFIX do {                                                        \
+  /* Output the prefix, if there is one.  Return immediately on error. */ \
+                                                                       \
+  if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxlim);                        \
+} while (0)
+
+#define PUT_SAVED do {                                                 \
+  /* Output the saved trailing blank material in the buffer. */                \
+                                                                       \
+  size_t n = fmt->w.len;                                               \
+  if (n && fwrite(fmt->w.buf, 1, n, fmt->fp) < n) return (-1);         \
+} while (0)
+
+#define PUT_PFXINB do {                                                        \
+  /* Output the initial nonblank portion of the prefix, if there is    \
+   * one.  Return immediately on error.                                        \
+   */                                                                  \
+                                                                       \
+  if (fmt->prefix) PUT_RANGE(fmt->prefix, fmt->pfxtail);               \
+} while (0)
+
+#define SAVE_PFXTAIL do {                                              \
+  /* Save the trailing blank portion of the prefix. */                 \
+                                                                       \
+  if (fmt->prefix)                                                     \
+    DPUTM(&fmt->w, fmt->pfxtail, fmt->pfxlim - fmt->pfxtail);          \
+} while (0)
+
+/* --- @init_fmt@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state to initialize
+ *             @FILE *fp@ = output file
+ *             @const char *prefix@ = prefix string (or null if empty)
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a formatting state.
+ */
+
+static void init_fmt(struct format *fmt, FILE *fp, const char *prefix)
+{
+  const char *q, *l;
+
+  /* Basics. */
+  fmt->fp = fp;
+  fmt->f = FMTF_NEWL;
+  dstr_create(&fmt->w);
+
+  /* Prefix portions. */
+  if (!prefix || !*prefix)
+    fmt->prefix = fmt->pfxtail = fmt->pfxlim = 0;
+  else {
+    fmt->prefix = prefix;
+    l = fmt->pfxlim = prefix + strlen(prefix);
+    SPLIT_RANGE(q, prefix, l); fmt->pfxtail = q;
+    DPUTM(&fmt->w, q, l - q);
+  }
+}
+
+/* --- @destroy_fmt@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state
+ *             @unsigned f@ = flags (@DFF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Releases a formatting state and the resources it holds.
+ *             Close the file if @DFF_CLOSE@ is set in @f@; otherwise leave
+ *             it open (in case it's @stderr@ or something).
+ */
+
+#define DFF_CLOSE 1u
+static void destroy_fmt(struct format *fmt, unsigned f)
+{
+  if (f&DFF_CLOSE) fclose(fmt->fp);
+  dstr_destroy(&fmt->w);
+}
+
+/* --- @format_char@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state
+ *             @int ch@ = character to write
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write a single character to the output.
+ */
+
+static int format_char(struct format *fmt, int ch)
+{
+  if (ch == '\n') {
+    if (fmt->f&FMTF_NEWL) PUT_PFXINB;
+    PUT_CHAR('\n'); fmt->f |= FMTF_NEWL; DRESET(&fmt->w);
+  } else if (isspace(ch))
+    DPUTC(&fmt->w, ch);
+  else {
+    if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; }
+    PUT_SAVED; PUT_CHAR(ch); DRESET(&fmt->w);
+  }
+  return (0);
+}
+
+/* --- @format_string@ --- *
+ *
+ * Arguments:  @struct format *fmt@ = formatting state
+ *             @const char *p@ = string to write
+ *             @size_t sz@ = length of string
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Write a string to the output.
+ */
+
+static int format_string(struct format *fmt, const char *p, size_t sz)
+{
+  const char *q, *r, *l = p + sz;
+
+  /* This is rather vexing.  There are a small number of jobs to do, but the
+   * logic for deciding which to do when gets rather hairy if, as I've tried
+   * here, one aims to minimize the number of decisions being checked, so
+   * it's worth canning them into macros.
+   *
+   * Here, a `blank' is a whitespace character other than newline.  The input
+   * buffer consists of one or more `segments', each of which consists of:
+   *
+   *   * an initial portion, which is either empty or ends with a nonblank
+   *    character;
+   *
+   *   * a suffix which consists only of blanks; and
+   *
+   *   * an optional newline.
+   *
+   * All segments except the last end with a newline.
+   */
+
+#define SPLIT_SEGMENT do {                                             \
+  /* Determine the bounds of the current segment.  If there is a final \
+   * newline, then q is non-null and points to this newline; otherwise,        \
+   * q is null.  The initial portion of the segment lies between p .. r        \
+   * and the blank suffix lies between r .. q (or r .. l if q is null).        \
+   * This sounds awkward, but the suffix is only relevant if there is  \
+   * no newline.                                                       \
+   */                                                                  \
+                                                                       \
+  q = memchr(p, '\n', l - p); SPLIT_RANGE(r, p, q ? q : l);            \
+} while (0)
+
+#define PUT_NONBLANK do {                                              \
+  /* Output the initial portion of the segment. */                     \
+                                                                       \
+  PUT_RANGE(p, r);                                                     \
+} while (0)
+
+#define PUT_NEWLINE do {                                               \
+  /* Write a newline, and advance to the next segment. */              \
+                                                                       \
+  PUT_CHAR('\n'); p = q + 1;                                           \
+} while (0)
+
+#define SAVE_TAIL do {                                                 \
+  /* Save the trailing blank portion of the segment in the buffer.     \
+   * Assumes that there is no newline, since otherwise the suffix would        \
+   * be omitted.                                                       \
+   */                                                                  \
+                                                                       \
+  DPUTM(&fmt->w, r, l - r);                                            \
+} while (0)
+
+  /* Determine the bounds of the first segment.  Handling this is the most
+   * complicated part of this function.
+   */
+  SPLIT_SEGMENT;
+
+  if (!q) {
+    /* This is the only segment.  We'll handle the whole thing here.
+     *
+     * If there's an initial nonblank portion, then we need to write that
+     * out.  Furthermore, if we're at the start of the line then we'll need
+     * to write the prefix, and if there's saved blank material then we'll
+     * need to write that.  Otherwise, there's only blank stuff, which we
+     * accumulate in the buffer.
+     *
+     * If we're at the start of a line here, then
+     */
+
+    if (r > p) {
+      if (fmt->f&FMTF_NEWL) { PUT_PFXINB; fmt->f &= ~FMTF_NEWL; }
+      PUT_SAVED; PUT_NONBLANK; DRESET(&fmt->w);
+    }
+    SAVE_TAIL;
+    return (0);
+  }
+
+  /* There is at least one more segment, so we know that there'll be a line
+   * to output.
+   */
+  if (fmt->f&FMTF_NEWL) PUT_PFXINB;
+  if (r > p) { PUT_SAVED; PUT_NONBLANK; }
+  PUT_NEWLINE; DRESET(&fmt->w);
+  SPLIT_SEGMENT;
+
+  /* Main loop over whole segments with trailing newlines.  For each one, we
+   * know that we're starting at the beginning of a line and there's a final
+   * newline, so we write the initial prefix and drop the trailing blanks.
+   */
+  while (q) {
+    PUT_PREFIX; PUT_NONBLANK; PUT_NEWLINE;
+    SPLIT_SEGMENT;
+  }
+
+  /* At the end, there's no final newline.  If there's nonblank material,
+   * then we can write the prefix and the nonblank stuff.  Otherwise, stash
+   * the blank stuff (including the trailing blanks of the prefix) and leave
+   * the newline flag set.
+   */
+  if (r > p) { PUT_PREFIX; PUT_NONBLANK; fmt->f &= ~FMTF_NEWL; }
+  else { fmt->f |= FMTF_NEWL; SAVE_PFXTAIL; }
+  SAVE_TAIL;
+
+#undef SPLIT_SEGMENT
+#undef PUT_NONBLANK
+#undef PUT_NEWLINE
+#undef SAVE_TAIL
+
+  return (0);
+}
+
+#undef SPLIT_RANGE
+#undef PUT_RANGE
+#undef PUT_PREFIX
+#undef PUT_PFXINB
+#undef PUT_SAVED
+#undef PUT_CHAR
+#undef SAVE_PFXTAIL
+
 /*----- Skeleton ----------------------------------------------------------*/
 /*
 static void ..._bsession(struct tvec_output *o, struct tvec_state *tv)
@@ -152,7 +457,8 @@ static const struct tvec_outops ..._ops = {
 struct human_output {
   struct tvec_output _o;
   struct tvec_state *tv;
-  FILE *fp;
+  struct format fmt;
+  char *outbuf; size_t outsz;
   dstr scoreboard;
   unsigned attr;
   int maxlen;
@@ -178,27 +484,29 @@ static void setattr(struct human_output *h, unsigned attr)
   int sep = 0;
 
   if (!diff || !(h->f&HOF_COLOUR)) return;
-  fputs("\x1b[", h->fp);
+  fputs("\x1b[", h->fmt.fp);
 
   if (diff&HAF_BOLD) {
-    if (attr&HAF_BOLD) putc('1', h->fp);
-    else { putc('0', h->fp); diff = h->attr; }
+    if (attr&HAF_BOLD) putc('1', h->fmt.fp);
+    else { putc('0', h->fmt.fp); diff = h->attr; }
     sep = ';';
   }
   if (diff&(HAF_FG | HAF_FGMASK)) {
     if (attr&HAF_FG)
-      set_colour(h->fp, &sep, "3", "9", (attr&HAF_FGMASK) >> HAF_FGSHIFT);
+      set_colour(h->fmt.fp, &sep, "3", "9",
+                (attr&HAF_FGMASK) >> HAF_FGSHIFT);
     else
-      { if (sep) putc(sep, h->fp); fputs("39", h->fp); sep = ';'; }
+      { if (sep) putc(sep, h->fmt.fp); fputs("39", h->fmt.fp); sep = ';'; }
   }
   if (diff&(HAF_BG | HAF_BGMASK)) {
     if (attr&HAF_BG)
-      set_colour(h->fp, &sep, "4", "10", (attr&HAF_BGMASK) >> HAF_BGSHIFT);
+      set_colour(h->fmt.fp, &sep, "4", "10",
+                (attr&HAF_BGMASK) >> HAF_BGSHIFT);
     else
-      { if (sep) putc(sep, h->fp); fputs("49", h->fp); sep = ';'; }
+      { if (sep) putc(sep, h->fmt.fp); fputs("49", h->fmt.fp); sep = ';'; }
   }
 
-  putc('m', h->fp); h->attr = attr;
+  putc('m', h->fmt.fp); h->attr = attr;
 
 #undef f_any
 }
@@ -209,7 +517,7 @@ static void clear_progress(struct human_output *h)
 
   if (h->f&HOF_PROGRESS) {
     n = strlen(h->tv->test->name) + 2 + h->scoreboard.len;
-    for (i = 0; i < n; i++) fputs("\b \b", h->fp);
+    for (i = 0; i < n; i++) fputs("\b \b", h->fmt.fp);
     h->f &= ~HOF_PROGRESS;
   }
 }
@@ -221,7 +529,7 @@ static void write_scoreboard_char(struct human_output *h, int ch)
     case '_': setattr(h, HA_SKIP); break;
     default: setattr(h, 0); break;
   }
-  putc(ch, h->fp); setattr(h, 0);
+  putc(ch, h->fmt.fp); setattr(h, 0);
 }
 
 static void show_progress(struct human_output *h)
@@ -230,12 +538,12 @@ static void show_progress(struct human_output *h)
   const char *p, *l;
 
   if (tv->test && (h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
-    fprintf(h->fp, "%s: ", tv->test->name);
+    fprintf(h->fmt.fp, "%s: ", tv->test->name);
     if (!(h->f&HOF_COLOUR))
-      dstr_write(&h->scoreboard, h->fp);
+      dstr_write(&h->scoreboard, h->fmt.fp);
     else for (p = h->scoreboard.buf, l = p + h->scoreboard.len; p < l; p++)
       write_scoreboard_char(h, *p);
-    fflush(h->fp); h->f |= HOF_PROGRESS;
+    fflush(h->fmt.fp); h->f |= HOF_PROGRESS;
   }
 }
 
@@ -247,29 +555,55 @@ static void report_location(struct human_output *h, FILE *fp,
 
 #define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
 
-  if (fp != h->fp) f |= f_flush;
+  if (fp != h->fmt.fp) f |= f_flush;
 
   if (file) {
-    setattr(h, HFG(CYAN)); FLUSH(h->fp); fputs(file, fp); FLUSH(fp);
-    setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp);
-    setattr(h, HFG(CYAN)); FLUSH(h->fp); fprintf(fp, "%u", lno); FLUSH(fp);
-    setattr(h, HFG(BLUE)); FLUSH(h->fp); fputc(':', fp); FLUSH(fp);
-    setattr(h, 0); FLUSH(h->fp); fputc(' ', fp);
+    setattr(h, HFG(CYAN));     FLUSH(h->fmt.fp);
+    fputs(file, fp);           FLUSH(fp);
+    setattr(h, HFG(BLUE));     FLUSH(h->fmt.fp);
+    fputc(':', fp);            FLUSH(fp);
+    setattr(h, HFG(CYAN));     FLUSH(h->fmt.fp);
+    fprintf(fp, "%u", lno);    FLUSH(fp);
+    setattr(h, HFG(BLUE));     FLUSH(h->fmt.fp);
+    fputc(':', fp);            FLUSH(fp);
+    setattr(h, 0);             FLUSH(h->fmt.fp);
+    fputc(' ', fp);
   }
 
 #undef f_flush
 #undef FLUSH
 }
 
+static int human_writech(void *go, int ch)
+  { struct human_output *h = go; return (format_char(&h->fmt, ch)); }
+
+static int human_writem(void *go, const char *p, size_t sz)
+  { struct human_output *h = go; return (format_string(&h->fmt, p, sz)); }
+
+static int human_nwritef(void *go, size_t maxsz, const char *p, ...)
+{
+  struct human_output *h = go;
+  size_t n;
+  va_list ap;
+
+  va_start(ap, p);
+  n = gprintf_memputf(&h->outbuf, &h->outsz, maxsz, p, ap);
+  va_end(ap);
+  return (format_string(&h->fmt, h->outbuf, n));
+}
+
+static const struct gprintf_ops human_printops =
+  { human_writech, human_writem, human_nwritef };
+
 static void human_bsession(struct tvec_output *o, struct tvec_state *tv)
   { struct human_output *h = (struct human_output *)o; h->tv = tv; }
 
 static void report_skipped(struct human_output *h, unsigned n)
 {
   if (n) {
-    fprintf(h->fp, " (%u ", n);
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
-    fputc(')', h->fp);
+    fprintf(h->fmt.fp, " (%u ", n);
+    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
+    fputc(')', h->fmt.fp);
   }
 }
 
@@ -284,28 +618,28 @@ static int human_esession(struct tvec_output *o)
     all_run = all_win + all_lose, grps_run = grps_win + grps_lose;
 
   if (!all_lose) {
-    setattr(h, HA_WIN); fputs("PASSED", h->fp); setattr(h, 0);
-    fprintf(h->fp, " %s%u %s",
+    setattr(h, HA_WIN); fputs("PASSED", h->fmt.fp); setattr(h, 0);
+    fprintf(h->fmt.fp, " %s%u %s",
            !(all_skip || grps_skip) ? "all " : "",
            all_win, all_win == 1 ? "test" : "tests");
     report_skipped(h, all_skip);
-    fprintf(h->fp, " in %u %s",
+    fprintf(h->fmt.fp, " in %u %s",
            grps_win, grps_win == 1 ? "group" : "groups");
     report_skipped(h, grps_skip);
   } else {
-    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-    fprintf(h->fp, " %u out of %u %s",
+    setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
+    fprintf(h->fmt.fp, " %u out of %u %s",
            all_lose, all_run, all_run == 1 ? "test" : "tests");
     report_skipped(h, all_skip);
-    fprintf(h->fp, " in %u out of %u %s",
+    fprintf(h->fmt.fp, " in %u out of %u %s",
            grps_lose, grps_run, grps_run == 1 ? "group" : "groups");
     report_skipped(h, grps_skip);
   }
-  fputc('\n', h->fp);
+  fputc('\n', h->fmt.fp);
 
   if (tv->f&TVSF_ERROR) {
-    setattr(h, HA_ERR); fputs("ERRORS", h->fp); setattr(h, 0);
-    fputs(" found in input; tests may not have run correctly\n", h->fp);
+    setattr(h, HA_ERR); fputs("ERRORS", h->fmt.fp); setattr(h, 0);
+    fputs(" found in input; tests may not have run correctly\n", h->fmt.fp);
   }
 
   h->tv = 0; return (tv->f&TVSF_ERROR ? 2 : tv->all[TVOUT_LOSE] ? 1 : 0);
@@ -326,13 +660,13 @@ static void human_skipgroup(struct tvec_output *o,
 
   if (!(~h->f&(HOF_TTY | HOF_PROGRESS))) {
     h->f &= ~HOF_PROGRESS;
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
   } else {
-    fprintf(h->fp, "%s: ", h->tv->test->name);
-    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+    fprintf(h->fmt.fp, "%s: ", h->tv->test->name);
+    setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
   }
-  if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); }
-  fputc('\n', h->fp);
+  if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_egroup(struct tvec_output *o)
@@ -343,17 +677,18 @@ static void human_egroup(struct tvec_output *o)
     skip = tv->curr[TVOUT_SKIP], run = win + lose;
 
   if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS;
-  else fprintf(h->fp, "%s:", h->tv->test->name);
+  else fprintf(h->fmt.fp, "%s:", h->tv->test->name);
 
   if (lose) {
-    fprintf(h->fp, " %u/%u ", lose, run);
-    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
+    fprintf(h->fmt.fp, " %u/%u ", lose, run);
+    setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
     report_skipped(h, skip);
   } else {
-    fputc(' ', h->fp); setattr(h, HA_WIN); fputs("ok", h->fp); setattr(h, 0);
+    fputc(' ', h->fmt.fp); setattr(h, HA_WIN);
+    fputs("ok", h->fmt.fp); setattr(h, 0);
     report_skipped(h, skip);
   }
-  fputc('\n', h->fp);
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_btest(struct tvec_output *o)
@@ -366,11 +701,11 @@ static void human_skip(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  report_location(h, h->fp, tv->infile, tv->test_lno);
-  fprintf(h->fp, "`%s' ", tv->test->name);
-  setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
-  if (excuse) { fputs(": ", h->fp); vfprintf(h->fp, excuse, *ap); }
-  fputc('\n', h->fp);
+  report_location(h, h->fmt.fp, tv->infile, tv->test_lno);
+  fprintf(h->fmt.fp, "`%s' ", tv->test->name);
+  setattr(h, HA_SKIP); fputs("skipped", h->fmt.fp); setattr(h, 0);
+  if (excuse) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, excuse, *ap); }
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_fail(struct tvec_output *o,
@@ -380,11 +715,11 @@ static void human_fail(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  report_location(h, h->fp, tv->infile, tv->test_lno);
-  fprintf(h->fp, "`%s' ", tv->test->name);
-  setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
-  if (detail) { fputs(": ", h->fp); vfprintf(h->fp, detail, *ap); }
-  fputc('\n', h->fp);
+  report_location(h, h->fmt.fp, tv->infile, tv->test_lno);
+  fprintf(h->fmt.fp, "`%s' ", tv->test->name);
+  setattr(h, HA_LOSE); fputs("FAILED", h->fmt.fp); setattr(h, 0);
+  if (detail) { fputs(": ", h->fmt.fp); vfprintf(h->fmt.fp, detail, *ap); }
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_dumpreg(struct tvec_output *o,
@@ -394,15 +729,17 @@ static void human_dumpreg(struct tvec_output *o,
   struct human_output *h = (struct human_output *)o;
   const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
 
-  fprintf(h->fp, "%*s%s %s = ", 10 + h->maxlen - n, "", ds, rd->name);
+  clear_progress(h);
+  gprintf(&human_printops, h, "%*s%s %s = ",
+         10 + h->maxlen - n, "", ds, rd->name);
   if (h->f&HOF_COLOUR) {
     if (!rv) setattr(h, HFG(YELLOW));
     else if (disp == TVRD_FOUND) setattr(h, HFG(RED));
     else if (disp == TVRD_EXPECT) setattr(h, HFG(GREEN));
   }
-  if (!rv) fprintf(h->fp, "#<unset>");
-  else rd->ty->dump(rv, rd, 0, &file_printops, h->fp);
-  setattr(h, 0); fputc('\n', h->fp);
+  if (!rv) gprintf(&human_printops, h, "#unset");
+  else rd->ty->dump(rv, rd, 0, &human_printops, h);
+  setattr(h, 0); format_char(&h->fmt, '\n');
 }
 
 static void human_etest(struct tvec_output *o, unsigned outcome)
@@ -419,7 +756,7 @@ static void human_etest(struct tvec_output *o, unsigned outcome)
       default: abort();
     }
     dstr_putc(&h->scoreboard, ch);
-    write_scoreboard_char(h, ch); fflush(h->fp);
+    write_scoreboard_char(h, ch); fflush(h->fmt.fp);
   }
 }
 
@@ -430,7 +767,7 @@ static void human_bbench(struct tvec_output *o,
   struct tvec_state *tv = h->tv;
 
   clear_progress(h);
-  fprintf(h->fp, "%s: %s: ", tv->test->name, ident); fflush(h->fp);
+  fprintf(h->fmt.fp, "%s: %s: ", tv->test->name, ident); fflush(h->fmt.fp);
 }
 
 static void human_ebench(struct tvec_output *o,
@@ -438,7 +775,9 @@ static void human_ebench(struct tvec_output *o,
                         const struct bench_timing *tm)
 {
   struct human_output *h = (struct human_output *)o;
-  tvec_benchreport(&file_printops, h->fp, unit, tm); fputc('\n', h->fp);
+
+  tvec_benchreport(&human_printops, h->fmt.fp, unit, tm);
+  fputc('\n', h->fmt.fp);
 }
 
 static void human_report(struct tvec_output *o, const char *msg, va_list *ap)
@@ -449,14 +788,14 @@ static void human_report(struct tvec_output *o, const char *msg, va_list *ap)
 
   dstr_vputf(&d, msg, ap); dstr_putc(&d, '\n');
 
-  clear_progress(h); fflush(h->fp);
+  clear_progress(h); fflush(h->fmt.fp);
   fprintf(stderr, "%s: ", QUIS);
   report_location(h, stderr, tv->infile, tv->lno);
   fwrite(d.buf, 1, d.len, stderr);
 
   if (h->f&HOF_DUPERR) {
-    report_location(h, h->fp, tv->infile, tv->lno);
-    fwrite(d.buf, 1, d.len, h->fp);
+    report_location(h, h->fmt.fp, tv->infile, tv->lno);
+    fwrite(d.buf, 1, d.len, h->fmt.fp);
   }
   show_progress(h);
 }
@@ -465,9 +804,9 @@ static void human_destroy(struct tvec_output *o)
 {
   struct human_output *h = (struct human_output *)o;
 
-  if (h->f&HOF_DUPERR) fclose(h->fp);
+  destroy_fmt(&h->fmt, h->f&HOF_DUPERR ? DFF_CLOSE : 0);
   dstr_destroy(&h->scoreboard);
-  xfree(h);
+  xfree(h->outbuf); xfree(h);
 }
 
 static const struct tvec_outops human_ops = {
@@ -487,7 +826,8 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
   h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops;
   h->f = 0; h->attr = 0;
 
-  h->fp = fp;
+  init_fmt(&h->fmt, fp, 0);
+  h->outbuf = 0; h->outsz = 0;
 
   switch (getenv_boolean("TVEC_TTY", -1)) {
     case 1: h->f |= HOF_TTY; break;
@@ -517,61 +857,27 @@ struct tvec_output *tvec_humanoutput(FILE *fp)
 struct tap_output {
   struct tvec_output _o;
   struct tvec_state *tv;
-  FILE *fp;
-  dstr d;
+  struct format fmt;
+  char *outbuf; size_t outsz;
   int maxlen;
-  unsigned f;
-#define TOF_FRESHLINE 1u
 };
 
 static int tap_writech(void *go, int ch)
-{
-  struct tap_output *t = go;
-
-  if (t->f&TOF_FRESHLINE) {
-    if (fputs("## ", t->fp) < 0) return (-1);
-    t->f &= ~TOF_FRESHLINE;
-  }
-  if (putc(ch, t->fp) < 0) return (-1);
-  if (ch == '\n') t->f |= TOF_FRESHLINE;
-  return (1);
-}
+  { struct tap_output *t = go; return (format_char(&t->fmt, ch)); }
 
 static int tap_writem(void *go, const char *p, size_t sz)
-{
-  struct tap_output *t = go;
-  const char *q, *l = p + sz;
-  size_t n;
-
-  if (p == l) return (0);
-  if (t->f&TOF_FRESHLINE)
-    if (fputs("## ", t->fp) < 0) return (-1);
-  for (;;) {
-    q = memchr(p, '\n', l - p); if (!q) break;
-    n = q + 1 - p; if (fwrite(p, 1, n, t->fp) < n) return (-1);
-    p = q + 1;
-    if (p == l) { t->f |= TOF_FRESHLINE; return (sz); }
-    if (fputs("## ", t->fp) < 0) return (-1);
-  }
-  n = l - p; if (fwrite(p, 1, n, t->fp) < n) return (-1);
-  t->f &= ~TOF_FRESHLINE; return (0);
-}
+  { struct human_output *t = go; return (format_string(&t->fmt, p, sz)); }
 
 static int tap_nwritef(void *go, size_t maxsz, const char *p, ...)
 {
-  struct tap_output *t = go;
+  struct human_output *t = go;
+  size_t n;
   va_list ap;
-  int n;
-
-  va_start(ap, p); DRESET(&t->d); DENSURE(&t->d, maxsz + 1);
-#ifdef HAVE_SNPRINTF
-  n = vsnprintf(t->d.buf, maxsz + 1, p, ap);
-#else
-  n = vsprintf(t->d.buf, p, ap);
-#endif
-  assert(0 <= n && n <= maxsz);
+
+  va_start(ap, p);
+  n = gprintf_memputf(&t->outbuf, &t->outsz, maxsz, p, ap);
   va_end(ap);
-  return (tap_writem(t, t->d.buf, n));
+  return (format_string(&t->fmt, t->outbuf, n));
 }
 
 static const struct gprintf_ops tap_printops =
@@ -582,7 +888,7 @@ static void tap_bsession(struct tvec_output *o, struct tvec_state *tv)
   struct tap_output *t = (struct tap_output *)o;
 
   t->tv = tv;
-  fputs("TAP version 13\n", t->fp);
+  fputs("TAP version 13\n", t->fmt.fp);
 }
 
 static unsigned tap_grpix(struct tap_output *t)
@@ -602,11 +908,11 @@ static int tap_esession(struct tvec_output *o)
   if (tv->f&TVSF_ERROR) {
     fputs("Bail out!  "
          "Errors found in input; tests may not have run correctly\n",
-         t->fp);
+         t->fmt.fp);
     return (2);
   }
 
-  fprintf(t->fp, "1..%u\n", tap_grpix(t));
+  fprintf(t->fmt.fp, "1..%u\n", tap_grpix(t));
   t->tv = 0; return (tv->all[TVOUT_LOSE] ? 1 : 0);
 }
 
@@ -621,9 +927,9 @@ static void tap_skipgroup(struct tvec_output *o,
 {
   struct tap_output *t = (struct tap_output *)o;
 
-  fprintf(t->fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name);
-  if (excuse) { fputc(' ', t->fp); vfprintf(t->fp, excuse, *ap); }
-  fputc('\n', t->fp);
+  fprintf(t->fmt.fp, "ok %u %s # SKIP", tap_grpix(t), t->tv->test->name);
+  if (excuse) { fputc(' ', t->fmt.fp); vfprintf(t->fmt.fp, excuse, *ap); }
+  fputc('\n', t->fmt.fp);
 }
 
 static void tap_egroup(struct tvec_output *o)
@@ -637,39 +943,37 @@ static void tap_egroup(struct tvec_output *o)
     skip = tv->curr[TVOUT_SKIP];
 
   if (lose) {
-    fprintf(t->fp, "not ok %u - %s: FAILED %u/%u",
+    fprintf(t->fmt.fp, "not ok %u - %s: FAILED %u/%u",
            grpix, tv->test->name, lose, win + lose);
-    if (skip) fprintf(t->fp, " (skipped %u)", skip);
+    if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip);
   } else {
-    fprintf(t->fp, "ok %u - %s: passed %u", grpix, tv->test->name, win);
-    if (skip) fprintf(t->fp, " (skipped %u)", skip);
+    fprintf(t->fmt.fp, "ok %u - %s: passed %u", grpix, tv->test->name, win);
+    if (skip) fprintf(t->fmt.fp, " (skipped %u)", skip);
   }
-  fputc('\n', t->fp);
+  fputc('\n', t->fmt.fp);
 }
 
 static void tap_btest(struct tvec_output *o) { ; }
 
-static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+static void tap_outcome(struct tvec_output *o, const char *outcome,
+                       const char *detail, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
   struct tvec_state *tv = t->tv;
 
-  fprintf(t->fp, "## %s:%u: `%s' skipped",
-         tv->infile, tv->test_lno, tv->test->name);
-  if (excuse) { fputs(": ", t->fp); vfprintf(t->fp, excuse, *ap); }
-  fputc('\n', t->fp);
+  gprintf(&tap_printops, t, "%s:%u: `%s' %s",
+         tv->infile, tv->test_lno, tv->test->name, outcome);
+  if (detail) {
+    format_string(&t->fmt, ": ", 2);
+    vgprintf(&tap_printops, t, detail, ap);
+  }
+  format_char(&t->fmt, '\n');
 }
 
+static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+  { tap_outcome(o, "skipped", excuse, ap); }
 static void tap_fail(struct tvec_output *o, const char *detail, va_list *ap)
-{
-  struct tap_output *t = (struct tap_output *)o;
-  struct tvec_state *tv = t->tv;
-
-  fprintf(t->fp, "## %s:%u: `%s' FAILED",
-         tv->infile, tv->test_lno, tv->test->name);
-  if (detail) { fputs(": ", t->fp); vfprintf(t->fp, detail, *ap); }
-  fputc('\n', t->fp);
-}
+  { tap_outcome(o, "FAILED", detail, ap); }
 
 static void tap_dumpreg(struct tvec_output *o,
                        unsigned disp, const union tvec_regval *rv,
@@ -678,14 +982,11 @@ static void tap_dumpreg(struct tvec_output *o,
   struct tap_output *t = (struct tap_output *)o;
   const char *ds = regdisp(disp); int n = strlen(ds) + strlen(rd->name);
 
-  fprintf(t->fp, "## %*s%s %s = ", 10 + t->maxlen - n, "", ds, rd->name);
-  if (!rv)
-    fprintf(t->fp, "#<unset>\n");
-  else {
-    t->f &= ~TOF_FRESHLINE;
-    rd->ty->dump(rv, rd, 0, &tap_printops, t);
-    fputc('\n', t->fp);
-  }
+  gprintf(&tap_printops, t, "%*s%s %s = ",
+         10 + t->maxlen - n, "", ds, rd->name);
+  if (!rv) gprintf(&tap_printops, t, "#<unset>");
+  else rd->ty->dump(rv, rd, 0, &tap_printops, t);
+  format_char(&t->fmt, '\n');
 }
 
 static void tap_etest(struct tvec_output *o, unsigned outcome) { ; }
@@ -701,38 +1002,43 @@ static void tap_ebench(struct tvec_output *o,
   struct tap_output *t = (struct tap_output *)o;
   struct tvec_state *tv = t->tv;
 
-  fprintf(t->fp, "## %s: %s: ", tv->test->name, ident);
-  t->f &= ~TOF_FRESHLINE; tvec_benchreport(&tap_printops, t, unit, tm);
-  fputc('\n', t->fp);
+  gprintf(&tap_printops, t, "%s: %s: ", tv->test->name, ident);
+  tvec_benchreport(&tap_printops, t, unit, tm);
+  format_char(&t->fmt, '\n');
 }
 
-static void tap_report(struct tap_output *t, const char *msg, va_list *ap)
+static void tap_report(struct tap_output *t,
+                      const struct gprintf_ops *gops, void *go,
+                      const char *msg, va_list *ap)
 {
   struct tvec_state *tv = t->tv;
 
-  if (tv->infile) fprintf(t->fp, "%s:%u: ", tv->infile, tv->lno);
-  vfprintf(t->fp, msg, *ap); fputc('\n', t->fp);
+  if (tv->infile) gprintf(gops, go, "%s:%u: ", tv->infile, tv->lno);
+  gprintf(gops, go, msg, ap); gops->putch(go, '\n');
 }
 
 static void tap_error(struct tvec_output *o, const char *msg, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
-  fputs("Bail out!  ", t->fp); tap_report(t, msg, ap);
+
+  fputs("Bail out!  ", t->fmt.fp);
+  tap_report(t, &file_printops, t->fmt.fp, msg, ap);
 }
 
 static void tap_notice(struct tvec_output *o, const char *msg, va_list *ap)
 {
   struct tap_output *t = (struct tap_output *)o;
-  fputs("## ", t->fp); tap_report(t, msg, ap);
+
+  tap_report(t, &tap_printops, t, msg, ap);
 }
 
 static void tap_destroy(struct tvec_output *o)
 {
   struct tap_output *t = (struct tap_output *)o;
 
-  if (t->fp != stdout && t->fp != stderr) fclose(t->fp);
-  dstr_destroy(&t->d);
-  xfree(t);
+  destroy_fmt(&t->fmt,
+             t->fmt.fp == stdout || t->fmt.fp == stderr ? 0 : DFF_CLOSE);
+  xfree(t->outbuf); xfree(t);
 }
 
 static const struct tvec_outops tap_ops = {
@@ -749,8 +1055,8 @@ struct tvec_output *tvec_tapoutput(FILE *fp)
   struct tap_output *t;
 
   t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops;
-  dstr_create(&t->d);
-  t->f = 0; t->fp = fp;
+  init_fmt(&t->fmt, fp, "## ");
+  t->outbuf = 0; t->outsz = 0;
   return (&t->_o);
 }
 
index e03297e16baeff6db27a0798a0442f6e7853039d..a00f04654409e2fe469bc3bb381d5b3dfe157765 100644 (file)
@@ -114,7 +114,7 @@ static int recv_all(struct tvec_state *tv, struct tvec_remote *r,
 #undef f_any
 }
 
-int tvec_send(struct tvec_state *tv, struct tvec_reomte *r)
+int tvec_send(struct tvec_state *tv, struct tvec_remote *r)
 {
   kludge64 k; unsigned char lenbuf[8];
   const char *p; size_t sz;
@@ -131,7 +131,7 @@ int tvec_send(struct tvec_state *tv, struct tvec_reomte *r)
   return (0);
 }
 
-int tvec_recv(struct tvec_state *tv, struct tvec_reomte *r, buf *b_out)
+int tvec_recv(struct tvec_state *tv, struct tvec_remote *r, buf *b_out)
 {
   kludge64 k, szmax; unsigned char lenbuf[8];
   unsigned char *p;
@@ -176,6 +176,7 @@ int tvec_recv(struct tvec_state *tv, struct tvec_reomte *r, buf *b_out)
 /*----- The output driver -------------------------------------------------*/
 
 #define SENDPK(ro, pk)                                                 \
+       if ((ro)->r.f&TVRF_BROKEN) /* do nothing */; else               \
        MC_BEFORE(setpk,                                                \
          { buf_reset(&(ro)->r.bout);                                   \
            buf_putu16l(&(ro)->r.bout.b, (pk)); })                      \
@@ -187,29 +188,26 @@ static int sendstr(struct tvec_output *o, unsigned pk,
 {
   struct remote_output *ro = (struct remote_output *)o;
 
-  if (ro->r.f&TVRF_BROKEN) return (-1);
-  dbuf_reset(&ro->r.bout);
-  buf_putu16l(&ro->r.bout.b, TVPK_ERROR);
-  buf_vputstrf16l(&ro->r.bout.b, msg, ap);
-  return (tvec_send(ro->_o.tv, &ro->r));
+  SENDPK(ro, pk) buf_vputstrf16l(&ro->r.bout.b, msg, ap);
+  return (ro->r.f&TVRF_BROKEN ? -1 : 0);
 }
 
-static void report(struct tvec_output *o, unsigned pk,
+static void report(struct tvec_output *o, unsigned pk, const char *what,
                   const char *msg, va_list *ap)
 {
   if (sendstr(o, pk, msg, ap)) {
-    fprintf(stderr, "%s: ", QUIS);
+    fprintf(stderr, "%s %s: ", QUIS, what);
     vfprintf(stderr, msg, *ap);
     fputc('\n', stderr);
   }
 }
 
 static void remote_error(struct tvec_output *o, const char *msg, va_list *ap)
-  { report(o, TVPK_ERROR, msg, ap); }
+  { report(o, TVPK_ERROR, "ERROR", msg, ap); }
 
 static void remote_notice(struct tvec_output *o,
                          const char *msg, va_list *ap)
-  { report(o, TVPK_NOTICE, msg, ap); }
+  { report(o, TVPK_NOTICE, "notice", msg, ap); }
 
 static void remote_setstatus(struct tvec_ouptut *o, int st)
 {
index 9709b6032401b3762aa0a469598ddd145b1d5b01..695d66dd9c965f598e64554c31cb5f5ce2d848bf 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 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; }
index 7f19f50b02d37907500acd46ed7b2dfd7f2ae27a..5ea18ca8006115f67e94028f4ee82d74260b0036 100644 (file)
@@ -186,79 +186,6 @@ struct tvec_regdef {
 #define TVEC_GREG(vec, i, regsz)                                       \
        ((struct tvec_reg *)((unsigned char *)(vec) + (i)*(regsz)))
 
-/*------ Serialization utilities ------------------------------------------*/
-
-/* --- @tvec_serialize@ --- *
- *
- * Arguments:  @const struct tvec_reg *rv@ = vector of registers
- *             @buf *b@ = buffer to write on
- *             @const struct tvec_regdef *regs@ = vector of register
- *                     descriptions, terminated by an entry with a null
- *                     @name@ slot
- *             @unsigned nr@ = number of entries in the @rv@ vector
- *             @size_t regsz@ = true size of each element of @rv@
- *
- * Returns:    Zero on success, @-1@ on failure.
- *
- * Use:                Serialize a collection of register values.
- *
- *             The `candidate register definitions' are those entries @r@ in
- *             the @regs@ vector whose index @r.i@ is strictly less than
- *             @nr@.  The `selected register definitions' are those
- *             candidate register definitions @r@ for which the indicated
- *             register @rv[r.i]@ has the @TVRF_LIVE@ flag set.  The
- *             serialized output begins with a header bitmap: if there are
- *             %$n$% candidate register definitions then the header bitmap
- *             consists of %$\lceil n/8 \rceil$% bytes.  Bits are ordered
- *             starting from the least significant bit of the first byte,
- *             end ending at the most significant bit of the final byte.
- *             The bit corresponding to a candidate register definition is
- *             set if and only if that register defintion is selected.  The
- *             header bitmap is then followed by the serializations of the
- *             selected registers -- i.e., for each selected register
- *             definition @r@, the serialized value of register @rv[r.i]@ --
- *             simply concatenated together, with no padding or alignment.
- *
- *             The serialized output is written to the buffer @b@.  Failure
- *             can be caused by running out of buffer space, or a failing
- *             type handler.
- */
-
-extern int tvec_serialize(const struct tvec_reg */*rv*/, buf */*b*/,
-                         const struct tvec_regdef */*regs*/,
-                         unsigned /*nr*/, size_t /*regsz*/);
-
-/* --- @tvec_deserialize@ --- *
- *
- * Arguments:  @struct tvec_reg *rv@ = vector of registers
- *             @buf *b@ = buffer to write on
- *             @const struct tvec_regdef *regs@ = vector of register
- *                     descriptions, terminated by an entry with a null
- *                     @name@ slot
- *             @unsigned nr@ = number of entries in the @rv@ vector
- *             @size_t regsz@ = true size of each element of @rv@
- *
- * Returns:    Zero on success, @-1@ on failure.
- *
- * Use:                Deserialize a collection of register values.
- *
- *             The size of the register vector @nr@ and the register
- *             definitions @regs@ must match those used when producing the
- *             serialization.  For each serialized register value,
- *             deserialize and store the value into the appropriate register
- *             slot, and set the @TVRF_LIVE@ flag on the register.  See
- *             @tvec_serialize@ for a description of the format.
- *
- *             On successful completion, store the address of the first byte
- *             after the serialized data in @*end_out@ if @end_out@ is not
- *             null.  Failure results only from a failing register type
- *             handler.
- */
-
-extern int tvec_deserialize(struct tvec_reg */*rv*/, buf */*b*/,
-                           const struct tvec_regdef */*regs*/,
-                           unsigned /*nr*/, size_t /*regsz*/);
-
 /*----- Test state --------------------------------------------------------*/
 
 enum {
@@ -308,6 +235,135 @@ struct tvec_state {
  */
 #define TVEC_REG(tv, vec, i) TVEC_GREG((tv)->vec, (i), (tv)->regsz)
 
+struct tvec_config {
+  /* An overall test configuration. */
+
+  const struct tvec_test *tests;       /* the tests to be performed */
+  unsigned nrout, nreg;                        /* number of output/total regs */
+  size_t regsz;                                /* size of a register */
+};
+
+/*----- Output formatting -------------------------------------------------*/
+
+struct tvec_output {
+  /* An output formatter. */
+  const struct tvec_outops *ops;       /* pointer to operations */
+};
+
+/* Benchmarking details. */
+enum {
+  TVBU_OP,                            /* counting operations of some kind */
+  TVBU_BYTE                            /* counting bytes (@rbuf >= 0@) */
+};
+struct bench_timing;                   /* forward declaration */
+
+struct tvec_outops {
+  /* Output operations. */
+
+  void (*bsession)(struct tvec_output */*o*/, struct tvec_state */*tv*/);
+       /* Begin a test session.  The output driver will probably want to
+        * save @tv@, because this isn't provided to any other methods.
+        */
+
+  int (*esession)(struct tvec_output */*o*/);
+       /* End a session, and return the suggested exit code. */
+
+  void (*bgroup)(struct tvec_output */*o*/);
+       /* Begin a test group.  The test group description is @tv->test@. */
+
+  void (*skipgroup)(struct tvec_output */*o*/,
+                   const char */*excuse*/, va_list */*ap*/);
+       /* The group is being skipped; @excuse@ may be null or a format
+        * string explaining why.  The @egroup@ method will not be called
+        * separately.
+        */
+
+  void (*egroup)(struct tvec_output */*o*/);
+       /* End a test group.  At least one test was attempted or @skipgroup@
+        * would have been called instead.  If @tv->curr[TVOUT_LOSE]@ is
+        * nonzero then the test group as a whole failed; otherwise it
+        * passed.
+        */
+
+  void (*btest)(struct tvec_output */*o*/);
+       /* Begin a test case. */
+
+  void (*skip)(struct tvec_output */*o*/,
+              const char */*excuse*/, va_list */*ap*/);
+       /* The test case is being skipped; @excuse@ may be null or a format
+        * string explaining why.  The @etest@ function will still be called
+        * (so this works differently from @skipgroup@ and @egroup@).  A test
+        * case can be skipped at most once, and, if skipped, it cannot fail.
+        */
+
+  void (*fail)(struct tvec_output */*o*/,
+              const char */*detail*/, va_list */*ap*/);
+       /* The test case failed.
+        *
+        * The output driver should preferably report the filename (@infile@)
+        * and line number (@test_lno@, not @lno@) for the failing test.
+        *
+        * The @detail@ may be null or a format string describing detail
+        * about how the failing test was run which can't be determined from
+        * the registers; a @detail@ is usually provided when (and only when)
+        * the test environment potentially invokes the test function more
+        * than once.
+        *
+        * A single test case can fail multiple times!
+        */
+
+  void (*dumpreg)(struct tvec_output */*o*/,
+                 unsigned /*disp*/, const union tvec_regval */*rv*/,
+                 const struct tvec_regdef */*rd*/);
+       /* Dump a register.  The `disposition' @disp@ explains what condition
+        * the register is in; see @tvec_dumpreg@ and the @TVRD_...@ codes.
+        * The register value is at @rv@, and its definition, including its
+        * type, at @rd@.  Note that this function may be called on virtual
+        * registers which aren't in either of the register vectors or
+        * mentioned by the test description.
+        */
+
+  void (*etest)(struct tvec_output */*o*/, unsigned /*outcome*/);
+       /* The test case concluded with the given @outcome@ (one of the
+        * @TVOUT_...@ codes.
+        */
+
+  void (*bbench)(struct tvec_output */*o*/,
+                const char */*ident*/, unsigned /*unit*/);
+       /* Begin a benchmark.  The @ident@ is a formatted string identifying
+        * the benchmark based on the values of the input registered marked
+        * @TVRF_ID@; the output driver is free to use this or come up with
+        * its own way to identify the test, e.g., by inspecting the register
+        * values for itself.  The @unit@ is one of the @TVBU_...@ constants
+        * explaining what sort of thing is being measured.
+        */
+
+  void (*ebench)(struct tvec_output */*o*/,
+                const char */*ident*/, unsigned /*unit*/,
+                const struct bench_timing */*tm*/);
+       /* End a benchmark.  The @ident@ and @unit@ arguments are as for
+        * @bbench@.  If @tm@ is zero then the measurement failed; otherwise
+        * @tm->n@ counts total number of things (operations or bytes, as
+        * indicated by @unit@) processed in the indicated time.
+        */
+
+  void (*error)(struct tvec_output */*o*/,
+               const char */*msg*/, va_list */*ap*/);
+       /* Report an error.  The driver should ideally report the filename
+        * (@infile@) and line number (@lno@) prompting the error.
+        */
+
+  void (*notice)(struct tvec_output */*o*/,
+                const char */*msg*/, va_list */*ap*/);
+       /* Report a miscellaneous message.  The driver should ideally report
+        * the filename (@infile@) and line number (@lno@) prompting the
+        * error.
+        */
+
+  void (*destroy)(struct tvec_output */*o*/);
+       /* Release any resources acquired by the driver. */
+};
+
 /*----- Test descriptions -------------------------------------------------*/
 
 typedef void tvec_testfn(const struct tvec_reg */*in*/,
@@ -342,9 +398,10 @@ struct tvec_env {
 
   int (*setup)(struct tvec_state */*tv*/, const struct tvec_env */*env*/,
               void */*pctx*/, void */*ctx*/);
-    /* Initialize the context; called at the start of a test group.  Return
-     * zero on success, or @-1@ on failure.  If setup fails, the context is
-     * freed, and the test group is skipped.
+    /* Initialize the context; called at the start of a test group; @pctx@ is
+     * null for environments called by the core, but may be non-null for
+     * subordinate environments.  Return zero on success, or @-1@ on failure.
+     * If setup fails, the context is freed, and the test group is skipped.
      */
 
   int (*set)(struct tvec_state */*tv*/, const char */*var*/,
@@ -402,55 +459,328 @@ enum {
   TVRD_EXPECT                          /* mismatching input register */
 };
 
-/* --- @tvec_skipgroup@, @tvec_skipgroup_v@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @const char *excuse@, @va_list *ap@ = reason why skipped
- *
- * Returns:    ---
- *
- * Use:                Skip the current group.  This should only be called from a
- *             test environment @setup@ function; a similar effect occurs if
- *             the @setup@ function fails.
- */
-
-extern void PRINTF_LIKE(2, 3)
-  tvec_skipgroup(struct tvec_state */*tv*/, const char */*excuse*/, ...);
-extern void tvec_skipgroup_v(struct tvec_state */*tv*/,
-                            const char */*excuse*/, va_list */*ap*/);
-
-/* --- @tvec_skip@, @tvec_skip_v@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @const char *excuse@, @va_list *ap@ = reason why test skipped
- *
- * Returns:    ---
- *
- * Use:                Skip the current test.  This should only be called from a
- *             test environment @run@ function; a similar effect occurs if
- *             the @before@ function fails.
- */
+/*----- Register types ----------------------------------------------------*/
 
-extern void PRINTF_LIKE(2, 3)
-  tvec_skip(struct tvec_state */*tv*/, const char */*excuse*/, ...);
-extern void tvec_skip_v(struct tvec_state */*tv*/,
-                       const char */*excuse*/, va_list */*ap*/);
+struct tvec_regty {
+  /* A register type. */
 
-/* --- @tvec_resetoutputs@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *
- * Returns:    ---
- *
- * Use:                Reset (releases and reinitializes) the output registers in
- *             the test state.  This is mostly of use to test environment
- *             @run@ functions, between invocations of the test function.
- */
+  void (*init)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/);
+       /* Initialize the value in @*rv@.  This will be called before any
+        * other function acting on the value, including @release@.
+        */
 
-extern void tvec_resetoutputs(struct tvec_state */*tv*/);
+  void (*release)(union tvec_regval */*rv*/,
+                 const struct tvec_regdef */*rd*/);
+       /* Release any resources associated with the value in @*rv@. */
 
-/* --- @tvec_checkregs@ --- *
- *
+  int (*eq)(const union tvec_regval */*rv0*/,
+           const union tvec_regval */*rv1*/,
+           const struct tvec_regdef */*rd*/);
+       /* Return nonzero if @*rv0@ and @*rv1@ are equal values.
+        * Asymmetric criteria are permitted: @tvec_checkregs@ calls @eq@
+        * with the input register as @rv0@ and the output as @rv1@.
+        */
+
+  int (*tobuf)(buf */*b*/, const union tvec_regval */*rv*/,
+              const struct tvec_regdef */*rd*/);
+       /* Serialize the value @*rv@, writing the result to @b@.  Return
+        * zero on success, or @-1@ on error.
+        */
+
+  int (*frombuf)(buf */*b*/, union tvec_regval */*rv*/,
+                const struct tvec_regdef */*rd*/);
+       /* Deserialize a value from @b@, storing it in @*rv@.  Return zero on
+        * success, or @-1@ on error.
+        */
+
+  int (*parse)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/,
+              struct tvec_state */*tv*/);
+       /* Parse a value from @tv->fp@, storing it in @*rv@.  Return zero on
+        * success, or @-1@ on error, having reported one or more errors via
+        * @tvec_error@ or @tvec_syntax@.  A successful return should leave
+        * the input position at the start of the next line; the caller will
+        * flush the remainder of the line itself.
+        */
+
+  void (*dump)(const union tvec_regval */*rv*/,
+              const struct tvec_regdef */*rd*/,
+              unsigned /*style*/,
+              const struct gprintf_ops */*gops*/, void */*go*/);
+#define TVSF_COMPACT 1u
+       /* Write a human-readable representation of the value @*rv@ using
+        * @gprintf@ on @gops@ and @go@.  The @style@ is a collection of
+        * flags: if @TVSF_COMPACT@ is set, then output should be minimal,
+        * and must fit on a single line; otherwise, output may consist of
+        * multiple lines and may contain redundant information if that is
+        * likely to be useful to a human reader.
+        */
+};
+
+/*----- Session lifecycle -------------------------------------------------*/
+
+/* Here's the overall flow for a testing session.
+ *
+ * @tvec_begin@
+ *                     -> output @bsession@
+ * @tvec_read@
+ *                     -> output @bgroup@
+ *                     -> env @setup@
+ *   one or more tests
+ *                     -> type @init@ (input and output)
+ *                     -> type @parse@ (input)
+ *                     -> output @btest@
+ *                     -> env @before@
+ *                     -> env @run@
+ *                             -> @tvec_skip@
+ *                                     -> output @skip@
+ *                             -> test @fn@
+ *                             -> @tvec_checkregs@
+ *                                     -> type @eq@
+ *                             -> @tvec_fail@
+ *                                     -> output @fail@
+ *                             -> @tvec_mismatch@
+ *                                     -> output @dumpreg@
+ *                                             -> type @dump@
+ *                     -> env @after@
+ *                     -> output @etest@
+ *   or
+ *                     -> output @skipgroup@
+ *   finally
+ *                     -> output @egroup@
+ *                     -> env @teardown@
+ *
+ * @tvec_adhoc@
+ *   @tvec_begingroup@
+ *                     -> output @bgroup@
+ *                     -> env @setup@
+ *     @tvec_begintest@
+ *                     -> output @btest@
+ *     @tvec_skip@
+ *                     -> output @skip@
+ *     @tvec_claimeq@
+ *                     -> @tvec_fail@
+ *                             -> output @fail@
+ *                     -> @tvec_mismatch@
+ *                             -> output @dumpreg@
+ *                             -> type @dump@
+ *     @tvec_endtest@
+ *                     -> output @etest@
+ *     or @tvec_skipgroup@
+ *                     -> output @skipgroup@
+ * @tvec_endgroup@
+ *                     -> output @egroup@
+ *
+ * @tvec_end@
+ *                     -> output @esession@
+ *                     -> output @destroy@
+ *
+ * @tvec_benchrun@
+ *                     -> type @dump@ (compact style)
+ *                     -> output @bbench@
+ *                     -> subenv @run@
+ *                             -> test @fn@
+ *                     -> output @ebench@
+ *                             -> @tvec_benchreport@
+ *
+ * The output functions @error@ and @notice@ can be called at arbitrary
+ * times.
+ */
+
+/* --- @tvec_begin@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv_out@ = state structure to fill in
+ *             @const struct tvec_config *config@ = test configuration
+ *             @struct tvec_output *o@ = output driver
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize a state structure ready to do some testing.
+ */
+
+extern void tvec_begin(struct tvec_state */*tv_out*/,
+                      const struct tvec_config */*config*/,
+                      struct tvec_output */*o*/);
+
+/* --- @tvec_end@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    A proposed exit code.
+ *
+ * Use:                Conclude testing and suggests an exit code to be returned to
+ *             the calling program.  (The exit code comes from the output
+ *             driver's @esession@ method.)
+ */
+
+extern int tvec_end(struct tvec_state */*tv*/);
+
+/* --- @tvec_read@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *infile@ = the name of the input file
+ *             @FILE *fp@ = stream to read from
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Read test vector data from @fp@ and exercise test functions.
+ *             THe return code doesn't indicate test failures: it's only
+ *             concerned with whether there were problems with the input
+ *             file or with actually running the tests.
+ */
+
+extern int tvec_read(struct tvec_state */*tv*/,
+                    const char */*infile*/, FILE */*fp*/);
+
+/*----- Command-line interface --------------------------------------------*/
+
+extern const struct tvec_config tvec_adhocconfig;
+/* A special @struct tvec_config@ to use for programs which perform ad-hoc
+ * testing.
+ */
+
+/* --- @tvec_parseargs@ --- *
+ *
+ * Arguments:  @int argc@ = number of command-line arguments
+ *             @char *argv[]@ = vector of argument strings
+ *             @struct tvec_state *tv_out@ = test vector state to initialize
+ *             @int *argpos_out@ = where to leave unread argument index
+ *             @const struct tvec_config *cofig@ = test vector configuration
+ *
+ * Returns:    ---
+ *
+ * Use:                Parse arguments and set up the test vector state @*tv_out@.
+ *             If errors occur, print messages to standard error and exit
+ *             with status 2.
+ */
+
+extern void tvec_parseargs(int /*argc*/, char */*argv*/[],
+                          struct tvec_state */*tv_out*/,
+                          int */*argpos_out*/,
+                          const struct tvec_config */*config*/);
+
+/* --- @tvec_readstdin@, @tvec_readfile@, @tvec_readarg@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *file@ = pathname of file to read
+ *             @const char *arg@ = argument to interpret
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Read test vector data from stdin or a named file.  The
+ *             @tvec_readarg@ function reads from stdin if @arg@ is `%|-|%',
+ *             and from the named file otherwise.
+ */
+
+extern int tvec_readstdin(struct tvec_state */*tv*/);
+extern int tvec_readfile(struct tvec_state */*tv*/, const char */*file*/);
+extern int tvec_readarg(struct tvec_state */*tv*/, const char */*arg*/);
+
+/* --- @tvec_readdflt@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *dflt@ = defsault filename or null
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Reads from the default test vector data.  If @file@ is null,
+ *             then read from standard input, unless that's a terminal; if
+ *             @file@ is not null, then read the named file, looking in the
+ *             directory named by the `%|srcdir|%' environment variable if
+ *             that's set, or otherwise in the current directory.
+ */
+
+extern int tvec_readdflt(struct tvec_state */*tv*/, const char */*file*/);
+
+/* --- @tvec_readargs@ --- *
+ *
+ * Arguments:  @int argc@ = number of command-line arguments
+ *             @char *argv[]@ = vector of argument strings
+ *             @struct tvec_state *tv@ = test vector state
+ *             @int *argpos_inout@ = current argument position (updated)
+ *             @const char *dflt@ = default filename or null
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Reads from the sources indicated by the command-line
+ *             arguments, in order, interpreting each as for @tvec_readarg@;
+ *             if no arguments are given then read from @dflt@ as for
+ *             @tvec_readdflt@.
+ */
+
+extern int tvec_readargs(int /*argc*/, char */*argv*/[],
+                        struct tvec_state */*tv*/,
+                        int */*argpos_inout*/, const char */*dflt*/);
+
+/* --- @tvec_main@ --- *
+ *
+ * Arguments:  @int argc@ = number of command-line arguments
+ *             @char *argv[]@ = vector of argument strings
+ *             @const struct tvec_config *cofig@ = test vector configuration
+ *             @const char *dflt@ = default filename or null
+ *
+ * Returns:    Exit code.
+ *
+ * Use:                All-in-one test vector front-end.  Parse options from the
+ *             command-line as for @tvec_parseargs@, and then process the
+ *             remaining positional arguments as for @tvec_readargs@.  The
+ *             function constructs and disposes of a test vector state.
+ */
+
+extern int tvec_main(int /*argc*/, char */*argv*/[],
+                    const struct tvec_config */*config*/,
+                    const char */*dflt*/);
+
+/*----- Test processing ---------------------------------------------------*/
+
+/* --- @tvec_skipgroup@, @tvec_skipgroup_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *excuse@, @va_list *ap@ = reason why skipped
+ *
+ * Returns:    ---
+ *
+ * Use:                Skip the current group.  This should only be called from a
+ *             test environment @setup@ function; a similar effect occurs if
+ *             the @setup@ function fails.
+ */
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_skipgroup(struct tvec_state */*tv*/, const char */*excuse*/, ...);
+extern void tvec_skipgroup_v(struct tvec_state */*tv*/,
+                            const char */*excuse*/, va_list */*ap*/);
+
+/* --- @tvec_skip@, @tvec_skip_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *excuse@, @va_list *ap@ = reason why test skipped
+ *
+ * Returns:    ---
+ *
+ * Use:                Skip the current test.  This should only be called from a
+ *             test environment @run@ function; a similar effect occurs if
+ *             the @before@ function fails.
+ */
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_skip(struct tvec_state */*tv*/, const char */*excuse*/, ...);
+extern void tvec_skip_v(struct tvec_state */*tv*/,
+                       const char */*excuse*/, va_list */*ap*/);
+
+/* --- @tvec_resetoutputs@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Reset (releases and reinitializes) the output registers in
+ *             the test state.  This is mostly of use to test environment
+ *             @run@ functions, between invocations of the test function.
+ */
+
+extern void tvec_resetoutputs(struct tvec_state */*tv*/);
+
+/* --- @tvec_checkregs@ --- *
+ *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
  *
  * Returns:    Zero on success, @-1@ on mismatch.
@@ -548,239 +878,53 @@ extern void PRINTF_LIKE(2, 3)
 extern void tvec_check_v(struct tvec_state */*tv*/,
                         const char */*detail*/, va_list */*ap*/);
 
-/*----- Session lifecycle -------------------------------------------------*/
-
-struct tvec_config {
-  /* An overall test configuration. */
-
-  const struct tvec_test *tests;       /* the tests to be performed */
-  unsigned nrout, nreg;                        /* number of output/total regs */
-  size_t regsz;                                /* size of a register */
-};
+/*----- Ad-hoc testing ----------------------------------------------------*/
 
-/* --- @tvec_begin@ --- *
+/* --- @tvec_adhoc@ --- *
  *
- * Arguments:  @struct tvec_state *tv_out@ = state structure to fill in
- *             @const struct tvec_config *config@ = test configuration
- *             @struct tvec_output *o@ = output driver
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct tvec_test *t@ = space for a test definition
  *
  * Returns:    ---
  *
- * Use:                Initialize a state structure ready to do some testing.
+ * Use:                Begin ad-hoc testing, i.e., without reading a file of
+ *             test-vector data.
+ *
+ *             The structure at @t@ will be used to record information about
+ *             the tests underway, which would normally come from a static
+ *             test definition.  The other functions in this section assume
+ *             that @tvec_adhoc@ has been called.
  */
 
-extern void tvec_begin(struct tvec_state */*tv_out*/,
-                      const struct tvec_config */*config*/,
-                      struct tvec_output */*o*/);
+extern void tvec_adhoc(struct tvec_state */*tv*/, struct tvec_test */*t*/);
 
-/* --- @tvec_end@ --- *
+/* --- @tvec_begingroup@, @TVEC_BEGINGROUP@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *name@ = name for this test group
+ *             @const char *file@, @unsigned @lno@ = calling file and line
  *
- * Returns:    A proposed exit code.
+ * Returns:    ---
  *
- * Use:                Conclude testing and suggests an exit code to be returned to
- *             the calling program.  (The exit code comes from the output
- *             driver's @esession@ method.)
+ * Use:                Begin an ad-hoc test group with the given name.  The @file@
+ *             and @lno@ can be anything, but it's usually best if they
+ *             refer to the source code performing the test: the macro
+ *             @TVEC_BEGINGROUP@ does this automatically.
  */
 
-extern int tvec_end(struct tvec_state */*tv*/);
+extern void tvec_begingroup(struct tvec_state */*tv*/, const char */*name*/,
+                           const char */*file*/, unsigned /*lno*/);
+#define TVEC_BEGINGROUP(tv, name)                                      \
+       do tvec_begingroup(tv, name, __FILE__, __LINE__); while (0)
 
-/* --- @tvec_read@ --- *
+/* --- @tvec_endgroup@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @const char *infile@ = the name of the input file
- *             @FILE *fp@ = stream to read from
  *
- * Returns:    Zero on success, @-1@ on error.
+ * Returns:    ---
  *
- * Use:                Read test vector data from @fp@ and exercise test functions.
- *             THe return code doesn't indicate test failures: it's only
- *             concerned with whether there were problems with the input
- *             file or with actually running the tests.
- */
-
-extern int tvec_read(struct tvec_state */*tv*/,
-                    const char */*infile*/, FILE */*fp*/);
-
-/*----- Input utilities ---------------------------------------------------*/
-
-/* These are provided by the core for the benefit of type @parse@ methods,
- * and test-environment @set@ functions, which get to read from the test
- * input file.  The latter are usually best implemented by calling on the
- * former.
- *
- * The two main rules are as follows.
- *
- *   * Leave the file position at the beginning of the line following
- *     whatever it was that you read.
- *
- *   * When you read and consume a newline (which you do at least once, by
- *     the previous rule), then increment @tv->lno@ to keep track of the
- *     current line number.
- */
-
-/* --- @tvec_skipspc@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *
- * Returns:    ---
- *
- * Use:                Advance over any whitespace characters other than newlines.
- *             This will stop at `;', end-of-file, or any other kind of
- *             non-whitespace; and it won't consume a newline.
- */
-
-extern void tvec_skipspc(struct tvec_state */*tv*/);
-
-/* --- @tvec_syntax@, @tvec_syntax_v@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @int ch@ = the character found (in @fgetc@ format)
- *             @const char *expect@, @va_list ap@ = what was expected
- *
- * Returns:    @-1@
- *
- * Use:                Report a syntax error quoting @ch@ and @expect@.  If @ch@ is
- *             a newline, then back up so that it can be read again (e.g.,
- *             by @tvec_flushtoeol@ or @tvec_nexttoken@, which will also
- *             advance the line number).
- */
-
-extern int PRINTF_LIKE(3, 4)
-  tvec_syntax(struct tvec_state */*tv*/, int /*ch*/,
-             const char */*expect*/, ...);
-extern int tvec_syntax_v(struct tvec_state */*tv*/, int /*ch*/,
-                        const char */*expect*/, va_list */*ap*/);
-
-/* --- @tvec_flushtoeol@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @unsigned f@ = flags (@TVFF_...@)
- *
- * Returns:    Zero on success, @-1@ on error.
- *
- * Use:                Advance to the start of the next line, consuming the
- *             preceding newline.
- *
- *             A syntax error is reported if no newline character is found,
- *             i.e., the file ends in mid-line.  A syntax error is also
- *             reported if material other than whitespace or a comment is
- *             found before the end of the line end, and @TVFF_ALLOWANY@ is
- *             not set in @f@.  The line number count is updated
- *             appropriately.
- */
-
-#define TVFF_ALLOWANY 1u
-extern int tvec_flushtoeol(struct tvec_state */*tv*/, unsigned /*f*/);
-
-/* --- @tvec_nexttoken@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *
- * Returns:    Zero if there is a next token which can be read; @-1@ if no
- *             token is available.
- *
- * Use:                Advance to the next whitespace-separated token, which may be
- *             on the next line.
- *
- *             Tokens are separated by non-newline whitespace, comments, and
- *             newlines followed by whitespace; a newline /not/ followed by
- *             whitespace instead begins the next assignment, and two
- *             newlines separated only by whitespace terminate the data for
- *             a test.
- *
- *             If this function returns zero, then the next character in the
- *             file begins a suitable token which can be read and
- *             processed.  If it returns @-1@ then there is no such token,
- *             and the file position is left correctly.  The line number
- *             count is updated appropriately.
- */
-
-extern int tvec_nexttoken(struct tvec_state */*tv*/);
-
-/* --- @tvec_readword@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @dstr *d@ = string to append the word to
- *             @const char *delims@ = additional delimiters to stop at
- *             @const char *expect@, @va_list ap@ = what was expected
- *
- * Returns:    Zero on success, @-1@ on failure.
- *
- * Use:                A `word' consists of characters other than whitespace, null
- *             characters, and other than those listed in @delims@;
- *             furthermore, a word does not begin with a `;'.  (If you want
- *             reading to stop at comments not preceded by whitespace, then
- *             include `;' in @delims@.  This is a common behaviour.)
- *
- *             If there is no word beginning at the current file position,
- *             then return @-1@; furthermore, if @expect@ is not null, then
- *             report an appropriate error via @tvec_syntax@.
- *
- *             Otherwise, the word is accumulated in @d@ and zero is
- *             returned; if @d@ was not empty at the start of the call, the
- *             newly read word is separated from the existing material by a
- *             single space character.  Since null bytes are never valid
- *             word constituents, a null terminator is written to @d@, and
- *             it is safe to treat the string in @d@ as being null-
- *             terminated.
- */
-
-extern int PRINTF_LIKE(4, 5)
-  tvec_readword(struct tvec_state */*tv*/, dstr */*d*/,
-               const char */*delims*/, const char */*expect*/, ...);
-extern int tvec_readword_v(struct tvec_state */*tv*/, dstr */*d*/,
-                          const char */*delims*/, const char */*expect*/,
-                          va_list */*ap*/);
-
-/*----- Ad-hoc testing ----------------------------------------------------*/
-
-/* --- @tvec_adhoc@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @struct tvec_test *t@ = space for a test definition
- *
- * Returns:    ---
- *
- * Use:                Begin ad-hoc testing, i.e., without reading a file of
- *             test-vector data.
- *
- *             The structure at @t@ will be used to record information about
- *             the tests underway, which would normally come from a static
- *             test definition.  The other functions in this section assume
- *             that @tvec_adhoc@ has been called.
- */
-
-extern void tvec_adhoc(struct tvec_state */*tv*/, struct tvec_test */*t*/);
-
-/* --- @tvec_begingroup@, @TVEC_BEGINGROUP@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *             @const char *name@ = name for this test group
- *             @const char *file@, @unsigned @lno@ = calling file and line
- *
- * Returns:    ---
- *
- * Use:                Begin an ad-hoc test group with the given name.  The @file@
- *             and @lno@ can be anything, but it's usually best if they
- *             refer to the source code performing the test: the macro
- *             @TVEC_BEGINGROUP@ does this automatically.
- */
-
-extern void tvec_begingroup(struct tvec_state */*tv*/, const char */*name*/,
-                           const char */*file*/, unsigned /*lno*/);
-#define TVEC_BEGINGROUP(tv, name)                                      \
-       do tvec_begingroup(tv, name, __FILE__, __LINE__); while (0)
-
-/* --- @tvec_endgroup@ --- *
- *
- * Arguments:  @struct tvec_state *tv@ = test-vector state
- *
- * Returns:    ---
- *
- * Use:                End an ad-hoc test group.  The statistics are updated and the
- *             outcome is reported to the output formatter.
+ * Use:                End an ad-hoc test group.  The statistics are updated and the
+ *             outcome is reported to the output formatter.
  */
 
 extern void tvec_endgroup(struct tvec_state */*tv*/);
@@ -925,126 +1069,154 @@ extern int tvec_claimeq(struct tvec_state */*tv*/,
                        const char */*file*/, unsigned /*lno*/,
                        const char */*expr*/);
 
-/*----- Output formatting -------------------------------------------------*/
+/*----- Benchmarking ------------------------------------------------------*/
 
-struct tvec_output {
-  /* An output formatter. */
-  const struct tvec_outops *ops;       /* pointer to operations */
+struct tvec_bench {
+  struct tvec_env _env;                        /* benchmarking is an environment */
+  struct bench_state **bst;            /* benchmark state anchor or null */
+  unsigned long niter;                 /* iterations done per unit */
+  int riter, rbuf;                     /* iterations and buffer registers */
+  const struct tvec_env *env;          /* subordinate environment */
 };
+#define TVEC_BENCHENV                                                  \
+  { sizeof(struct tvec_benchctx),                                      \
+    tvec_benchsetup,                                                   \
+    tvec_benchset,                                                     \
+    tvec_benchbefore,                                                  \
+    tvec_benchrun,                                                     \
+    tvec_benchafter,                                                   \
+    tvec_benchteardown }
+#define TVEC_BENCHINIT TVEC_BENCHENV, &tvec_benchstate
 
-/* Benchmarking details. */
-enum {
-  TVBU_OP,                            /* counting operations of some kind */
-  TVBU_BYTE                            /* counting bytes (@RBUF >= 0@) */
+struct tvec_benchctx {
+  const struct tvec_bench *b;
+  struct bench_state *bst;
+  double dflt_target;
+  void *subctx;
 };
-struct bench_timing;                   /* forward declaration */
 
-struct tvec_outops {
-  /* Output operations. */
+extern struct bench_state *tvec_benchstate;
 
-  void (*bsession)(struct tvec_output */*o*/, struct tvec_state */*tv*/);
-       /* Begin a test session.  The output driver will probably want to
-        * save @tv@, because this isn't provided to any other methods.
-        */
+/* --- @tvec_benchsetup@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const struct tvec_env *env@ = environment description
+ *             @void *pctx@ = parent context (ignored)
+ *             @void *ctx@ = context pointer to initialize
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Initialize a benchmarking environment context.
+ *
+ *             The environment description must really be a @struct
+ *             tvec_bench@.  If the @bst@ slot is null, then a temporary
+ *             benchmark state is allocated for the current test group and
+ *             released at the end.  Otherwise, it must be the address of a
+ *             pointer to a benchmark state: if the pointer is null, then a
+ *             fresh state is allocated and initialized and the pointer is
+ *             updated; otherwise, the pointer is assumed to refer to an
+ *             existing valid benchmark state.
+ */
 
-  int (*esession)(struct tvec_output */*o*/);
-       /* End a session, and return the suggested exit code. */
+extern int tvec_benchsetup(struct tvec_state */*tv*/,
+                          const struct tvec_env */*env*/,
+                          void */*pctx*/, void */*ctx*/);
 
-  void (*bgroup)(struct tvec_output */*o*/);
-       /* Begin a test group.  The test group description is @tv->test@. */
+/* --- @tvec_benchset@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *var@ = variable name to set
+ *             @const struct tvec_env *env@ = environment description
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Set a special variable.  The following special variables are
+ *             supported.
+ *
+ *               * %|@target|% is the (approximate) number of seconds to run
+ *                 the benchmark.
+ *
+ *             Unrecognized variables are passed to the subordinate
+ *             environment, if there is one.
+ */
 
-  void (*skipgroup)(struct tvec_output */*o*/,
-                   const char */*excuse*/, va_list */*ap*/);
-       /* The group is being skipped; @excuse@ may be null or a format
-        * string explaining why.  The @egroup@ method will not be called
-        * separately.
-        */
+extern int tvec_benchset(struct tvec_state */*tv*/, const char */*var*/,
+                        const struct tvec_env */*env*/, void */*ctx*/);
 
-  void (*egroup)(struct tvec_output */*o*/);
-       /* End a test group.  At least one test was attempted or @skipgroup@
-        * would have been called instead.  If @tv->curr[TVOUT_LOSE]@ is
-        * nonzero then the test group as a whole failed; otherwise it
-        * passed.
-        */
+/* --- @tvec_benchbefore@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Invoke the subordinate environment's @before@ function to
+ *             prepare for the benchmark.
+ */
 
-  void (*btest)(struct tvec_output */*o*/);
-       /* Begin a test case. */
+extern int tvec_benchbefore(struct tvec_state */*tv*/, void */*ctx*/);
 
-  void (*skip)(struct tvec_output */*o*/,
-              const char */*excuse*/, va_list */*ap*/);
-       /* The test case is being skipped; @excuse@ may be null or a format
-        * string explaining why.  The @etest@ function will still be called
-        * (so this works differently from @skipgroup@ and @egroup@).  A test
-        * case can be skipped at most once, and, if skipped, it cannot fail.
-        */
+/* --- @tvec_benchrun@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @tvec_testfn *fn@ = test function to run
+ *             @void *ctx@ = context pointer for the test function
+ *
+ * Returns:    ---
+ *
+ * Use:                Measures and reports the performance of a test function.
+ *
+ *
+ */
 
-  void (*fail)(struct tvec_output */*o*/,
-              const char */*detail*/, va_list */*ap*/);
-       /* The test case failed.
-        *
-        * The output driver should preferably report the filename (@infile@)
-        * and line number (@test_lno@, not @lno@) for the failing test.
-        *
-        * The @detail@ may be null or a format string describing detail
-        * about how the failing test was run which can't be determined from
-        * the registers; a @detail@ is usually provided when (and only when)
-        * the test environment potentially invokes the test function more
-        * than once.
-        *
-        * A single test case can fail multiple times!
-        */
+extern void tvec_benchrun(struct tvec_state */*tv*/,
+                         tvec_testfn */*fn*/, void */*ctx*/);
 
-  void (*dumpreg)(struct tvec_output */*o*/,
-                 unsigned /*disp*/, const union tvec_regval */*rv*/,
-                 const struct tvec_regdef */*rd*/);
-       /* Dump a register.  The `disposition' @disp@ explains what condition
-        * the register is in; see @tvec_dumpreg@ and the @TVRD_...@ codes.
-        * The register value is at @rv@, and its definition, including its
-        * type, at @rd@.  Note that this function may be called on virtual
-        * registers which aren't in either of the register vectors or
       * mentioned by the test description.
       */
+/* --- @tvec_benchafter@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
+ * Use:                Invoke the subordinate environment's @after@ function to
*             clean up after the benchmark.
+ */
 
-  void (*etest)(struct tvec_output */*o*/, unsigned /*outcome*/);
-       /* The test case concluded with the given @outcome@ (one of the
-        * @TVOUT_...@ codes.
-        */
+extern void tvec_benchafter(struct tvec_state */*tv*/, void */*ctx*/);
 
-  void (*bbench)(struct tvec_output */*o*/,
-                const char */*ident*/, unsigned /*unit*/);
-       /* Begin a benchmark.  The @ident@ is a formatted string identifying
-        * the benchmark based on the values of the input registered marked
-        * @TVRF_ID@; the output driver is free to use this or come up with
-        * its own way to identify the test, e.g., by inspecting the register
-        * values for itself.  The @unit@ is one of the @TVBU_...@ constants
       * explaining what sort of thing is being measured.
       */
+/* --- @tvec_benchteardown@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    ---
+ *
* Use:                Tear down the benchmark environment.
+ */
 
-  void (*ebench)(struct tvec_output */*o*/,
-                const char */*ident*/, unsigned /*unit*/,
-                const struct bench_timing */*tm*/);
-       /* End a benchmark.  The @ident@ and @unit@ arguments are as for
-        * @bbench@.  If @tm@ is zero then the measurement failed; otherwise
-        * @tm->n@ counts total number of things (operations or bytes, as
-        * indicated by @unit@) processed in the indicated time.
-        */
+extern void tvec_benchteardown(struct tvec_state */*tv*/, void */*ctx*/);
 
-  void (*error)(struct tvec_output */*o*/,
-               const char */*msg*/, va_list */*ap*/);
-       /* Report an error.  The driver should ideally report the filename
-        * (@infile@) and line number (@lno@) prompting the error.
-        */
+/* --- @tvec_benchreport@ --- *
+ *
+ * Arguments:  @const struct gprintf_ops *gops@ = print operations
+ *             @void *go@ = print destination
+ *             @unsigned unit@ = the unit being measured (~TVBU_...@)
+ *             @const struct bench_timing *tm@ = the benchmark result
+ *
+ * Returns:    ---
+ *
+ * Use:                Formats a report about the benchmark performance.  This
+ *             function is intended to be called on by an output
+ *             @ebench@ function.
+ */
 
-  void (*notice)(struct tvec_output */*o*/,
-                const char */*msg*/, va_list */*ap*/);
-       /* Report a miscellaneous message.  The driver should ideally report
-        * the filename (@infile@) and line number (@lno@) prompting the
-        * error.
-        */
+extern void tvec_benchreport
+  (const struct gprintf_ops */*gops*/, void */*go*/,
+   unsigned /*unit*/, const struct bench_timing */*tm*/);
 
-  void (*destroy)(struct tvec_output */*o*/);
-       /* Release any resources acquired by the driver. */
-};
+/*----- Output functions --------------------------------------------------*/
 
 /* --- @tvec_error@, @tvec_error_v@ --- *
  *
@@ -1134,64 +1306,211 @@ extern struct tvec_output *tvec_tapoutput(FILE */*fp*/);
 
 extern struct tvec_output *tvec_dfltout(FILE */*fp*/);
 
-/*----- Register types ----------------------------------------------------*/
+/*------ Serialization utilities ------------------------------------------*/
 
-struct tvec_regty {
-  /* A register type. */
+/* --- @tvec_serialize@ --- *
+ *
+ * Arguments:  @const struct tvec_reg *rv@ = vector of registers
+ *             @buf *b@ = buffer to write on
+ *             @const struct tvec_regdef *regs@ = vector of register
+ *                     descriptions, terminated by an entry with a null
+ *                     @name@ slot
+ *             @unsigned nr@ = number of entries in the @rv@ vector
+ *             @size_t regsz@ = true size of each element of @rv@
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Serialize a collection of register values.
+ *
+ *             The `candidate register definitions' are those entries @r@ in
+ *             the @regs@ vector whose index @r.i@ is strictly less than
+ *             @nr@.  The `selected register definitions' are those
+ *             candidate register definitions @r@ for which the indicated
+ *             register @rv[r.i]@ has the @TVRF_LIVE@ flag set.  The
+ *             serialized output begins with a header bitmap: if there are
+ *             %$n$% candidate register definitions then the header bitmap
+ *             consists of %$\lceil n/8 \rceil$% bytes.  Bits are ordered
+ *             starting from the least significant bit of the first byte,
+ *             end ending at the most significant bit of the final byte.
+ *             The bit corresponding to a candidate register definition is
+ *             set if and only if that register defintion is selected.  The
+ *             header bitmap is then followed by the serializations of the
+ *             selected registers -- i.e., for each selected register
+ *             definition @r@, the serialized value of register @rv[r.i]@ --
+ *             simply concatenated together, with no padding or alignment.
+ *
+ *             The serialized output is written to the buffer @b@.  Failure
+ *             can be caused by running out of buffer space, or a failing
+ *             type handler.
+ */
 
-  void (*init)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/);
-       /* Initialize the value in @*rv@.  This will be called before any
-        * other function acting on the value, including @release@.
-        */
+extern int tvec_serialize(const struct tvec_reg */*rv*/, buf */*b*/,
+                         const struct tvec_regdef */*regs*/,
+                         unsigned /*nr*/, size_t /*regsz*/);
 
-  void (*release)(union tvec_regval */*rv*/,
-                 const struct tvec_regdef */*rd*/);
-       /* Release any resources associated with the value in @*rv@. */
+/* --- @tvec_deserialize@ --- *
+ *
+ * Arguments:  @struct tvec_reg *rv@ = vector of registers
+ *             @buf *b@ = buffer to write on
+ *             @const struct tvec_regdef *regs@ = vector of register
+ *                     descriptions, terminated by an entry with a null
+ *                     @name@ slot
+ *             @unsigned nr@ = number of entries in the @rv@ vector
+ *             @size_t regsz@ = true size of each element of @rv@
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                Deserialize a collection of register values.
+ *
+ *             The size of the register vector @nr@ and the register
+ *             definitions @regs@ must match those used when producing the
+ *             serialization.  For each serialized register value,
+ *             deserialize and store the value into the appropriate register
+ *             slot, and set the @TVRF_LIVE@ flag on the register.  See
+ *             @tvec_serialize@ for a description of the format.
+ *
+ *             On successful completion, store the address of the first byte
+ *             after the serialized data in @*end_out@ if @end_out@ is not
+ *             null.  Failure results only from a failing register type
+ *             handler.
+ */
 
-  int (*eq)(const union tvec_regval */*rv0*/,
-           const union tvec_regval */*rv1*/,
-           const struct tvec_regdef */*rd*/);
-       /* Return nonzero if @*rv0@ and @*rv1@ are equal values.
-        * Asymmetric criteria are permitted: @tvec_checkregs@ calls @eq@
-        * with the input register as @rv0@ and the output as @rv1@.
-        */
+extern int tvec_deserialize(struct tvec_reg */*rv*/, buf */*b*/,
+                           const struct tvec_regdef */*regs*/,
+                           unsigned /*nr*/, size_t /*regsz*/);
 
-  int (*tobuf)(buf */*b*/, const union tvec_regval */*rv*/,
-              const struct tvec_regdef */*rd*/);
-       /* Serialize the value @*rv@, writing the result to @b@.  Return
-        * zero on success, or @-1@ on error.
-        */
+/*----- Input utilities ---------------------------------------------------*/
+
+/* These are provided by the core for the benefit of type @parse@ methods,
+ * and test-environment @set@ functions, which get to read from the test
+ * input file.  The latter are usually best implemented by calling on the
+ * former.
+ *
+ * The two main rules are as follows.
+ *
+ *   * Leave the file position at the beginning of the line following
+ *     whatever it was that you read.
+ *
+ *   * When you read and consume a newline (which you do at least once, by
+ *     the previous rule), then increment @tv->lno@ to keep track of the
+ *     current line number.
+ */
+
+/* --- @tvec_skipspc@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Advance over any whitespace characters other than newlines.
+ *             This will stop at `;', end-of-file, or any other kind of
+ *             non-whitespace; and it won't consume a newline.
+ */
+
+extern void tvec_skipspc(struct tvec_state */*tv*/);
+
+/* --- @tvec_syntax@, @tvec_syntax_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @int ch@ = the character found (in @fgetc@ format)
+ *             @const char *expect@, @va_list ap@ = what was expected
+ *
+ * Returns:    @-1@
+ *
+ * Use:                Report a syntax error quoting @ch@ and @expect@.  If @ch@ is
+ *             a newline, then back up so that it can be read again (e.g.,
+ *             by @tvec_flushtoeol@ or @tvec_nexttoken@, which will also
+ *             advance the line number).
+ */
+
+extern int PRINTF_LIKE(3, 4)
+  tvec_syntax(struct tvec_state */*tv*/, int /*ch*/,
+             const char */*expect*/, ...);
+extern int tvec_syntax_v(struct tvec_state */*tv*/, int /*ch*/,
+                        const char */*expect*/, va_list */*ap*/);
+
+/* --- @tvec_flushtoeol@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned f@ = flags (@TVFF_...@)
+ *
+ * Returns:    Zero on success, @-1@ on error.
+ *
+ * Use:                Advance to the start of the next line, consuming the
+ *             preceding newline.
+ *
+ *             A syntax error is reported if no newline character is found,
+ *             i.e., the file ends in mid-line.  A syntax error is also
+ *             reported if material other than whitespace or a comment is
+ *             found before the end of the line end, and @TVFF_ALLOWANY@ is
+ *             not set in @f@.  The line number count is updated
+ *             appropriately.
+ */
+
+#define TVFF_ALLOWANY 1u
+extern int tvec_flushtoeol(struct tvec_state */*tv*/, unsigned /*f*/);
+
+/* --- @tvec_nexttoken@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero if there is a next token which can be read; @-1@ if no
+ *             token is available.
+ *
+ * Use:                Advance to the next whitespace-separated token, which may be
+ *             on the next line.
+ *
+ *             Tokens are separated by non-newline whitespace, comments, and
+ *             newlines followed by whitespace; a newline /not/ followed by
+ *             whitespace instead begins the next assignment, and two
+ *             newlines separated only by whitespace terminate the data for
+ *             a test.
+ *
+ *             If this function returns zero, then the next character in the
+ *             file begins a suitable token which can be read and
+ *             processed.  If it returns @-1@ then there is no such token,
+ *             and the file position is left correctly.  The line number
+ *             count is updated appropriately.
+ */
 
-  int (*frombuf)(buf */*b*/, union tvec_regval */*rv*/,
-                const struct tvec_regdef */*rd*/);
-       /* Deserialize a value from @b@, storing it in @*rv@.  Return zero on
-        * success, or @-1@ on error.
-        */
+extern int tvec_nexttoken(struct tvec_state */*tv*/);
 
-  int (*parse)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/,
-              struct tvec_state */*tv*/);
-       /* Parse a value from @tv->fp@, storing it in @*rv@.  Return zero on
-        * success, or @-1@ on error, having reported one or more errors via
-        * @tvec_error@ or @tvec_syntax@.  A successful return should leave
-        * the input position at the start of the next line; the caller will
-        * flush the remainder of the line itself.
-        */
+/* --- @tvec_readword@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @dstr *d@ = string to append the word to
+ *             @const char *delims@ = additional delimiters to stop at
+ *             @const char *expect@, @va_list ap@ = what was expected
+ *
+ * Returns:    Zero on success, @-1@ on failure.
+ *
+ * Use:                A `word' consists of characters other than whitespace, null
+ *             characters, and other than those listed in @delims@;
+ *             furthermore, a word does not begin with a `;'.  (If you want
+ *             reading to stop at comments not preceded by whitespace, then
+ *             include `;' in @delims@.  This is a common behaviour.)
+ *
+ *             If there is no word beginning at the current file position,
+ *             then return @-1@; furthermore, if @expect@ is not null, then
+ *             report an appropriate error via @tvec_syntax@.
+ *
+ *             Otherwise, the word is accumulated in @d@ and zero is
+ *             returned; if @d@ was not empty at the start of the call, the
+ *             newly read word is separated from the existing material by a
+ *             single space character.  Since null bytes are never valid
+ *             word constituents, a null terminator is written to @d@, and
+ *             it is safe to treat the string in @d@ as being null-
+ *             terminated.
+ */
 
-  void (*dump)(const union tvec_regval */*rv*/,
-              const struct tvec_regdef */*rd*/,
-              unsigned /*style*/,
-              const struct gprintf_ops */*gops*/, void */*go*/);
-#define TVSF_COMPACT 1u
-       /* Write a human-readable representation of the value @*rv@ using
-        * @gprintf@ on @gops@ and @go@.  The @style@ is a collection of
-        * flags: if @TVSF_COMPACT@ is set, then output should be minimal,
-        * and must fit on a single line; otherwise, output may consist of
-        * multiple lines and may contain redundant information if that is
-        * likely to be useful to a human reader.
-        */
-};
+extern int PRINTF_LIKE(4, 5)
+  tvec_readword(struct tvec_state */*tv*/, dstr */*d*/,
+               const char */*delims*/, const char */*expect*/, ...);
+extern int tvec_readword_v(struct tvec_state */*tv*/, dstr */*d*/,
+                          const char */*delims*/, const char */*expect*/,
+                          va_list */*ap*/);
 
-/*----- Integer types: signed and unsigned ------------------------------- */
+/*----- Integer types: signed and unsigned --------------------------------*/
 
 /* Integers may be input in decimal, hex, binary, or octal, following
  * approximately usual conventions.
@@ -1233,7 +1552,7 @@ extern const struct tvec_urange
   tvrange_uchar, tvrange_ushort, tvrange_uint, tvrange_ulong, tvrange_size,
   tvrange_byte, tvrange_u16, tvrange_u32;
 
-/* --- tvec_claimeq_int@, @TVEC_CLAIMEQ_INT@ --- *
+/* --- @tvec_claimeq_int@, @TVEC_CLAIMEQ_INT@ --- *
  *
  * Arguments:  @struct tvec_state *tv@ = test-vector state
  *             @long i0, i1@ = two signed integers
@@ -1399,10 +1718,22 @@ extern int tvec_claimeq_float(struct tvec_state */*tv*/,
 
 /*----- Enumerated types --------------------------------------------------*/
 
-/* There is a distinct enumerated type for each of the branches of
+/* An enumeration describes a set of values of some underlying type, each of
+ * which has a symbolic name.  Values outside of the defined set can occur --
+ * on output, because of bugs in the tested code, or on input to test
+ * handling of unexpected values.
+ *
+ * There is a distinct enumerated type for each of the branches of
  * @tvec_misc@.  In the following, we write @t@ for the type code, which is
  * @i@ for signed integer, @u@ for unsigned integer, @f@ for floating-point,
  * and @p@ for pointer.
+ *
+ * On input, 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.  On output, names are preferred (with the underlying value given
+ * in a comment).
  */
 
 #define DEFENUMTY(tag, ty, slot)                                       \
@@ -1416,6 +1747,8 @@ TVEC_MISCSLOTS(DEFENUMTY)
 TVEC_MISCSLOTS(DEFASSOC)
 #undef DEFASSOC
 
+#define TVEC_ENDENUM { 0, 0 }
+
 /* Information about an enumerated type. */
 #define DEFINFO(tag, ty, slot)                                         \
        struct tvec_##slot##enuminfo {                                  \
@@ -1450,6 +1783,7 @@ const struct tvec_ienuminfo tvenum_bool;
 /* --- @tvec_claimeq_tenum@, @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
@@ -1460,13 +1794,13 @@ const struct tvec_ienuminfo tvenum_bool;
  *             @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.
+ *             mismatched values are dumped: @t0@ is printed as the output
+ *             value and @t1@ is printed as the input reference.
  *
  *             The @TVEC_CLAIM_TENUM@ macro is similar, only it (a)
  *             identifies the file and line number of the call site
  *             automatically, and (b) implicitly quotes the source text of
- *             the @u0@ and @u1@ arguments in the failure message.
+ *             the @t0@ and @t1@ arguments in the failure message.
  */
 
 #define DECLCLAIM(tag, ty, slot)                                       \
@@ -1490,7 +1824,26 @@ TVEC_MISCSLOTS(DECLCLAIM)
        (tvec_claimeq_penum(tv, ei, p0, p1,                             \
                            __FILE__, __LINE__, #p0 " /= " #p1))
 
-/*----- Flags type -------------------------------------------------------*/
+/*----- Flags type --------------------------------------------------------*/
+
+/* A flags value packs a number of fields into a single nonnegative integer.
+ * Symbolic names are associated with the possible values of the various
+ * fields; more precisely, each name is associated with a value and a
+ * covering bitmask.
+ *
+ * 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.
+ *
+ * On 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.
+ */
 
 extern const struct tvec_regty tvty_flags;
 
@@ -1498,20 +1851,46 @@ struct tvec_flag {
   /* Definition of a single flag or bitfield value.
    *
    * Each named setting comes with a value @v@ and a mask @m@; the mask
-   * should cover all of the value bits, i.e., @(v&~m) == 0@.  On input,
-   * exactly 
+   * should cover all of the value bits, i.e., @(v&~m) == 0@.
    */
 
   const char *tag;                     /* name */
   unsigned long m, v;                  /* mask and value */
 };
 
+#define TVEC_ENDFLAGS { 0, 0, 0 }
+
 struct tvec_flaginfo {
-  const char *name;
-  const struct tvec_flag *fv;
-  const struct tvec_urange *range;
+  /* Information about a flags type. */
+
+  const char *name;                    /* type name for diagnostics  */
+  const struct tvec_flag *fv;          /* name/mask/value mappings */
+  const struct tvec_urange *range;     /* permitted range for literals */
 };
 
+/* --- @tvec_claimeq_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.
+ *
+ *             The @TVEC_CLAIM_FLAGS@ macro is similar, only it (a)
+ *             identifies the file and line number of the call site
+ *             automatically, and (b) implicitly quotes the source text of
+ *             the @f0@ and @f1@ arguments in the failure message.
+ */
+
 extern int tvec_claimeq_flags(struct tvec_state */*tv*/,
                              const struct tvec_flaginfo */*fi*/,
                              unsigned long /*f0*/, unsigned long /*f1*/,
@@ -1521,7 +1900,89 @@ extern int tvec_claimeq_flags(struct tvec_state */*tv*/,
        (tvec_claimeq_flags(tv, fi, f0, f1,                             \
                            __FILE__, __LINE__, #f0 " /= " #f1))
 
+/*----- Character type ----------------------------------------------------*/
+
+/* A character value holds a character, as read by @fgetc@.  The special
+ * @EOF@ value can also be represented.
+ *
+ * On input, a character value can be given by 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.
+ */
+
 extern const struct tvec_regty tvty_char;
+
+/* --- @tvec_claimeq_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.
+ *
+ *             The @TVEC_CLAIM_CHAR@ macro is similar, only it (a)
+ *             identifies the file and line number of the call site
+ *             automatically, and (b) implicitly quotes the source text of
+ *             the @ch0@ and @ch1@ arguments in the failure message.
+ */
+
 extern int tvec_claimeq_char(struct tvec_state */*tv*/,
                             int /*ch0*/, int /*ch1*/,
                             const char */*file*/, unsigned /*lno*/,
@@ -1529,100 +1990,177 @@ extern int tvec_claimeq_char(struct tvec_state */*tv*/,
 #define TVEC_CLAIMEQ_CHAR(tv, c0, c1)                                  \
        (tvec_claimeq_char(tv, c0, c1, __FILE__, __LINE__, #c0 " /= " #c1))
 
+/*----- Text and binary string types --------------------------------------*/
+
+/* A string is a sequence of octets.  Text and binary strings differ
+ * primarily in presentation: text strings are shown as raw characters where
+ * possible; binary strings are shown as hex dumps with an auxiliary text
+ * display.
+ *
+ * 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.
+ *
+ * Either kind of string can contain internal nul characters.  A trailing nul
+ * is appended -- beyond the stated input length -- to input strings as a
+ * convenience to test functions.  Test functions may include such a nul
+ * character on output but this is not checked by the equality test.
+ *
+ * A @struct tvec_urange@ may be supplied as an argument: the length of the
+ * string (in bytes) will be checked against the permitted range.
+ */
+
 extern const struct tvec_regty tvty_string, tvty_bytes;
 
+/* --- @tvec_claimeq_string@, @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.
+ *
+ *             The @TVEC_CLAIM_STRING@ macro is similar, only it (a)
+ *             identifies the file and line number of the call site
+ *             automatically, and (b) implicitly quotes the source text of
+ *             the @ch0@ and @ch1@ arguments in the failure message.
+ */
+
 extern 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*/);
+#define TVEC_CLAIMEQ_STRING(tv, p0, sz0, p1, sz1)                      \
+       (tvec_claimeq_string(tv, p0, sz0, p1, sz1, __FILE__, __LINE__,  \
+                            #p0 "[" #sz0 "] /= " #p1 "[" #sz1 "]"))
+
+/* --- @tvec_claimeq_strz@, @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.  The macro is similarly like
+ *             @TVEC_CLAIMEQ_STRING@.
+ */
+
 extern int tvec_claimeq_strz(struct tvec_state */*tv*/,
                             const char */*p0*/, const char */*p1*/,
                             const char */*file*/, unsigned /*lno*/,
                             const char */*expr*/);
+#define TVEC_CLAIMEQ_STRZ(tv, p0, p1)                                  \
+       (tvec_claimeq_strz(tv, p0, p1, __FILE__, __LINE__, #p0 " /= " #p1))
+
+/* --- @tvec_claimeq_bytes@, @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.
+ *
+ *             The @TVEC_CLAIM_STRING@ macro is similar, only it (a)
+ *             identifies the file and line number of the call site
+ *             automatically, and (b) implicitly quotes the source text of
+ *             the @ch0@ and @ch1@ arguments in the failure message.
+ */
+
 extern int tvec_claimeq_bytes(struct tvec_state */*tv*/,
                               const void */*p0*/, size_t /*sz0*/,
                               const void */*p1*/, size_t /*sz1*/,
                               const char */*file*/, unsigned /*lno*/,
                               const char */*expr*/);
-#define TVEC_CLAIMEQ_STRING(tv, p0, sz0, p1, sz1)                      \
-       (tvec_claimeq_string(tv, p0, sz0, p1, sz1, __FILE__, __LINE__,  \
-                            #p0 "[" #sz0 "] /= " #p1 "[" #sz1 "]"))
-#define TVEC_CLAIMEQ_STRZ(tv, p0, p1)                                  \
-       (tvec_claimeq_strz(tv, p0, p1, __FILE__, __LINE__, #p0 " /= " #p1))
 #define TVEC_CLAIMEQ_BYTES(tv, p0, sz0, p1, sz1)                       \
        (tvec_claimeq(tv, p0, sz0, p1, sz1, __FILE__, __LINE__,         \
                      #p0 "[" #sz0 "] /= " #p1 "[" #sz1 "]"))
 
-extern const struct tvec_regty tvty_buffer;
+/* --- @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.
+ */
 
 extern void tvec_allocstring(union tvec_regval */*rv*/, size_t /*sz*/);
 extern void tvec_allocbytes(union tvec_regval */*rv*/, size_t /*sz*/);
 
-/*----- Benchmarking ------------------------------------------------------*/
-
-struct tvec_bench {
-  struct tvec_env _env;                        /* benchmarking is an environment */
-  struct bench_state **bst;            /* benchmark state anchor or null */
-  unsigned long niter;                 /* iterations done per unit */
-  int riter, rbuf;                     /* iterations and buffer registers */
-  const struct tvec_env *env;          /* environment (per test, not grp) */
-};
-#define TVEC_BENCHENV                                                  \
-  { sizeof(struct tvec_benchctx),                                      \
-    tvec_benchsetup,                                                   \
-    tvec_benchset,                                                     \
-    tvec_benchbefore,                                                  \
-    tvec_benchrun,                                                     \
-    tvec_benchafter,                                                   \
-    tvec_benchteardown }
-#define TVEC_BENCHINIT TVEC_BENCHENV, &tvec_benchstate
-
-struct tvec_benchctx {
-  const struct tvec_bench *b;
-  struct bench_state *bst;
-  double dflt_target;
-  void *subctx;
-};
-
-extern struct bench_state *tvec_benchstate;
-
-extern int tvec_benchsetup(struct tvec_state */*tv*/,
-                          const struct tvec_env */*env*/,
-                          void */*pctx*/, void */*ctx*/);
-extern int tvec_benchset(struct tvec_state */*tv*/, const char */*var*/,
-                        const struct tvec_env */*env*/, void */*ctx*/);
-extern int tvec_benchbefore(struct tvec_state */*tv*/, void */*ctx*/);
-extern void tvec_benchrun(struct tvec_state */*tv*/,
-                         tvec_testfn */*fn*/, void */*ctx*/);
-extern void tvec_benchafter(struct tvec_state */*tv*/, void */*ctx*/);
-extern void tvec_benchteardown(struct tvec_state */*tv*/, void */*ctx*/);
-
-extern void tvec_benchreport
-  (const struct gprintf_ops */*gops*/, void */*go*/,
-   unsigned /*unit*/, const struct bench_timing */*tm*/);
-
-/*----- Command-line interface --------------------------------------------*/
-
-extern const struct tvec_config tvec_adhocconfig;
-
-extern void tvec_parseargs(int /*argc*/, char */*argv*/[],
-                          struct tvec_state */*tv_out*/,
-                          int */*argpos_out*/,
-                          const struct tvec_config */*config*/);
-
-extern int tvec_readstdin(struct tvec_state */*tv*/);
-extern int tvec_readfile(struct tvec_state */*tv*/, const char */*file*/);
-extern int tvec_readdflt(struct tvec_state */*tv*/, const char */*file*/);
-extern int tvec_readarg(struct tvec_state */*tv*/, const char */*arg*/);
+/*----- Buffer type -------------------------------------------------------*/
 
-extern int tvec_readargs(int /*argc*/, char */*argv*/[],
-                        struct tvec_state */*tv*/,
-                        int */*argpos_inout*/, const char */*dflt*/);
+/* Buffer registers are primarily used for benchmarking.  Only a buffer's
+ * size is significant: its contents are ignored on comparison and output,
+ * and unspecified on input.
+ *
+ * The input is simply the buffer size, as an integer, optionally suffixed
+ * with a unit `kB', `MB', `GB', `TB', `PB', `EB', `ZB', `YB' (with or
+ * without the `B') denoting a power of 1024.  Units are used on output only
+ * when the size would be expressed exactly.
+ */
 
-extern int tvec_main(int /*argc*/, char */*argv*/[],
-                    const struct tvec_config */*config*/,
-                    const char */*dflt*/);
+extern const struct tvec_regty tvty_buffer;
 
 /*----- That's all, folks -------------------------------------------------*/
 
index 780772120495de66aae9756f836ec7a99b9781ff..ffbe92021fb13627c30429b426e7445b316eaeb3 100644 (file)
@@ -82,6 +82,10 @@ pkginclude_HEADERS   += linreg.h
 libutils_la_SOURCES    += linreg.c
 ##LIBMANS              += linreg.3
 
+## Mathematics.
+pkginclude_HEADERS     += maths.h
+##LIBMANS              += maths.3
+
 ## String handling.
 pkginclude_HEADERS     += str.h
 libutils_la_SOURCES    += str.c
index f527186373599d53b271ff4a12fa666610052996..b575e1f4ef495d148cf6cb16e57c69822b599d8b 100644 (file)
@@ -563,6 +563,54 @@ int gprintf(const struct gprintf_ops *ops, void *out, const char *p, ...)
   return (n);
 }
 
+/*----- Utilities ---------------------------------------------------------*/
+
+/* --- @gprintf_memputf@ --- *
+ *
+ * Arguments:  @char **buf_inout@ = address of output buffer pointer
+ *             @size_t *sz_inout@ = address of buffer size
+ *             @size_t maxsz@ = buffer size needed for this operation
+ *             @const char *p@ = pointer to format string
+ *             @va_list *ap@ = captured format-arguments tail
+ *
+ * Returns:    The formatted length.
+ *
+ * Use:                Generic utility for mostly implementing the @nputf@ output
+ *             function, if you don't have a better option.
+ *
+ *             On entry, @*buf_inout@ should be null or a buffer pointer,
+ *             with @*sz_inout@ either zero or the buffer's size,
+ *             respectively.  On exit, @*buf_input@ and @*sz_inout@ will be
+ *             updated, if necessary, to describe a sufficiently large
+ *             buffer, and the formatted string will have been written to
+ *             the buffer.
+ *
+ *             When the buffer is no longer required, free it using @xfree@.
+ */
+
+size_t gprintf_memputf(char **buf_inout, size_t *sz_inout,
+                   size_t maxsz, const char *p, va_list ap)
+{
+  char *buf = *buf_inout;
+  size_t sz = *sz_inout;
+  int n;
+
+  if (sz <= maxsz) {
+    if (!sz) sz = 32;
+    while (sz <= maxsz) sz *= 2;
+    if (buf) xfree(buf);
+    buf = xmalloc(sz); *buf_inout = buf; *sz_inout = sz;
+  }
+
+#ifdef HAVE_SNPRINTF
+  n = vsnprintf(buf, maxsz + 1, p, ap);
+#else
+  n = vsprintf(buf, p, ap);
+#endif
+  assert(0 <= n && n <= maxsz);
+  return (n);
+}
+
 /*----- Standard printers -------------------------------------------------*/
 
 static int file_putch(void *out, int ch)
index 9e32338827690f0a4534f4a506a00e2b301a6d27..0f06dd38ed4d01062e79e13283c34142f38be17f 100644 (file)
@@ -86,6 +86,33 @@ extern int PRINTF_LIKE(3, 4)
   gprintf(const struct gprintf_ops */*ops*/, void */*out*/,
          const char */*p*/, ...);
 
+/* --- @gprintf_memputf@ --- *
+ *
+ * Arguments:  @char **buf_inout@ = address of output buffer pointer
+ *             @size_t *sz_inout@ = address of buffer size
+ *             @size_t maxsz@ = buffer size needed for this operation
+ *             @const char *p@ = pointer to format string
+ *             @va_list *ap@ = captured format-arguments tail
+ *
+ * Returns:    The formatted length.
+ *
+ * Use:                Generic utility for mostly implementing the @nputf@ output
+ *             function, if you don't have a better option.
+ *
+ *             On entry, @*buf_inout@ should be null or a buffer pointer,
+ *             with @*sz_inout@ either zero or the buffer's size,
+ *             respectively.  On exit, @*buf_input@ and @*sz_inout@ will be
+ *             updated, if necessary, to describe a sufficiently large
+ *             buffer, and the formatted string will have been written to
+ *             the buffer.
+ *
+ *             When the buffer is no longer required, free it using @xfree@.
+ */
+
+extern size_t gprintf_memputf(char **/*buf_inout*/, size_t */*sz_inout*/,
+                             size_t /*maxsz*/,
+                             const char */*p*/, va_list /*ap*/);
+
 /*----- That's all, folks -------------------------------------------------*/
 
 #ifdef __cplusplus
diff --git a/utils/maths.h b/utils/maths.h
new file mode 100644 (file)
index 0000000..37227e9
--- /dev/null
@@ -0,0 +1,91 @@
+/* -*-c-*-
+ *
+ * Mathematical utilities
+ *
+ * (c) 2023 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the mLib utilities library.
+ *
+ * mLib is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Library General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or (at
+ * your option) any later version.
+ *
+ * mLib is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with mLib.  If not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
+ * USA.
+ */
+
+#ifndef MLIB_MATHS_H
+#define MLIB_MATHS_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <math.h>
+
+/*----- Macros provided ---------------------------------------------------*/
+
+/* --- @NANP@ --- *
+ *
+ * Arguments:  @floatish x@ = a floating-point number (evaluated multiple
+ *                     times)
+ *
+ * Returns:    Nonzero if @x@ is not-a-number.
+ */
+
+#ifdef isnan
+#  define NANP(x) isnan(x)
+#else
+#  define NANP(x) (!((x) == (x)))
+#endif
+
+/* --- @INFP@ --- *
+ *
+ * Arguments:  @floatish x@ = a floating-point number (evaluated multiple
+ *                     times)
+ *
+ * Returns:    Nonzero if @x@ is infinite.
+ */
+
+#ifdef isinf
+#  define INFP(x) isinf(x)
+#else
+#  define INFP(x) ((x) > LDBL_MAX || (x) < -LDBL_MAX)
+#endif
+
+/* --- @NEGP@ --- *
+ *
+ * Arguments:  @floatish x@ = a floating-point number (evaluated multiple
+ *                     times)
+ *
+ * Returns:    Nonzero if @x@ is negative.  This won't give the right answer
+ *             for negative zero unless the system provides explicit
+ *             support.
+ */
+
+#ifdef signbit
+#  define NEGP(x) signbit(x)
+#else
+#  define NEGP(x) ((x) < 0)
+#endif
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif