@@@ misc wip
[mLib] / test / tvec-core.c
index 03873dc..d2a08c3 100644 (file)
 #include <string.h>
 
 #include "alloc.h"
+#include "growbuf.h"
 #include "tvec.h"
 
 /*----- Output ------------------------------------------------------------*/
 
-int tvec_error(struct tvec_state *tv, const char *msg, ...)
+/* --- @tvec_strlevel@ --- *
+ *
+ * Arguments:  @unsigned level@ = level code
+ *
+ * Returns:    A human-readable description.
+ *
+ * Use:                Converts a level code into something that you can print in a
+ *             message.
+ */
+
+const char *tvec_strlevel(unsigned level)
+{
+  switch (level) {
+#define CASE(tag, name, val)                                           \
+    case TVLEV_##tag: return (name);
+    TVEC_LEVELS(CASE)
+#undef CASE
+    default: return ("??");
+  }
+}
+
+/* --- @tvec_report@, @tvec_report_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned level@ = severity level (@TVlEV_...@)
+ *             @const char *msg@, @va_list ap@ = error message
+ *
+ * Returns:    ---
+ *
+ * Use:                Report an message with a given severity.  Messages with level
+ *             @TVLEV_ERR@ or higher force a nonzero exit code.
+ */
+
+void tvec_report(struct tvec_state *tv, unsigned level, 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);
+  va_start(ap, msg); tvec_report_v(tv, level, msg, &ap); va_end(ap);
 }
-int tvec_error_v(struct tvec_state *tv, const char *msg, va_list *ap)
-  { tv->output->ops->error(tv->output, msg, ap); return (-1); }
 
-void tvec_notice(struct tvec_state *tv, const char *msg, ...)
+void tvec_report_v(struct tvec_state *tv, unsigned level,
+                  const char *msg, va_list *ap)
 {
-  va_list ap;
-  va_start(ap, msg); tvec_notice_v(tv, msg, &ap); va_end(ap);
+  tv->output->ops->report(tv->output, level, msg, ap);
+  if (level >= TVLEV_ERR) tv->f |= TVSF_ERROR;
 }
-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, ...)
+/* --- @tvec_error@, @tvec_notice@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *msg@, @va_list ap@ = error message
+ *
+ * Returns:    The @tvec_error@ function returns @-1@ as a trivial
+ *             convenience; @tvec_notice@ does not return a value.
+ *
+ * Use:                Report an error or a notice.  Errors are distinct from test
+ *             failures, and indicate that a problem was encountered which
+ *             compromised the activity of testing.  Notices are important
+ *             information which doesn't fit into any other obvious
+ *             category.
+ */
+
+int tvec_error(struct tvec_state *tv, const char *msg, ...)
 {
   va_list ap;
 
-  va_start(ap, expect); tvec_syntax_v(tv, ch, expect, &ap); va_end(ap);
+  va_start(ap, msg); tvec_report_v(tv, TVLEV_ERR, msg, &ap); va_end(ap);
   return (-1);
 }
-int tvec_syntax_v(struct tvec_state *tv, int ch,
-                  const char *expect, va_list *ap)
+
+void tvec_notice(struct tvec_state *tv, const char *msg, ...)
 {
-  dstr d = DSTR_INIT;
-  char found[8];
+  va_list ap;
 
-  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);
+  va_start(ap, msg); tvec_report_v(tv, TVLEV_NOTE, msg, &ap); va_end(ap);
 }
 
+/*----- 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.
+ */
+
 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)
 {
-  tv->f |= TVSF_SKIP; tv->grps[TVOUT_SKIP]++;
-  tv->output->ops->skipgroup(tv->output, excuse, ap);
+  if (!(tv->f&TVSF_SKIP)) {
+    tv->f |= TVSF_SKIP; tv->grps[TVOUT_SKIP]++;
+    tv->output->ops->skipgroup(tv->output, excuse, ap);
+  }
 }
 
+/* --- @set_outcome@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned out@ = the new outcome
+ *
+ * Returns:    ---
+ *
+ * Use:                Sets the outcome bits in the test state flags, and clears
+ *             @TVSF_ACTIVE@.
+ */
+
 static void set_outcome(struct tvec_state *tv, unsigned out)
-{
-  tv->f &= ~(TVSF_ACTIVE | TVSF_OUTMASK);
-  tv->f |= out << TVSF_OUTSHIFT;
-}
+  { tv->f = (tv->f&~(TVSF_ACTIVE | TVSF_OUTMASK)) | (out << TVSF_OUTSHIFT); }
+
+/* --- @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.
+ */
 
 void tvec_skip(struct tvec_state *tv, const char *excuse, ...)
 {
   va_list ap;
   va_start(ap, excuse); tvec_skip_v(tv, excuse, &ap); va_end(ap);
 }
+
 void tvec_skip_v(struct tvec_state *tv, const char *excuse, va_list *ap)
 {
   assert(tv->f&TVSF_ACTIVE);
@@ -110,11 +183,29 @@ void tvec_skip_v(struct tvec_state *tv, const char *excuse, va_list *ap)
   tv->output->ops->skip(tv->output, excuse, ap);
 }
 
+/* --- @tvec_fail@, @tvec_fail_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *detail@, @va_list *ap@ = description of test
+ *
+ * Returns:    ---
+ *
+ * Use:                Report the current test as a failure.  This function can be
+ *             called multiple times for a single test, e.g., if the test
+ *             environment's @run@ function invokes the test function
+ *             repeatedly; but a single test that fails repeatedly still
+ *             only counts as a single failure in the statistics.  The
+ *             @detail@ string and its format parameters can be used to
+ *             distinguish which of several invocations failed; it can
+ *             safely be left null if the test function is run only once.
+ */
+
 void tvec_fail(struct tvec_state *tv, const char *detail, ...)
 {
   va_list ap;
   va_start(ap, detail); tvec_fail_v(tv, detail, &ap); va_end(ap);
 }
+
 void tvec_fail_v(struct tvec_state *tv, const char *detail, va_list *ap)
 {
   assert((tv->f&TVSF_ACTIVE) ||
@@ -122,18 +213,50 @@ void tvec_fail_v(struct tvec_state *tv, const char *detail, va_list *ap)
   set_outcome(tv, TVOUT_LOSE); tv->output->ops->fail(tv->output, detail, ap);
 }
 
+/* --- @tvec_dumpreg@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned disp@ = the register disposition (@TVRD_...@)
+ *             @const union tvec_regval *tv@ = register value, or null
+ *             @const struct tvec_regdef *rd@ = register definition
+ *
+ * Returns:    ---
+ *
+ * Use:                Dump a register value to the output.  This is the lowest-
+ *             level function for dumping registers, and calls the output
+ *             formatter directly.
+ *
+ *             Usually @tvec_mismatch@ is much more convenient.  Low-level
+ *             access is required for reporting `virtual' registers
+ *             corresponding to test environment settings.
+ */
+
 void tvec_dumpreg(struct tvec_state *tv,
                  unsigned disp, const union tvec_regval *r,
                  const struct tvec_regdef *rd)
   { tv->output->ops->dumpreg(tv->output, disp, r, rd); }
 
+/* --- @tvec_mismatch@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @unsigned f@ = flags (@TVMF_...@)
+ *
+ * Returns:    ---
+ *
+ * Use:                Dumps registers suitably to report a mismatch.  The flag bits
+ *             @TVMF_IN@ and @TVF_OUT@ select input-only and output
+ *             registers.  If both are reset then nothing happens.
+ *             Suppressing the output registers may be useful, e.g., if the
+ *             test function crashed rather than returning outputs.
+ */
+
 void tvec_mismatch(struct tvec_state *tv, unsigned f)
 {
   const struct tvec_regdef *rd;
   const struct tvec_reg *rin, *rout;
 
   for (rd = tv->test->regs; rd->name; rd++) {
-    if (rd->i >= tv->nrout) {
+    if (rd->i >= tv->cfg.nrout) {
       if (!(f&TVMF_IN)) continue;
       rin = TVEC_REG(tv, in, rd->i);
       tvec_dumpreg(tv, TVRD_INPUT, rin->f&TVRF_LIVE ? &rin->v : 0, rd);
@@ -152,12 +275,90 @@ void tvec_mismatch(struct tvec_state *tv, unsigned f)
   }
 }
 
-/*----- Main machinery ----------------------------------------------------*/
+/*----- Parsing -----------------------------------------------------------*/
 
-struct groupstate {
-  void *ctx;
-};
-#define GROUPSTATE_INIT { 0 }
+/* --- @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).
+ */
+
+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);
+}
+
+/* --- @tvec_unkregerr@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *name@ = register or pseudoregister name
+ *
+ * Returns:    %$-1$%.
+ *
+ * Use:                Reports an error that the register or pseudoregister is
+ *             unrecognized.
+ */
+
+int tvec_unkregerr(struct tvec_state *tv, const char *name)
+{
+  return (tvec_error(tv, "unknown special register `%s' for test `%s'",
+                    name, tv->test->name));
+}
+
+/* --- @tvec_dupregerr@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *name@ = register or pseudoregister name
+ *
+ * Returns:    %$-1$%.
+ *
+ * Use:                Reports an error that the register or pseudoregister has been
+ *             assigned already in the current test.
+ */
+
+int tvec_dupregerr(struct tvec_state *tv, const char *name)
+  { return (tvec_error(tv, "register `%s' is already set", name)); }
+
+/* --- @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.
+ */
 
 void tvec_skipspc(struct tvec_state *tv)
 {
@@ -170,6 +371,24 @@ void tvec_skipspc(struct tvec_state *tv)
   }
 }
 
+/* --- @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.
+ */
+
 int tvec_flushtoeol(struct tvec_state *tv, unsigned f)
 {
   int ch, rc = 0;
@@ -190,6 +409,29 @@ 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 tvec_nexttoken(struct tvec_state *tv)
 {
   enum { TAIL, NEWLINE, INDENT, COMMENT };
@@ -224,174 +466,419 @@ int tvec_nexttoken(struct tvec_state *tv)
   }
 }
 
-int tvec_readword(struct tvec_state *tv, dstr *d, const char *delims,
-                 const char *expect, ...)
+/* --- @tvec_readword@, @tvec_readword_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @dstr *d@ = string to append the word to
+ *             @const char **p_inout@ = pointer into string, updated
+ *             @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.
+ *
+ *             If @p_inout@ is not null, then @*p_inout@ must be a pointer
+ *             into @d->buf@, which will be adjusted so that it will
+ *             continue to point at the same position even if the buffer is
+ *             reallocated.  As a subtle tweak, if @*p_inout@ initially
+ *             points at the end of the buffer, then it will be adjusted to
+ *             point at the beginning of the next word, rather than at the
+ *             additional intervening space.
+ */
+
+int tvec_readword(struct tvec_state *tv, dstr *d, const char **p_inout,
+                 const char *delims, const char *expect, ...)
 {
   va_list ap;
   int rc;
 
   va_start(ap, expect);
-  rc = tvec_readword_v(tv, d, delims, expect, &ap);
+  rc = tvec_readword_v(tv, d, p_inout, delims, expect, &ap);
   va_end(ap);
   return (rc);
 }
-int tvec_readword_v(struct tvec_state *tv, dstr *d, const char *delims,
-                   const char *expect, va_list *ap)
+
+int tvec_readword_v(struct tvec_state *tv, dstr *d, const char **p_inout,
+                   const char *delims, const char *expect, va_list *ap)
 {
+  size_t pos = 0;
   int ch;
 
+  tvec_skipspc(tv);
+
   ch = getc(tv->fp);
   if (!ch || ch == '\n' || ch == EOF || ch == ';' ||
       (delims && strchr(delims, ch))) {
     if (expect) return (tvec_syntax(tv, ch, expect, ap));
     else { ungetc(ch, tv->fp); return (-1); }
   }
-  if (d->len) DPUTC(d, ' ');
+  if (p_inout) pos = *p_inout - d->buf;
+  if (d->len) {
+    if (pos == d->len) pos++;
+    DPUTC(d, ' ');
+  }
   do {
     DPUTC(d, ch);
     ch = getc(tv->fp);
   } while (ch && ch != EOF && !isspace(ch) &&
           (!delims || !strchr(delims, ch)));
   DPUTZ(d); if (ch != EOF) ungetc(ch, tv->fp);
+  if (p_inout) *p_inout = d->buf + pos;
   return (0);
 }
 
-void tvec_resetoutputs(struct tvec_state *tv)
+/*----- Main machinery ----------------------------------------------------*/
+
+struct groupstate {
+  void *ctx;                           /* test environment context */
+  unsigned f;                          /* flags */
+#define GRPF_SETOUTC 1u                        /*   set outcome */
+#define GRPF_SETMASK (GRPF_SETOUTC)    /*   mask of all variable flags */
+};
+#define GROUPSTATE_INIT { 0, 0 }
+
+/* --- @tvec_initregs@, @tvec_releaseregs@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Initialize or release, respectively, the registers required
+ *             by the current test.  All of the registers, both input and
+ *             output, are effected.  Initialized registers are not marked
+ *             live.
+ */
+
+void tvec_initregs(struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
   struct tvec_reg *r;
 
   for (rd = tv->test->regs; rd->name; rd++) {
-    assert(rd->i < tv->nreg);
-    if (rd->i >= tv->nrout) continue;
-    r = TVEC_REG(tv, out, rd->i);
-    rd->ty->release(&r->v, rd);
-    rd->ty->init(&r->v, rd);
-    r->f = TVEC_REG(tv, in, rd->i)->f&TVRF_LIVE;
+    assert(rd->i < tv->cfg.nreg); r = TVEC_REG(tv, in, rd->i);
+    rd->ty->init(&r->v, rd); r->f = 0;
+    if (rd->i < tv->cfg.nrout)
+      { r = TVEC_REG(tv, out, rd->i); rd->ty->init(&r->v, rd); r->f = 0; }
   }
 }
 
-static void init_registers(struct tvec_state *tv)
+void tvec_releaseregs(struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
   struct tvec_reg *r;
 
   for (rd = tv->test->regs; rd->name; rd++) {
-    assert(rd->i < tv->nreg); r = TVEC_REG(tv, in, rd->i);
-    rd->ty->init(&r->v, rd); r->f = 0;
-    if (rd->i < tv->nrout)
-      { r = TVEC_REG(tv, out, rd->i); rd->ty->init(&r->v, rd); r->f = 0; }
+    assert(rd->i < tv->cfg.nreg); r = TVEC_REG(tv, in, rd->i);
+    rd->ty->release(&r->v, rd); r->f = 0;
+    if (rd->i < tv->cfg.nrout)
+      { r = TVEC_REG(tv, out, rd->i); rd->ty->release(&r->v, rd); r->f = 0; }
   }
 }
 
-static void release_registers(struct tvec_state *tv)
+/* --- @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.
+ *             Output registers are marked live if and only if the
+ *             corresponding input register is live.
+ */
+
+void tvec_resetoutputs(struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
   struct tvec_reg *r;
 
   for (rd = tv->test->regs; rd->name; rd++) {
-    assert(rd->i < tv->nreg); r = TVEC_REG(tv, in, rd->i);
-    rd->ty->release(&r->v, rd); r->f = 0;
-    if (rd->i < tv->nrout)
-      { r = TVEC_REG(tv, out, rd->i); rd->ty->release(&r->v, rd); r->f = 0; }
+    assert(rd->i < tv->cfg.nreg);
+    if (rd->i >= tv->cfg.nrout) continue;
+    r = TVEC_REG(tv, out, rd->i);
+    rd->ty->release(&r->v, rd);
+    rd->ty->init(&r->v, rd);
+    r->f = TVEC_REG(tv, in, rd->i)->f&TVRF_LIVE;
   }
 }
 
+/* --- @tvec_checkregs@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    Zero on success, @-1@ on mismatch.
+ *
+ * Use:                Compare the active output registers (according to the current
+ *             test group definition) with the corresponding input register
+ *             values.  A mismatch occurs if the two values differ
+ *             (according to the register type's @eq@ method), or if the
+ *             input is live but the output is dead.
+ *
+ *             This function only checks for a mismatch and returns the
+ *             result; it takes no other action.  In particular, it doesn't
+ *             report a failure, or dump register values.
+ */
+
 int tvec_checkregs(struct tvec_state *tv)
 {
   const struct tvec_regdef *rd;
   const struct tvec_reg *rin, *rout;
 
   for (rd = tv->test->regs; rd->name; rd++) {
-    if (rd->i >= tv->nrout) continue;
+    if (rd->i >= tv->cfg.nrout) continue;
     rin = TVEC_REG(tv, in, rd->i); rout = TVEC_REG(tv, out, rd->i);
-    if (!rin->f&TVRF_LIVE) continue;
+    if (!(rin->f&TVRF_LIVE)) continue;
     if (!(rout->f&TVRF_LIVE) || !rd->ty->eq(&rin->v, &rout->v, rd))
       return (-1);
   }
   return (0);
 }
 
+/* --- @tvec_check@, @tvec_check_v@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *detail@, @va_list *ap@ = description of test
+ *
+ * Returns:    ---
+ *
+ * Use:                Check the register values, reporting a failure and dumping
+ *             the registers in the event of a mismatch.  This just wraps up
+ *             @tvec_checkregs@, @tvec_fail@ and @tvec_mismatch@ in the
+ *             obvious way.
+ */
+
 void tvec_check(struct tvec_state *tv, const char *detail, ...)
 {
   va_list ap;
   va_start(ap, detail); tvec_check_v(tv, detail, &ap); va_end(ap);
 }
+
 void tvec_check_v(struct tvec_state *tv, const char *detail, va_list *ap)
 {
   if (tvec_checkregs(tv))
     { tvec_fail_v(tv, detail, ap); tvec_mismatch(tv, TVMF_IN | TVMF_OUT); }
 }
 
+/* --- @open_test@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Note that we are now collecting data for a new test.  The
+ *             current line number is recorded in @test_lno@.  The
+ *             @TVSF_OPEN@ flag is set, and @TVSF_XFAIL@ is reset.
+ *
+ *             If a test is already open, then do nothing.
+ */
+
+static void open_test(struct tvec_state *tv)
+{
+  if (!(tv->f&TVSF_OPEN)) {
+    tv->test_lno = tv->lno;
+    tv->f |= TVSF_OPEN; tv->f &= ~TVSF_XFAIL;
+  }
+}
+
+/* --- @begin_test@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Note that we're about to try running a state.  This is called
+ *             before the test environment's @before@ function.  Mark the
+ *             test as active, clear the outcome, and inform the output
+ *             driver.
+ */
+
 static void begin_test(struct tvec_state *tv)
 {
   tv->f |= TVSF_ACTIVE; tv->f &= ~TVSF_OUTMASK;
   tv->output->ops->btest(tv->output);
 }
 
+/* --- @tvec_endtest@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                End an ad-hoc test case,  The statistics are updated and the
+ *             outcome is reported to the output formatter.
+ */
+
 void tvec_endtest(struct tvec_state *tv)
 {
   unsigned out;
 
-  if (tv->f&TVSF_ACTIVE) out = TVOUT_WIN;
-  else out = (tv->f&TVSF_OUTMASK) >> TVSF_OUTSHIFT;
+  if (!(tv->f&TVSF_ACTIVE)) /* nothing to do */;
+  else if (tv->f&TVSF_XFAIL) set_outcome(tv, TVOUT_XFAIL);
+  else set_outcome(tv, TVOUT_WIN);
+  out = (tv->f&TVSF_OUTMASK) >> TVSF_OUTSHIFT;
   assert(out < TVOUT_LIMIT); tv->curr[out]++;
   tv->output->ops->etest(tv->output, out);
   tv->f &= ~TVSF_OPEN;
 }
 
+/* --- @tvec_xfail@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Mark the current test as an `expected failure'.  That is, the
+ *             behaviour -- if everything works as expected -- is known to
+ *             be incorrect, perhaps as a result of a longstanding bug, so
+ *             calling it a `success' would be inappropriate.  A failure, as
+ *             reported by @tvec_fail@, means that the behaviour is not as
+ *             expected -- either the long-standing bug has been fixed, or a
+ *             new bug has been introduced -- so investigation is required.
+ *
+ *             An expected failure doesn't cause the test group or the
+ *             session as a whole to fail, but it isn't counted as a win
+ *             either.
+ */
+
+void tvec_xfail(struct tvec_state *tv)
+  { tv->f |= TVSF_XFAIL; }
+
+/* --- @check@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct groupstate *g@ = private test group state
+ *
+ * Returns:    ---
+ *
+ * Use:                Run the current test.
+ *
+ *             This function is called once the data for a test has been
+ *             collected.  It's responsible for checking that all of the
+ *             necessary registers have been assigned values.  It marks the
+ *             output registers as live if the corresponding inputs are
+ *             live.  It calls the environment's @before@, @run@, and
+ *             @after@ functions if provided; if there is no @run@ function,
+ *             then it calls the test function directly, passing it the
+ *             environment's context pointer, and then calls @tvec_check@ to
+ *             verify the output values.
+ */
+
 static void check(struct tvec_state *tv, struct groupstate *g)
 {
   const struct tvec_test *t = tv->test;
-  const struct tvec_env *env;
+  const struct tvec_env *env = t->env;
   const struct tvec_regdef *rd;
+  struct tvec_reg *r;
+  unsigned f = 0;
+#define f_err 1u
 
   if (!(tv->f&TVSF_OPEN)) return;
 
   for (rd = t->regs; rd->name; rd++) {
-    if (TVEC_REG(tv, in, rd->i)->f&TVRF_LIVE)
-      { if (rd->i < tv->nrout) TVEC_REG(tv, out, rd->i)->f |= TVRF_LIVE; }
-    else if (!(rd->f&TVRF_OPT)) {
+    r = TVEC_REG(tv, in, rd->i);
+    if (r->f&TVRF_LIVE) {
+      if (rd->i < tv->cfg.nrout)
+       TVEC_REG(tv, out, rd->i)->f |= TVRF_LIVE;
+    } else if (!(r->f&TVRF_SEEN) && !(rd->f&TVRF_OPT)) {
       tvec_error(tv, "required register `%s' not set in test `%s'",
                 rd->name, t->name);
-      goto end;
+      f |= f_err;
     }
   }
 
   if (!(tv->f&TVSF_SKIP)) {
     begin_test(tv);
-    env = t->env;
-    if (env && env->before && env->before(tv, g->ctx))
-      tvec_skip(tv, "test setup failed");
+    if (f&f_err) tvec_skip(tv, "erroneous test data");
+    if (env && env->before) env->before(tv, g->ctx);
+    if (!(tv->f&TVSF_ACTIVE))
+      /* forced a skip */;
+    else if (env && env->run)
+      env->run(tv, t->fn, g->ctx);
     else {
-      if (env && env->run) env->run(tv, t->fn, g->ctx);
-      else { t->fn(tv->in, tv->out, g->ctx); tvec_check(tv, 0); }
+      t->fn(tv->in, tv->out, g->ctx);
+      tvec_check(tv, 0);
     }
-    if (env && env->after) env->after(tv, g->ctx);
     tvec_endtest(tv);
   }
 
-end:
-  tv->f &= ~TVSF_OPEN; release_registers(tv); init_registers(tv);
+  if (env && env->after) env->after(tv, g->ctx);
+  g->f &= ~GRPF_SETMASK;
+  tv->f &= ~TVSF_OPEN; tvec_releaseregs(tv); tvec_initregs(tv);
+
+#undef f_err
 }
 
+/* --- @begin_test_group@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct groupstate *g@ = private test group state
+ *
+ * Returns:    ---
+ *
+ * Use:                Begins a test group.  Expects @tv->test@ to have been set
+ *             already.  Calls the output driver, initializes the registers,
+ *             clears the @tv->curr@ counters, allocates the environment
+ *             context and calls the environment @setup@ function.
+ */
+
 static void begin_test_group(struct tvec_state *tv, struct groupstate *g)
 {
   const struct tvec_test *t = tv->test;
   const struct tvec_env *env = t->env;
+  const struct tvec_regdef *rd0, *rd1;
   unsigned i;
 
+#ifndef NDEBUG
+  /* Check that the register names and indices are distinct. */
+  for (rd0 = t->regs; rd0->name; rd0++) {
+    assert(rd0->i < tv->cfg.nreg);
+    for (rd1 = t->regs; rd1->name; rd1++)
+      if (rd0 != rd1) {
+       assert(rd0->i != rd1->i);
+       assert(STRCMP(rd0->name, !=, rd1->name));
+      }
+  }
+#endif
+
   tv->output->ops->bgroup(tv->output);
-  tv->f &= ~TVSF_SKIP;
-  init_registers(tv);
+  tv->f &= ~(TVSF_SKIP | TVSF_MUFFLE);
+  tvec_initregs(tv);
   for (i = 0; i < TVOUT_LIMIT; i++) tv->curr[i] = 0;
   if (env && env->ctxsz) g->ctx = xmalloc(env->ctxsz);
-  if (env && env->setup && env->setup(tv, env, 0, g->ctx)) {
-    tvec_skipgroup(tv, "setup failed");
-    xfree(g->ctx); g->ctx = 0;
-  }
+  if (env && env->setup) env->setup(tv, env, 0, g->ctx);
 }
 
+/* --- @report_group@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *
+ * Returns:    ---
+ *
+ * Use:                Reports the result of the test group to the output driver.
+ *
+ *             If all of the tests have been skipped then report this as a
+ *             group skip.  Otherwise, determine and report the group
+ *             outcome.
+ */
+
 static void report_group(struct tvec_state *tv)
 {
   unsigned i, out, nrun;
@@ -408,6 +895,24 @@ static void report_group(struct tvec_state *tv)
   }
 }
 
+/* --- @end_test_group@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @struct groupstate *g@ = private test group state
+ *
+ * Returns:    ---
+ *
+ * Use:                Handles the end of a test group.  Called at the end of the
+ *             input file or when a new test group header is found.
+ *
+ *             If a test is open, call @check@ to see whether it worked.  If
+ *             the test group is not being skipped, report the group
+ *             result.  Call the test environment @teardown@ function.  Free
+ *             the environment context and release the registers.
+ *
+ *             If there's no test group active, then nothing happens.
+ */
+
 static void end_test_group(struct tvec_state *tv, struct groupstate *g)
 {
   const struct tvec_test *t = tv->test;
@@ -417,94 +922,284 @@ static void end_test_group(struct tvec_state *tv, struct groupstate *g)
   if (tv->f&TVSF_OPEN) check(tv, g);
   if (!(tv->f&TVSF_SKIP)) report_group(tv);
   env = t->env; if (env && env->teardown) env->teardown(tv, g->ctx);
-  release_registers(tv); tv->test = 0; xfree(g->ctx); g->ctx = 0;
+  tvec_releaseregs(tv); tv->test = 0; xfree(g->ctx); g->ctx = 0;
+}
+
+/* --- @core_findvar@, @core_setvar@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test vector state
+ *             @const char *var@ = variable name to set
+ *             @const union tvec_regval *rv@ = register value
+ *             @void **ctx_out@ = where to put the @setvar@ context
+ *             @void *ctx@ = context pointer
+ *
+ * Returns:    @core_findvar@ returns a pointer to the variable definition,
+ *             or null; @core_setvar@ returns zero on success or %$-1$% on
+ *             error.
+ *
+ * Use:                Find a definition for a special variable.  The following
+ *             special variables are supported.
+ *
+ *               * %|@outcome|% is a token describing how a successful
+ *                 outcome of the test should be interpreted: %|success|% or
+ *                 %|win|% are the default: a successful test is counted as
+ *                 a pass; or %|expected-failure|% or %|xfail|% means a
+ *                 successful test is counted as an expected failure.  A
+ *                 mismatch is always considered a failure.
+ */
+
+enum { WIN, XFAIL, NOUT };
+
+static int core_setvar(struct tvec_state *tv, const char *name,
+                      const union tvec_regval *rv, void *ctx)
+{
+  struct groupstate *g = ctx;
+
+  if (STRCMP(name, ==, "@outcome")) {
+    if (g->f&GRPF_SETOUTC) return (tvec_dupregerr(tv, name));
+    if (rv->u == XFAIL) tvec_xfail(tv);
+    g->f |= GRPF_SETOUTC;
+  } else assert(!"unknown var");
+  return (0);
 }
 
+static const struct tvec_uassoc outcome_assoc[] = {
+  { "success",         WIN },
+  { "win",             WIN },
+  { "expected-failure",        XFAIL },
+  { "xfail",           XFAIL },
+  TVEC_ENDENUM
+};
+static const struct tvec_urange outcome_range = { 0, NOUT - 1 };
+static const struct tvec_uenuminfo outcome_enum =
+  { "test-outcome", outcome_assoc, &outcome_range };
+static const struct tvec_vardef outcome_vardef =
+  { sizeof(struct tvec_reg), core_setvar,
+    { "@outcome", &tvty_uenum, -1, 0, { &outcome_enum } } };
+
+static const struct tvec_vardef *core_findvar
+  (struct tvec_state *tv, const char *name, void **ctx_out, void *ctx)
+{
+  if (STRCMP(name, ==, "@outcome"))
+    { *ctx_out = ctx; return (&outcome_vardef); }
+  else
+    return (0);
+}
+
+/* --- @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.
+ */
+
 int tvec_read(struct tvec_state *tv, const char *infile, FILE *fp)
 {
   dstr d = DSTR_INIT;
   const struct tvec_test *test;
   const struct tvec_env *env;
   const struct tvec_regdef *rd;
-  struct tvec_reg *r;
+  const struct tvec_vardef *vd = 0; void *varctx;
+  struct tvec_reg *r = 0, rbuf, *r_alloc = 0; size_t rsz = 0;
   struct groupstate g = GROUPSTATE_INIT;
-  int ch, ret, rc = 0;
+  int ch, rc = 0;
 
+  /* Set the initial location. */
   tv->infile = infile; tv->lno = 1; tv->fp = fp;
 
   for (;;) {
+
+    /* Get the next character and dispatch.  Note that we're always at the
+     * start of a line here.
+     */
     ch = getc(tv->fp);
     switch (ch) {
 
       case EOF:
+       /* End of the file.  Exit the loop. */
+
        goto end;
 
       case '[':
+       /* A test group header. */
+
+       /* End the current group, if there is one. */
        end_test_group(tv, &g);
-       tvec_skipspc(tv);
-       DRESET(&d); tvec_readword(tv, &d, "];", "group name");
+
+       /* Read the group name.  There may be leading and trailing
+        * whitespace.
+        */
+       DRESET(&d); tvec_readword(tv, &d, 0, "];", "group name");
        tvec_skipspc(tv);
        ch = getc(tv->fp); if (ch != ']') tvec_syntax(tv, ch, "`]'");
-       for (test = tv->tests; test->name; test++)
+
+       /* Find the matching test definition. */
+       for (test = tv->cfg.tests; test->name; test++)
          if (STRCMP(d.buf, ==, test->name)) goto found_test;
-       tvec_error(tv, "unknown test group `%s'", d.buf); goto flush_line;
+
+       /* There wasn't one.  Report the error.  Muffle errors about the
+        * contents of this section because they won't be interesting.
+        */
+       tvec_error(tv, "unknown test group `%s'", d.buf);
+       tv->f |= TVSF_MUFFLE; goto flush_line;
+
       found_test:
-       tvec_flushtoeol(tv, 0); tv->test = test; begin_test_group(tv, &g);
+       /* Eat trailing whitespace and comments. */
+       tvec_flushtoeol(tv, 0);
+
+       /* Set up the new test group. */
+       tv->test = test; begin_test_group(tv, &g);
        break;
 
       case '\n':
+       /* A newline, so this was a completely empty line.  Advance the line
+        * counter, and run the current test.
+        */
+
        tv->lno++;
        if (tv->f&TVSF_OPEN) check(tv, &g);
        break;
 
+      case ';':
+       /* A semicolon.  Skip the comment. */
+
+       tvec_flushtoeol(tv, TVFF_ALLOWANY);
+       break;
+
       default:
+       /* Something else. */
+
        if (isspace(ch)) {
-         tvec_skipspc(tv);
-         ch = getc(tv->fp);
+         /* Whitespace.  Skip and see what we find. */
+
+         tvec_skipspc(tv); ch = getc(tv->fp);
+
+         /* If the file ends, then we're done.  If we find a comment then we
+          * skip it.  If there's some non-whitespace, then report an error.
+          * Otherwise the line was effectively blank, so run the test.
+          */
          if (ch == EOF) goto end;
          else if (ch == ';') tvec_flushtoeol(tv, TVFF_ALLOWANY);
          else if (tvec_flushtoeol(tv, 0)) rc = -1;
          else check(tv, &g);
-       } else if (ch == ';')
-         tvec_flushtoeol(tv, TVFF_ALLOWANY);
-       else {
+       } else {
+         /* Some non-whitespace thing. */
+
+         /* If there's no test, then report an error.  Set the muffle flag,
+          * because there's no point in complaining about every assignment
+          * in this block.
+          */
+         if (!tv->test) {
+           if (!(tv->f&TVSF_MUFFLE)) tvec_error(tv, "no current test");
+           tv->f |= TVSF_MUFFLE; goto flush_line;
+         }
+
+         /* Put the character back and read a word, which ought to be a
+          * register name.
+          */
          ungetc(ch, tv->fp);
          DRESET(&d);
-         if (tvec_readword(tv, &d, "=:;", "register name")) goto flush_line;
-         tvec_skipspc(tv); ch = getc(tv->fp);
-         if (ch != '=' && ch != ':')
-           { tvec_syntax(tv, ch, "`=' or `:'"); goto flush_line; }
-         tvec_skipspc(tv);
-         if (!tv->test)
-           { tvec_error(tv, "no current test"); goto flush_line; }
+         if (tvec_readword(tv, &d, 0, "=:*;", "register name"))
+           goto flush_line;
+
+         /* Open the test.  This is syntactically a paragraph of settings,
+          * so it's fair to report on missing register assignments.
+          */
+         open_test(tv);
+
+         /* See what sort of thing we have found. */
          if (d.buf[0] == '@') {
+           /* A special register assignment.  */
+
            env = tv->test->env;
-           if (!env || !env->set) ret = 0;
-           else ret = env->set(tv, d.buf, env, g.ctx);
-           if (ret <= 0) {
-             if (!ret)
-               tvec_error(tv, "unknown special register `%s'", d.buf);
-             goto flush_line;
+
+           /* Find a variable definition. */
+           vd = core_findvar(tv, d.buf, &varctx, &g);
+             if (vd) goto found_var;
+           if (env && env->findvar) {
+             vd = env->findvar(tv, d.buf, &varctx, g.ctx);
+               if (vd) goto found_var;
            }
-           if (!(tv->f&TVSF_OPEN))
-             { tv->test_lno = tv->lno; tv->f |= TVSF_OPEN; }
+           tvec_unkregerr(tv, d.buf); goto flush_line;
+         found_var:
+           rd = &vd->def;
          } else {
+           /* A standard register. */
+
+           /* Find the definition. */
            for (rd = tv->test->regs; rd->name; rd++)
              if (STRCMP(rd->name, ==, d.buf)) goto found_reg;
            tvec_error(tv, "unknown register `%s' for test `%s'",
                       d.buf, tv->test->name);
            goto flush_line;
          found_reg:
-           if (!(tv->f&TVSF_OPEN))
-             { tv->test_lno = tv->lno; tv->f |= TVSF_OPEN; }
-           tvec_skipspc(tv);
+
+           /* Complain if the register is already set. */
            r = TVEC_REG(tv, in, rd->i);
-           if (r->f&TVRF_LIVE) {
-             tvec_error(tv, "register `%s' already set", rd->name);
+           if (r->f&TVRF_SEEN)
+             { tvec_dupregerr(tv, rd->name); goto flush_line; }
+         }
+
+         /* Now there should be a separator. */
+         tvec_skipspc(tv); ch = getc(tv->fp);
+
+         if (ch == '*') {
+           /* Register explicitly marked unset. */
+
+           if (vd) {
+             tvec_error(tv, "can't unset special variables");
+             goto flush_line;
+           }
+           if (!(rd->f&(TVRF_OPT | TVRF_UNSET))) {
+             tvec_error(tv, "register `%s' must be assigned "
+                        "a value in test `%s'", rd->name, tv->test->name);
              goto flush_line;
            }
-           if (rd->ty->parse(&r->v, rd, tv)) goto flush_line;
-           r->f |= TVRF_LIVE;
+           r->f |= TVRF_SEEN;
+           if (tvec_flushtoeol(tv, 0)) goto bad;
+         } else {
+           /* Common case of a proper assignment. */
+
+           /* We must have a separator. */
+           if (ch != '=' && ch != ':')
+             { tvec_syntax(tv, ch, "`=', `:', or `*'"); goto flush_line; }
+           tvec_skipspc(tv);
+
+           if (!vd) {
+             /* An ordinary register.  Parse a value and mark the register
+              * as live.
+              */
+
+             if (rd->ty->parse(&r->v, rd, tv)) goto flush_line;
+             r->f |= TVRF_LIVE | TVRF_SEEN;
+           } else {
+             /* A special register defined by an environment. */
+
+             /* Set up the register. */
+             if (vd->regsz <= sizeof(rbuf))
+               r = &rbuf;
+             else {
+               GROWBUF_REPLACE(&arena_stdlib, r_alloc, rsz, vd->regsz,
+                               8*sizeof(void *), 1);
+               r = r_alloc;
+             }
+
+             /* Read and set the value. */
+             rd->ty->init(&r->v, rd);
+             if (rd->ty->parse(&r->v, rd, tv)) goto flush_line;
+             if (!(tv->f&TVSF_SKIP) && vd->setvar(tv, d.buf, &r->v, varctx))
+               goto bad;
+
+             /* Clean up. */
+             rd->ty->release(&r->v, &vd->def); vd = 0;
+           }
          }
        }
        break;
@@ -512,19 +1207,48 @@ int tvec_read(struct tvec_state *tv, const char *infile, FILE *fp)
     continue;
 
   flush_line:
-    tvec_flushtoeol(tv, TVFF_ALLOWANY); rc = -1;
+    /* This is a general parse-failure handler.  Skip to the next line and
+     * remember that things didn't go so well.
+     */
+    tvec_flushtoeol(tv, TVFF_ALLOWANY);
+  bad:
+    if (vd) { vd->def.ty->release(&r->v, &vd->def); vd = 0; }
+    rc = -1;
   }
+
+  /* We reached the end.  If that was actually an I/O error then report it.
+   */
   if (ferror(tv->fp))
     { tvec_error(tv, "error reading input: %s", strerror(errno)); rc = -1; }
+
 end:
+  /* Process the final test, if there was one, and wrap up the final
+   * group.
+   */
   end_test_group(tv, &g);
+
+  /* Clean up. */
   tv->infile = 0; tv->fp = 0;
   dstr_destroy(&d);
+  xfree(r_alloc);
   return (rc);
+
+#undef rlive
 }
 
 /*----- Session lifecycle -------------------------------------------------*/
 
+/* --- @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.
+ */
+
 void tvec_begin(struct tvec_state *tv_out,
                const struct tvec_config *config,
                struct tvec_output *o)
@@ -534,27 +1258,38 @@ void tvec_begin(struct tvec_state *tv_out,
   tv_out->f = 0;
 
   assert(config->nrout <= config->nreg);
-  tv_out->nrout = config->nrout; tv_out->nreg = config->nreg;
-  tv_out->regsz = config->regsz;
-  tv_out->in = xmalloc(tv_out->nreg*tv_out->regsz);
-  tv_out->out = xmalloc(tv_out->nrout*tv_out->regsz);
-  for (i = 0; i < tv_out->nreg; i++) {
+  tv_out->cfg = *config;
+  tv_out->in = xmalloc(tv_out->cfg.nreg*tv_out->cfg.regsz);
+  tv_out->out = xmalloc(tv_out->cfg.nrout*tv_out->cfg.regsz);
+  for (i = 0; i < tv_out->cfg.nreg; i++) {
     TVEC_REG(tv_out, in, i)->f = 0;
-    if (i < tv_out->nrout) TVEC_REG(tv_out, out, i)->f = 0;
+    if (i < tv_out->cfg.nrout) TVEC_REG(tv_out, out, i)->f = 0;
   }
 
   for (i = 0; i < TVOUT_LIMIT; i++)
     tv_out->curr[i] = tv_out->all[i] = tv_out->grps[i] = 0;
 
-  tv_out->tests = config->tests; tv_out->test = 0;
+  tv_out->test = 0;
   tv_out->infile = 0; tv_out->lno = 0; tv_out->fp = 0;
   tv_out->output = o; tv_out->output->ops->bsession(tv_out->output, tv_out);
 }
 
+/* --- @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.)
+ */
+
 int tvec_end(struct tvec_state *tv)
 {
   int rc = tv->output->ops->esession(tv->output);
 
+  if (tv->test) tvec_releaseregs(tv);
   tv->output->ops->destroy(tv->output);
   xfree(tv->in); xfree(tv->out);
   return (rc);
@@ -562,6 +1297,25 @@ int tvec_end(struct tvec_state *tv)
 
 /*----- Serialization and deserialization ---------------------------------*/
 
+/* --- @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 serialized output is written to the buffer @b@.  Failure
+ *             can be caused by running out of buffer space, or a failing
+ *             type handler.
+ */
+
 int tvec_serialize(const struct tvec_reg *rv, buf *b,
                   const struct tvec_regdef *regs,
                   unsigned nr, size_t regsz)
@@ -580,12 +1334,36 @@ int tvec_serialize(const struct tvec_reg *rv, buf *b,
   for (rd = regs, i = 0; rd->name; rd++, i++) {
     if (rd->i >= nr) continue;
     r = TVEC_GREG(rv, rd->i, regsz); if (!(r->f&TVRF_LIVE)) continue;
-    bitmap = BBASE(b) + bitoff; bitmap[rd->i/8] |= 1 << rd->i%8;
+    bitmap = BBASE(b) + bitoff; bitmap[i/8] |= 1 << i%8;
     if (rd->ty->tobuf(b, &r->v, rd)) return (-1);
   }
   return (0);
 }
 
+/* --- @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.
+ *
+ *             Failure results only from a failing register type handler.
+ */
+
 int tvec_deserialize(struct tvec_reg *rv, buf *b,
                     const struct tvec_regdef *regs,
                     unsigned nr, size_t regsz)
@@ -601,7 +1379,7 @@ int tvec_deserialize(struct tvec_reg *rv, buf *b,
   bitmap = buf_get(b, bitsz); if (!bitmap) return (-1);
   for (rd = regs, i = 0; rd->name; rd++, i++) {
     if (rd->i >= nr) continue;
-    if (!(bitmap[rd->i/8]&(1 << rd->i%8))) continue;
+    if (!(bitmap[i/8]&(1 << i%8))) continue;
     r = TVEC_GREG(rv, rd->i, regsz);
     if (rd->ty->frombuf(b, &r->v, rd)) return (-1);
     r->f |= TVRF_LIVE;
@@ -616,32 +1394,85 @@ static const struct tvec_regdef no_regs = { 0, 0, 0, 0, { 0 } };
 static void fakefn(const struct tvec_reg *in, struct tvec_reg *out, void *p)
   { assert(!"fake test function"); }
 
+/* --- @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.
+ */
+
 void tvec_adhoc(struct tvec_state *tv, struct tvec_test *t)
 {
   t->name = "<unset>"; t->regs = &no_regs; t->env = 0; t->fn = fakefn;
-  tv->tests = t;
+  tv->cfg.tests = t;
 }
 
+/* --- @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.
+ */
+
 void tvec_begingroup(struct tvec_state *tv, const char *name,
                     const char *file, unsigned lno)
 {
-  struct tvec_test *t = (/*unconst*/ struct tvec_test *)tv->tests;
+  struct tvec_test *t = (/*unconst*/ struct tvec_test *)tv->cfg.tests;
 
   t->name = name; tv->test = t;
   tv->infile = file; tv->lno = tv->test_lno = lno;
   begin_test_group(tv, 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.
+ */
+
 void tvec_endgroup(struct tvec_state *tv)
 {
   if (!(tv->f&TVSF_SKIP)) report_group(tv);
   tv->test = 0;
 }
 
+/* --- @tvec_begintest@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *
+ * Returns:    ---
+ *
+ * Use:                Begin an ad-hoc test case.  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.
+ */
+
 void tvec_begintest(struct tvec_state *tv, const char *file, unsigned lno)
 {
   tv->infile = file; tv->lno = tv->test_lno = lno;
-  begin_test(tv); tv->f |= TVSF_OPEN;
+  open_test(tv); begin_test(tv);
 }
 
 struct adhoc_claim {
@@ -678,6 +1509,31 @@ static void adhoc_claim_teardown(struct tvec_state *tv,
   if (ck->f&ACF_FRESH) tvec_endtest(tv);
 }
 
+/* --- @tvec_claim@, @tvec_claim_v@, @TVEC_CLAIM@ --- *
+ *
+ * Arguments:  @struct tvec_state *tv@ = test-vector state
+ *             @int ok@ = a flag
+ *             @const char *file@, @unsigned @lno@ = calling file and line
+ *             @const char *msg@, @va_list *ap@ = message to report on
+ *                     failure
+ *
+ * Returns:    The value @ok@.
+ *
+ * Use:                Check that a claimed condition holds, as (part of) a test.
+ *             If no test case is underway (i.e., if @TVSF_OPEN@ is reset in
+ *             @tv->f@), then a new test case is begun and ended.  The
+ *             @file@ and @lno@ are passed to the output formatter to be
+ *             reported in case of a failure.  If @ok@ is nonzero, then
+ *             nothing else happens; so, in particular, if @tvec_claim@
+ *             established a new test case, then the test case succeeds.  If
+ *             @ok@ is zero, then a failure is reported, quoting @msg@.
+ *
+ *             The @TVEC_CLAIM@ 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 @ok@ condition in
+ *             the failure message.
+ */
+
 int tvec_claim_v(struct tvec_state *tv, int ok,
                 const char *file, unsigned lno,
                 const char *msg, va_list *ap)
@@ -719,7 +1575,7 @@ int tvec_claimeq(struct tvec_state *tv,
   adhoc_claim_setup(tv, &ck, regs, file, lno);
   ok = ty->eq(&tv->in[0].v, &tv->out[0].v, &regs[0]);
   if (!ok)
-    { tvec_fail(tv, "%s", expr ); tvec_mismatch(tv, TVMF_IN | TVMF_OUT); }
+    { tvec_fail(tv, "%s", expr); tvec_mismatch(tv, TVMF_IN | TVMF_OUT); }
   adhoc_claim_teardown(tv, &ck);
   return (ok);
 }