chiark / gitweb /
@@@ all the mess ever
authorMark Wooding <mdw@distorted.org.uk>
Mon, 8 May 2023 23:32:27 +0000 (00:32 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 9 May 2023 22:20:51 +0000 (23:20 +0100)
34 files changed:
configure.ac
hash/Makefile.am
hash/t/crc32-test.c [deleted file]
hash/t/crc32.tests
hash/t/hash-test.c [new file with mode: 0644]
hash/t/hash.bench [new file with mode: 0644]
hash/t/unihash-test.c [deleted file]
hash/t/unihash-testgen.py
hash/tests.at
struct/buf.c
struct/buf.h
t/Makefile.am
test/Makefile.am
test/bench.c [new file with mode: 0644]
test/bench.h [new file with mode: 0644]
test/t/tvec-test.c [new file with mode: 0644]
test/tests.at [new file with mode: 0644]
test/tvec-core.c [new file with mode: 0644]
test/tvec-main.c [new file with mode: 0644]
test/tvec-output.c [new file with mode: 0644]
test/tvec-types.c [new file with mode: 0644]
test/tvec.h [new file with mode: 0644]
utils/Makefile.am
utils/control.3 [new file with mode: 0644]
utils/control.h
utils/linreg.c [new file with mode: 0644]
utils/linreg.h [new file with mode: 0644]
utils/macros.h
utils/t/bits-test.c
utils/t/bits-testgen.py
utils/t/control-test.c
utils/t/versioncmp-test.c
utils/t/versioncmp.tests
utils/tests.at

index 60f82b9856b404ad46f468acfc1e5694ea73ff68..d144ebfff9d85533bf1d1058689823c532c00831 100644 (file)
@@ -58,6 +58,7 @@ AC_CHECK_HEADERS([stdint.h])
 
 dnl Libraries.
 mdw_ORIG_LIBS=$LIBS LIBS=$MLIB_LIBS
+AC_SEARCH_LIBS([sqrt], [m])
 AC_SEARCH_LIBS([socket], [socket])
 AC_SEARCH_LIBS([gethostbyname], [nsl resolv])
 MLIB_LIBS=$LIBS LIBS=$mdw_ORIG_LIBS
@@ -120,6 +121,19 @@ case $want_adns,$have_adns in
 esac
 AM_CONDITIONAL([WITH_ADNS], [test "$use_adns" = yes])
 
+dnl--------------------------------------------------------------------------
+dnl Timers.
+
+AC_CHECK_HEADERS([linux/perf_event.h])
+
+mdw_ORIG_LIBS=$LIBS LIBS=$MLIB_LIBS
+AC_SEARCH_LIBS([clock_gettime], [rt])
+MLIB_LIBS=$LIBS LIBS=$mdw_ORIG_LIBS
+if test "$ac_cv_search_clock_gettime" != no; then
+  AC_DEFINE([HAVE_CLOCK_GETTIME], [1],
+           [Define if you have the \`clock_gettime' function.])
+fi
+
 dnl--------------------------------------------------------------------------
 dnl Python (used for testing).
 
index 877cd3aca61413bc7064b61ae8d1323045924577..564be16364a0a1429079e27be04a580eaa14f830 100644 (file)
@@ -56,11 +56,6 @@ $(precomp)/crc32-tab.c:
        mv $@.new $@
 endif
 
-check_PROGRAMS         += t/crc32.t
-t_crc32_t_SOURCES       = t/crc32-test.c
-t_crc32_t_CPPFLAGS      = $(TEST_CPPFLAGS)
-t_crc32_t_LDFLAGS       = -static
-
 EXTRA_DIST             += t/crc32.tests
 
 ## Universal hashing.
@@ -85,11 +80,12 @@ $(precomp)/unihash-global.c:
                -o$@.new && mv $@.new $@
 endif
 
-check_PROGRAMS         += t/unihash.t
-t_unihash_t_SOURCES     = t/unihash-test.c
-t_unihash_t_CPPFLAGS    = $(TEST_CPPFLAGS)
-t_unihash_t_LDFLAGS     = -static
-
 EXTRA_DIST             += t/unihash-testgen.py
 
+## Test program.
+check_PROGRAMS         += t/hash.t
+t_hash_t_SOURCES        = t/hash-test.c
+t_hash_t_CPPFLAGS       = $(TEST_CPPFLAGS)
+t_hash_t_LDFLAGS        = -static
+
 ###----- That's all, folks --------------------------------------------------
diff --git a/hash/t/crc32-test.c b/hash/t/crc32-test.c
deleted file mode 100644 (file)
index 8e6d820..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-/* -*-c-*-
- *
- * Test driver for CRC
- *
- * (c) 2009 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.
- */
-
-/*----- Header files ------------------------------------------------------*/
-
-#include <stdio.h>
-
-#include "crc32.h"
-#include "testrig.h"
-
-/*----- Main code ---------------------------------------------------------*/
-
-static int verify(dstr *v)
-{
-  uint32 h, hh;
-  size_t n;
-  int i, c;
-  const char *p;
-  int ok = 1;
-
-  static const int step[] = { 0, 1, 5, 6, 7, 8, 23, -1 };
-
-  /* --- Set up for using this key --- */
-
-  h = *(uint32 *)v[1].buf;
-
-  /* --- Hash the data a lot --- */
-
-  for (i = 0; step[i] >= 0; i++) {
-    c = step[i];
-    if (!c)
-      hh = crc32(0, v[0].buf, v[0].len);
-    else {
-      hh = 0;
-      p = v[0].buf;
-      n = v[0].len;
-      while (n) {
-       if (c > n) c = n;
-       hh = crc32(hh, p, c);
-       p += c;
-       n -= c;
-      }
-    }
-    if (h != hh) {
-      ok = 0;
-      fprintf(stderr, "\ncrc32 failed\n");
-      fprintf(stderr, "         data = %s\n", v[0].buf);
-      fprintf(stderr, "         step = %d\n", step[i]);
-      fprintf(stderr, "         expected = %08lx\n", (unsigned long)h);
-      fprintf(stderr, "         computed = %08lx\n", (unsigned long)hh);
-    }
-  }
-  return (ok);
-}
-
-static const test_chunk tests[] = {
-  { "hash", verify, { &type_string, &type_uint32 } },
-  { 0, 0, { 0 } }
-};
-
-int main(int argc, char *argv[])
-{
-  test_run(argc, argv, tests, "crc32.in");
-  return (0);
-}
-
-/*----- That's all, folks -------------------------------------------------*/
index 9fb2baff40e58039f01505b1899fc83df103d6ba..92fbee7357373e87e0030c63e60600683e388147 100644 (file)
@@ -1,11 +1,22 @@
-### -*-conf-*-
-### crc32 tests
-
-hash {
-  ""                                                           0;
-  "foo"                                                                0x8c736521;
-  "anything you like"                                          0x2c090211;
-  "an exaple test string"                                      0x686a05aa;
-  "The quick brown fox jumps over the lazy dog."               0x519025e9;
-  "A man, a plan, a canal: Panama!"                            0x27f3faee;
-}
+;;; -*-conf-*-
+;;; crc32 tests
+
+[crc32]
+
+m = ""
+h = 0
+
+m = "foo"
+h = 0x8c736521
+
+m = "anything you like"
+h = 0x2c090211
+
+m = "an exaple test string"
+h = 0x686a05aa
+
+m = "The quick brown fox jumps over the lazy dog."
+h = 0x519025e9
+
+m = "A man, a plan, a canal: Panama!"
+h = 0x27f3faee
diff --git a/hash/t/hash-test.c b/hash/t/hash-test.c
new file mode 100644 (file)
index 0000000..12d8222
--- /dev/null
@@ -0,0 +1,149 @@
+/* -*-c-*-
+ *
+ * Test driver for universal hashing
+ *
+ * (c) 2009 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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "crc32.h"
+#include "unihash.h"
+#include "tvec.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+enum {
+  RH, NROUT,
+  RK = NROUT, RM, NREG
+};
+
+struct step { size_t s; };
+
+static void test_crc32(const struct tvec_reg *in, struct tvec_reg *out,
+                      void *ctx)
+{
+  const struct step *step = ctx;
+  const unsigned char *p = in[RM].v.bytes.p; size_t sz = in[RM].v.bytes.sz;
+  uint32 h;
+
+  if (!step)
+    out[RH].v.u = crc32(0, p, sz);
+  else {
+    for (h = 0; sz > step->s; p += step->s, sz -= step->s)
+      h = crc32(h, p, step->s);
+    out[RH].v.u = crc32(h, p, sz);
+  }
+}
+
+static void bench_crc32(const struct tvec_reg *in, struct tvec_reg *out,
+                       void *ctx)
+  { crc32(0, in[RM].v.bytes.p, in[RM].v.bytes.sz); }
+
+static void test_unihash(const struct tvec_reg *in, struct tvec_reg *out,
+                        void *ctx)
+{
+  const struct step *step = ctx;
+  unihash_info ui;
+  const unsigned char *p = in[RM].v.bytes.p; size_t sz = in[RM].v.bytes.sz;
+  uint32 h;
+
+  unihash_setkey(&ui, in[RK].v.u);
+  if (!step)
+    out[RH].v.u = unihash(&ui, p, sz);
+  else {
+    for (h = UNIHASH_INIT(&ui); sz > step->s; p += step->s, sz -= step->s)
+      h = unihash_hash(&ui, h, p, step->s);
+    out[RH].v.u = unihash_hash(&ui, h, p, sz);
+  }
+}
+
+static void bench_unihash(const struct tvec_reg *in, struct tvec_reg *out,
+                         void *ctx)
+  { unihash_hash(ctx, 0, in[RM].v.bytes.p, in[RM].v.bytes.sz); }
+
+static int setup_unihash(const struct tvec_reg *in, struct tvec_reg *out,
+                        const union tvec_misc *arg, void *ctx)
+  { unihash_setkey(ctx, 0); return (0); }
+
+static void run_step(struct tvec_state *tv)
+{
+  static const size_t steps[] = { 1, 5, 6, 7, 8, 23 };
+  struct step step;
+  size_t i;
+
+  tv->test->fn(tv->in, tv->out, 0);
+  tvec_check(tv, "whole buffer");
+
+  for (i = 0; i < N(steps); i++) {
+    step.s = steps[i];
+    tv->test->fn(tv->in, tv->out, &step);
+    tvec_check(tv, "step = %lu", (unsigned long)steps[i]);
+  }
+}
+
+static const struct tvec_regdef unihash_regs[] = {
+  { "k", RK, &tvty_uint, 0, { &tvrange_u32 } },
+  { "m", RM, &tvty_bytes, 0 },
+  { "h", RH, &tvty_uint, 0, { &tvrange_u32 } },
+  { 0, 0, 0, 0 }
+};
+
+static const struct tvec_regdef crc32_regs[] = {
+  { "m", RM, &tvty_bytes, 0 },
+  { "h", RH, &tvty_uint, 0, { &tvrange_u32 } },
+  { 0, 0, 0, 0 }
+};
+
+static const struct tvec_regdef bench_regs[] = {
+  { "msz", RM, &tvty_buffer, TVRF_ID },
+  { 0, 0, 0, 0 }
+};
+
+static const struct tvec_bench crc32_bench = {
+  1, -1, RM,
+  0, 0, 0, 0, { 0 }
+};
+
+static const struct tvec_bench unihash_bench = {
+  1, -1, RM,
+  sizeof(unihash_info), setup_unihash, 0, 0, { 0 }
+};
+
+static const struct tvec_test tests[] = {
+  { "crc32",crc32_regs, 0, run_step, test_crc32 },
+  { "unihash", unihash_regs, 0, run_step, test_unihash },
+  { "crc32-bench", bench_regs, 0,
+    tvec_bench, bench_crc32, { &crc32_bench } },
+  { "unihash-bench", bench_regs, 0,
+    tvec_bench, bench_unihash, { &unihash_bench } },
+  { 0, 0, 0, 0, 0 }
+};
+
+static const struct tvec_info testinfo =
+  { tests, NROUT, NREG, sizeof(struct tvec_reg) };
+
+int main(int argc, char *argv[])
+  { return (tvec_main(argc, argv, &testinfo, 0)); }
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/hash/t/hash.bench b/hash/t/hash.bench
new file mode 100644 (file)
index 0000000..7d877ac
--- /dev/null
@@ -0,0 +1,26 @@
+;;; -*-conf-*-
+;;; hash benchmarking
+
+[crc32-bench]
+
+msz = 16
+
+msz = 256
+
+msz = 1 kB
+
+msz = 16 kB
+
+msz = 1 MB
+
+[unihash-bench]
+
+msz = 16
+
+msz = 256
+
+msz = 1 kB
+
+msz = 16 kB
+
+msz = 1 MB
diff --git a/hash/t/unihash-test.c b/hash/t/unihash-test.c
deleted file mode 100644 (file)
index 3e56f10..0000000
+++ /dev/null
@@ -1,96 +0,0 @@
-/* -*-c-*-
- *
- * Test driver for universal hashing
- *
- * (c) 2009 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.
- */
-
-/*----- Header files ------------------------------------------------------*/
-
-#include <stdio.h>
-
-#include "unihash.h"
-#include "testrig.h"
-
-/*----- Main code ---------------------------------------------------------*/
-
-static int verify(dstr *v)
-{
-  unihash_info ui;
-  uint32 k;
-  uint32 h, hh;
-  size_t n;
-  int i, c;
-  const char *p;
-  int ok = 1;
-
-  static const int step[] = { 0, 1, 5, 6, 7, 8, 23, -1 };
-
-  /* --- Set up for using this key --- */
-
-  k = *(uint32 *)v[0].buf;
-  h = *(uint32 *)v[2].buf;
-  unihash_setkey(&ui, k);
-
-  /* --- Hash the data a lot --- */
-
-  for (i = 0; step[i] >= 0; i++) {
-    c = step[i];
-    if (!c)
-      hh = unihash(&ui, v[1].buf, v[1].len);
-    else {
-      hh = UNIHASH_INIT(&ui);
-      p = v[1].buf;
-      n = v[1].len;
-      while (n) {
-       if (c > n) c = n;
-       hh = unihash_hash(&ui, hh, p, c);
-       p += c;
-       n -= c;
-      }
-    }
-    if (h != hh) {
-      ok = 0;
-      fprintf(stderr, "\nunihash failed\n");
-      fprintf(stderr, "         key = %08lx\n", (unsigned long)k);
-      fprintf(stderr, "         data = %s\n", v[1].buf);
-      fprintf(stderr, "         step = %d\n", step[i]);
-      fprintf(stderr, "         expected = %08lx\n", (unsigned long)h);
-      fprintf(stderr, "         computed = %08lx\n", (unsigned long)hh);
-    }
-  }
-  return (ok);
-}
-
-static const test_chunk tests[] = {
-  { "hash", verify, { &type_uint32, &type_string, &type_uint32 } },
-  { 0, 0, { 0 } }
-};
-
-int main(int argc, char *argv[])
-{
-  test_run(argc, argv, tests, "unihash.in");
-  return (0);
-}
-
-/*----- That's all, folks -------------------------------------------------*/
index 1fde21f947ada40c99840dd0bf2d7fc8518f78ee..a8365aef297f0e72827ef569a6b13614c03b8b88 100644 (file)
@@ -20,17 +20,22 @@ def gfmul(x, y):
 def hashtest(k, m):
   h = k
   for ch in m: h = gfmul(h ^ ord(ch), k)
-  print('  0x%08x "%s" 0x%08x;' % (k, m, h))
+  print('')
+  print('k = 0x%08x' % k)
+  print('m = "%s"' % m)
+  print('h = 0x%08x' % h)
 
 print('''\
-### Test vectors for universal hashing
-###   [generated]
+;;; -*-conf-*- Test vectors for universal hashing
+;;;   [generated]
 
-hash {''')
+[unihash]''')
 
 for k, m in [(0x00000000, 'anything you like'),
              (0x12345678, 'an exaple test string'),
              (0xb8a171f0, 'The quick brown fox jumps over the lazy dog.'),
+             (0xcc825ed5, 'Jackdaws love my big sphinx of quartz.'),
+             (0x16e98e46, 'Waltz, bad nymph, for quick jigs vex!'),
              (0x2940521b, 'A man, a plan, a canal: Panama!')]:
   hashtest(k, m)
 
@@ -38,5 +43,3 @@ k, m = 0x94b22a73, 0xbb7b1fef
 for i in xrange(48):
   hashtest(k, "If we don't succeed, we run the risk of failure.")
   k = gfmul(k, m)
-
-print('}')
index 65456d81dd76ad56158751106769b9f45305dc8d..45d4c133653b59e27d793cbc3bdec761d9e194d9 100644 (file)
@@ -31,8 +31,7 @@
 AT_SETUP([hash: crc32])
 AT_KEYWORDS([hash crc32])
 
-AT_CHECK([BUILDDIR/t/crc32.t -f SRCDIR/t/crc32.tests],
-        [0], [ignore], [ignore])
+AT_CHECK([BUILDDIR/t/hash.t SRCDIR/t/crc32.tests], [0], [ignore], [ignore])
 
 AT_CLEANUP
 
@@ -41,8 +40,7 @@ AT_SETUP([hash: unihash])
 AT_KEYWORDS([hash unihash])
 
 $PYTHON SRCDIR/t/unihash-testgen.py >unihash.tests
-AT_CHECK([BUILDDIR/t/unihash.t -f unihash.tests],
-        [0], [ignore], [ignore])
+AT_CHECK([BUILDDIR/t/hash.t unihash.tests], [0], [ignore], [ignore])
 
 AT_CLEANUP
 
index bd16ec71473787df0afd393d908ade00a3c00576..11b1425b0d1623d4f4edab7b6a574da5f3836f7f 100644 (file)
@@ -187,6 +187,34 @@ int buf_putbyte(buf *b, int ch)
   }
 DOUINTCONV(BUF_GETU_)
 
+/* --- @buf_getk64{,l,b}@ --- *
+ *
+ * Arguments:  @buf *b@ = pointer to a buffer block
+ *             @kludge64 *w@ = where to put the word
+ *
+ * Returns:    Zero if OK, or nonzero if there wasn't a word there.
+ *
+ * Use:                Gets a word of appropriate size and order from a buffer.
+ */
+
+int buf_getk64(buf *b, kludge64 *w)
+{
+  if (BENSURE(b, 8)) return (-1);
+  LOAD64_(*w, b->p); BSTEP(b, 8); return (0);
+}
+
+int buf_getk64l(buf *b, kludge64 *w)
+{
+  if (BENSURE(b, 8)) return (-1);
+  LOAD64_L_(*w, b->p); BSTEP(b, 8); return (0);
+}
+
+int buf_getk64b(buf *b, kludge64 *w)
+{
+  if (BENSURE(b, 8)) return (-1);
+  LOAD64_B_(*w, b->p); BSTEP(b, 8); return (0);
+}
+
 /* --- @buf_putu{8,{16,24,32,64}{,l,b}}@ --- *
  *
  * Arguments:  @buf *b@ = pointer to a buffer block
@@ -207,6 +235,34 @@ DOUINTCONV(BUF_GETU_)
   }
 DOUINTCONV(BUF_PUTU_)
 
+/* --- @buf_putk64{,l,b}@ --- *
+ *
+ * Arguments:  @buf *b@ = pointer to a buffer block
+ *             @kludge64 w@ = word to write
+ *
+ * Returns:    Zero if OK, or nonzero if there wasn't enough space
+ *
+ * Use:                Gets a word of appropriate size and order from a buffer.
+ */
+
+int buf_putk64(buf *b, kludge64 w)
+{
+  if (BENSURE(b, 8)) return (-1);
+  STORE64_(b->p, w); BSTEP(b, 8); return (0);
+}
+
+int buf_putk64l(buf *b, kludge64 w)
+{
+  if (BENSURE(b, 8)) return (-1);
+  STORE64_L_(b->p, w); BSTEP(b, 8); return (0);
+}
+
+int buf_putk64b(buf *b, kludge64 w)
+{
+  if (BENSURE(b, 8)) return (-1);
+  STORE64_B_(b->p, w); BSTEP(b, 8); return (0);
+}
+
 /* --- @findz@ --- *
  *
  * Arguments:  @buf *b@ = pointer to a buffer block
index a3585b46297cff28fbcf719f4f2b44ce1e62d60c..97dbd105b8aff5c4b5a36547f2b783696f5cfcd6 100644 (file)
@@ -199,6 +199,20 @@ extern int buf_putbyte(buf */*b*/, int /*ch*/);
   extern int buf_getu##w(buf */*b*/, uint##n */*w*/);
 DOUINTCONV(BUF_DECL_GETU_)
 
+/* --- @buf_getk64{,l,b}@ --- *
+ *
+ * Arguments:  @buf *b@ = pointer to a buffer block
+ *             @kludge64 *w@ = where to put the word
+ *
+ * Returns:    Zero if OK, or nonzero if there wasn't a word there.
+ *
+ * Use:                Gets a word of appropriate size and order from a buffer.
+ */
+
+extern int buf_getk64(buf */*b*/, kludge64 */*w*/);
+extern int buf_getk64l(buf */*b*/, kludge64 */*w*/);
+extern int buf_getk64b(buf */*b*/, kludge64 */*w*/);
+
 /* --- @buf_putu{8,{16,24,32,64}{,l,b}}@ --- *
  *
  * Arguments:  @buf *b@ = pointer to a buffer block
@@ -213,6 +227,20 @@ DOUINTCONV(BUF_DECL_GETU_)
   extern int buf_putu##w(buf */*b*/, uint##n /*w*/);
 DOUINTCONV(BUF_DECL_PUTU_)
 
+/* --- @buf_putk64{,l,b}@ --- *
+ *
+ * Arguments:  @buf *b@ = pointer to a buffer block
+ *             @kludge64 w@ = word to write
+ *
+ * Returns:    Zero if OK, or nonzero if there wasn't enough space
+ *
+ * Use:                Gets a word of appropriate size and order from a buffer.
+ */
+
+extern int buf_putk64(buf */*b*/, kludge64 /*w*/);
+extern int buf_putk64l(buf */*b*/, kludge64 /*w*/);
+extern int buf_putk64b(buf */*b*/, kludge64 /*w*/);
+
 /* --- @buf_getmem{8,{16,24,32,64}{,l,b},z} --- *
  *
  * Arguments:  @buf *b@ = pointer to a buffer block
index 617af72e82c82b073505e5c437055daaab8f25b5..8a9e1057abffc1e106470daf2f9bb5fff4f5a590 100644 (file)
@@ -43,6 +43,7 @@ autotest_TESTS                += $(top_srcdir)/codec/tests.at
 autotest_TESTS         += $(top_srcdir)/hash/tests.at
 autotest_TESTS         += $(top_srcdir)/struct/tests.at
 autotest_TESTS         += $(top_srcdir)/sys/tests.at
+autotest_TESTS         += $(top_srcdir)/test/tests.at
 autotest_TESTS         += $(top_srcdir)/utils/tests.at
 
 ###----- That's all, folks --------------------------------------------------
index e81daed929747cb11d00236fc6ca9fc8c543aeca..cf1a19838edb71ad34c2f7d133dbb7ee7062b37f 100644 (file)
@@ -32,9 +32,25 @@ libtest_la_SOURCES    =
 ###--------------------------------------------------------------------------
 ### Component files.
 
-## Testing.
+## Benchmarking.
+pkginclude_HEADERS     += bench.h
+libtest_la_SOURCES     += bench.c
+
+## Old `testrig' testing framework.
 pkginclude_HEADERS     += testrig.h
 libtest_la_SOURCES     += testrig.c
 LIBMANS                        += testrig.3
 
+## New `tvec' testing framework.
+pkginclude_HEADERS     += tvec.h
+libtest_la_SOURCES     += tvec-core.c
+libtest_la_SOURCES     += tvec-output.c
+libtest_la_SOURCES     += tvec-types.c
+libtest_la_SOURCES     += tvec-main.c
+
+check_PROGRAMS         += t/tvec.t
+t_tvec_t_SOURCES        = t/tvec-test.c
+t_tvec_t_CPPFLAGS       = $(TEST_CPPFLAGS)
+t_tvec_t_LDFLAGS        = -static
+
 ###----- That's all, folks --------------------------------------------------
diff --git a/test/bench.c b/test/bench.c
new file mode 100644 (file)
index 0000000..17ed4ed
--- /dev/null
@@ -0,0 +1,487 @@
+/* -*-c-*-
+ *
+ * Benchmarking support
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "config.h"
+
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include "alloc.h"
+#include "bench.h"
+#include "bits.h"
+#include "linreg.h"
+#include "macros.h"
+
+/*----- Data structures ---------------------------------------------------*/
+
+struct timer {
+  struct bench_timer _t;
+  const struct timer_ops *clkops, *cyops;
+  union { int fd; } u_cy;
+};
+
+struct timer_ops {
+  void (*now)(struct bench_time *t_out, struct timer *t);
+  void (*teardown)(struct timer *t);
+};
+
+/*----- Preliminaries -----------------------------------------------------*/
+
+#define NS_PER_S 1000000000
+
+static void PRINTF_LIKE(1, 2) debug(const char *fmt, ...)
+{
+  const char *p;
+  va_list ap;
+
+  p = getenv("MLIB_BENCH_DEBUG");
+  if (p && *p != 'n' && *p != '0') {
+    va_start(ap, fmt);
+    fputs("mLib BENCH: ", stderr);
+    vfprintf(stderr, fmt, ap);
+    fputc('\n', stderr);
+    va_end(ap);
+  }
+}
+
+/*----- The null clock ----------------------------------------------------*/
+
+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 };
+
+static int null_cyinit(struct timer *t)
+  { t->cyops = &null_ops; return (0); }
+
+#define NULL_CYENT { "null", null_cyinit },
+
+/*----- Linux performance counters ----------------------------------------*/
+
+#if defined(HAVE_LINUX_PERF_EVENT_H) && defined(HAVE_UINT64)
+
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <linux/perf_event.h>
+#include <asm/unistd.h>
+
+static void perfevent_now(struct bench_time *t_out, struct timer *t)
+{
+  ssize_t n;
+
+  n = read(t->u_cy.fd, &t_out->cy.i, sizeof(t_out->cy.i));
+    if (n != sizeof(t_out->cy.i)) {
+      debug("failed to read perf-event counter: %s", strerror(errno));
+      return;
+    }
+  t_out->f |= BTF_CYOK;
+}
+
+static void perfevent_teardown(struct timer *t)
+  { close(t->u_cy.fd); }
+
+static const struct timer_ops perfevent_ops =
+  { perfevent_now, perfevent_teardown };
+
+static int perfevent_init(struct timer *t)
+{
+  struct perf_event_attr attr = { 0 };
+  struct bench_time tm;
+
+  attr.type = PERF_TYPE_HARDWARE;
+  attr.size = sizeof(attr);
+  attr.config = PERF_COUNT_HW_CPU_CYCLES;
+  attr.disabled = 0;
+  attr.exclude_kernel = 1;
+  attr.exclude_hv = 1;
+
+  t->u_cy.fd = syscall(__NR_perf_event_open, &attr, 0, -1, -1, 0);
+  if (t->u_cy.fd < 0) {
+    debug("couldn't open perf evvent: %s", strerror(errno));
+    return (-1);
+  }
+
+  tm.f = 0; perfevent_now(&tm, t);
+  if (!(tm.f&BTF_CYOK)) { close(t->u_cy.fd); return (-1); }
+
+  t->cyops = &perfevent_ops; return (0);
+}
+#  define PERFEVENT_CYENT { "linux-perf-event", perfevent_init },
+#else
+#  define PERFEVENT_CYENT
+#endif
+
+/*----- Intel time-stamp counter ------------------------------------------*/
+
+#if defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__))
+
+#define EFLAGS_ID (1u << 21)
+#define CPUID_1D_TSC (1u << 4)
+
+static uint32 set_flags(unsigned long m, unsigned long x)
+{
+  unsigned long r;
+
+#ifdef __x86_64__
+#  define TMP "%%rcx"
+#else
+#  define TMP "%%ecx"
+#endif
+
+  __asm__ ("pushf\n\t"
+          "pop %0\n\t"
+          "mov %0, " TMP "\n\t"
+          "and %1, %0\n\t"
+          "xor %2, %0\n\t"
+          "push %0\n\t"
+          "popf\n\t"
+          "pushf\n\t"
+          "pop %0\n\t"
+          "push " TMP "\n\t"
+          "popf"
+          : "=r"(r)
+          : "g"(m), "g"(x)
+          : "ecx");
+  return (r);
+}
+
+struct cpuid { uint32 a, b, c, d; };
+
+static void cpuid(struct cpuid *info_out, uint32 a, uint32 c)
+{
+  __asm__ ("movl %1, %%eax\n\t"
+          "movl %2, %%ecx\n\t"
+          "cpuid\n\t"
+          "movl %%eax, 0(%0)\n\t"
+          "movl %%ebx, 4(%0)\n\t"
+          "movl %%ecx, 8(%0)\n\t"
+          "movl %%edx, 12(%0)\n\t"
+          : /* no outputs */
+          : "r"(info_out), "g"(a), "g"(c)
+          : "eax", "ebx", "ecx", "edx", "cc");
+}
+
+static void x86rdtsc_now(struct bench_time *t_out, struct timer *t)
+{
+  uint32 lo, hi;
+
+  __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
+  SET64(t_out->cy, hi, lo); t_out->f |= BTF_CYOK;
+}
+
+static const struct timer_ops x86rdtsc_ops =
+  { x86rdtsc_now, null_teardown };
+
+static int x86rdtsc_init(struct timer *t)
+{
+  struct cpuid info;
+
+  if ((set_flags(~EFLAGS_ID, 0)&EFLAGS_ID) ||
+      !(set_flags(~EFLAGS_ID, EFLAGS_ID)&EFLAGS_ID))
+    { debug("no `cpuid' instruction"); return (-1); }
+  cpuid(&info, 0, 0);
+  if (info.a < 1) { debug("no `cpuid' leaf 1"); return (-1); }
+  cpuid(&info, 1, 0);
+  if (!(info.d&CPUID_1D_TSC))
+    { debug("no `rdtsc' instrunction"); return (-1); }
+  t->cyops = &x86rdtsc_ops; return (0);
+}
+
+#  define X86RDTSC_CYENT { "x86-rdtsc", x86rdtsc_init },
+#else
+#  define X86RDTWC_CYENT
+#endif
+
+/*----- POSIX `clock_gettime' ---------------------------------------------*/
+
+#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_THREAD_CPUTIME_ID)
+
+static void gettime_now(struct bench_time *t_out, struct timer *t)
+{
+  struct timespec now;
+
+  if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now))
+    { debug("error reading POSIX clock: %s", strerror(errno)); return; }
+  ASSIGN64(t_out->s, now.tv_sec); t_out->ns = now.tv_nsec;
+  t_out->f |= BTF_TIMEOK;
+}
+
+static const struct timer_ops gettime_ops = { gettime_now, null_teardown };
+
+static int gettime_init(struct timer *t)
+{
+  struct bench_time tm;
+
+  tm.f = 0; gettime_now(&tm, t); if (!tm.f&BTF_TIMEOK) return (-1);
+  t->clkops = &gettime_ops; return (0);
+}
+
+#  define GETTIME_CLKENT { "posix-clock_gettime", gettime_init },
+#else
+#  define GETTIME_CLKENT
+#endif
+
+/*----- Standard C `clock' ------------------------------------------------*/
+
+static void clock_now(struct bench_time *t_out, struct timer *t)
+{
+  clock_t now, x;
+  unsigned long s; uint32 ns;
+
+  now = clock();
+    if (now == (clock_t)-1) {
+      debug("error reading standard clock: %s", strerror(errno));
+      return;
+    }
+  x = now/CLOCKS_PER_SEC;
+    if (x > ULONG_MAX) { debug("standard clock out of range"); return; }
+
+  s = x; x = now - CLOCKS_PER_SEC*s;
+  if (!(NS_PER_S%CLOCKS_PER_SEC))
+    ns = x*(NS_PER_S/CLOCKS_PER_SEC);
+  else if (NS_PER_S <= ULONG_MAX/CLOCKS_PER_SEC)
+    ns = (x*NS_PER_S)/CLOCKS_PER_SEC;
+  else
+    ns = x*((NS_PER_S + 0.0)/CLOCKS_PER_SEC);
+  ASSIGN64(t_out->s, s); t_out->ns = ns; t_out->f |= BTF_TIMEOK;
+}
+
+static const struct timer_ops clock_ops = { clock_now, null_teardown };
+
+static int clock_init(struct timer *t)
+{
+  struct bench_time tm;
+
+  tm.f = 0; clock_now(&tm, t); if (!tm.f&BTF_TIMEOK) return (-1);
+  t->clkops = &clock_ops; return (0);
+}
+
+#define CLOCK_CLKENT { "clock", clock_init },
+
+/*----- Timing setup ------------------------------------------------------*/
+
+static const struct timerent {
+  const char *name;
+  int (*init)(struct timer */*t*/);
+}
+  clktab[] = { GETTIME_CLKENT CLOCK_CLKENT { 0, 0 } },
+  cytab[] = { PERFEVENT_CYENT X86RDTSC_CYENT NULL_CYENT { 0, 0 } };
+
+static const struct timerent *find_timer_n(const char *name, size_t sz,
+                                          const struct timerent *timers,
+                                          const char *what)
+{
+  while (timers->name) {
+    if (strlen(timers->name) == sz && MEMCMP(name, ==, timers->name, sz))
+      return (timers);
+    timers++;
+  }
+  debug("%s timer `%.*s' not found", what, (int)sz, name); return (0);
+}
+
+static int try_timer(struct timer *t,
+                    const struct timerent *timer, const char *what)
+{
+  if (timer->init(t)) return (-1);
+  debug("selected %s timer `%s'", what, timer->name); return (0);
+}
+
+static int select_timer(struct timer *t, const struct timerent *timers,
+                       const char *varname, const char *what)
+{
+  const char *p; size_t n;
+  const struct timerent *timer;
+
+  p = getenv(varname);
+  if (!p) {
+    while (timers->name)
+      if (!try_timer(t, timers++, what)) return (0);
+  } else {
+    for (;;) {
+      n = strcspn(p, ",");
+      timer = find_timer_n(p, n, timers, what);
+      if (timer && !try_timer(t, timer, what)) return (0);
+      if (!p[n]) break;
+      p += n + 1;
+    }
+  }
+  debug("no suitable %s timer found", what); return (-1);
+}
+
+static void timer_now(struct bench_timer *tm, struct bench_time *t_out)
+{
+  struct timer *t = (struct timer *)tm;
+
+  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;
+
+  if (!t) return;
+  if (t->clkops) t->clkops->teardown(t);
+  if (t->cyops) t->cyops->teardown(t);
+  xfree(t);
+}
+
+static const struct bench_timerops timer_ops = { timer_now, timer_destroy };
+
+struct bench_timer *bench_createtimer(void)
+{
+  struct timer *t = 0;
+  struct bench_timer *ret = 0;
+
+  t = xmalloc(sizeof(*t)); t->cyops = 0; t->clkops = 0;
+  if (select_timer(t, clktab, "MLIB_BENCH_CLKTIMER", "clock")) goto end;
+  if (select_timer(t, cytab, "MLIB_BENCH_CYCLETIMER", "cycle")) goto end;
+  t->_t.ops = &timer_ops; ret = &t->_t; t = 0;
+end:
+  if (t) timer_destroy(&t->_t);
+  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);
+  }
+}
+
+/*----- Calibration -------------------------------------------------------*/
+
+void bench_init(struct bench_state *b, struct bench_timer *tm)
+  { b->tm = tm; b->target_s = 1.0; b->f = 0; }
+
+void bench_destroy(struct bench_state *b)
+  { b->tm->ops->destroy(b->tm); }
+
+static void do_nothing(unsigned long n, void *p)
+  { while (n--) RELAX; }
+
+int bench_calibrate(struct bench_state *b)
+{
+  struct linreg lr_clk = LINREG_INIT, lr_cy = LINREG_INIT;
+  unsigned long n;
+  unsigned i;
+  struct bench_timer *tm = b->tm;
+  struct bench_time t0, t1;
+  struct bench_timing delta;
+  bench_fn *fn = LAUNDER(&do_nothing);
+  unsigned f = BTF_ANY;
+  int rc;
+
+  if (b->f&BTF_ANY) return (0);
+
+  for (i = 0; i < 10; i++)
+    { tm->ops->now(tm, &t0); fn(1, 0); tm->ops->now(tm, &t1); }
+
+  debug("calibrating...");
+  n = 1;
+  for (;;) {
+    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; }
+    linreg_update(&lr_clk, n, delta.t);
+    if (!(f&BTF_CYOK))
+      debug("  n = %10lu; t = %12g s", n, delta.t);
+    else {
+      linreg_update(&lr_cy, n, delta.cy);
+      debug("  n = %10lu; t = %12g s, cy = %10.0f", n, delta.t, delta.cy);
+    }
+    if (delta.t >= b->target_s/20.0) break;
+    if (n >= ULONG_MAX - n/3) break;
+    n += n/3 + 1;
+  }
+
+  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);
+  }
+  b->f |= f; rc = 0;
+end:
+  return (rc);
+}
+
+int bench_measure(struct bench_timing *t_out, struct bench_state *b,
+                 bench_fn *fn, void *p)
+{
+  struct bench_timer *tm = b->tm;
+  struct bench_time t0, t1;
+  unsigned long n;
+
+  if (bench_calibrate(b)) return (-1);
+  debug("measuring..."); n = 1;
+  for (;;) {
+    tm->ops->now(tm, &t0); fn(n, p); 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);
+    else debug("  n = %10lu; t = %12g, cy = %10.0f", n, t_out->t, t_out->cy);
+    if (t_out->t >= 0.72*b->target_s) break;
+    n *= 1.44*b->target_s/t_out->t;
+  }
+  if (!(t_out->f&BTF_CYOK))
+    debug("  %g s per op; %g ops/s", t_out->t/n, n/t_out->t);
+  else
+    debug("  %g s (%g cy) per op; %g ops/s",
+         t_out->t/n, t_out->cy/n, n/t_out->t);
+  t_out->n = n; return (0);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/test/bench.h b/test/bench.h
new file mode 100644 (file)
index 0000000..f9472cd
--- /dev/null
@@ -0,0 +1,94 @@
+/* -*-c-*-
+ *
+ * Benchmarking support
+ *
+ * (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_BENCH_H
+#define MLIB_BENCH_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Header files ------------------------------------------------------*/
+
+#ifndef MLIB_BITS_H
+#  include "bits.h"
+#endif
+
+/*----- 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;
+};
+
+struct bench_timing {
+  unsigned f;
+  unsigned long n;
+  double t, cy;
+};
+
+struct bench_timer { const struct bench_timerops *ops; };
+
+struct bench_timerops {
+  void (*now)(struct bench_timer */*bt*/, struct bench_time */*t_out*/);
+  void (*destroy)(struct bench_timer */*bt*/);
+};
+
+struct bench_state {
+  struct bench_timer *tm;
+  double target_s;
+  unsigned f;
+  struct { double m, c; } clk, cy;
+};
+
+typedef void bench_fn(unsigned long /*n*/, void */*p*/);
+
+/*----- Functions provided ------------------------------------------------*/
+
+extern struct bench_timer *bench_createtimer(void);
+
+extern void bench_init(struct bench_state *b, struct bench_timer *tm);
+
+extern void bench_destroy(struct bench_state *b);
+
+extern int bench_calibrate(struct bench_state */*b*/);
+
+extern int bench_measure(struct bench_timing */*t_out*/,
+                        struct bench_state */*b*/,
+                        bench_fn */*fn*/, void */*p*/);
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif
diff --git a/test/t/tvec-test.c b/test/t/tvec-test.c
new file mode 100644 (file)
index 0000000..6bcf025
--- /dev/null
@@ -0,0 +1,274 @@
+/* -*-c-*-
+ *
+ * Test the test-vector framework
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "tvec.h"
+
+/*----- Register definitions ----------------------------------------------*/
+
+enum {
+  /* Standard outputs. */
+  RRC,                                 /* return code from deserialize */
+
+  /* Output registers, one for each register type. */
+  RI, RU, RIE, RUE, RPE, RF, RSTR, RBY, RBUF,
+
+  /* Additional diagnostic outputs. */
+  RSER,                                        /* serialized data */
+
+  NROUT,
+
+  /* Some additional inputs. */
+  RSAB = NROUT,                                /* which register to sabotage */
+
+  NREG,
+
+  /* Single register for copy tests. */
+  RV = 0
+};
+
+static const struct tvec_iassoc ienum_assocs[] = {
+  { "less",           -1 },
+  { "equal",           0 },
+  { "greater",        +1 },
+  { 0 }
+};
+
+static const struct tvec_uassoc uenum_assocs[] = {
+  { "apple",           0 },
+  { "banana",          1 },
+  { "clementine",      2 },
+  { 0 }
+};
+
+static const struct tvec_passoc penum_assocs[] = {
+  { "alice",           &uenum_assocs[0] },
+  { "bob",             &uenum_assocs[1] },
+  { "carol",           &uenum_assocs[2] },
+  { 0 }
+};
+
+#if __STDC_VERSION__ >= 199901
+#  define DSGINIT(x) x
+#else
+#  define DSGINIT(x)
+#endif
+
+static DSGINIT(const) struct tvec_enuminfo
+  ienum_info = { "order", TVMISC_INT,
+                DSGINIT({ .i = { ienum_assocs COMMA &tvrange_i16 } }) },
+  uenum_info = { "fruit", TVMISC_UINT,
+                DSGINIT({ .u = { uenum_assocs COMMA &tvrange_u16 } }) },
+  penum_info = { "player", TVMISC_PTR,
+                DSGINIT({ .p = { penum_assocs } }) };
+
+static const struct tvec_flag attr_flags[] = {
+  { "black-fg",                0x07, 0x00 },
+  { "blue-fg",         0x07, 0x01 },
+  { "red-fg",          0x07, 0x02 },
+  { "magenta-fg",      0x07, 0x03 },
+  { "green-fg",                0x07, 0x04 },
+  { "cyan-fg",         0x07, 0x05 },
+  { "yellow-fg",       0x07, 0x06 },
+  { "white-fg",                0x07, 0x07 },
+
+  { "black-bg",                0x38, 0x00 },
+  { "blue-bg",         0x38, 0x08 },
+  { "red-bg",          0x38, 0x10 },
+  { "magenta-bg",      0x38, 0x18 },
+  { "green-bg",                0x38, 0x20 },
+  { "cyan-bg",         0x38, 0x28 },
+  { "yellow-bg",       0x38, 0x30 },
+  { "white-bg",                0x38, 0x38 },
+
+  { "normal",          0xc0, 0x00 },
+  { "bright",          0x40, 0x40 },
+  { "flash",           0x80, 0x80 },
+
+  { 0 }
+};
+
+static const struct tvec_flaginfo attr_info =
+  { "attr", attr_flags, &tvrange_u16 };
+
+static const struct tvec_urange range_32 = { 0, 31 };
+
+#define TYPEREGS(_)                                                    \
+  _(int,       RI,     int,                    p, &tvrange_i16)        \
+  _(uint,      RU,     uint,                   p, &tvrange_u16)        \
+  _(ienum,     RIE,    enum,                   p, &ienum_info)         \
+  _(uenum,     RUE,    enum,                   p, &uenum_info)         \
+  _(penum,     RPE,    enum,                   p, &penum_info)         \
+  _(flags,     RF,     flags,                  p, &attr_info)          \
+  _(string,    RSTR,   string,                 p, &range_32)           \
+  _(bytes,     RBY,    bytes,                  p, &tvrange_byte)       \
+  _(buffer,    RBUF,   buffer,                 p, &tvrange_u16)
+
+/*----- Serialization test ------------------------------------------------*/
+
+struct test_context {
+  struct tvec_state *tv;
+};
+
+static void capture_state_and_run(struct tvec_state *tv)
+{
+  struct test_context tctx;
+
+  tctx.tv = tv; tv->test->fn(tv->in, tv->out, &tctx);
+  if (!(tv->in[RRC].f&TVRF_LIVE)) {
+    tv->in[RRC].v.i = 0;
+    tv->in[RRC].f |= TVRF_LIVE; tv->out[RRC].f |= TVRF_LIVE;
+  }
+  tvec_check(tv, 0);
+}
+
+static void test_serialization
+  (const struct tvec_reg *in, struct tvec_reg *out, void *ctx)
+{
+  struct test_context *tctx = ctx;
+  struct tvec_state *tv = tctx->tv;
+  const struct tvec_regdef *rd;
+  union tvec_regval *rv;
+  void *p; size_t sz;
+
+  if (tvec_serialize(tv->in, tv->test->regs,
+                    NROUT, sizeof(struct tvec_reg), &p, &sz))
+    { out[RRC].v.i = -1; return; }
+  out[RSER].f |= TVRF_LIVE;
+  out[RSER].v.bytes.p = p; out[RSER].v.bytes.sz = sz;
+
+  if (tvec_deserialize(tv->out, tv->test->regs,
+                       NROUT, sizeof(struct tvec_reg), p, sz))
+    { out[RRC].v.i = -1; return; }
+
+  if (in[RSAB].f&TVRF_LIVE) {
+    for (rd = tv->test->regs; rd->name; rd++)
+      if (STRCMP(in[RSAB].v.str.p, ==, rd->name)) {
+       rv = &out[rd->i].v;
+       if (rd->ty == &tvty_int ||
+           (rd->ty == &tvty_enum &&
+            ((struct tvec_enuminfo *)rd->arg.p)->mv == TVMISC_INT))
+         rv->i ^= 1;
+       else if (rd->ty == &tvty_uint || rd->ty == &tvty_flags ||
+                (rd->ty == &tvty_enum &&
+                 ((struct tvec_enuminfo *)rd->arg.p)->mv == TVMISC_INT))
+         rv->u ^= 1;
+       else if (rd->ty == &tvty_string)
+         { if (rv->str.sz) rv->str.p[0] ^= 1; }
+       else if (rd->ty == &tvty_bytes)
+         { if (rv->bytes.sz) rv->bytes.p[0] ^= 1; }
+      }
+  }
+
+  out[RRC].v.i = 0;
+}
+
+DSGINIT(static) const struct tvec_regdef test_regs[] = {
+#define DEFREG(name, i, ty, argslot, argval)                           \
+  { #name,     i,      &tvty_##ty,     TVRF_OPT,                       \
+                                           DSGINIT({ .argslot = argval }) },
+  TYPEREGS(DEFREG)
+#undef DEFREG
+  { "rc",      RRC,    &tvty_int,      TVRF_OPT,       { &tvrange_int } },
+  { "serialized", RSER,        &tvty_bytes,    TVRF_OPT },
+  { "sabotage",        RSAB,   &tvty_string,   TVRF_OPT,       { &tvrange_byte } },
+
+  { 0 }
+};
+
+/*----- Single-type copy tests --------------------------------------------*/
+
+static void test_copy_simple
+  (const struct tvec_reg *in, struct tvec_reg *out, void *ctx)
+  { out->v = in->v; }
+
+static void test_copy_string
+  (const struct tvec_reg *in, struct tvec_reg *out, void *ctx)
+{
+  tvec_allocstring(&out->v, in->v.str.sz);
+  memcpy(out->v.str.p, in->v.str.p, in->v.str.sz);
+}
+
+static void test_copy_bytes
+  (const struct tvec_reg *in, struct tvec_reg *out, void *ctx)
+{
+  tvec_allocstring(&out->v, in->v.str.sz);
+  memcpy(out->v.str.p, in->v.str.p, in->v.str.sz);
+}
+
+#define test_copy_int test_copy_simple
+#define test_copy_uint test_copy_simple
+#define test_copy_ienum test_copy_simple
+#define test_copy_uenum test_copy_simple
+#define test_copy_penum test_copy_simple
+#define test_copy_flags test_copy_simple
+#define test_copy_buffer test_copy_bytes
+
+#define SINGLEREG(name, i, ty, argslot, argval)                                \
+       DSGINIT(const) struct tvec_regdef name##_regs[] = {             \
+         { #name, RV, &tvty_##ty, 0, DSGINIT({ .argslot = argval }) }, \
+         { 0 }                                                         \
+       };
+TYPEREGS(SINGLEREG)
+#undef SINGLEREG
+
+/*----- Front end ---------------------------------------------------------*/
+
+static const struct tvec_test tests[] = {
+  { "types",   test_regs,      0,      capture_state_and_run,
+                                                       test_serialization },
+
+#define DEFCOPY(name, i, ty, argslot, argval)                          \
+  { #name,     name##_regs,    0,      tvec_runtest,   test_copy_##name },
+  TYPEREGS(DEFCOPY)
+#undef DEFCOPY
+
+  { 0 }
+};
+
+static const struct tvec_info testinfo = {
+  tests,
+  NROUT, NREG, sizeof(struct tvec_reg)
+};
+
+int main(int argc, char *argv[])
+{
+#if __STDC_VERSION__ < 199901
+#  define POKE(tag, ty, slot)                                          \
+       slot##enum_info.u.slot.av = slot##enum_assocs;                  \
+  TVEC_MISCSLOTS(POKE)
+#  undef POKE
+#  define POKE(name, i, ty, argslot, argval)                           \
+       name##_regs->arg.argslot = argval;
+  TYPEREGS(POKE)
+#  undef POKE
+#endif
+  return (tvec_main(argc, argv, &testinfo, 0));
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/test/tests.at b/test/tests.at
new file mode 100644 (file)
index 0000000..7088199
--- /dev/null
@@ -0,0 +1,132 @@
+### -*-autotest-*-
+###
+### Test script for test machinery
+###
+### (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.
+
+###--------------------------------------------------------------------------
+### tvec
+
+dnl test_filter(CMD, SED, RC, STDOUT, STDERR)
+m4_define([test_filter], [
+AT_CHECK([$1], [$3], [stdout-nolog], [stderr-nolog])
+##mv stdout rawout; mv stderr rawerr
+AT_CHECK([sed "AS_ESCAPE([$2])" stdout], [0], [$4])
+AT_CHECK([sed "AS_ESCAPE([$2])" stderr], [0], [$5])])
+
+dnl mismatch_filter
+m4_define([mismatch_filter],
+       [s/\(@%:@<@<:@0-9a-zA-Z_-@:>@*\) @<:@^>@:>@*>/\1 ...>/g])
+
+dnl test_parse(TY, IN, OUT)
+m4_define([test_parse], [
+AT_DATA([tv],
+[;;; -*-conf-*-
+[[$1]]
+$1 = $2
+@status = ?
+])
+test_filter([BUILDDIR/t/tvec.t -fh tv], [mismatch_filter], [1],
+[tv:3: `$1' FAILED
+    actual status = `.'
+  expected status = `?'
+   matched $1 = $3
+$1: 1/1 FAILED 
+FAILED 1 out of 1 test in 1 out of 1 group
+])])
+
+dnl test_parserr(TY, IN, LNO, ERR)
+m4_define([test_parserr], [
+AT_DATA([tv],
+[;;; -*-conf-*-
+[[$1]]
+$1 = $2
+@status = ?
+])
+AT_CHECK([BUILDDIR/t/tvec.t -fh tv], [2], [],
+[tvec.t: tv:$3: $4
+])])
+
+AT_SETUP(tvec type-int)
+test_parse([int], [4], [4 ; = 0x04])
+test_parse([int], [ 17; comment], [17 ; = 0x11])
+test_parserr([int], [17 : badness], [3],
+       [syntax error: expected end-of-line but found `:'])
+test_parserr([int], [17: badness], [3],
+       [syntax error: expected end-of-line but found `:'])
+test_parse([int], [0x234], [564 ; = 0x0234])
+test_parse([int], [033], [27 ; = 0x1b])
+test_parse([int], [ +192], [192 ; = 0xc0])
+test_parse([int], [ -192], [-192 ; = -0xc0])
+test_parserr([int], [xyzzy], [3],
+       [syntax error: expected signed integer but found `x'])
+test_parserr([int], [-splat], [3],
+       [syntax error: expected signed integer but found `s'])
+test_parserr([int], [0xq], [3],
+       [syntax error: expected end-of-line but found `x'])
+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>])
+test_parserr([int], [123456], [3],
+       [integer 123456 out of range (must be in [[-32768 .. 32767]])])
+AT_CLEANUP
+
+AT_SETUP(tvec type-uint)
+test_parse([uint], [4], [4 ; = 0x04])
+test_parse([uint], [ 17; comment], [17 ; = 0x11])
+test_parserr([uint], [17 : badness], [3],
+       [syntax error: expected end-of-line but found `:'])
+test_parserr([uint], [17: badness], [3],
+       [syntax error: expected end-of-line but found `:'])
+test_parse([uint], [0x234], [564 ; = 0x0234])
+test_parse([uint], [033], [27 ; = 0x1b])
+test_parserr([uint], [ +192], [3],
+       [syntax error: expected unsigned integer but found `+'])
+test_parserr([uint], [ -192], [3],
+       [syntax error: expected unsigned integer but found `-'])
+test_parserr([uint], [xyzzy], [3],
+       [syntax error: expected unsigned integer but found `x'])
+test_parserr([uint], [0xq], [3],
+       [syntax error: expected end-of-line but found `x'])
+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>])
+test_parserr([uint], [123456], [3],
+       [integer 123456 out of range (must be in [[0 .. 65535]])])
+AT_CLEANUP
+
+AT_SETUP([tvec type-enum])
+test_parse([ienum], [less], [less ; = -1 = -0x01])
+test_parse([ienum], [+1], [greater ; = 1 = 0x01])
+test_parse([ienum], [17], [17 ; = 0x11])
+test_parse([uenum], [banana], [banana ; = 1 = 0x01])
+test_parse([uenum], [clementine], [clementine ; = 2 = 0x02])
+test_parse([uenum], [17], [17 ; = 0x11])
+test_parse([penum], [carol], [carol ; = @%:@<player ...>])
+test_parse([penum], [alice], [alice ; = @%:@<player ...>])
+test_parse([penum], [@%:@nil], [@%:@nil])
+AT_CLEANUP
+
+###----- That's all, folks --------------------------------------------------
diff --git a/test/tvec-core.c b/test/tvec-core.c
new file mode 100644 (file)
index 0000000..dc2f8ab
--- /dev/null
@@ -0,0 +1,746 @@
+/* -*-c-*-
+ *
+ * Main test vector driver
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <assert.h>
+#include <ctype.h>
+#include <string.h>
+
+#include "alloc.h"
+#include "bench.h"
+#include "tvec.h"
+
+/*----- Output ------------------------------------------------------------*/
+
+void tvec_error(struct tvec_state *tv, const char *msg, ...)
+  {  va_list ap; va_start(ap, msg); tvec_error_v(tv, msg, &ap); }
+void tvec_error_v(struct tvec_state *tv, const char *msg, va_list *ap)
+  { tv->output->ops->error(tv->output, msg, ap); exit(2); }
+
+void tvec_notice(struct tvec_state *tv, const char *msg, ...)
+{
+  va_list ap;
+  va_start(ap, msg); tvec_notice_v(tv, msg, &ap); va_end(ap);
+}
+void tvec_notice_v(struct tvec_state *tv, const char *msg, va_list *ap)
+  { tv->output->ops->notice(tv->output, msg, ap); }
+
+void 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);
+}
+void 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>"); 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);
+}
+
+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);
+}
+
+static void set_outcome(struct tvec_state *tv, unsigned out)
+{
+  tv->f &= ~(TVSF_ACTIVE | TVSF_OUTMASK);
+  tv->f |= out << TVSF_OUTSHIFT;
+}
+
+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);
+  set_outcome(tv, TVOUT_SKIP);
+  tv->output->ops->skip(tv->output, excuse, ap);
+}
+
+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) ||
+        (tv->f&TVSF_OUTMASK) == (TVOUT_LOSE << TVSF_OUTSHIFT));
+  set_outcome(tv, TVOUT_LOSE); tv->output->ops->fail(tv->output, detail, ap);
+}
+
+void tvec_mismatch(struct tvec_state *tv)
+  { tv->output->ops->mismatch(tv->output); }
+
+void tvec_write(struct tvec_state *tv, const char *p, ...)
+{
+  va_list ap;
+  va_start(ap, p); tvec_write_v(tv, p, &ap); va_end(ap);
+}
+void tvec_write_v(struct tvec_state *tv, const char *p, va_list *ap)
+{
+  dstr d = DSTR_INIT;
+
+  dstr_vputf(&d, p, ap); tv->output->ops->write(tv->output, d.buf, d.len);
+  DDESTROY(&d);
+}
+
+/*----- Serialization and deserialization ---------------------------------*/
+
+int tvec_serialize(const struct tvec_reg *rv,
+                  const struct tvec_regdef *regs,
+                  unsigned nr, size_t regsz,
+                  void **p_out, size_t *sz_out)
+{
+  void *p = 0; buf b;
+  unsigned char *bitmap;
+  size_t i, nbits, bitsz, sz;
+  const struct tvec_regdef *rd;
+  const struct tvec_reg *r;
+  int rc;
+
+  for (rd = regs, nbits = 0, sz = 0; rd->name; rd++, nbits++) {
+    if (rd->i >= nr) continue;
+    r = TVEC_GREG(rv, rd->i, regsz); if (!(r->f&TVRF_LIVE)) continue;
+    sz += rd->ty->measure(&r->v, rd);
+  }
+  bitsz = (nbits + 7)/8; sz += bitsz;
+
+  p = xmalloc(sz); buf_init(&b, p, sz);
+  bitmap = buf_get(&b, bitsz); assert(bitmap); memset(bitmap, 0, bitsz);
+  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[rd->i/8] |= 1 << rd->i%8;
+    if (rd->ty->tobuf(&b, &r->v, rd)) { rc = -1; goto end; }
+  }
+
+  if (BBAD(&b)) { rc = -1; goto end; }
+  *p_out = p; *sz_out = BLEN(&b); p = 0; rc = 0;
+end:
+  xfree(p);
+  return (rc);
+}
+
+int tvec_deserialize(struct tvec_reg *rv,
+                    const struct tvec_regdef *regs,
+                    unsigned nr, size_t regsz,
+                    const void *p, size_t sz)
+{
+  buf b;
+  const unsigned char *bitmap;
+  size_t i, nbits, bitsz;
+  const struct tvec_regdef *rd;
+  struct tvec_reg *r;
+  int rc;
+
+  for (rd = regs, nbits = 0; rd->name; rd++, nbits++);
+  bitsz = (nbits + 7)/8; sz += bitsz;
+
+  buf_init(&b, (/*unconst*/ void *)p, sz);
+  bitmap = buf_get(&b, bitsz); if (!bitmap) { rc = -1; goto end; }
+  for (rd = regs, i = 0; rd->name; rd++, i++) {
+    if (rd->i >= nr) continue;
+    if (!(bitmap[rd->i/8]&(1 << rd->i%8))) continue;
+    r = TVEC_GREG(rv, rd->i, regsz);
+    if (rd->ty->frombuf(&b, &r->v, rd)) { rc = -1; goto end; }
+    r->f |= TVRF_LIVE;
+  }
+
+  if (BBAD(&b)) { rc = -1; goto end; }
+  rc = 0;
+end:
+  return (rc);
+}
+
+/*----- Benchmarking ------------------------------------------------------*/
+
+struct bench_state *tvec_benchstate;
+
+struct benchrun {
+  unsigned long *n;
+  tvec_testfn *fn;
+  const struct tvec_reg *in; struct tvec_reg *out;
+  void *ctx;
+};
+
+static void benchloop_outer(unsigned long n, void *p)
+  { struct benchrun *r = p; while (n--) r->fn(r->in, r->out, r->ctx); }
+
+static void benchloop_inner(unsigned long n, void *p)
+  { struct benchrun *r = p; *r->n = n; r->fn(r->in, r->out, r->ctx); }
+
+int tvec_ensurebench(struct tvec_state *tv, struct bench_state **b_out)
+{
+  const struct tvec_bench *tvb = tv->test->arg.p;
+  struct bench_state **bb;
+  struct bench_timer *bt;
+
+  if (tvb->b) bb = tvb->b;
+  else bb = &tvec_benchstate;
+
+  if (!*bb) {
+    bt = bench_createtimer();
+    if (!bt) { tvec_skip(tv, "failed to create timer"); return (-1); }
+    *bb = xmalloc(sizeof(**bb)); bench_init(*bb, bt);
+  } else if (!(*bb)->tm)
+    { tvec_skip(tv, "failed to create timer"); return (-1); }
+
+  *b_out = *bb;
+  return (0);
+}
+
+void tvec_bench(struct tvec_state *tv)
+{
+  const struct tvec_bench *tvb = tv->test->arg.p;
+  struct bench_state *b;
+  struct bench_timing tm;
+  struct benchrun r;
+  bench_fn *loopfn;
+
+  if (tvec_ensurebench(tv, &b)) goto end_0;
+
+  r.in = tv->in; r.out = tv->out; r.fn = tv->test->fn;
+  if (tvb->ctxsz) r.ctx = xmalloc(tvb->ctxsz);
+  else r.ctx = 0;
+  if (tvb->setup && tvb->setup(tv->in, tv->out, &tvb->arg, r.ctx))
+    { tvec_skip(tv, "benchmark setup failed"); goto end_1; }
+
+  if (tvb->riter < 0)
+    { r.n = 0; loopfn = benchloop_outer; }
+  else
+    { r.n = &TVEC_REG(tv, in, tvb->riter)->v.u; loopfn = benchloop_inner; }
+
+  tv->output->ops->bbench(tv->output);
+  if (bench_measure(&tm, b, loopfn, &r))
+    { tv->output->ops->ebench(tv->output, 0); goto end_2; }
+  tv->output->ops->ebench(tv->output, &tm);
+
+end_2:
+  if (tvb->teardown) tvb->teardown(r.ctx);
+end_1:
+  if (r.ctx) xfree(r.ctx);
+end_0:
+  return;
+}
+
+/*----- Main machinery ----------------------------------------------------*/
+
+void tvec_skipspc(struct tvec_state *tv)
+{
+  int ch;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    if (ch == EOF) break;
+    else if (ch == '\n' || !isspace(ch)) { ungetc(ch, tv->fp); break; }
+  }
+}
+
+void tvec_flushtoeol(struct tvec_state *tv, unsigned f)
+{
+  int ch;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    switch (ch) {
+      case '\n': tv->lno++; return;
+      case EOF: return;
+      case ';': f |= TVFF_ALLOWANY; break;
+      default:
+       if (!(f&TVFF_ALLOWANY) && !isspace(ch))
+         tvec_syntax(tv, ch, "end-of-line");
+       break;
+    }
+  }
+}
+
+int tvec_nexttoken(struct tvec_state *tv)
+{
+  enum { TAIL, NEWLINE, INDENT, COMMENT };
+  int ch;
+  unsigned s = TAIL;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    switch (ch) {
+      case EOF:
+       return (-1);
+
+      case ';':
+       s = COMMENT;
+       break;
+
+      case '\n':
+       if (s == NEWLINE || s == INDENT) { ungetc(ch, tv->fp); return (-1); }
+       else { tv->lno++; s = NEWLINE; }
+       break;
+
+      default:
+       if (isspace(ch))
+         { if (s == NEWLINE) s = INDENT; }
+       else if (s != COMMENT) {
+         ungetc(ch, tv->fp);
+         if (s == NEWLINE) return (-1);
+         else return (0);
+       }
+       break;
+    }
+  }
+}
+
+int tvec_readword(struct tvec_state *tv, dstr *d, const char *delims,
+                 const char *expect, ...)
+{
+  va_list ap;
+  int rc;
+
+  va_start(ap, expect);
+  rc = tvec_readword_v(tv, d, 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 ch;
+
+  ch = getc(tv->fp);
+  if (ch == '\n' || ch == EOF || ch == ';' ||
+      (delims && strchr(delims, ch))) {
+    if (expect) tvec_syntax(tv, ch, expect, ap);
+    else { ungetc(ch, tv->fp); return (-1); }
+  }
+  if (d->len) DPUTC(d, ' ');
+  do {
+    DPUTC(d, ch);
+    ch = getc(tv->fp);
+  } while (ch != EOF && !isspace(ch) && (!delims || !strchr(delims, ch)));
+  DPUTZ(d); if (ch != EOF) ungetc(ch, tv->fp);
+  return (0);
+}
+
+static void init_registers(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; }
+  }
+  tv->expst = '.';
+}
+
+static void release_registers(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; }
+  }
+}
+
+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)
+{
+  const struct tvec_regdef *rd;
+  const struct tvec_reg *rin, *rout;
+  unsigned f = 0;
+#define f_mismatch 1u
+
+  if (tv->expst != tv->st) f |= f_mismatch;
+  for (rd = tv->test->regs; rd->name; rd++) {
+    if (rd->i >= tv->nrout) continue;
+    rin = TVEC_REG(tv, in, rd->i); rout = TVEC_REG(tv, out, rd->i);
+    if (!rin->f&TVRF_LIVE) continue;
+    if (!rd->ty->eq(&rin->v, &rout->v, rd)) f |= f_mismatch;
+  }
+  if (!(f&f_mismatch)) return;
+
+  tvec_fail_v(tv, detail, ap);
+  tvec_mismatch(tv);
+
+#undef f_mismatch
+}
+
+void tvec_runtest(struct tvec_state *tv)
+{
+  tv->test->fn(tv->in, tv->out, (/*unconst*/ void *)tv->test->arg.p);
+  tvec_check(tv, 0);
+}
+
+static void begin_test(struct tvec_state *tv)
+{
+  tv->f |= TVSF_ACTIVE; tv->f &= ~TVSF_OUTMASK; tv->st = '.';
+  tv->output->ops->btest(tv->output);
+}
+
+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;
+  assert(out < TVOUT_LIMIT); tv->curr[out]++;
+  tv->output->ops->etest(tv->output, out);
+  tv->f &= ~TVSF_OPEN;
+}
+
+static void check(struct tvec_state *tv)
+{
+  const struct tvec_regdef *rd;
+
+  if (!(tv->f&TVSF_OPEN)) return;
+
+  for (rd = tv->test->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))
+      tvec_error(tv, "required register `%s' not set in test `%s'",
+                rd->name, tv->test->name);
+  }
+
+  if (!(tv->f&TVSF_SKIP))
+    { begin_test(tv); tv->test->run(tv); tvec_endtest(tv); }
+
+  tv->f &= ~TVSF_OPEN; release_registers(tv); init_registers(tv);
+}
+
+static void begin_test_group(struct tvec_state *tv)
+{
+  unsigned i;
+
+  tv->output->ops->bgroup(tv->output);
+  tv->f &= ~TVSF_SKIP;
+  init_registers(tv);
+  for (i = 0; i < TVOUT_LIMIT; i++) tv->curr[i] = 0;
+  if (tv->test->preflight) tv->test->preflight(tv);
+}
+
+void tvec_reportgroup(struct tvec_state *tv)
+{
+  unsigned i, out, nrun;
+
+  for (i = 0, nrun = 0; i < TVOUT_LIMIT; i++)
+    { nrun += tv->curr[i]; tv->all[i] += tv->curr[i]; }
+
+  if (tv->curr[TVOUT_SKIP] == nrun)
+    { out = TVOUT_SKIP; tvec_skipgroup(tv, nrun ? 0 : "no tests to run"); }
+  else {
+    if (tv->curr[TVOUT_LOSE]) out = TVOUT_LOSE;
+    else out = TVOUT_WIN;
+    tv->grps[out]++; tv->output->ops->egroup(tv->output, out);
+  }
+}
+
+static void end_test_group(struct tvec_state *tv)
+{
+  if (!tv->test) return;
+  if (tv->f&TVSF_OPEN) check(tv);
+  if (!(tv->f&TVSF_SKIP)) tvec_reportgroup(tv);
+  release_registers(tv); tv->test = 0;
+}
+
+void tvec_read(struct tvec_state *tv, const char *infile, FILE *fp)
+{
+  dstr d = DSTR_INIT;
+  const struct tvec_test *test;
+  const struct tvec_regdef *rd;
+  struct tvec_reg *r;
+  int ch;
+
+  tv->infile = infile; tv->lno = 1; tv->fp = fp;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    switch (ch) {
+
+      case EOF:
+       goto end;
+
+      case '[':
+       tvec_skipspc(tv);
+       DRESET(&d); tvec_readword(tv, &d, "];", "group name");
+       for (test = tv->tests; test->name; test++)
+         if (STRCMP(d.buf, ==, test->name)) goto found_test;
+       tvec_error(tv, "unknown test group `%s'", d.buf);
+      found_test:
+       tvec_skipspc(tv);
+       ch = getc(tv->fp); if (ch != ']') tvec_syntax(tv, ch, "`]'");
+       tvec_flushtoeol(tv, 0);
+       end_test_group(tv);
+       tv->test = test;
+       begin_test_group(tv);
+       break;
+
+      case '\n':
+       tv->lno++;
+       if (tv->f&TVSF_OPEN) check(tv);
+       break;
+
+      default:
+       if (isspace(ch)) {
+         tvec_skipspc(tv);
+         ch = getc(tv->fp);
+         if (ch == EOF)
+           goto end;
+         else if (ch == ';')
+           { tvec_flushtoeol(tv, TVFF_ALLOWANY); continue; }
+         else
+           { tvec_flushtoeol(tv, 0); check(tv); }
+       } else if (ch == ';')
+         { tvec_flushtoeol(tv, TVFF_ALLOWANY); continue; }
+       else {
+         ungetc(ch, tv->fp);
+         DRESET(&d); tvec_readword(tv, &d, "=:;", "register name");
+         tvec_skipspc(tv); ch = getc(tv->fp);
+         if (ch != '=' && ch != ':') tvec_syntax(tv, ch, "`=' or `:'");
+         tvec_skipspc(tv);
+         if (d.buf[0] == '@') {
+           if (STRCMP(d.buf, ==, "@status")) {
+             if (!(tv->f&TVSF_OPEN))
+               { tv->test_lno = tv->lno; tv->f |= TVSF_OPEN; }
+             ch = getc(tv->fp);
+             if (ch == EOF || ch == '\n' || ch == ';')
+               tvec_syntax(tv, ch, "status character");
+             if (ch == '\\') {
+               ch = getc(tv->fp);
+               if (ch == EOF || ch == '\n')
+                 tvec_syntax(tv, ch, "escaped status character");
+             }
+             tv->expst = ch;
+             tvec_flushtoeol(tv, 0);
+           } else
+             tvec_error(tv, "unknown special register `%s'", d.buf);
+         } else {
+           if (!tv->test) tvec_error(tv, "no current test");
+           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);
+         found_reg:
+           if (!(tv->f&TVSF_OPEN))
+             { tv->test_lno = tv->lno; tv->f |= TVSF_OPEN; }
+           tvec_skipspc(tv);
+           r = TVEC_REG(tv, in, rd->i);
+           if (r->f&TVRF_LIVE)
+             tvec_error(tv, "register `%s' already set", rd->name);
+           rd->ty->parse(&r->v, rd, tv); r->f |= TVRF_LIVE;
+         }
+       }
+       break;
+    }
+  }
+end:
+  end_test_group(tv);
+  tv->infile = 0; tv->fp = 0;
+  dstr_destroy(&d);
+}
+
+/*----- Ad-hoc testing ----------------------------------------------------*/
+
+static const struct tvec_regdef no_regs = { 0, 0, 0, 0, { 0 } };
+
+static void fakerun(struct tvec_state *tv)
+  { assert(!"fake run function"); }
+static void fakefn(const struct tvec_reg *in, struct tvec_reg *out, void *p)
+  { assert(!"fake test function"); }
+
+void tvec_adhoc(struct tvec_state *tv, struct tvec_test *t)
+{
+  t->name = "<unset>"; t->regs = &no_regs;
+  t->preflight = 0; t->run = fakerun; t->fn = fakefn; t->arg.p = 0;
+  tv->tests = t;
+}
+
+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;
+
+  t->name = name; tv->test = t;
+  tv->infile = file; tv->lno = tv->test_lno = lno;
+  begin_test_group(tv);
+}
+
+void tvec_endgroup(struct tvec_state *tv)
+{
+  if (!(tv->f&TVSF_SKIP)) tvec_reportgroup(tv);
+  tv->test = 0;
+}
+
+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;
+}
+
+struct adhoc_claim {
+  unsigned f;
+#define ACF_FRESH 1u
+  const char *saved_file; unsigned saved_lno;
+};
+
+static void adhoc_claim_setup(struct tvec_state *tv,
+                             struct adhoc_claim *ck,
+                             const struct tvec_regdef *regs,
+                             const char *file, unsigned lno)
+{
+  struct tvec_test *t = (/*unconst*/ struct tvec_test *)tv->test;
+
+  ck->f = 0;
+
+  if (!(tv->f&TVSF_OPEN))
+    { ck->f |= ACF_FRESH; tvec_begintest(tv, file, lno); }
+
+  ck->saved_file = tv->infile; if (file) tv->infile = file;
+  ck->saved_lno = tv->test_lno; if (file) tv->test_lno = lno;
+  t->regs = regs ? regs : &no_regs;
+
+  tv->st = '.';
+}
+
+static void adhoc_claim_teardown(struct tvec_state *tv,
+                                struct adhoc_claim *ck)
+{
+  struct tvec_test *t = (/*unconst*/ struct tvec_test *)tv->test;
+
+  t->regs = &no_regs;
+  tv->infile = ck->saved_file; tv->test_lno = ck->saved_lno;
+
+  if (ck->f&ACF_FRESH) tvec_endtest(tv);
+}
+
+int tvec_claim(struct tvec_state *tv, int ok,
+              const char *file, unsigned lno, const char *expr, ...)
+{
+  struct adhoc_claim ck;
+  va_list ap;
+
+  adhoc_claim_setup(tv, &ck, 0, file, lno);
+  if (!ok)
+    { va_start(ap, expr); tvec_fail_v(tv, expr, &ap); va_end(ap); }
+  adhoc_claim_teardown(tv, &ck);
+  return (ok);
+}
+
+int tvec_claimeq(struct tvec_state *tv,
+                const struct tvec_regty *ty, const union tvec_misc *arg,
+                const char *file, unsigned lno, const char *expr)
+{
+  struct tvec_regdef regs[2];
+  struct adhoc_claim ck;
+  int ok;
+
+  tv->in[0].f = tv->out[0].f = TVRF_LIVE;
+
+  regs[0].name = "value"; regs[0].i = 0;
+  regs[0].ty = ty; regs[0].f = 0;
+  if (arg) regs[0].arg = *arg;
+  else regs[0].arg.p = 0;
+
+  regs[1].name = 0;
+
+  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); }
+  adhoc_claim_teardown(tv, &ck);
+  return (ok);
+}
+
+/*----- Session lifecycle -------------------------------------------------*/
+
+void tvec_begin(struct tvec_state *tv_out,
+               const struct tvec_info *info,
+               struct tvec_output *o)
+{
+  unsigned i;
+
+  tv_out->f = 0;
+
+  assert(info->nrout <= info->nreg);
+  tv_out->nrout = info->nrout; tv_out->nreg = info->nreg;
+  tv_out->regsz = info->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++) {
+    TVEC_REG(tv_out, in, i)->f = 0;
+    if (i < tv_out->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 = info->tests; tv_out->test = 0;
+  tv_out->infile = 0; tv_out->lno = 0; tv_out->fp = 0;
+  o->tv = tv_out; tv_out->output = o;
+
+  tv_out->output->ops->bsession(tv_out->output);
+}
+
+int tvec_end(struct tvec_state *tv)
+{
+  int rc = tv->output->ops->esession(tv->output);
+
+  tv->output->ops->destroy(tv->output);
+  xfree(tv->in); xfree(tv->out);
+  return (rc);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/test/tvec-main.c b/test/tvec-main.c
new file mode 100644 (file)
index 0000000..ea597ad
--- /dev/null
@@ -0,0 +1,234 @@
+/* -*-c-*-
+ *
+ * Main entry point for test-vector processing
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "config.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <unistd.h>
+
+#include "bench.h"
+#include "dstr.h"
+#include "macros.h"
+#include "mdwopt.h"
+#include "quis.h"
+#include "report.h"
+#include "tvec.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+static const struct outform {
+  const char *name;
+  struct tvec_output *(*makefn)(FILE *fp);
+} outtab[] = {
+  { "human",           tvec_humanoutput },
+  { "tap",             tvec_tapoutput },
+  { 0,                 0 }
+};
+
+static struct bench_state benchstate;
+
+const struct tvec_info tvec_adhocinfo =
+  { 0, 1, 1, sizeof(struct tvec_reg) };
+
+static const struct outform *find_outform(const char *p)
+{
+  const struct outform *best = 0, *of;
+  size_t n = strlen(p);
+
+  for (of = outtab; of->name; of++)
+    if (!STRNCMP(p, ==, of->name, n))
+      ;
+    else if (!of->name[n])
+      return (of);
+    else if (best)
+      die(2, "ambiguous output format name (`%s' or `%s'?)",
+         best->name, of->name);
+    else
+      best = of;
+  if (best) return (best);
+  else die(2, "unknown output format `%s'", optarg);
+}
+
+static void version(FILE *fp)
+  { pquis(fp, "$, mLib test-vector framework version " VERSION "\n"); }
+
+static void usage(FILE *fp)
+{
+  pquis(fp, "\
+usage: $ [-f FORMAT] [-o OUTPUT] [-t SECS] [TEST-FILE ...]\n\
+");
+}
+
+static void help(FILE *fp)
+{
+  version(fp); fputc('\n', fp);
+  usage(fp); fputs("\
+Options:\n\
+  -h, --help           show this help text.\n\
+  -v, --version                show version number.\n\
+  -u, --usage          show usage synopsis.\n\
+\n\
+  -f, --format=FORMAT  produce output in FORMAT.\n\
+  -o, --output=OUTPUT  write output to OUTPUT file.\n\
+  -t, --target=SECS    aim to run benchmarks for SECS.\n\
+", fp);
+}
+
+void tvec_parseargs(int argc, char *argv[], struct tvec_state *tv_out,
+                   int *argpos_out, const struct tvec_info *info)
+{
+  FILE *ofp = 0;
+  const struct outform *of = 0;
+  struct tvec_output *o;
+  struct bench_timer *tm;
+  const char *p; char *q;
+  int opt;
+  double t;
+  unsigned f = 0;
+#define f_bogus 1u
+
+  static const struct option options[] = {
+    { "help",          0,              0,      'h' },
+    { "version",       0,              0,      'v' },
+    { "usage",         0,              0,      'u' },
+
+    { "format",                OPTF_ARGREQ,    0,      'f' },
+    { "output",                OPTF_ARGREQ,    0,      'o' },
+    { "target",                OPTF_ARGREQ,    0,      't' },
+    { 0,               0,              0,      0 }
+  };
+
+  tvec_benchstate = &benchstate;
+  tm = bench_createtimer(); bench_init(&benchstate, tm);
+  ego(argv[0]);
+  for (;;) {
+    opt = mdwopt(argc, argv, "hvu" "f:o:t:", options, 0, 0, 0);
+    if (opt < 0) break;
+    switch (opt) {
+      case 'h': help(stdout); exit(0);
+      case 'v': version(stdout); exit(0);
+      case 'u': usage(stdout); exit(0);
+
+      case 'f': of = find_outform(optarg); break;
+      case 'o':
+       if (ofp) fclose(ofp);
+       ofp = fopen(optarg, "w");
+       if (!ofp)
+         die(2, "failed to open `%s' for writing: %s",
+             optarg, strerror(errno));
+       break;
+      case 't':
+       errno = 0; t = strtod(optarg, &q);
+       while (ISSPACE(*q)) q++;
+       if (errno || *q || t < 0) die(2, "invalid time `%s'", optarg);
+       benchstate.target_s = t;
+       break;
+
+      default:
+       f |= f_bogus;
+       break;
+    }
+  }
+  if (f&f_bogus) { usage(stderr); exit(2); }
+  if (!ofp) ofp = stdout;
+  if (!of) {
+    p = getenv("TVEC_FORMAT");
+    if (p) of = find_outform(p);
+  }
+  if (of) o = of->makefn(ofp);
+  else o = tvec_dfltout(ofp);
+
+  tvec_begin(tv_out, info, o); *argpos_out = optind;
+}
+
+void tvec_readstdin(struct tvec_state *tv)
+  { tvec_read(tv, "<stdin>", stdin); }
+
+void tvec_readfile(struct tvec_state *tv, const char *file)
+{
+  FILE *fp;
+
+  fp = fopen(file, "r");
+  if (!fp)
+    tvec_error(tv, "failed to open `%s' for reading: %s",
+              file, strerror(errno));
+  tvec_read(tv, file, fp);
+  fclose(fp);
+}
+
+void tvec_readdflt(struct tvec_state *tv, const char *file)
+{
+  dstr d = DSTR_INIT;
+  const char *p;
+
+  if (file) {
+    p = getenv("srcdir");
+    if (p)
+      { dstr_putf(&d, "%s/%s", p, file); file = d.buf; }
+    tvec_readfile(tv, file);
+  } else if (isatty(0))
+    tvec_error(tv, "use `-' to force reading from interactive stdin");
+  else
+    tvec_readstdin(tv);
+}
+
+void tvec_readarg(struct tvec_state *tv, const char *arg)
+{
+  if (STRCMP(arg, ==, "-")) tvec_readstdin(tv);
+  else tvec_readfile(tv, arg);
+}
+
+void tvec_readargs(int argc, char *argv[], struct tvec_state *tv,
+                  int *argpos_inout, const char *dflt)
+{
+  int i = *argpos_inout;
+
+
+  if (i == argc) tvec_readdflt(tv, dflt);
+  else while (i < argc) tvec_readarg(tv, argv[i++]);
+  *argpos_inout = i;
+}
+
+int tvec_main(int argc, char *argv[],
+             const struct tvec_info *info, const char *dflt)
+{
+  struct tvec_state tv;
+  int argpos;
+
+  tvec_parseargs(argc, argv, &tv, &argpos, info);
+  tvec_readargs(argc, argv, &tv, &argpos, dflt);
+  return (tvec_end(&tv));
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/test/tvec-output.c b/test/tvec-output.c
new file mode 100644 (file)
index 0000000..af120d9
--- /dev/null
@@ -0,0 +1,874 @@
+/* -*-c-*-
+ *
+ * Test vector output management
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <assert.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <unistd.h>
+
+#include "alloc.h"
+#include "bench.h"
+#include "dstr.h"
+#include "quis.h"
+#include "report.h"
+#include "tvec.h"
+
+/*----- Common machinery --------------------------------------------------*/
+
+enum { INPUT, OUTPUT, MATCH, EXPECT, FOUND };
+struct mismatchfns {
+  void (*report_status)(unsigned /*disp*/, int /*st*/,
+                       struct tvec_state */*tv*/);
+  void (*report_register)(unsigned /*disp*/,
+                         const struct tvec_reg */*r*/,
+                         const struct tvec_regdef */*rd*/,
+                         struct tvec_state */*tv*/);
+};
+
+static const char *stdisp(unsigned disp)
+{
+  switch (disp) {
+    case MATCH: return "final";
+    case EXPECT: return "expected";
+    case FOUND: return "actual";
+    default: abort();
+  }
+}
+
+static const char *regdisp(unsigned disp)
+{
+  switch (disp) {
+    case INPUT: return "input";
+    case OUTPUT: return "output";
+    case MATCH: return "matched";
+    case EXPECT: return "expected";
+    case FOUND: return "computed";
+    default: abort();
+  }
+}
+
+static int getenv_boolean(const char *var, int dflt)
+{
+  const char *p;
+
+  p = getenv(var);
+  if (!p)
+    return (dflt);
+  else if (STRCMP(p, ==, "y") || STRCMP(p, ==, "yes") ||
+          STRCMP(p, ==, "t") || STRCMP(p, ==, "true") ||
+          STRCMP(p, ==, "on") || STRCMP(p, ==, "force") ||
+          STRCMP(p, ==, "1"))
+    return (1);
+  else if (STRCMP(p, ==, "n") || STRCMP(p, ==, "no") ||
+          STRCMP(p, ==, "nil") || STRCMP(p, ==, "off") ||
+          STRCMP(p, ==, "0"))
+    return (0);
+  else {
+    moan("unexpected value `%s' for boolean environment variable `%s'",
+        var, p);
+    return (dflt);
+  }
+}
+
+static void basic_report_status(unsigned disp, int st, struct tvec_state *tv)
+  { tvec_write(tv, "  %8s status = `%c'\n", stdisp(disp), st); }
+
+static void basic_report_register(unsigned disp,
+                                 const struct tvec_reg *r,
+                                 const struct tvec_regdef *rd,
+                                 struct tvec_state *tv)
+{
+  tvec_write(tv, "  %8s %s = ", regdisp(disp), rd->name);
+  if (r->f&TVRF_LIVE) rd->ty->dump(&r->v, rd, tv, 0);
+  else tvec_write(tv, "#<unset>");
+  tvec_write(tv, "\n");
+}
+
+static const struct mismatchfns basic_mismatchfns =
+  { basic_report_status, basic_report_register };
+
+static void dump_inreg(const struct tvec_regdef *rd,
+                      const struct mismatchfns *fns, struct tvec_state *tv)
+  { fns->report_register(INPUT, TVEC_REG(tv, in, rd->i), rd, tv); }
+
+static void dump_outreg(const struct tvec_regdef *rd,
+                       const struct mismatchfns *fns, struct tvec_state *tv)
+{
+  const struct tvec_reg
+    *rin = TVEC_REG(tv, in, rd->i), *rout = TVEC_REG(tv, out, rd->i);
+
+  if (tv->st == '.') {
+    if (!(rout->f&TVRF_LIVE)) {
+      if (!(rin->f&TVRF_LIVE))
+       fns->report_register(INPUT, rin, rd, tv);
+      else {
+       fns->report_register(FOUND, rout, rd, tv);
+       fns->report_register(EXPECT, rin, rd, tv);
+      }
+    } else {
+      if (!(rin->f&TVRF_LIVE)) fns->report_register(OUTPUT, rout, rd, tv);
+      else if (rd->ty->eq(&rin->v, &rout->v, rd))
+       fns->report_register(MATCH, rin, rd, tv);
+      else {
+       fns->report_register(FOUND, rout, rd, tv);
+       fns->report_register(EXPECT, rin, rd, tv);
+      }
+    }
+  }
+}
+
+static void mismatch(const struct mismatchfns *fns, struct tvec_state *tv)
+{
+  const struct tvec_regdef *rd;
+
+  if (tv->st != tv->expst) {
+    fns->report_status(FOUND, tv->st, tv);
+    fns->report_status(EXPECT, tv->expst, tv);
+  } else if (tv->st != '.')
+    fns->report_status(MATCH, tv->st, tv);
+
+  for (rd = tv->test->regs; rd->name; rd++) {
+    if (rd->i < tv->nrout) dump_outreg(rd, fns, tv);
+    else dump_inreg(rd, fns, tv);
+  }
+}
+
+static void bench_summary(struct tvec_state *tv)
+{
+  const struct tvec_regdef *rd;
+  unsigned f = 0;
+#define f_any 1u
+
+  for (rd = tv->test->regs; rd->name; rd++)
+    if (rd->f&TVRF_ID) {
+      if (f&f_any) tvec_write(tv, ", ");
+      else f |= f_any;
+      tvec_write(tv, "%s = ", rd->name);
+      rd->ty->dump(&TVEC_REG(tv, in, rd->i)->v, rd, tv, TVSF_COMPACT);
+    }
+
+#undef f_any
+}
+
+static void normalize(double *x_inout, const char **unit_out, double scale)
+{
+  static const char
+    *const nothing = "",
+    *const big[] = { "k", "M", "G", "T", "P", "E", 0 },
+    *const little[] = { "m", "µ", "n", "p", "f", "a", 0 };
+  const char *const *u;
+  double x = *x_inout;
+
+  if (x < 1)
+    for (u = little, x *= scale; x < 1 && u[1]; u++, x *= scale);
+  else if (x >= scale)
+    for (u = big, x /= scale; x >= scale && u[1]; u++, x /= scale);
+  else
+    u = &nothing;
+
+  *x_inout = x; *unit_out = *u;
+}
+
+static void bench_report(struct tvec_state *tv,
+                        const struct bench_timing *tm)
+{
+  const struct tvec_bench *b = tv->test->arg.p;
+  double n = (double)tm->n*b->niter;
+  double x, scale;
+  const char *u, *what, *whats;
+
+  assert(tm->f&BTF_TIMEOK);
+
+  if (b->rbuf == -1) {
+    tvec_write(tv, " -- %.0f iterations ", n);
+    what = "op"; whats = "ops"; scale = 1000;
+  } else {
+    n *= TVEC_REG(tv, in, b->rbuf)->v.bytes.sz;
+    x = n; normalize(&x, &u, 1024); tvec_write(tv, " -- %.3f %sB ", x, u);
+    what = whats = "B"; scale = 1024;
+  }
+  x = tm->t; normalize(&x, &u, 1000);
+  tvec_write(tv, "in %.3f %ss", x, u);
+  if (tm->f&BTF_CYOK) {
+    x = tm->cy; normalize(&x, &u, 1000);
+    tvec_write(tv, " (%.3f %scy)", x, u);
+  }
+  tvec_write(tv, ": ");
+
+  x = n/tm->t; normalize(&x, &u, scale);
+  tvec_write(tv, "%.3f %s%s/s", x, u, whats);
+  x = tm->t/n; normalize(&x, &u, 1000);
+  tvec_write(tv, ", %.3f %ss/%s", x, u, what);
+  if (tm->f&BTF_CYOK) {
+    x = tm->cy/n; normalize(&x, &u, 1000);
+    tvec_write(tv, " (%.3f %scy/%s)", x, u, what);
+  }
+  tvec_write(tv, "\n");
+}
+
+/*----- Skeleton ----------------------------------------------------------*/
+/*
+static void ..._error(struct tvec_output *o, const char *msg, va_list *ap)
+static void ..._notice(struct tvec_output *o, const char *msg, va_list *ap)
+static void ..._write(struct tvec_output *o, const char *p, size_t sz)
+static void ..._bsession(struct tvec_output *o)
+static int ..._esession(struct tvec_output *o)
+static void ..._bgroup(struct tvec_output *o)
+static void ..._egroup(struct tvec_output *o, unsigned outcome)
+static void ..._skipgroup(struct tvec_output *o,
+                         const char *excuse, va_list *ap)
+static void ..._btest(struct tvec_output *o)
+static void ..._skip(struct tvec_output *o, const char *detail, va_list *ap)
+static void ..._fail(struct tvec_output *o, const char *detail, va_list *ap)
+static void ..._mismatch(struct tvec_output *o)
+static void ..._etest(struct tvec_output *o, unsigned outcome)
+static void ..._bbench(struct tvec_output *o)
+static void ..._ebench(struct tvec_output *o, const struct tvec_timing *t)
+static void ..._destroy(struct tvec_output *o)
+
+static const struct tvec_outops ..._ops = {
+  ..._error, ..._notice, ..._write,
+  ..._bsession, ..._esession,
+  ..._bgroup, ..._egroup, ..._skip,
+  ..._btest, ..._skip, ..._fail, ..._mismatch, ..._etest,
+  ..._bbench, ..._ebench,
+  ..._destroy
+};
+*/
+/*----- Human-readable output ---------------------------------------------*/
+
+#define HAF_FGMASK 0x0f
+#define HAF_FGSHIFT 0
+#define HAF_BGMASK 0xf0
+#define HAF_BGSHIFT 4
+#define HAF_FG 256u
+#define HAF_BG 512u
+#define HAF_BOLD 1024u
+#define HCOL_BLACK 0u
+#define HCOL_RED 1u
+#define HCOL_GREEN 2u
+#define HCOL_YELLOW 3u
+#define HCOL_BLUE 4u
+#define HCOL_MAGENTA 5u
+#define HCOL_CYAN 6u
+#define HCOL_WHITE 7u
+#define HCF_BRIGHT 8u
+#define HFG(col) (HAF_FG | (HCOL_##col) << HAF_FGSHIFT)
+#define HBG(col) (HAF_BG | (HCOL_##col) << HAF_BGSHIFT)
+
+#define HA_WIN (HFG(GREEN))
+#define HA_LOSE (HFG(RED) | HAF_BOLD)
+#define HA_SKIP (HFG(YELLOW))
+
+struct human_output {
+  struct tvec_output _o;
+  FILE *fp;
+  dstr scoreboard;
+  unsigned attr;
+  unsigned f;
+#define HOF_TTY 1u
+#define HOF_DUPERR 2u
+#define HOF_COLOUR 4u
+#define HOF_PROGRESS 8u
+};
+
+static void set_colour(FILE *fp, int *sep_inout,
+                      const char *norm, const char *bright,
+                      unsigned colour)
+{
+  if (*sep_inout) putc(*sep_inout, fp);
+  fprintf(fp, "%s%d", colour&HCF_BRIGHT ? bright : norm, colour&7);
+  *sep_inout = ';';
+}
+
+static void setattr(struct human_output *h, unsigned attr)
+{
+  unsigned diff = h->attr ^ attr;
+  int sep = 0;
+
+  if (!diff || !(h->f&HOF_COLOUR)) return;
+  fputs("\x1b[", h->fp);
+
+  if (diff&HAF_BOLD) {
+    if (attr&HAF_BOLD) putc('1', h->fp);
+    else { putc('0', h->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);
+    else
+      { if (sep) putc(sep, h->fp); fputs("39", h->fp); sep = ';'; }
+  }
+  if (diff&(HAF_BG | HAF_BGMASK)) {
+    if (attr&HAF_BG)
+      set_colour(h->fp, &sep, "4", "10", (attr&HAF_BGMASK) >> HAF_BGSHIFT);
+    else
+      { if (sep) putc(sep, h->fp); fputs("49", h->fp); sep = ';'; }
+  }
+
+  putc('m', h->fp); h->attr = attr;
+
+#undef f_any
+}
+
+static void clear_progress(struct human_output *h)
+{
+  size_t i, n;
+
+  if (h->f&HOF_PROGRESS) {
+    n = strlen(h->_o.tv->test->name) + 2 + h->scoreboard.len;
+    for (i = 0; i < n; i++) fputs("\b \b", h->fp);
+    h->f &= ~HOF_PROGRESS;
+  }
+}
+
+static void write_scoreboard_char(struct human_output *h, int ch)
+{
+  switch (ch) {
+    case 'x': setattr(h, HA_LOSE); break;
+    case '_': setattr(h, HA_SKIP); break;
+    default: setattr(h, 0); break;
+  }
+  putc(ch, h->fp); setattr(h, 0);
+}
+
+static void show_progress(struct human_output *h)
+{
+  const char *p, *l;
+
+  if ((h->f&HOF_TTY) && !(h->f&HOF_PROGRESS)) {
+    fprintf(h->fp, "%s: ", h->_o.tv->test->name);
+    if (!(h->f&HOF_COLOUR))
+      dstr_write(&h->scoreboard, h->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;
+  }
+}
+
+static void report_location(struct human_output *h, FILE *fp,
+                           const char *file, unsigned lno)
+{
+  unsigned f = 0;
+#define f_flush 1u
+
+#define FLUSH(fp) do if (f&f_flush) fflush(fp); while (0)
+
+  if (fp != h->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);
+  }
+
+#undef f_flush
+#undef FLUSH
+}
+
+static void human_report(struct human_output *h,
+                        const char *msg, va_list *ap)
+{
+  struct tvec_state *tv = h->_o.tv;
+
+  fprintf(stderr, "%s: ", QUIS);
+  report_location(h, stderr, tv->infile, tv->lno);
+  vfprintf(stderr, msg, *ap);
+  fputc('\n', stderr);
+
+  if (h->f&HOF_DUPERR) {
+    report_location(h, stderr, tv->infile, tv->lno);
+    vfprintf(h->fp, msg, *ap);
+    fputc('\n', h->fp);
+  }
+}
+
+static void human_error(struct tvec_output *o, const char *msg, va_list *ap)
+{
+  struct human_output *h = (struct human_output *)o;
+
+  if (h->f&HOF_PROGRESS) fputc('\n', h->fp);
+  human_report(h, msg, ap);
+}
+
+static void human_notice(struct tvec_output *o, const char *msg, va_list *ap)
+{
+  struct human_output *h = (struct human_output *)o;
+  clear_progress(h); human_report(h, msg, ap); show_progress(h);
+}
+
+static void human_write(struct tvec_output *o, const char *p, size_t sz)
+{
+  struct human_output *h = (struct human_output *)o;
+  fwrite(p, 1, sz, h->fp);
+}
+
+static void human_bsession(struct tvec_output *o) { ; }
+
+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);
+  }
+}
+
+static int human_esession(struct tvec_output *o)
+{
+  struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->_o.tv;
+  unsigned
+    all_win = tv->all[TVOUT_WIN], grps_win = tv->grps[TVOUT_WIN],
+    all_lose = tv->all[TVOUT_LOSE], grps_lose = tv->grps[TVOUT_LOSE],
+    all_skip = tv->all[TVOUT_SKIP], grps_skip = tv->grps[TVOUT_SKIP],
+    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",
+           !(all_skip || grps_skip) ? "all " : "",
+           all_win, all_win == 1 ? "test" : "tests");
+    report_skipped(h, all_skip);
+    fprintf(h->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",
+           all_lose, all_run, all_run == 1 ? "test" : "tests");
+    report_skipped(h, all_skip);
+    fprintf(h->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);
+
+  return (tv->all[TVOUT_LOSE] ? 1 : 0);
+}
+
+static void human_bgroup(struct tvec_output *o)
+{
+  struct human_output *h = (struct human_output *)o;
+  dstr_reset(&h->scoreboard); show_progress(h);
+}
+
+static void human_grpsumm(struct human_output *h, unsigned outcome)
+{
+  struct tvec_state *tv = h->_o.tv;
+  unsigned win = tv->curr[TVOUT_WIN], lose = tv->curr[TVOUT_LOSE],
+    skip = tv->curr[TVOUT_SKIP], run = win + lose;
+
+  if (lose) {
+    assert(outcome == TVOUT_LOSE);
+    fprintf(h->fp, " %u/%u ", lose, run);
+    setattr(h, HA_LOSE); fputs("FAILED", h->fp); setattr(h, 0);
+    report_skipped(h, skip);
+  } else {
+    assert(outcome == TVOUT_WIN);
+    fputc(' ', h->fp); setattr(h, HA_WIN); fputs("ok", h->fp); setattr(h, 0);
+    report_skipped(h, skip);
+  }
+  fputc('\n', h->fp);
+}
+
+static void human_egroup(struct tvec_output *o, unsigned outcome)
+{
+  struct human_output *h = (struct human_output *)o;
+
+  if (h->f&HOF_TTY) h->f &= ~HOF_PROGRESS;
+  else fprintf(h->fp, "%s:", h->_o.tv->test->name);
+  human_grpsumm(h, outcome);
+}
+
+static void human_skipgroup(struct tvec_output *o,
+                           const char *excuse, va_list *ap)
+{
+  struct human_output *h = (struct human_output *)o;
+
+  if (!(~h->f&(HOF_TTY | HOF_PROGRESS))) {
+    h->f &= ~HOF_PROGRESS;
+    setattr(h, HA_SKIP); fputs("skipped", h->fp); setattr(h, 0);
+  } else {
+    fprintf(h->fp, "%s: ", h->_o.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);
+}
+
+static void human_btest(struct tvec_output *o)
+  { struct human_output *h = (struct human_output *)o; show_progress(h); }
+
+static void human_skip(struct tvec_output *o,
+                      const char *excuse, va_list *ap)
+{
+  struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->_o.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);
+}
+
+static void human_fail(struct tvec_output *o,
+                      const char *detail, va_list *ap)
+{
+  struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->_o.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);
+}
+
+static void set_dispattr(struct human_output *h, unsigned disp)
+{
+  switch (disp) {
+    case EXPECT: setattr(h, HFG(GREEN)); break;
+    case FOUND: setattr(h, HFG(RED)); break;
+    default: setattr(h, 0); break;
+  }
+}
+
+static void human_report_status(unsigned disp, int st, struct tvec_state *tv)
+{
+  struct human_output *h = (struct human_output *)tv->output;
+
+  fprintf(h->fp, "  %8s status = ", stdisp(disp));
+  set_dispattr(h, disp); fprintf(h->fp, "`%c'", st); setattr(h, 0);
+  fputc('\n', h->fp);
+}
+
+static void human_report_register(unsigned disp,
+                                 const struct tvec_reg *r,
+                                 const struct tvec_regdef *rd,
+                                 struct tvec_state *tv)
+{
+  struct human_output *h = (struct human_output *)tv->output;
+
+  fprintf(h->fp, "  %8s %s = ", regdisp(disp), rd->name);
+  if (!(r->f&TVRF_LIVE))
+    tvec_write(tv, "#<unset>");
+  else {
+    set_dispattr(h, disp);
+    rd->ty->dump(&r->v, rd, tv, 0);
+    setattr(h, 0);
+  }
+  tvec_write(tv, "\n");
+}
+
+static const struct mismatchfns human_mismatchfns =
+  { human_report_status, human_report_register };
+
+static void human_mismatch(struct tvec_output *o)
+{
+  struct human_output *h = (struct human_output *)o;
+
+  if (h->f&HOF_COLOUR) mismatch(&human_mismatchfns, h->_o.tv);
+  else mismatch(&basic_mismatchfns, h->_o.tv);
+}
+
+static void human_etest(struct tvec_output *o, unsigned outcome)
+{
+  struct human_output *h = (struct human_output *)o;
+  int ch;
+
+  if (h->f&HOF_TTY) {
+    show_progress(h);
+    switch (outcome) {
+      case TVOUT_WIN: ch = '.'; break;
+      case TVOUT_LOSE: ch = 'x'; break;
+      case TVOUT_SKIP: ch = '_'; break;
+      default: abort();
+    }
+    dstr_putc(&h->scoreboard, ch);
+    write_scoreboard_char(h, ch); fflush(h->fp);
+  }
+}
+
+static void human_bbench(struct tvec_output *o)
+{
+  struct human_output *h = (struct human_output *)o;
+  struct tvec_state *tv = h->_o.tv;
+
+  clear_progress(h);
+  fprintf(h->fp, "%s: ", tv->test->name);
+  bench_summary(tv); fflush(h->fp);
+}
+
+static void human_ebench(struct tvec_output *o,
+                        const struct bench_timing *tm)
+{
+  struct human_output *h = (struct human_output *)o;
+  bench_report(h->_o.tv, tm);
+}
+
+static void human_destroy(struct tvec_output *o)
+{
+  struct human_output *h = (struct human_output *)o;
+
+  if (h->f&HOF_DUPERR) fclose(h->fp);
+  dstr_destroy(&h->scoreboard);
+  xfree(h);
+}
+
+static const struct tvec_outops human_ops = {
+  human_error, human_notice, human_write,
+  human_bsession, human_esession,
+  human_bgroup, human_egroup, human_skipgroup,
+  human_btest, human_skip, human_fail, human_mismatch, human_etest,
+  human_bbench, human_ebench,
+  human_destroy
+};
+
+struct tvec_output *tvec_humanoutput(FILE *fp)
+{
+  struct human_output *h;
+  const char *p;
+
+  h = xmalloc(sizeof(*h)); h->_o.ops = &human_ops;
+  h->f = 0; h->attr = 0;
+
+  h->fp = fp;
+  if (fp != stdout && fp != stderr) h->f |= HOF_DUPERR;
+
+  switch (getenv_boolean("TVEC_TTY", -1)) {
+    case 1: h->f |= HOF_TTY; break;
+    case 0: break;
+    default:
+      if (isatty(fileno(fp))) h->f |= HOF_TTY;
+      break;
+  }
+  switch (getenv_boolean("TVEC_COLOUR", -1)) {
+    case 1: h->f |= HOF_COLOUR; break;
+    case 0: break;
+    default:
+      if (h->f&HOF_TTY) {
+       p = getenv("TERM");
+       if (p && STRCMP(p, !=, "dumb")) h->f |= HOF_COLOUR;
+      }
+      break;
+  }
+
+  dstr_create(&h->scoreboard);
+  return (&h->_o);
+}
+
+/*----- Perl's `Test Anything Protocol' -----------------------------------*/
+
+struct tap_output {
+  struct tvec_output _o;
+  FILE *fp;
+  unsigned f;
+#define TOF_FRESHLINE 1u
+};
+
+static void tap_report(struct tap_output *t, const char *msg, va_list *ap)
+{
+  struct tvec_state *tv = t->_o.tv;
+
+  if (tv->infile) fprintf(t->fp, "%s:%u: ", tv->infile, tv->lno);
+  vfprintf(t->fp, msg, *ap); fputc('\n', t->fp);
+}
+
+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);
+}
+
+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);
+}
+
+static void tap_write(struct tvec_output *o, const char *p, size_t sz)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  const char *q, *l = p + sz;
+
+  if (p == l) return;
+  if (t->f&TOF_FRESHLINE) fputs("## ", t->fp);
+  for (;;) {
+    q = memchr(p, '\n', l - p); if (!q) break;
+    fwrite(p, 1, q + 1 - p, t->fp); p = q + 1;
+    if (p == l) { t->f |= TOF_FRESHLINE; return; }
+    fputs("## ", t->fp);
+  }
+  fwrite(p, 1, l - p, t->fp); t->f &= ~TOF_FRESHLINE;
+}
+
+static void tap_bsession(struct tvec_output *o) { ; }
+
+static unsigned tap_grpix(struct tap_output *t)
+{
+  struct tvec_state *tv = t->_o.tv;
+
+  return (tv->grps[TVOUT_WIN] +
+         tv->grps[TVOUT_LOSE] +
+         tv->grps[TVOUT_SKIP]);
+}
+
+static int tap_esession(struct tvec_output *o)
+{
+  struct tap_output *t = (struct tap_output *)o;
+
+  fprintf(t->fp, "1..%u\n", tap_grpix(t));
+  return (0);
+}
+
+static void tap_bgroup(struct tvec_output *o) { ; }
+
+static void tap_egroup(struct tvec_output *o, unsigned outcome)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->_o.tv;
+  unsigned
+    grpix = tap_grpix(t),
+    win = tv->curr[TVOUT_WIN],
+    lose = tv->curr[TVOUT_LOSE],
+    skip = tv->curr[TVOUT_SKIP];
+
+  if (lose) {
+    assert(outcome == TVOUT_LOSE);
+    fprintf(t->fp, "not ok %u %s: FAILED %u/%u",
+           grpix, tv->test->name, lose, win + lose);
+    if (skip) fprintf(t->fp, " (skipped %u)", skip);
+  } else {
+    assert(outcome == TVOUT_WIN);
+    fprintf(t->fp, "ok %u %s: passed %u", grpix, tv->test->name, win);
+    if (skip) fprintf(t->fp, " (skipped %u)", skip);
+  }
+  fputc('\n', t->fp);
+}
+
+static void tap_skipgroup(struct tvec_output *o,
+                         const char *excuse, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+
+  fprintf(t->fp, "ok %u %s # SKIP", tap_grpix(t), t->_o.tv->test->name);
+  if (excuse)
+    { fputc(' ', t->fp); vfprintf(t->fp, excuse, *ap); }
+  fputc('\n', t->fp);
+}
+
+static void tap_btest(struct tvec_output *o) { ; }
+
+static void tap_skip(struct tvec_output *o, const char *excuse, va_list *ap)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->_o.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);
+}
+
+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->_o.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);
+}
+
+static void tap_mismatch(struct tvec_output *o)
+  { mismatch(&basic_mismatchfns, o->tv); }
+
+static void tap_etest(struct tvec_output *o, unsigned outcome) { ; }
+
+static void tap_bbench(struct tvec_output *o) { ; }
+
+static void tap_ebench(struct tvec_output *o,
+                      const struct bench_timing *tm)
+{
+  struct tap_output *t = (struct tap_output *)o;
+  struct tvec_state *tv = t->_o.tv;
+
+  tvec_write(tv, "%s: ", tv->test->name); bench_summary(tv);
+  bench_report(tv, tm);
+}
+
+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);
+  xfree(t);
+}
+
+static const struct tvec_outops tap_ops = {
+  tap_error, tap_notice, tap_write,
+  tap_bsession, tap_esession,
+  tap_bgroup, tap_egroup, tap_skipgroup,
+  tap_btest, tap_skip, tap_fail, tap_mismatch, tap_etest,
+  tap_bbench, tap_ebench,
+  tap_destroy
+};
+
+struct tvec_output *tvec_tapoutput(FILE *fp)
+{
+  struct tap_output *t;
+
+  t = xmalloc(sizeof(*t)); t->_o.ops = &tap_ops;
+  t->f = TOF_FRESHLINE;
+  t->fp = fp;
+  return (&t->_o);
+}
+
+/*----- Default output ----------------------------------------------------*/
+
+struct tvec_output *tvec_dfltout(FILE *fp)
+{
+  int ttyp = getenv_boolean("TVEC_TTY", -1);
+
+  if (ttyp == -1) ttyp = isatty(fileno(fp));
+  if (ttyp) return (tvec_humanoutput(fp));
+  else return (tvec_tapoutput(fp));
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/test/tvec-types.c b/test/tvec-types.c
new file mode 100644 (file)
index 0000000..a5d57db
--- /dev/null
@@ -0,0 +1,1136 @@
+/* -*-c-*-
+ *
+ * Types for the test-vector framework
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "buf.h"
+#include "codec.h"
+#  include "base32.h"
+#  include "base64.h"
+#  include "hex.h"
+#include "dstr.h"
+#include "tvec.h"
+
+/*----- Preliminary utilities ---------------------------------------------*/
+
+static int signed_to_buf(buf *b, long i)
+{
+  kludge64 k;
+  unsigned long u;
+
+  u = i;
+  if (i >= 0) ASSIGN64(k, u);
+  else { ASSIGN64(k, ~u); CPL64(k, k); }
+  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)); }
+
+static int unsigned_from_buf(buf *b, unsigned long *u_out)
+{
+  kludge64 k, ulmax;
+
+  ASSIGN64(ulmax, ULONG_MAX);
+  if (buf_getk64l(b, &k)) return (-1);
+  if (CMP64(k, >, ulmax)) return (-1);
+  *u_out = GET64(unsigned long, k); return (0);
+}
+
+static int hex_width(unsigned long u)
+{
+  int wd;
+  unsigned long t;
+
+  for (t = u >> 4, wd = 4; t >>= wd, wd *= 2, t; );
+  return (wd/4);
+}
+
+static void check_signed_range(long i,
+                              const struct tvec_irange *ir,
+                              struct tvec_state *tv)
+{
+  if (ir && (ir->min > i || i > ir->max))
+    tvec_error(tv, "integer %ld out of range (must be in [%ld .. %ld])",
+              i, ir->min, ir->max);
+}
+
+static void check_unsigned_range(unsigned long u,
+                                const struct tvec_urange *ur,
+                                struct tvec_state *tv)
+{
+  if (ur && (ur->min > u || u > ur->max))
+    tvec_error(tv, "integer %lu out of range (must be in [%lu .. %lu])",
+              u, ur->min, ur->max);
+}
+
+static void parse_signed(long *i_out, const char *p,
+                        const struct tvec_irange *ir,
+                        struct tvec_state *tv)
+{
+  char *q; const char *pp;
+  int olderr;
+  long i;
+
+  olderr = errno; errno = 0;
+  pp = p; if (*pp == '-' || *pp == '+') pp++;
+  if (!ISDIGIT(*pp)) tvec_syntax(tv, *pp, "signed integer");
+  i = strtol(p, &q, 0);
+  if (*q && !ISSPACE(*q)) tvec_syntax(tv, *q, "end-of-line");
+  if (errno) tvec_error(tv, "invalid integer `%s'", p);
+  check_signed_range(i, ir, tv);
+  errno = olderr; *i_out = i;
+}
+
+static void parse_unsigned(unsigned long *u_out, const char *p,
+                          const struct tvec_urange *ur,
+                          struct tvec_state *tv)
+{
+  char *q;
+  int olderr;
+  unsigned long u;
+
+  olderr = errno; errno = 0;
+  if (!ISDIGIT(*p)) tvec_syntax(tv, *p, "unsigned integer");
+  u = strtoul(p, &q, 0);
+  if (*q && !ISSPACE(*q)) tvec_syntax(tv, *q, "end-of-line");
+  if (errno) tvec_error(tv, "invalid integer `%s'", p);
+  check_unsigned_range(u, ur, tv);
+  errno = olderr; *u_out = u;
+}
+
+static int convert_hex(char ch, int *v_out)
+{
+  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);
+}
+
+static void read_quoted_string(dstr *d, int quote, struct tvec_state *tv)
+{
+  char expect[4];
+  int ch, i, esc;
+  unsigned f = 0;
+#define f_brace 1u
+
+  sprintf(expect, "`%c'", quote);
+
+  for (;;) {
+    ch = getc(tv->fp);
+  reinsert:
+    switch (ch) {
+      case EOF: case '\n':
+       tvec_syntax(tv, ch, expect);
+
+      case '\\':
+       if (quote == '\'') goto ordinary;
+       ch = getc(tv->fp);
+       switch (ch) {
+         case EOF: tvec_syntax(tv, ch, expect);
+         case '\n': tv->lno++; break;
+         case '\'': DPUTC(d, '\''); break;
+         case '\\': DPUTC(d, '\\'); break;
+         case '"': DPUTC(d, '"'); break;
+         case 'a': DPUTC(d, '\a'); break;
+         case 'b': DPUTC(d, '\b'); break;
+         case 'e': DPUTC(d, '\x1b'); break;
+         case 'f': DPUTC(d, '\f'); break;
+         case 'n': DPUTC(d, '\n'); break;
+         case 'r': DPUTC(d, '\r'); break;
+         case 't': DPUTC(d, '\t'); break;
+         case 'v': DPUTC(d, '\v'); break;
+
+         case 'x':
+           ch = getc(tv->fp);
+           if (ch == '{') { f |= f_brace; ch = getc(tv->fp); }
+           else f &= ~f_brace;
+           if (convert_hex(ch, &esc)) tvec_syntax(tv, ch, "hex digit");
+           for (;;) {
+             ch = getc(tv->fp); if (convert_hex(ch, &i)) break;
+             esc = 8*esc + i;
+             if (esc > UCHAR_MAX)
+               tvec_error(tv, "character code %d out of range", esc);
+           }
+           DPUTC(d, esc);
+           if (!(f&f_brace)) goto reinsert;
+           else if (ch != '}') tvec_syntax(tv, ch, "`}'");
+           break;
+
+         default:
+           if ('0' <= ch && ch < '8') {
+             i = 1; esc = ch - '0';
+             for (;;) {
+               ch = getc(tv->fp);
+               if (i > 3 || '0' > ch || ch >= '8') break;
+               esc = 8*esc + ch - '0'; i++;
+             }
+             if (esc > UCHAR_MAX)
+               tvec_error(tv, "character code %d out of range", esc);
+             DPUTC(d, esc);
+             goto reinsert;
+           }
+           tvec_syntax(tv, ch, "string escape");
+           break;
+       }
+       break;
+
+      default:
+       if (ch == quote) goto end;
+      ordinary:
+       DPUTC(d, ch);
+       break;
+    }
+  }
+
+end:
+  DPUTZ(d);
+
+#undef f_brace
+}
+
+enum { TVCODE_BARE, TVCODE_HEX, TVCODE_BASE64, TVCODE_BASE32 };
+
+static int collect_bare(dstr *d, struct tvec_state *tv)
+{
+  size_t pos = d->len;
+  enum { WORD, SPACE, ESCAPE }; unsigned s = WORD;
+  int ch, rc;
+
+  for (;;) {
+    ch = getc(tv->fp);
+    switch (ch) {
+      case EOF:
+       goto bad;
+      case '\n':
+       if (s == ESCAPE) { tv->lno++; goto addch; }
+       if (s == WORD) pos = d->len;
+       ungetc(ch, tv->fp); if (tvec_nexttoken(tv)) { rc = -1; goto done; }
+       DPUTC(d, ' '); s = SPACE;
+       break;
+      case '"': case '\'': case '!':
+       if (s == SPACE) { ungetc(ch, tv->fp); rc = 0; goto done; }
+       goto addch;
+      case '\\':
+       s = ESCAPE;
+       break;
+      default:
+       if (s != ESCAPE && isspace(ch)) {
+         if (s == WORD) pos = d->len;
+         DPUTC(d, ch); s = SPACE;
+         break;
+       }
+      addch:
+       DPUTC(d, ch); s = WORD;
+    }
+  }
+
+done:
+  if (s == SPACE) d->len = pos;
+  DPUTZ(d); return (rc);
+
+bad:
+  tvec_syntax(tv, ch, "bareword");
+}
+
+static void set_up_encoding(const codec_class **ccl_out, unsigned *f_out,
+                           unsigned code)
+{
+  switch (code) {
+    case TVCODE_BARE:
+      *ccl_out = 0; *f_out = 0;
+      break;
+    case TVCODE_HEX:
+      *ccl_out = &hex_class; *f_out = CDCF_IGNCASE;
+      break;
+    case TVCODE_BASE32:
+      *ccl_out = &base32_class; *f_out = CDCF_IGNCASE | CDCF_IGNEQPAD;
+      break;
+    case TVCODE_BASE64:
+      *ccl_out = &base64_class; *f_out = CDCF_IGNEQPAD;
+      break;
+    default:
+      abort();
+  }
+}
+
+static void read_compound_string(void **p_inout, size_t *sz_inout,
+                                unsigned code, struct tvec_state *tv)
+{
+  const codec_class *ccl; unsigned f;
+  codec *cdc;
+  dstr d = DSTR_INIT, w = DSTR_INIT;
+  char *p;
+  int ch, err;
+
+  set_up_encoding(&ccl, &f, code);
+  if (tvec_nexttoken(tv)) tvec_syntax(tv, fgetc(tv->fp), "string");
+  do {
+    ch = getc(tv->fp);
+    if (ch == '"' || ch == '\'')
+      read_quoted_string(&d, ch, tv);
+    else if (ch == '!') {
+      ungetc(ch, tv->fp);
+      DRESET(&w); tvec_readword(tv, &w, ";", "`!'-keyword");
+      if (STRCMP(w.buf, ==, "!bare")) code = TVCODE_BARE;
+      else if (STRCMP(w.buf, ==, "!hex")) code = TVCODE_HEX;
+      else if (STRCMP(w.buf, ==, "!base32")) code = TVCODE_BASE32;
+      else if (STRCMP(w.buf, ==, "!base64")) code = TVCODE_BASE64;
+      else tvec_error(tv, "unknown string keyword `%s'", w.buf);
+      set_up_encoding(&ccl, &f, code);
+    } else if (ccl) {
+      ungetc(ch, tv->fp);
+      DRESET(&w);
+       tvec_readword(tv, &w, ";", "%s-encoded fragment", ccl->name);
+      cdc = ccl->decoder(f);
+      err = cdc->ops->code(cdc, w.buf, w.len, &d);
+      if (!err) err = cdc->ops->code(cdc, 0, 0, &d);
+      if (err)
+       tvec_error(tv, "invalid %s fragment `%s': %s",
+                  ccl->name, w.buf, codec_strerror(err));
+      cdc->ops->destroy(cdc);
+    } else switch (code) {
+      case TVCODE_BARE:
+       ungetc(ch, tv->fp);
+       if (collect_bare(&d, tv)) goto done;
+       break;
+      default:
+       abort();
+    }
+  } while (!tvec_nexttoken(tv));
+
+done:
+  if (*sz_inout <= d.len)
+    { xfree(*p_inout); *p_inout = xmalloc(d.len + 1); }
+  p = *p_inout; memcpy(p, d.buf, d.len); p[d.len] = 0; *sz_inout = d.len;
+  dstr_destroy(&d); dstr_destroy(&w);
+}
+
+/*----- Skeleton ----------------------------------------------------------*/
+/*
+static void init_...(union tvec_regval *rv, const struct tvec_regdef *rd)
+static void release_...(union tvec_regval *rv, const struct tvec_regdef *rd)
+static int eq_...(const union tvec_regval *rv0, const union tvec_regval *rv1,
+                 const struct tvec_regdef *rd)
+static size_t measure_...(const union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
+static int tobuf_...(buf *b, const union tvec_regval *rv,
+                    const struct tvec_regdef *rd)
+static int frombuf_...(buf *b, union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+static void parse_...(union tvec_regval *rv, const struct tvec_regdef *rd,
+                     struct tvec_state *tv)
+static void dump_...(const union tvec_regval *rv,
+                    const struct tvec_regdef *rd,
+                    struct tvec_state *tv, unsigned style)
+
+const struct tvec_regty tvty_... = {
+  init_..., release_..., eq_..., measure_...,
+  tobuf_..., frombuf_...,
+  parse_..., dump_...
+};
+*/
+/*----- Signed and unsigned integer types ---------------------------------*/
+
+static void init_int(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->i = 0; }
+
+static void init_uint(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->u = 0; }
+
+static void release_int(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { ; }
+
+static int eq_int(const union tvec_regval *rv0, const union tvec_regval *rv1,
+                 const struct tvec_regdef *rd)
+  { return (rv0->i == rv1->i); }
+
+static int eq_uint(const union tvec_regval *rv0,
+                  const union tvec_regval *rv1,
+                  const struct tvec_regdef *rd)
+  { return (rv0->u == rv1->u); }
+
+static size_t measure_int(const union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
+  { return (8); }
+
+static int tobuf_int(buf *b, const union tvec_regval *rv,
+                    const struct tvec_regdef *rd)
+  { return (signed_to_buf(b, rv->i)); }
+
+static int tobuf_uint(buf *b, const union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+  { return (unsigned_to_buf(b, rv->u)); }
+
+static int frombuf_int(buf *b, union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+  { return signed_from_buf(b, &rv->i); }
+
+static int frombuf_uint(buf *b, union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+  { return (unsigned_from_buf(b, &rv->u)); }
+
+static void parse_int(union tvec_regval *rv, const struct tvec_regdef *rd,
+                     struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+
+  tvec_readword(tv, &d, ";", "signed integer");
+  parse_signed(&rv->i, d.buf, rd->arg.p, tv);
+  tvec_flushtoeol(tv, 0);
+  dstr_destroy(&d);
+}
+
+static void parse_uint(union tvec_regval *rv, const struct tvec_regdef *rd,
+                      struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+
+  tvec_readword(tv, &d, ";", "unsigned integer");
+  parse_unsigned(&rv->u, d.buf, rd->arg.p, tv);
+  tvec_flushtoeol(tv, 0);
+  dstr_destroy(&d);
+}
+
+static void dump_int(const union tvec_regval *rv,
+                    const struct tvec_regdef *rd,
+                    struct tvec_state *tv, unsigned style)
+{
+  unsigned long u;
+
+  tvec_write(tv, "%ld", rv->i);
+  if (!(style&TVSF_COMPACT)) {
+    if (rv->i >= 0) u = rv->i;
+    else u = -(unsigned long)rv->i;
+    tvec_write(tv, " ; = %s0x%0*lx", rv->i < 0 ? "-" : "", hex_width(u), u);
+  }
+}
+
+static void dump_uint(const union tvec_regval *rv,
+                     const struct tvec_regdef *rd,
+                     struct tvec_state *tv, unsigned style)
+{
+  tvec_write(tv, "%lu", rv->u);
+  if (!(style&TVSF_COMPACT))
+    tvec_write(tv, " ; = 0x%0*lx", hex_width(rv->u), rv->u);
+}
+
+const struct tvec_regty tvty_int = {
+  init_int, release_int, eq_int, measure_int,
+  tobuf_int, frombuf_int,
+  parse_int, dump_int
+};
+
+const struct tvec_irange
+  tvrange_schar = { SCHAR_MIN, SCHAR_MAX },
+  tvrange_short = { SHRT_MIN, SHRT_MAX },
+  tvrange_int = { INT_MIN, INT_MAX },
+  tvrange_long = { LONG_MIN, LONG_MAX },
+  tvrange_sbyte = { -128, 127 },
+  tvrange_i16 = { -32768, +32767 },
+  tvrange_i32 = { -2147483648, 2147483647 };
+
+const struct tvec_regty tvty_uint = {
+  init_uint, release_int, eq_uint, measure_int,
+  tobuf_uint, frombuf_uint,
+  parse_uint, dump_uint
+};
+
+const struct tvec_urange
+  tvrange_uchar = { 0, UCHAR_MAX },
+  tvrange_ushort = { 0, USHRT_MAX },
+  tvrange_uint = { 0, UINT_MAX },
+  tvrange_ulong = { 0, ULONG_MAX },
+  tvrange_size = { 0, (size_t)-1 },
+  tvrange_byte = { 0, 255 },
+  tvrange_u16 = { 0, 65535 },
+  tvrange_u32 = { 0, 4294967296 };
+
+int tvec_claimeq_int(struct tvec_state *tv, long i0, long i1,
+                    const char *file, unsigned lno, const char *expr)
+{
+  tv->in[0].v.i = i0; tv->out[0].v.i = i1;
+  return (tvec_claimeq(tv, &tvty_int, 0, file, lno, expr));
+}
+
+int tvec_claimeq_uint(struct tvec_state *tv,
+                     unsigned long u0, unsigned long u1,
+                     const char *file, unsigned lno, const char *expr)
+{
+  tv->in[0].v.u = u0; tv->out[0].v.u = u1;
+  return (tvec_claimeq(tv, &tvty_uint, 0, file, lno, expr));
+}
+
+/*----- Enumerations ------------------------------------------------------*/
+
+static void init_enum(union tvec_regval *rv, const struct tvec_regdef *rd)
+{
+  const struct tvec_enuminfo *ei = rd->arg.p;
+
+  switch (ei->mv) {
+#define CASE(tag, ty, slot)                                            \
+       case TVMISC_##tag: rv->slot = 0; break;
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+    default: abort();
+  }
+}
+
+static int eq_enum(const union tvec_regval *rv0,
+                  const union tvec_regval *rv1,
+                  const struct tvec_regdef *rd)
+{
+  const struct tvec_enuminfo *ei = rd->arg.p;
+
+  switch (ei->mv) {
+#define CASE(tag, ty, slot)                                            \
+       case TVMISC_##tag: return (rv0->slot == rv1->slot);
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+    default: abort();
+  }
+}
+
+static int tobuf_enum(buf *b, const union tvec_regval *rv,
+                     const struct tvec_regdef *rd)
+{
+  const struct tvec_enuminfo *ei = rd->arg.p;
+
+  switch (ei->mv) {
+#define CASE(tag, ty, slot)                                            \
+       case TVMISC_##tag: return (HANDLE_##tag);
+#define HANDLE_INT     signed_to_buf(b, rv->i)
+#define HANDLE_UINT    unsigned_to_buf(b, rv->u)
+#define HANDLE_PTR     -1
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+#undef HANDLE_INT
+#undef HANDLE_UINT
+#undef HANDLE_PTR
+    default: abort();
+  }
+  return (0);
+}
+
+static int frombuf_enum(buf *b, union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+{
+  const struct tvec_enuminfo *ei = rd->arg.p;
+
+  switch (ei->mv) {
+#define CASE(tag, ty, slot)                                            \
+       case TVMISC_##tag: return (HANDLE_##tag);
+#define HANDLE_INT     signed_from_buf(b, &rv->i)
+#define HANDLE_UINT    unsigned_from_buf(b, &rv->u)
+#define HANDLE_PTR     -1
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+#undef HANDLE_INT
+#undef HANDLE_UINT
+#undef HANDLE_PTR
+    default: abort();
+  }
+}
+
+static void parse_enum(union tvec_regval *rv, const struct tvec_regdef *rd,
+                      struct tvec_state *tv)
+{
+  const struct tvec_enuminfo *ei = rd->arg.p;
+#define DECLS(tag, ty, slot)                                           \
+       const struct tvec_##slot##assoc *slot##a;
+  TVEC_MISCSLOTS(DECLS)
+#undef DECLS
+  dstr d = DSTR_INIT;
+
+  tvec_readword(tv, &d, ";", "enumeration tag or literal integer");
+  switch (ei->mv) {
+#define CASE(tag_, ty, slot)                                           \
+       case TVMISC_##tag_:                                             \
+         for (slot##a = ei->u.slot.av; slot##a->tag; slot##a++)        \
+           if (STRCMP(d.buf, ==, slot##a->tag))                        \
+             { rv->slot = FETCH_##tag_; goto end; }
+#define FETCH_INT (ia->i)
+#define FETCH_UINT (ua->u)
+#define FETCH_PTR ((/*unconst*/ void *)(pa->p))
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+#undef FETCH_INT
+#undef FETCH_UINT
+#undef FETCH_PTR
+  }
+
+  switch (ei->mv) {
+#define CASE(tag, ty, slot)                                            \
+       case TVMISC_##tag: HANDLE_##tag goto end;
+#define HANDLE_INT     parse_signed(&rv->i, d.buf, ei->u.i.ir, tv);
+#define HANDLE_UINT    parse_unsigned(&rv->u, d.buf, ei->u.u.ur, tv);
+#define HANDLE_PTR     if (STRCMP(d.buf, ==, "#nil")) rv->p = 0;       \
+                       else goto tagonly;
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+#undef HANDLE_INT
+#undef HANDLE_UINT
+#undef HANDLE_PTR
+    default: tagonly:
+      tvec_error(tv, "unknown `%s' value `%s'", ei->name, d.buf);
+  }
+
+end:
+  tvec_flushtoeol(tv, 0);
+  dstr_destroy(&d);
+}
+
+static void dump_enum(const union tvec_regval *rv,
+                     const struct tvec_regdef *rd,
+                     struct tvec_state *tv, unsigned style)
+{
+  const struct tvec_enuminfo *ei = rd->arg.p;
+#define DECLS(tag, ty, slot)                                           \
+       const struct tvec_##slot##assoc *slot##a;
+  TVEC_MISCSLOTS(DECLS)
+#undef DECLS
+  const char *tag;
+  unsigned long u;
+  unsigned f = 0;
+#define f_known 1u
+
+  switch (ei->mv) {
+#define CASE(tag_, ty, slot)                                           \
+       case TVMISC_##tag_:                                             \
+         for (slot##a = ei->u.slot.av; slot##a->tag; slot##a++)        \
+           if (rv->slot == slot##a->slot)                              \
+             { tag = slot##a->tag; goto found; }                       \
+         break;
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+    default: abort();
+  }
+  goto print_int;
+
+found:
+  f |= f_known;
+  tvec_write(tv, "%s", tag);
+  if (style&TVSF_COMPACT) return;
+  tvec_write(tv, " ; = ");
+
+print_int:
+  switch (ei->mv) {
+#define CASE(tag, ty, slot)                                            \
+       case TVMISC_##tag: HANDLE_##tag break;
+#define HANDLE_INT     tvec_write(tv, "%ld", rv->i);
+#define HANDLE_UINT    tvec_write(tv, "%lu", rv->u);
+#define HANDLE_PTR     if (!rv->p) tvec_write(tv, "#nil");             \
+                       else tvec_write(tv, "#<%s %p>", ei->name, rv->p);
+    TVEC_MISCSLOTS(CASE)
+#undef CASE
+#undef HANDLE_INT
+#undef HANDLE_UINT
+#undef HANDLE_PTR
+  }
+
+  switch (ei->mv) {
+    case TVMISC_INT:
+      if (!(f&f_known)) tvec_write(tv, " ;");
+      if (rv->i >= 0) u = rv->i;
+      else u = -(unsigned long)rv->i;
+      tvec_write(tv, " = %s0x%0*lx", rv->i < 0 ? "-" : "", hex_width(u), u);
+      break;
+    case TVMISC_UINT:
+      if (!(f&f_known)) tvec_write(tv, " ;");
+      tvec_write(tv, " = 0x%0*lx", hex_width(rv->u), rv->u);
+      break;
+  }
+}
+
+const struct tvec_regty tvty_enum = {
+  init_enum, release_int, eq_enum, measure_int,
+  tobuf_enum, frombuf_enum,
+  parse_enum, dump_enum
+};
+
+#define DEFCLAIM(tag, ty, slot)                                                \
+       int tvec_claimeq_##slot##enum(struct tvec_state *tv,            \
+                                     const struct tvec_enuminfo *ei,   \
+                                     ty e0, ty e1,                     \
+                                     const char *file, unsigned lno,   \
+                                     const char *expr)                 \
+       {                                                               \
+         union tvec_misc arg;                                          \
+                                                                       \
+         assert(ei->mv == TVMISC_##tag);                               \
+         arg.p = ei;                                                   \
+         tv->in[0].v.slot = GET_##tag(e0);                             \
+         tv->out[0].v.slot = GET_##tag(e1);                            \
+         return (tvec_claimeq(tv, &tvty_enum, &arg, file, lno, expr)); \
+       }
+#define GET_INT(e) (e)
+#define GET_UINT(e) (e)
+#define GET_PTR(e) ((/*unconst*/ void *)(e))
+TVEC_MISCSLOTS(DEFCLAIM)
+#undef DEFCLAIM
+#undef GET_INT
+#undef GET_UINT
+#undef GET_PTR
+
+/*----- Flag types --------------------------------------------------------*/
+
+static void parse_flags(union tvec_regval *rv, const struct tvec_regdef *rd,
+                       struct tvec_state *tv)
+{
+  const struct tvec_flaginfo *fi = rd->arg.p;
+  const struct tvec_flag *f;
+  unsigned long m = 0, v = 0, t;
+  dstr d = DSTR_INIT;
+  int ch;
+
+  for (;;) {
+    DRESET(&d); tvec_readword(tv, &d, "|;", "flag name or integer");
+
+    for (f = fi->fv; f->tag; f++)
+      if (STRCMP(f->tag, ==, d.buf)) {
+       if (m&f->m) tvec_error(tv, "colliding flag setting");
+       else { m |= f->m; v |= f->v; goto next; }
+      }
+
+    parse_unsigned(&t, d.buf, fi->range, tv); v |= t;
+  next:
+    if (tvec_nexttoken(tv)) break;
+    ch = getc(tv->fp); if (ch != '|') tvec_syntax(tv, ch, "`|'");
+    if (tvec_nexttoken(tv)) tvec_syntax(tv, '\n', "flag name or integer");
+  }
+  rv->u = v;
+}
+
+static void dump_flags(const union tvec_regval *rv,
+                      const struct tvec_regdef *rd,
+                      struct tvec_state *tv, unsigned style)
+{
+  const struct tvec_flaginfo *fi = rd->arg.p;
+  const struct tvec_flag *f;
+  unsigned long m = ~(unsigned long)0, v = rv->u;
+  const char *sep;
+
+  for (f = fi->fv, sep = ""; f->tag; f++)
+    if ((m&f->m) && (v&f->m) == f->v) {
+      tvec_write(tv, "%s%s", sep, f->tag); m &= ~f->m;
+      sep = style&TVSF_COMPACT ? "|" : " | ";
+    }
+
+  if (v&m) tvec_write(tv, "%s0x%0*lx", sep, hex_width(v), v&m);
+
+  if (!(style&TVSF_COMPACT))
+    tvec_write(tv, " ; = 0x%0*lx", hex_width(rv->u), rv->u);
+}
+
+const struct tvec_regty tvty_flags = {
+  init_uint, release_int, eq_uint, measure_int,
+  tobuf_uint, frombuf_uint,
+  parse_flags, dump_flags
+};
+
+int tvec_claimeq_flags(struct tvec_state *tv,
+                      const struct tvec_flaginfo *fi,
+                      unsigned long f0, unsigned long f1,
+                      const char *file, unsigned lno, const char *expr)
+{
+  union tvec_misc arg;
+
+  arg.p = fi; tv->in[0].v.u = f0; tv->out[0].v.u = f1;
+  return (tvec_claimeq(tv, &tvty_flags, &arg, file, lno, expr));
+}
+
+/*----- Text and byte strings ---------------------------------------------*/
+
+void tvec_allocstring(union tvec_regval *rv, size_t sz)
+{
+  if (rv->str.sz < sz) { xfree(rv->str.p); rv->str.p = xmalloc(sz); }
+  rv->str.sz = sz;
+}
+
+void tvec_allocbytes(union tvec_regval *rv, size_t sz)
+{
+  if (rv->bytes.sz < sz) { xfree(rv->bytes.p); rv->bytes.p = xmalloc(sz); }
+  rv->bytes.sz = sz;
+}
+
+static void init_string(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->str.p = 0; rv->str.sz = 0; }
+
+static void init_bytes(union tvec_regval *rv, const struct tvec_regdef *rd)
+  { rv->bytes.p = 0; rv->bytes.sz = 0; }
+
+static void release_string(union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
+  { xfree(rv->str.p); }
+
+static void release_bytes(union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
+  { xfree(rv->bytes.p); }
+
+static int eq_string(const union tvec_regval *rv0,
+                    const union tvec_regval *rv1,
+                    const struct tvec_regdef *rd)
+{
+  return (rv0->str.sz == rv1->str.sz &&
+         (!rv0->bytes.sz ||
+          MEMCMP(rv0->str.p, ==, rv1->str.p, rv1->str.sz)));
+}
+
+static int eq_bytes(const union tvec_regval *rv0,
+                   const union tvec_regval *rv1,
+                   const struct tvec_regdef *rd)
+{
+  return (rv0->bytes.sz == rv1->bytes.sz &&
+         (!rv0->bytes.sz ||
+          MEMCMP(rv0->bytes.p, ==, rv1->bytes.p, rv1->bytes.sz)));
+}
+
+static size_t measure_string(const union tvec_regval *rv,
+                            const struct tvec_regdef *rd)
+  { return (rv->str.sz + 4); }
+
+static size_t measure_bytes(const union tvec_regval *rv,
+                           const struct tvec_regdef *rd)
+  { return (rv->bytes.sz + 4); }
+
+static int tobuf_string(buf *b, const union tvec_regval *rv,
+                       const struct tvec_regdef *rd)
+  { return (buf_putmem32l(b, rv->str.p, rv->str.sz)); }
+
+static int tobuf_bytes(buf *b, const union tvec_regval *rv,
+                      const struct tvec_regdef *rd)
+  { return (buf_putmem32l(b, rv->bytes.p, rv->bytes.sz)); }
+
+static int frombuf_string(buf *b, union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
+{
+  const void *p;
+  size_t sz;
+
+  p = buf_getmem32l(b, &sz); if (!p) return (-1);
+  tvec_allocstring(rv, sz); memcpy(rv->str.p, p, sz);
+  return (0);
+}
+
+static int frombuf_bytes(buf *b, union tvec_regval *rv,
+                        const struct tvec_regdef *rd)
+{
+  const void *p;
+  size_t sz;
+
+  p = buf_getmem32l(b, &sz); if (!p) return (-1);
+  tvec_allocbytes(rv, sz); memcpy(rv->bytes.p, p, sz);
+  return (0);
+}
+
+static void check_string_length(size_t sz, const struct tvec_urange *ur,
+                               struct tvec_state *tv)
+{
+  if (ur && (ur->min > sz || sz > ur->max))
+    tvec_error(tv, "invalid string length %lu; must be in [%lu..%lu]",
+              (unsigned long)sz, ur->min, ur->max);
+}
+
+static void parse_string(union tvec_regval *rv, const struct tvec_regdef *rd,
+                        struct tvec_state *tv)
+{
+  void *p = rv->str.p;
+
+  read_compound_string(&p, &rv->str.sz, TVCODE_BARE, tv); rv->str.p = p;
+  check_string_length(rv->str.sz, rd->arg.p, tv);
+}
+
+static void parse_bytes(union tvec_regval *rv, const struct tvec_regdef *rd,
+                       struct tvec_state *tv)
+{
+  void *p = rv->bytes.p;
+
+  read_compound_string(&p, &rv->bytes.sz, TVCODE_HEX, tv); rv->bytes.p = p;
+  check_string_length(rv->bytes.sz, rd->arg.p, tv);
+}
+
+static void dump_string(const union tvec_regval *rv,
+                       const struct tvec_regdef *rd,
+                       struct tvec_state *tv, unsigned style)
+{
+  const unsigned char *p, *q, *l;
+  int ch;
+  unsigned f = 0;
+#define f_nonword 1u
+#define f_newline 2u
+
+  if (!rv->str.sz) { tvec_write(tv, "\"\""); return; }
+
+  p = (const unsigned char *)rv->str.p; l = p + rv->str.sz;
+  if (*p == '!' || *p == ';' || *p == '"' || *p == '\'') goto quote;
+  for (q = p; q < l; q++)
+    if (*q == '\n' && q != l - 1) f |= f_newline;
+    else if (!*q || !isgraph(*q) || *q == '\\') f |= f_nonword;
+  if (f&f_newline) { tvec_write(tv, "\n\t"); goto quote; }
+  else if (f&f_nonword) goto quote;
+  tv->output->ops->write(tv->output, (const char *)p, rv->str.sz); return;
+
+quote:
+  tvec_write(tv, "\"");
+  for (q = p; q < l; q++)
+    switch (*q) {
+      case '"': case '\\': ch = *q; goto escape;
+      case '\a': ch = 'a'; goto escape;
+      case '\b': ch = 'b'; goto escape;
+      case '\x1b': ch = 'e'; goto escape;
+      case '\f': ch = 'f'; goto escape;
+      case '\r': ch = 'r'; goto escape;
+      case '\t': ch = 't'; goto escape;
+      case '\v': ch = 'v'; goto escape;
+      escape:
+       if (p < q)
+         tv->output->ops->write(tv->output, (const char *)p, q - p);
+       tvec_write(tv, "\\%c", ch); p = q + 1;
+       break;
+
+      case '\n':
+       if (p < q)
+         tv->output->ops->write(tv->output, (const char *)p, q - p);
+       tvec_write(tv, "\\n"); p = q + 1;
+       if (!(style&TVSF_COMPACT) && q < l) tvec_write(tv, "\"\t\"");
+       break;
+
+      default:
+       if (isprint(*q)) break;
+       if (p < q)
+         tv->output->ops->write(tv->output, (const char *)p, q - p);
+       tvec_write(tv, "\\x{%0*x}", hex_width(UCHAR_MAX), *q); p = q + 1;
+       break;
+    }
+  if (p < q) tv->output->ops->write(tv->output, (const char *)p, q - p);
+  tvec_write(tv, "\"");
+
+#undef f_nonword
+#undef f_newline
+}
+
+static void dump_bytes(const union tvec_regval *rv,
+                      const struct tvec_regdef *rd,
+                      struct tvec_state *tv, unsigned style)
+{
+  const unsigned char *p = rv->bytes.p, *l = p + rv->bytes.sz;
+  size_t off, sz = rv->bytes.sz;
+  unsigned i, n;
+  int wd;
+
+  if (!sz) {
+    tvec_write(tv, style&TVSF_COMPACT ? "\"\"" : "\"\" ; empty");
+    return;
+  }
+
+  if (style&TVSF_COMPACT) {
+    while (p < l) tvec_write(tv, "%02x", *p++);
+    return;
+  }
+
+  if (sz > 16) tvec_write(tv, "\n\t");
+
+  off = 0; wd = hex_width(sz);
+  while (p < l) {
+    if (l - p < 16) n = l - p;
+    else n = 16;
+
+    for (i = 0; i < 16; i++) {
+      if (i < n) tvec_write(tv, "%02x", p[i]);
+      else tvec_write(tv, "  ");
+      if (i%4 == 3) tvec_write(tv, " ");
+    }
+    tvec_write(tv, " ; ");
+    if (sz > 16) tvec_write(tv, "[%0*lx] ", wd, (unsigned long)off);
+    for (i = 0; i < n; i++)
+      tvec_write(tv, "%c", isprint(p[i]) ? p[i] : '.');
+    p += n; off += n;
+    if (p < l) tvec_write(tv, "\n\t");
+  }
+}
+
+const struct tvec_regty tvty_string = {
+  init_string, release_string, eq_string, measure_string,
+  tobuf_string, frombuf_string,
+  parse_string, dump_string
+};
+
+const struct tvec_regty tvty_bytes = {
+  init_bytes, release_bytes, eq_bytes, measure_bytes,
+  tobuf_bytes, frombuf_bytes,
+  parse_bytes, dump_bytes
+};
+
+int tvec_claimeq_string(struct tvec_state *tv,
+                       const char *p0, size_t sz0,
+                       const char *p1, size_t sz1,
+                       const char *file, unsigned lno, const char *expr)
+{
+  tv->in[0].v.str.p = (/*unconst*/ char *)p0; tv->in[0].v.str.sz = sz0;
+  tv->out[0].v.str.p =(/*unconst*/ char *) p1; tv->out[0].v.str.sz = sz1;
+  return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr));
+}
+
+int tvec_claimeq_strz(struct tvec_state *tv,
+                     const char *p0, const char *p1,
+                     const char *file, unsigned lno, const char *expr)
+{
+  tv->in[0].v.str.p = (/*unconst*/ char *)p0;
+    tv->in[0].v.str.sz = strlen(p0);
+  tv->out[0].v.str.p = (/*unconst*/ char *)p1;
+    tv->out[0].v.str.sz = strlen(p1);
+  return (tvec_claimeq(tv, &tvty_string, 0, file, lno, expr));
+}
+
+int tvec_claimeq_bytes(struct tvec_state *tv,
+                      const void *p0, size_t sz0,
+                      const void *p1, size_t sz1,
+                      const char *file, unsigned lno, const char *expr)
+{
+  tv->in[0].v.bytes.p = (/*unconst*/ void *)p0;
+    tv->in[0].v.bytes.sz = sz0;
+  tv->out[0].v.bytes.p = (/*unconst*/ void *)p1;
+    tv->out[0].v.bytes.sz = sz1;
+  return (tvec_claimeq(tv, &tvty_bytes, 0, file, lno, expr));
+}
+
+/*----- Buffer type -------------------------------------------------------*/
+
+static int eq_buffer(const union tvec_regval *rv0,
+                    const union tvec_regval *rv1,
+                    const struct tvec_regdef *rd)
+  { return (rv0->bytes.sz == rv1->bytes.sz); }
+
+static int tobuf_buffer(buf *b, const union tvec_regval *rv,
+                        const struct tvec_regdef *rd)
+  { return (unsigned_to_buf(b, rv->bytes.sz)); }
+
+static int frombuf_buffer(buf *b, union tvec_regval *rv,
+                         const struct tvec_regdef *rd)
+{
+  unsigned long u;
+
+  if (unsigned_from_buf(b, &u)) return (-1);
+  if (u > (size_t)-1) return (-1);
+  tvec_allocbytes(rv, u); memset(rv->bytes.p, '!', u);
+  return (0);
+}
+
+static const char units[] = "kMGTPEZY";
+
+static void parse_buffer(union tvec_regval *rv,
+                        const struct tvec_regdef *rd,
+                        struct tvec_state *tv)
+{
+  dstr d = DSTR_INIT;
+  char *q; const char *unit;
+  int olderr;
+  size_t pos;
+  unsigned long u, t;
+  unsigned f = 0;
+#define f_range 1u
+
+  tvec_readword(tv, &d, ";", "buffer length");
+  olderr = errno; errno = 0;
+  u = strtoul(d.buf, &q, 0);
+  if (errno) goto bad;
+  errno = olderr;
+  if (!*q) {
+    tvec_skipspc(tv); pos = d.len;
+    if (!tvec_readword(tv, &d, ";", 0)) pos++;
+    q = d.buf + pos;
+  }
+
+  if (u > (size_t)-1) goto rangerr;
+  for (t = u, unit = units; *unit; unit++) {
+    if (t > (size_t)-1/1024) f |= f_range;
+    else t *= 1024;
+    if (*q == *unit && (!q[1] || q[1] == 'B')) {
+      if (f&f_range) goto rangerr;
+      u = t; q += 2; break;
+    }
+  }
+  if (*q && *q != ';') goto bad;
+  check_string_length(u, rd->arg.p, tv);
+
+  tvec_flushtoeol(tv, 0);
+  tvec_allocbytes(rv, u); memset(rv->bytes.p, 0, u);
+  DDESTROY(&d); return;
+
+bad:
+  tvec_error(tv, "invalid buffer length `%s'", d.buf);
+
+rangerr:
+  tvec_error(tv, "buffer length `%s' out of range", d.buf);
+
+#undef f_range
+}
+
+static void dump_buffer(const union tvec_regval *rv,
+                       const struct tvec_regdef *rd,
+                       struct tvec_state *tv, unsigned style)
+{
+  const char *unit;
+  unsigned long u = rv->bytes.sz;
+
+  if (!u || u%1024)
+    tvec_write(tv, "%lu B", u);
+  else {
+    for (unit = units, u /= 1024; !(u%1024) && unit[1]; u /= 1024, unit++);
+    tvec_write(tv, "%lu %cB", u, *unit);
+  }
+}
+
+const struct tvec_regty tvty_buffer = {
+  init_bytes, release_bytes, eq_buffer, measure_int,
+  tobuf_buffer, frombuf_buffer,
+  parse_buffer, dump_buffer
+};
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/test/tvec.h b/test/tvec.h
new file mode 100644 (file)
index 0000000..f6cbcc8
--- /dev/null
@@ -0,0 +1,571 @@
+/* -*-c-*-
+ *
+ * Test vector processing framework
+ *
+ * (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_TVEC_H
+#define MLIB_TVEC_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+
+#ifndef MLIB_BUF_H
+#  include "buf.h"
+#endif
+
+#ifndef MLIB_CONTROL_H
+#  include "control.h"
+#endif
+
+#ifndef MLIB_BUF_H
+#  include "dstr.h"
+#endif
+
+#ifndef MLIB_MACROS_H
+#  include "macros.h"
+#endif
+
+/*----- Miscellaneous values ----------------------------------------------*/
+
+/* These are attached to structures which represent extension points, as a
+ * way to pass an opaque parameter to whatever things are hooked onto them.
+ */
+
+#define TVEC_MISCSLOTS(_)                                              \
+       _(PTR, const void *, p)         /* arbitrary pointer */         \
+       _(INT, long, i)                 /* signed integer */            \
+       _(UINT, unsigned long, u)       /* signed integer */
+
+union tvec_misc {
+#define TVEC_DEFSLOT(tag, ty, slot) ty slot;
+  TVEC_MISCSLOTS(TVEC_DEFSLOT)
+#undef TVEC_DEFSLOT
+};
+enum {
+#define TVEC_DEFCONST(tag, ty, slot) TVMISC_##tag,
+  TVEC_MISCSLOTS(TVEC_DEFCONST)
+  TVMISC_LIMIT
+};
+
+/*----- Register values ---------------------------------------------------*/
+
+/* The framework doesn't have a preconceived idea about what's in a register
+ * value: it just allocates them and accesses them through the register type
+ * functions.  It doesn't even have a baked-in idea of how big a register
+ * value is: instead, it gets that via the `regsz' slot in `struct
+ * tvec_testinfo'.  So, as far as the framework is concerned, it's safe to
+ * add new slots to this union, even if they make the overall union larger.
+ * This can be done by defining the preprocessor macro `TVEC_REGSLOTS' to be
+ * a `union' fragment defining any additional union members.
+ *
+ * This creates a distinction between code which does and doesn't know the
+ * size of a register value.  Code which does, which typically means the test
+ * functions, benchmarking setup and teardown functions, and tightly-bound
+ * runner functions, is free to index the register vectors directly.  Code
+ * which doesn't, which means the framework core itself and output formatting
+ * machinery, must use the `TVEC_REG' macro (or its more general `TVEC_GREG'
+ * companion) for indexing register vectors.  (In principle, register type
+ * handlers also fit into this category, but they have no business peering
+ * into register values other than the one's they're given.)
+ */
+
+union tvec_regval {
+       /* The actual register value.  This is what the type handler sees.
+        * Additional members can be added by setting `TVEC_REGSLOTS' before
+        * including this file.
+        *
+        * A register value can be /initialized/, which simply means that its
+        * contents represent a valid value according to its type -- the
+        * register can be compared, dumped, serialized, parsed into, etc.
+        * You can't do anything safely to an uninitialized register value
+        * other than initialize it.
+        */
+
+  long i;                              /* signed integer */
+  unsigned long u;                     /* unsigned integer */
+  void *p;                             /* pointer */
+  struct { unsigned char *p; size_t sz; } bytes; /* binary string of bytes */
+  struct { char *p; size_t sz; } str;  /* text string */
+#ifdef TVEC_REGSLOTS
+  TVEC_REGSLOTS
+#endif
+};
+
+struct tvec_reg {
+       /* A register.
+        *
+        * Note that all of the registers listed as being used by a
+        * particular test group are initialized at all times[1] while that
+        * test group is being processed.  (The other register slots don't
+        * even have types associated with them, so there's nothing useful we
+        * could do with them.)
+        *
+        * The `TVRF_LIVE' flag indicates that the register was assigned a
+        * value by the test vector file: it's the right thing to use to
+        * check whether an optional register is actually present.  Even
+        * `dead' registers are still initialized, though.
+        *
+        * [1] This isn't quite true.  Between individual tests, the
+        *     registers are released and reinitialized in order to reset
+        *     them to known values ready for the next test.  But you won't
+        *     see them at this point.
+        */
+
+  unsigned f;                          /* flags */
+#define TVRF_LIVE 1u                   /*   used in current test  */
+  union tvec_regval v;                 /* register value */
+};
+
+struct tvec_regdef {
+       /* A register definition.  Register definitions list the registers
+        * which are used by a particular test group (see `struct tvec_test'
+        * below).
+        *
+        * A vector of register definitions is terminated by a definition
+        * whose `name' slot is null.
+        */
+
+  const char *name;                    /* register name (for input files) */
+  unsigned i;                          /* register index */
+  const struct tvec_regty *ty;         /* register type descriptor */
+  unsigned f;                          /* flags */
+#define TVRF_OPT 1u                    /*   optional register */
+#define TVRF_ID 2u                     /*   part of test identity  */
+  union tvec_misc arg;                 /* extra detail for the type */
+};
+
+extern int tvec_serialize(const struct tvec_reg */*rv*/,
+                         const struct tvec_regdef */*regs*/,
+                         unsigned /*nr*/, size_t /*regsz*/,
+                         void **/*p_out*/, size_t */*sz_out*/);
+
+extern int tvec_deserialize(struct tvec_reg */*rv*/,
+                           const struct tvec_regdef */*regs*/,
+                           unsigned /*nr*/, size_t /*regsz*/,
+                           const void */*p*/, size_t /*sz*/);
+
+/*----- Test state --------------------------------------------------------*/
+
+enum { TVOUT_LOSE, TVOUT_SKIP, TVOUT_WIN, TVOUT_LIMIT };
+
+struct tvec_state {
+  unsigned f;                          /* flags */
+#define TVSF_SKIP 1u                   /*   skip this test group */
+#define TVSF_OPEN 2u                   /*   test is open */
+#define TVSF_ACTIVE 4u                 /*   test is active */
+#define TVSF_OUTMASK 0xf0              /*   test outcome */
+#define TVSF_OUTSHIFT 4
+  unsigned nrout, nreg;                       /* number of output/total registers */
+  size_t regsz;                                /* size of register entry */
+  struct tvec_reg *in, *out;           /* register vectors */
+  char expst, st;                      /* progress status codes */
+  const struct tvec_test *tests, *test;        /* all tests and current test */
+  unsigned curr[TVOUT_LIMIT], all[TVOUT_LIMIT], grps[TVOUT_LIMIT];
+  struct tvec_output *output;          /* output formatter */
+  const char *infile; unsigned lno, test_lno; /* input file name, line */
+  FILE *fp;                            /* input file stream */
+};
+
+#define TVEC_GREG(vec, i, regsz)                                       \
+       ((struct tvec_reg *)((unsigned char *)(vec) + (i)*(regsz)))
+#define TVEC_REG(tv, vec, i) TVEC_GREG((tv)->vec, (i), (tv)->regsz)
+
+/*----- Test descriptions -------------------------------------------------*/
+
+typedef void tvec_hookfn(struct tvec_state */*tv*/);
+typedef void tvec_testfn(const struct tvec_reg */*in*/,
+                        struct tvec_reg */*out*/,
+                        void */*ctx*/);
+
+struct tvec_test {
+  const char *name;                    /* name of the test */
+  const struct tvec_regdef *regs;      /* descriptions of the registers */
+  tvec_hookfn *preflight;              /* check before starting */
+  tvec_hookfn *run;                    /* test runner */
+  tvec_testfn *fn;                     /* test function */
+  union tvec_misc arg;                 /* additional parameter to `run' */
+};
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_check(struct tvec_state */*tv*/, const char */*detail*/, ...);
+extern void tvec_check_v(struct tvec_state */*tv*/,
+                        const char */*detail*/, va_list */*ap*/);
+
+extern void tvec_runtest(struct tvec_state */*tv*/);
+
+/*----- Input utilities ---------------------------------------------------*/
+
+extern void tvec_skipspc(struct tvec_state */*tv*/);
+
+#define TVFF_ALLOWANY 1u
+extern void tvec_flushtoeol(struct tvec_state */*tv*/, unsigned /*f*/);
+
+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*/);
+
+extern int tvec_nexttoken(struct tvec_state */*tv*/);
+
+/*----- Session lifecycle -------------------------------------------------*/
+
+struct tvec_info {
+  const struct tvec_test *tests;
+  unsigned nrout, nreg;
+  size_t regsz;
+};
+
+extern void tvec_begin(struct tvec_state */*tv_out*/,
+                      const struct tvec_info */*info*/,
+                      struct tvec_output */*o*/);
+extern int tvec_end(struct tvec_state */*tv*/);
+
+extern void tvec_read(struct tvec_state */*tv*/,
+                     const char */*infile*/, FILE */*fp*/);
+
+
+/*----- Benchmarking ------------------------------------------------------*/
+
+struct tvec_bench {
+  unsigned long niter;                 /* iterations done per unit */
+  int riter, rbuf;                     /* iterations and buffer registers */
+  size_t ctxsz;                                /* size of context */
+  int (*setup)(const struct tvec_reg */*in*/, struct tvec_reg */*out*/,
+              const union tvec_misc */*arg*/, void */*ctx*/); /* setup fn */
+  void (*teardown)(void */*ctx*/);     /* teardown function, or null */
+  struct bench_state **b;              /* benchmark state anchor or null */
+  union tvec_misc arg;                 /* argument to setup */
+};
+
+extern struct bench_state *tvec_benchstate;
+
+extern int tvec_ensurebench(struct tvec_state */*tv*/,
+                           struct bench_state **/*b_out*/);
+extern void tvec_bench(struct tvec_state */*tv*/);
+
+/*----- Ad-hoc testing ----------------------------------------------------*/
+
+extern void tvec_adhoc(struct tvec_state */*tv*/, struct tvec_test */*t*/);
+
+extern void tvec_begingroup(struct tvec_state */*tv*/, const char */*name*/,
+                           const char */*file*/, unsigned /*lno*/);
+extern void tvec_reportgroup(struct tvec_state */*tv*/);
+extern void tvec_endgroup(struct tvec_state */*tv*/);
+
+#define TVEC_BEGINGROUP(tv, name)                                      \
+       do tvec_begingroup(tv, name, __FILE__, __LINE__); while (0)
+
+#define TVEC_TESTGROUP(tag, tv, name)                                  \
+       MC_WRAP(tag##__around,                                          \
+         { TVEC_BEGINGROUP(tv, name); },                               \
+         { tvec_endgroup(tv); },                                       \
+         { if (!((tv)->f&TVSF_SKIP)) tvec_skipgroup(tv, 0);            \
+           tvec_endgroup(tv); })
+
+extern void tvec_begintest(struct tvec_state */*tv*/,
+                          const char */*file*/, unsigned /*lno*/);
+extern void tvec_endtest(struct tvec_state */*tv*/);
+
+#define TVEC_BEGINTEST(tv)                                             \
+       do tvec_begintest(tv, __FILE__, __LINE__); while (0)
+
+#define TVEC_TEST(tag, tv)                                             \
+       MC_WRAP(tag##__around,                                          \
+         { TVEC_BEGINTEST(tv); },                                      \
+         { tvec_endtest(tv); },                                        \
+         { if ((tv)->f&TVSF_ACTIVE) tvec_skipgroup((tv), 0);           \
+           tvec_endtest(tv); })
+
+extern int PRINTF_LIKE(5, 6)
+  tvec_claim(struct tvec_state */*tv*/, int /*ok*/,
+            const char */*file*/, unsigned /*lno*/,
+            const char */*expr*/, ...);
+
+#define TVEC_CLAIM(tv, cond)                                           \
+       (tvec_claim(tv, !!(cond), __FILE__, __LINE__, #cond " untrue"))
+
+extern int tvec_claimeq(struct tvec_state */*tv*/,
+                       const struct tvec_regty */*ty*/,
+                       const union tvec_misc */*arg*/,
+                       const char */*file*/, unsigned /*lno*/,
+                       const char */*expr*/);
+
+/*----- Command-line interface --------------------------------------------*/
+
+extern const struct tvec_info tvec_adhocinfo;
+
+extern void tvec_parseargs(int /*argc*/, char */*argv*/[],
+                          struct tvec_state */*tv_out*/,
+                          int */*argpos_out*/,
+                          const struct tvec_info */*info*/);
+
+extern void tvec_readstdin(struct tvec_state */*tv*/);
+extern void tvec_readfile(struct tvec_state */*tv*/, const char */*file*/);
+extern void tvec_readdflt(struct tvec_state */*tv*/, const char */*file*/);
+extern void tvec_readarg(struct tvec_state */*tv*/, const char */*arg*/);
+
+extern void tvec_readargs(int /*argc*/, char */*argv*/[],
+                         struct tvec_state */*tv*/,
+                         int */*argpos_inout*/, const char */*dflt*/);
+
+extern int tvec_main(int /*argc*/, char */*argv*/[],
+                    const struct tvec_info */*info*/,
+                    const char */*dflt*/);
+
+/*----- Output formatting -------------------------------------------------*/
+
+struct tvec_output {
+  const struct tvec_outops *ops;
+  struct tvec_state *tv;
+};
+
+struct bench_timing;
+
+struct tvec_outops {
+  void (*error)(struct tvec_output */*o*/,
+               const char */*msg*/, va_list */*ap*/);
+  void (*notice)(struct tvec_output */*o*/,
+                const char */*msg*/, va_list */*ap*/);
+  void (*write)(struct tvec_output */*o*/, const char */*p*/, size_t /*sz*/);
+
+  void (*bsession)(struct tvec_output */*o*/);
+  int (*esession)(struct tvec_output */*o*/);
+
+  void (*bgroup)(struct tvec_output */*o*/);
+  void (*egroup)(struct tvec_output */*o*/, unsigned /*outcome*/);
+  void (*skipgroup)(struct tvec_output */*o*/,
+                   const char */*excuse*/, va_list */*ap*/);
+
+  void (*btest)(struct tvec_output */*o*/);
+  void (*skip)(struct tvec_output */*o*/,
+              const char */*excuse*/, va_list */*ap*/);
+  void (*fail)(struct tvec_output */*o*/,
+              const char */*detail*/, va_list */*ap*/);
+  void (*mismatch)(struct tvec_output */*o*/);
+  void (*etest)(struct tvec_output */*o*/, unsigned /*outcome*/);
+
+  void (*bbench)(struct tvec_output */*o*/);
+  void (*ebench)(struct tvec_output */*o*/,
+                const struct bench_timing */*tm*/);
+
+  void (*destroy)(struct tvec_output */*o*/);
+};
+
+extern void PRINTF_LIKE(2, 3) NORETURN
+  tvec_error(struct tvec_state */*tv*/, const char */*msg*/, ...);
+extern void NORETURN tvec_error_v(struct tvec_state */*tv*/,
+                                 const char */*msg*/, va_list */*ap*/);
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_notice(struct tvec_state */*tv*/, const char */*msg*/, ...);
+extern void tvec_notice_v(struct tvec_state */*tv*/,
+                         const char */*msg*/, va_list */*ap*/);
+
+extern void PRINTF_LIKE(3, 4) NORETURN
+  tvec_syntax(struct tvec_state */*tv*/, int /*ch*/,
+             const char */*expect*/, ...);
+extern void NORETURN tvec_syntax_v(struct tvec_state */*tv*/, int /*ch*/,
+                                  const char */*expect*/, va_list */*ap*/);
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_skipgroup(struct tvec_state */*tv*/, const char */*note*/, ...);
+extern void tvec_skipgroup_v(struct tvec_state */*tv*/,
+                            const char */*note*/, va_list */*ap*/);
+
+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*/);
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_fail(struct tvec_state */*tv*/, const char */*detail*/, ...);
+extern void tvec_fail_v(struct tvec_state */*tv*/,
+                       const char */*detail*/, va_list */*ap*/);
+
+extern void tvec_mismatch(struct tvec_state */*tv*/);
+
+extern void PRINTF_LIKE(2, 3)
+  tvec_write(struct tvec_state */*tv*/, const char */*p*/, ...);
+extern void tvec_write_v(struct tvec_state */*tv*/,
+                        const char */*p*/, va_list */*ap*/);
+
+extern struct tvec_output *tvec_humanoutput(FILE */*fp*/);
+extern struct tvec_output *tvec_tapoutput(FILE */*fp*/);
+extern struct tvec_output *tvec_dfltout(FILE */*fp*/);
+
+/*----- Register types ----------------------------------------------------*/
+
+struct tvec_regty {
+  void (*init)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/);
+  void (*release)(union tvec_regval */*rv*/,
+                 const struct tvec_regdef */*rd*/);
+  int (*eq)(const union tvec_regval */*rv0*/,
+           const union tvec_regval */*rv1*/,
+           const struct tvec_regdef */*rd*/);
+  size_t (*measure)(const union tvec_regval */*rv*/,
+                   const struct tvec_regdef */*rd*/);
+  int (*tobuf)(buf */*b*/, const union tvec_regval */*rv*/,
+              const struct tvec_regdef */*rd*/);
+  int (*frombuf)(buf */*b*/, union tvec_regval */*rv*/,
+                const struct tvec_regdef */*rd*/);
+  void (*parse)(union tvec_regval */*rv*/, const struct tvec_regdef */*rd*/,
+               struct tvec_state */*tv*/);
+  void (*dump)(const union tvec_regval */*rv*/,
+              const struct tvec_regdef */*rd*/,
+              struct tvec_state */*tv*/, unsigned /*style*/);
+#define TVSF_COMPACT 1u
+};
+
+extern const struct tvec_regty tvty_int, tvty_uint;
+struct tvec_irange { long min, max; };
+struct tvec_urange { unsigned long min, max; };
+
+extern const struct tvec_irange
+  tvrange_schar, tvrange_short, tvrange_int, tvrange_long,
+  tvrange_sbyte, tvrange_i16, tvrange_i32;
+extern const struct tvec_urange
+  tvrange_uchar, tvrange_ushort, tvrange_uint, tvrange_ulong, tvrange_size,
+  tvrange_byte, tvrange_u16, tvrange_u32;
+
+extern int tvec_claimeq_int(struct tvec_state */*tv*/,
+                           long /*i0*/, long /*i1*/,
+                           const char */*file*/, unsigned /*lno*/,
+                           const char */*expr*/);
+extern int tvec_claimeq_uint(struct tvec_state */*tv*/,
+                           unsigned long /*u0*/, unsigned long /*u1*/,
+                           const char */*file*/, unsigned /*lno*/,
+                           const char */*expr*/);
+#define TVEC_CLAIMEQ_INT(tv, i0, i1)                                   \
+       (tvec_claimeq_int(tv, i0, i1, __FILE__, __LINE__, #i0 " /= " #i1))
+#define TVEC_CLAIMEQ_UINT(tv, u0, u1)                                  \
+       (tvec_claimeq_uint(tv, u0, u1, __FILE__, __LINE__, #u0 " /= " #u1))
+
+extern const struct tvec_regty tvty_enum;
+
+#define DEFASSOC(tag_, ty, slot)                                       \
+       struct tvec_##slot##assoc { const char *tag; ty slot; };
+TVEC_MISCSLOTS(DEFASSOC)
+#undef DEFASSOC
+
+struct tvec_enuminfo {
+  const char *name; unsigned mv;
+  union {
+#define DEFENUMINFO(tag, ty, slot) struct {                            \
+         const struct tvec_##slot##assoc *av;                          \
+         RANGESLOT_##tag                                               \
+       } slot;
+#define RANGESLOT_INT const struct tvec_irange *ir;
+#define RANGESLOT_UINT const struct tvec_urange *ur;
+#define RANGESLOT_PTR
+    TVEC_MISCSLOTS(DEFENUMINFO)
+#undef DEFENUMINFO
+#undef RANGESLOT_INT
+#undef RANGESLOT_UINT
+#undef RANGESLOT_PTR
+  } u;
+};
+
+#define DECLCLAIM(tag, ty, slot)                                       \
+       extern int tvec_claimeq_##slot##enum                            \
+         (struct tvec_state */*tv*/,                                   \
+          const struct tvec_enuminfo */*ei*/, ty /*e0*/, ty /*e1*/,    \
+          const char */*file*/, unsigned /*lno*/, const char */*expr*/);
+TVEC_MISCSLOTS(DECLCLAIM)
+#undef DECLCLAIM
+#define TVEC_CLAIMEQ_IENUM(tv, ei, e0, e1)                             \
+       (tvec_claimeq_ienum(tv, ei, e0, e1,                             \
+                           __FILE__, __LINE__, #e0 " /= " #e1))
+#define TVEC_CLAIMEQ_UENUM(tv, ei, e0, e1)                             \
+       (tvec_claimeq_uenum(tv, ei, e0, e1,                             \
+                           __FILE__, __LINE__, #e0 " /= " #e1))
+#define TVEC_CLAIMEQ_PENUM(tv, ei, e0, e1)                             \
+       (tvec_claimeq_penum(tv, ei, e0, e1,                             \
+                           __FILE__, __LINE__, #e0 " /= " #e1))
+
+extern const struct tvec_regty tvty_flags;
+struct tvec_flag { const char *tag; unsigned long m, v; };
+struct tvec_flaginfo {
+  const char *name;
+  const struct tvec_flag *fv;
+  const struct tvec_urange *range;
+};
+
+extern int tvec_claimeq_flags(struct tvec_state */*tv*/,
+                             const struct tvec_flaginfo */*fi*/,
+                             unsigned long /*f0*/, unsigned long /*f1*/,
+                             const char */*file*/, unsigned /*lno*/,
+                             const char */*expr*/);
+#define TVEC_CLAIMEQ_FLAGS(tv, fi, f0, f1)                             \
+       (tvec_claimeq_flags(tv, fi, f0, f1,                             \
+                           __FILE__, __LINE__, #f0 " /= " #f1))
+
+extern const struct tvec_regty tvty_string, tvty_bytes;
+
+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*/);
+extern int tvec_claimeq_strz(struct tvec_state */*tv*/,
+                            const char */*p0*/, const char */*p1*/,
+                            const char */*file*/, unsigned /*lno*/,
+                            const char */*expr*/);
+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;
+
+extern void tvec_allocstring(union tvec_regval */*rv*/, size_t /*sz*/);
+extern void tvec_allocbytes(union tvec_regval */*rv*/, size_t /*sz*/);
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif
index 70c18f0ec45257523222b5ac666a5bd4d8ffa90d..9f4562ec705880678e8e1d1eba080ba23bd3c6f2 100644 (file)
@@ -72,6 +72,11 @@ check_PROGRAMS               += t/exc.t
 t_exc_t_SOURCES                 = t/exc-test.c
 t_exc_t_LDFLAGS                 = -static
 
+## Linear regression.
+pkginclude_HEADERS     += linreg.h
+libutils_la_SOURCES    += linreg.c
+##LIBMANS              += linreg.3
+
 ## String handling.
 pkginclude_HEADERS     += str.h
 libutils_la_SOURCES    += str.c
diff --git a/utils/control.3 b/utils/control.3
new file mode 100644 (file)
index 0000000..79e7df9
--- /dev/null
@@ -0,0 +1,492 @@
+.\" -*-nroff-*-
+.TH control 3 "23 April 2023" "Straylight/Edgeware" "mLib utilities library"
+.SH NAME
+control \- control structure metaprogramming
+.\" @MC_BEFORE
+.\" @MC_AFTER
+.\" @MC_WRAP
+.\" @MC_FINALLY
+.\" @MC_DOWHILE
+.\" @MC_DECL
+.\" @MC_LOOPELSE
+.\" @MC_LOOPBETWEEN
+.\" @MC_ALLOWELSE
+.\" @MC_GOELSE
+.\" @MC_TARGET
+.\" @MC_GOTARGET
+.\" @MC_ACT
+.\" @MC_LABEL
+.\" @MC_GOTO
+.SH SYNOPSIS
+.nf
+.B "#include <mLib/control.h>"
+
+.BI MC_BEFORE( tag ", " stmts ") " body
+.BI MC_AFTER( tag ", " stmts ") " body
+.BI MC_WRAP( tag ", " before_stmt ", " onend_stmt ", " onbreak_stmt ") " body
+.BI MC_DOWHILE( tag ", " cond ") " body
+.BI MC_DECL( tag ", " decl ") " body
+.BI MC_LOOPELSE( tag ", " head ") " loop_body " \fR[\fBelse " else_body \fR]
+
+.BI MC_TARGET( tag ", " stmt ") " body
+.BI MC_GOTARGET( tag );
+.BI MC_ALLOWELSE( tag ") " main_body " \fR[\fBelse " else_body \fR]
+.BI MC_GOELSE( tag );
+
+.BI MC_ACT( stmt )
+.BI MC_LABEL( tag )
+.BI MC_GOTO( tag )
+.fi
+.SH DESCRIPTION
+The header file
+.B <mLib/control.h>
+defines a number of macros which are useful when defining new
+control structures for C.  They are inspired by Simon Tatham's article
+.IR "Metaprogramming custom control structures in C",
+though these macros differ from Tatham's in a few respects.
+.SS "Common features"
+Each of these macros takes a
+.I tag
+argument.  A
+.I tag
+is lexically like an identifier, except that it may begin with a digit,
+so, for example, plain integers are acceptable tags.  Each use of an
+action macro by a user-level macro must have a distinct
+.IR tag .
+If you're writing a new prefix action macro written in terms of these
+existing actions, your macro should receive a
+.I tag
+from its caller, and pass this tag, along with a distinctive component
+of its own, down to any prefix actions that it calls; the
+.IR tag s
+from each layer should be separated by a pair of underscores.
+.PP
+Some of these macros work by wrapping a loop around the
+.I body
+statement.  This interferes with the way that `free'
+.B break
+and
+.B continue
+statements within the
+.I body
+behave: we say that these statements are
+.I captured
+by the macro.
+A
+.B break
+or
+.B continue
+statement is `free' if it doesn't appear lexically within a loop or
+(for
+.B break)
+.B switch
+statement that is part of the
+.IR body .
+So
+.IP
+.B "if (!x) break;"
+.PP
+contains a free
+.B break
+statement, while
+.IP
+.nf
+.ft B
+for (i = 0; i < n; i++)
+\h'4n'if (interestingp(i)) break;
+.ft
+.fi
+.PP
+does not.
+.PP
+Some of these macros take special care to give you control over what
+happens when a captured
+.B break
+is executed.  Alas, proper handling of
+.B continue
+doesn't seem possible.  Free
+.B break
+and
+.B continue
+statements
+.I within
+arguments to these macros are never captured.
+.SS "Prefix action macros"
+.B MC_BEFORE
+macro is the simplest to understand.  Executing
+.IP
+.BI MC_BEFORE( tag ", " stmt ") " body
+.PP
+has the same effect as executing
+.I stmt
+followed by
+.IR body ,
+except that the whole thing is syntactically a single statement, so, for
+example, it doesn't need to be enclosed in braces to be the body of a
+.B for
+loop.
+.B MC_BEFORE
+does not capture free
+.B break
+or
+.B continue
+statements.
+.PP
+Executing
+.IP
+.BI MC_AFTER( tag ", " stmt ") " body
+.PP
+has
+.I nearly
+the same effect as executing
+.I stmt
+followed by
+.IR body .
+Again, the whole thing is syntactically a single statement.  However,
+.B MC_AFTER
+captures free
+.B break
+and
+.B continue
+statements within the
+.IR body .
+A free
+.B break
+or
+.B continue
+abruptly ends execution of the
+.IR body ,
+immediately transferring control to the
+.IR stmt .
+.PP
+Executing
+.IP
+.BI MC_WRAP( tag ", " before ", " onend ", " onbreak ") " body
+.PP
+has
+.I nearly
+the same effect as executing
+.IR before ,
+.IR body ,
+and then
+.IR onend .
+If the
+.I body
+executes a free
+.B break
+statement, then control abruptly continues with the
+.I onbreak
+statement, and
+.I onend
+is not executed.  Currently, if the
+.I body
+executes a free
+.B continue
+statement, then control abruptly continues with the
+.I onend
+statement, but this behaviour is a bug and may be fixed in the future.
+.PP
+.\" @@@ mc_finally
+.PP
+Executing
+.IP
+.BI MC_DOWHILE( tag ", " cond ") " body
+.PP
+has exactly the same effect as
+.BI "do " body " while (" cond ); \fR,
+the only difference being that the
+.I body
+appears in tail position rather than sandwiched in the middle.
+.PP
+Executing
+.BI MC_DECL( tag ", " decl ") " body
+has the same effect as
+.BI "{ " decl "; " body " }" \fR,
+except that free
+.B break
+and
+.B continue
+statements are captured.  Currently, a free
+.B continue
+statement will simply abruptly terminate execution of the
+.IR body ,
+while a free
+.B break
+statement abruptly
+.I restarts
+executing the
+.I body
+without leaving the scope of the
+.IR decl ;
+but these behaviours are bugs and may be fixed in the future.
+.PP
+The
+.B MC_DECL
+macro makes use of the fact that a
+.B for
+statement can introduce a declaration into its body's scope in C99 and
+C++; the macro is not available in C89.
+.PP
+Executing
+.IP
+.nf
+.BI MC_LOOPELSE( head ", " tag ") "
+.RI \h'4n' loop_body
+.RB [ else
+.RI \h'4n' else_body ]
+.fi
+.PP
+results in Python-like loop behaviour.  The
+.I head
+must be a valid loop head with one of the forms
+.IP
+.BI "while (" cond ")"
+.br
+.BI "for (" decl "; " cond "; " next_expr ")"
+.br
+.BI "MC_DOWHILE(" tag ", " cond ")"
+.PP
+The resulting loop executes the same as
+.IP
+.nf
+.I head
+.RI \h'4n' loop_body
+.fi
+.PP
+If the loop ends abruptly, as a result of
+.BR break ,
+then control is passed to the statement following the loop in the usual
+way.  However, if the loop completes naturally, and the optional
+.B else
+clause is present, then the
+.I else_body
+is executed.  A free
+.B continue
+statement within the
+.I loop_body
+behaves normally.  Free
+.B break
+and
+.B continue
+statements within the
+.I else_body
+are not captured.
+.PP
+.\" @@@ loopbetween
+.SS "Lower-level machinery"
+Executing
+.IP
+.BI MC_TARGET( tag ", " stmt ") " body
+.PP
+has exactly the same effect as simply executing
+.IR body .
+Executing
+.B MC_GOTARGET
+immediately transfers control to
+.IR stmt ,
+with control continuing with the following statement, skipping the
+.IR body .
+Free
+.B break
+or
+.B continue
+statements in
+.I body
+are not captured.
+.PP
+This is most commonly useful in loops in order to arrange the correct
+behaviour of a free
+.B break
+within the loop body.  See the example below, which shows the definition
+of
+.BR MC_LOOPELSE .
+.PP
+Executing
+.IP
+.BI MC_ALLOWELSE( tag ") " main_body " \fR[\fBelse " else_body \fR]
+.PP
+has exactly the same effect as just
+.IR main_body .
+Executing
+.IP
+.BI MC_GOELSE( tag );
+.PP
+transfers control immediately to
+.I else_body
+(if present); control then naturally transfers to the following
+statement as usual.  Free
+.B break
+or
+.B continue
+statements in either of
+.I main_body
+or
+.I else_body
+are not captured.
+.PP
+Note that
+.B MC_ALLOWELSE
+works by secretly inserting an
+.B if
+statement head before the
+.IR main_body ,
+so things will likely to wrong if
+.I main_body
+is itself an
+.B if
+statement: if
+.I main_body
+lacks an
+.B else
+clause, then an
+.B else
+intended to match
+.B MC_ALLOWELSE
+will be mis-associated; and even if
+.I main_body
+.I does
+have an
+.B else
+clause, the resulting program text is likely to provoke a compiler
+warning about `dangling
+.BR else '.
+.PP
+Using these tools, it's relatively straightforward to define a macro
+like
+.BR MC_LOOPELSE ,
+described above:
+.IP
+.nf
+.ft B
+#define MC_LOOPELSE(tag, head) \e
+\h'4n'MC_TARGET(tag##__exit, { ; }) \e
+\h'4n'MC_ALLOWELSE(tag##__else) \e
+\h'4n'MC_AFTER(tag##__after, { MC_GOELSE(tag##__else); }) \e
+\h'4n'head \e
+\h'8n'MC_WRAP(tag##__body, { ; }, { ; }, \e
+\h'8n+\w'MC_WRAP(tag##__body, ''{ MC_GOTARGET(tag##__exit); })
+.ft R
+.fi
+.PP
+The main `trick' for these control-flow macros is
+.BR MC_ACT ,
+which wraps up a statement as an
+.IR action .
+An action is a valid
+.IR  "statement head" ,
+like
+.B if
+or
+.BR while :
+i.e., it must be completed by following it with a
+.I body
+statement.  Executing
+.IP
+.BI MC_ACT( stmt ") " body
+.PP
+has the same effect as simply executing
+.IR stmt ;
+the
+.I body
+is usually ignored.  Note that
+.B ;
+is a valid statement which does nothing, so
+.BI MC_ACT( stmt );
+is also a valid statement with the same effect as
+.IR stmt .
+The only way to cause
+.I body
+to be executed is to attach a label to it and transfer control using
+.BR goto .
+.PP
+Executing
+.IP
+.BI MC_LABEL( tag ") " body
+.PP
+has the same effect as
+.IR body .
+Executing
+.IP
+.BI MC_GOTO( tag )
+.PP
+immediately transfers control to the
+.IR body .
+Note that
+.B MC_GOTO
+is syntactically an action
+(i.e., it's wrapped in
+.BR MC_ACT ).
+The
+.IR tag s
+here are scoped to the top-level source line, like all
+.IR tag s
+in this macro package.
+.PP
+All of the control-flow macros in this package are mainly constructed from
+.BR MC_ACT ,
+.BR MC_LABEL ,
+and
+.BR MC_GOTO ,
+sometimes with one or two other statement heads thrown into the mix.
+For example,
+.B MC_AFTER
+is defined as
+.IP
+.nf
+.ft B
+#define MC_AFTER(tag, stmt) \e
+\h'28n'MC_GOTO(tag##__body) \e
+\h'4n'MC_LABEL(tag##__end) \e
+\h'28n'MC_ACT(stmt) \e
+\h'28n'for (;;) \e
+\h'32n'MC_GOTO(tag##__end) \e
+\h'4n'MC_LABEL(tag##__body)
+.ft R
+.fi
+.PP
+(The unusual layout is conventional, to make the overall structure of
+the code clear despite visual interference from the labels.)
+The
+.I body
+appears at the end, labelled as
+.IB tag __body \fR.
+Control enters at the start, and is immediately transferred to the
+.I body ;
+but the
+.I body
+is enclosed in a
+.B for
+loop, so when the
+.I body
+completes, the loop restarts, transferring control to
+.IB tag __end
+and the
+.IR stmt .
+Since it is enclosed in
+.BR MC_ACT ,
+once
+.I stmt
+completes, control transfers to the following statement.
+.SH BUGS
+Some macros cause free
+.B break
+and/or
+.B continue
+statements to behave in unexpected ways.
+.PP
+The need for tagging is ugly, and the restriction on having two
+user-facing control-flow macros on the same line is objectionable.  The
+latter could be avoided by using nonstandard features such as GCC's
+.B __COUNTER__
+macro, but adopting that would do programmers a disservice by
+introducing a hazard for those trying to port code to other compilers
+which lack any such feature.
+.SH "SEE ALSO"
+.BR mLib (3),
+.BR macros (3).
+.PP
+Simon Tatham,
+.IR "Metaprogramming custom control structures in C",
+.BR "https://www.chiark.greenend.org.uk/~sgtatham/mp/" .
+.SH "AUTHOR"
+Mark Wooding, <mdw@distorted.org.uk>
index 8be1cf1bdfbd1c0ebe9c4bb3294fc69af2dcd283..97be8b6b51338b2c286a784df5312e036471b986 100644 (file)
   extern "C" {
 #endif
 
+/*----- Notes on the control operator machinery ---------------------------*
+ *
+ * These macros owe an obvious and immense debt to Simon Tatham's article
+ * `Metaprogramming custom control structures in C', available from
+ * https://www.chiark.greenend.org.uk/~sgtatham/mp/.  The basic tricks are
+ * all Tatham's, as are most of the provided operators.  The focus on
+ * @MC_ACT@ as a significant primitive is probably my main original
+ * contribution.
+ */
+
 /*----- Header files ------------------------------------------------------*/
 
 #ifndef MLIB_MACROS_H
@@ -48,7 +58,6 @@
 #define MCTRL__LABEL(tag) GLUE(_mctrl__##tag##__, __LINE__)
 
 /* @MC_ACT(stmt)@
- * @MC_PASS@
  *
  * @MC_ACT@ is the main `trick' for constructing these flow-control
  * operators.  It wraps up a statement as what we call an `action'.  Actions
  * its body.
  */
 #define MC_ACT(stmt)   if (1) { stmt; } else
-#define MC_PASS                MC_ACT(;)
-
 
 /* @MC_LABEL(tag)@
- * @MC_GOTO(tag)@
+ * @MC_GOTO(tag);@
  *
  * @MC_LABEL@ just establishes a label which can be invoked (only) from the
  * same top-level macro; and @MC_GOTO@ transfers control to it.
 #define MC_LABEL(tag)  MCTRL__LABEL(tag):
 #define MC_GOTO(tag)   MC_ACT(goto MCTRL__LABEL(tag))
 
-/* @BEFORE(tag, stmt_0) stmt_1@
+/* @MC_TARGET(tag, stmt) body@
+ * @MC_GOTARGET(tag);@
+ *
+ * Executing @TARGET@ statement executes the @body@, as if the @TARGET@
+ * weren't there.  Executing a @GOTARGET@ transfers control to the @stmt@
+ * (but not normally the @body@).  In either case, the normal flow of control
+ * is then to the following statement.
+ */
+#define MC_TARGET(tag, stmt)                                           \
+                       MC_GOTO(tag##__body)                            \
+  MC_LABEL(tag##__tgt) MC_ACT(stmt)                                    \
+  MC_LABEL(tag##__body)
+#define MC_GOTARGET(tag) do MC_GOTO(tag##__tgt); while (0)
+
+/* @MC_BEFORE(tag, stmt) body@
  *
- * Execute @stmt_0@ and then @stmt_1@.
+ * Execute @stmt@ and then @body@.
  */
-#define BEFORE(tag, stmt)                                              \
+#define MC_BEFORE(tag, stmt)                                           \
                        MC_ACT(stmt; MC_GOTO(tag##__body))              \
   MC_LABEL(tag##__body)
 
-/* @AFTER(tag, stmt_0) stmt_1@
+/* @MC_AFTER(tag, stmt) body@
  *
- * Execute @stmt_1@ and then @stmt_0@.  If either statement invokes @break@
- * then control immediately transfers to the statement following @AFTER@.  If
- * either invokes @continue@, then control returns to @stmt_0@.
+ * Execute @body@ and then @stmt@.  If @body@ invokes @break@ then control
+ * immediately transfers to the statement following @MC_AFTER@.  If @body
+ * invokes @continue@, then control returns to @stmt@.
  */
-#define AFTER(tag, stmt)                                               \
+#define MC_AFTER(tag, stmt)                                            \
                        MC_GOTO(tag##__body)                            \
   MC_LABEL(tag##__end) MC_ACT(stmt)                                    \
                        for (;;)                                        \
                          MC_GOTO(tag##__end)                           \
   MC_LABEL(tag##__body)
 
-/* @WRAP(tag, before, onend, onbreak) stmt@
+/* @MC_DOWHILE(tag, cond) body@
+ *
+ * Repeatedly execute @body@ until @cond@ evaluates to zero.  The @body@ is
+ * executed once before @cond@ is checked the first time.  The @break@ and
+ * @continue@ statements work within @body@ as one would expect.
+ */
+#define MC_DOWHILE(tag, cond)                                          \
+                       MC_GOTO(tag##__body)                            \
+                       while (cond)                                    \
+  MC_LABEL(tag##__body)
+
+/* @MC_ALLOWELSE(tag) apodosis_body [else haeresis_body]@
+ * @MC_GOELSE(tag);@
  *
- * Execute the @before@ statement, followed by @stmt@.  If @stmt@ invokes
- * @break@, then @onbreak@ is immediately executed; if @stmt@ completes
+ * Executing @MC_ALLOWELSE@ executes @apodosis_body@, but not
+ * @haeresis_body@.  If @MC_GOELSE(tag)@ is executed, then control continues
+ * from @haeresis_body@.
+ */
+#define MC_ALLOWELSE(tag)                                              \
+                       MC_GOTO(tag##__body)                            \
+  MC_LABEL(tag##__else)        if (0)                                          \
+  MC_LABEL(tag##__body)
+#define MC_GOELSE(tag) do MC_GOTO(tag##__else); while (0)
+
+/* @MC_WRAP(tag, before, onend, onbreak) body@
+ *
+ * Execute the @before@ statement, followed by @body@.  If @body@ invokes
+ * @break@, then @onbreak@ is immediately executed; if @body@ completes
  * normally, or invokes @continue@ then @onend@ is immediately executed.
  * Any @break@ and @continue@ in the @before@, @onend@, and @onbreak@
  * statements behave as one would expect from their context.
  */
-#define WRAP(tag, before, onend, onbreak)                              \
+#define MC_WRAP(tag, before, onend, onbreak)                           \
                        MC_ACT(before; MC_GOTO(tag##__body))            \
   MC_LABEL(tag##__end) MC_ACT(onend)                                   \
   MC_LABEL(tag##__brk) MC_ACT(onbreak)                                 \
                            MC_GOTO(tag##__end)                         \
   MC_LABEL(tag##__body)
 
-/* @ALLOWELSE(tag) stmt_0 [else stmt_1]@
- * @GOELSE(tag);@
+/* @MC_FINALLY(tag, cleanup) body@
  *
- * Executing @ALLOWELSE@ executes @stmt_0@, but not @stmt_1@.  If
- * @GOELSE(tag)@ is executed, then control continues from @stmt_1@.
- */
-#define ALLOWELSE(tag)                                                 \
-                       MC_GOTO(tag##__body)                            \
-  MC_LABEL(tag##__else)        if (0)                                          \
-  MC_LABEL(tag##__body)
-#define GOELSE(tag)    do MC_GOTO(tag##__else); while (0)
-
-/* @DOWHILE(tag, cond) stmt@
+ * Execute @cleanup@ when @body@ completes or ends with @break@.  In the
+ * latter case, propagate the @break@ to the enclosing context -- for which
+ * it must be syntactically appropriate.
  *
- * Repeatedly execute @stmt@ until @cond@ evaluates to zero.  Execute @stmt@
- * at least once.  The @break@ and @continue@ statements work within @stmt@
- * as one would expect.
+ * The @cleanup@ code is duplicated.  If it arrange to have private long-term
+ * state, e.g, by declaring @static@ variables, then the two copies will not
+ * share the same state, so probably don't do this.
  */
-#define DOWHILE(tag, cond)                                             \
-                       MC_GOTO(tag##__body)                            \
-                       while (cond)                                    \
-  MC_LABEL(tag##__body)
+#define MC_FINALLY(tag, cleanup)                                       \
+       MP_WRAP(tag##__final, { ; } cleanup, { cleanup break; })
 
-/* @DECL(tag, decl) stmt@
+/* @MC_DECL(tag, decl) body@
  *
- * Execute @stmt@ with @decl@ in scope.  If @stmt@ completes or invokes
- * @break@ or @continue@ then control continues with the statement following
- * @DECL@.  Internally, this uses @for@, so it only works in C99 or later, or
- * C++.
+ * Execute @body@ with @decl@ in scope.  If @body@ completes or invokes
+ * @continue@ then control continues with the statement following @MC_DECL@;
+ * if it invokes @break@ then it will be restarted without leaving the scope
+ * of @decl@.  Internally, this uses @for@, so it only works in C99 or later,
+ * or C++.
  */
 #if __STDC_VERSION__ >= 199901 || defined(__cplusplus)
-#  define DECL(tag, decl)                                              \
+#  define MC_DECL(tag, decl)                                           \
                        for (decl;;)                                    \
                          MC_GOTO(tag##__body)                          \
-  MC_LABEL(tag##__end)   MC_ACT(break)                                 \
+  MC_LABEL(tag##__exit)          MC_ACT(break)                                 \
                          for (;;)                                      \
-                           MC_GOTO(tag##__end)                         \
+                           MC_GOTO(tag##__exit)                        \
   MC_LABEL(tag##__body)
 #endif
 
+/* @MC_LOOPELSE(tag, head) loop_body [else else_body]@
+ *
+ * Python-like looping with optional @else@ clause.  @head loop_body@ must be
+ * a syntactically valid @for@, @while@, or @MC_DOWHILE@ loop; if the loop
+ * exits because of @break@ then control continues in the usual way;
+ * otherwise, the @else_body@ (if any) is executed.
+ */
+#define MC_LOOPELSE(tag, head)                                         \
+       MC_TARGET(tag##__exit, { ; })                                   \
+       MC_ALLOWELSE(tag##__else)                                       \
+       MC_AFTER(tag##__after, { MC_GOELSE(tag##__else); })             \
+       head                                                            \
+         MC_WRAP(tag##__body, { ; }, { ; }, { MC_GOTARGET(tag##__exit); })
+
+/* @MC_LOOPBETWEEN(tag, setup, cond, step) loop_body [else else_body]@
+ *
+ * This is essentially a @for@ loop with a twist.  The @setup@, @cond@, and
+ * @step@ arguments are the parts of the @for@ head clause; because of the
+ * limitations of the C macro syntax, they're separated by commas rather than
+ * semicolons.
+ *
+ * The twist is that, once the @loop_body@ has finished, the @step@
+ * expression evaluated, and the @cond@ evaluated and determined to be
+ * nonzero, the @else_body@ (if any) is executed before re-entering the
+ * @loop_body@.  This makes it a useful place to insert any kind of
+ * interstitial material, e.g., printing commas between list items.
+ *
+ * The @cond@ is textually duplicated.  You'll get some code bloat if the
+ * condition is very complex.  If it somehow arranges to have private
+ * long-term state (e.g., as a result of declaring static variables inside
+ * GCC statement expressions), then the two copies will not share this state,
+ * so probably don't do this.
+ *
+ * Note that by the time that the @else_body@ is executed, the decision has
+ * already been made that another iteration will be performed, and, in
+ * particular, the @step@ has occurred.  The @else_body@ is therefore looking
+ * at the next item to be processed, not the item that has just finished
+ * being processed.
+ */
+#define MC_LOOPBETWEEN(tag, setup, cond, step)                         \
+       for (setup;;)                                                   \
+         if (!(cond)) break; else                                      \
+         MC_TARGET(tag##__exit, { break; })                            \
+         for (;;)                                                      \
+           MC_WRAP(tag##__tailwrap, { ; },                             \
+                                    { ; },                             \
+                                    { MC_GOTARGET(tag##__exit); })     \
+           MC_ALLOWELSE(tag##__tail)                                   \
+           MC_WRAP(tag##__bodywrap, { ; },                             \
+                                    { if ((step), !(cond))             \
+                                        MC_GOTARGET(tag##__exit);      \
+                                      else                             \
+                                        MC_GOELSE(tag##__tail); },     \
+                                    { MC_GOTARGET(tag##__exit); })
+
 /*----- That's all, folks -------------------------------------------------*/
 
 #ifdef __cplusplus
diff --git a/utils/linreg.c b/utils/linreg.c
new file mode 100644 (file)
index 0000000..8d999c0
--- /dev/null
@@ -0,0 +1,108 @@
+/* -*-c-*-
+ *
+ * Simple linear regression
+ *
+ * (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.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <assert.h>
+#include <math.h>
+
+#include "linreg.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @linreg_init@ --- *
+ *
+ * Arguments:  @struct linreg *lr@ = linear regression state
+ *
+ * Returns:    ---
+ *
+ * Use:                Initializes a linear-regression state ready for use.
+ */
+
+void linreg_init(struct linreg *lr)
+{
+  lr->sum_x = lr->sum_x2 = lr->sum_y = lr->sum_y2 = lr->sum_x_y = 0.0;
+  lr->n = 0;
+}
+
+/* --- @linreg_update@ --- *
+ *
+ * Arguments:  @struct linreg *lr@ = linear regression state
+ *             @double x, y@ = point coordinates
+ *
+ * Returns:    ---
+ *
+ * Use:                Informs the linear regression machinery of a point.
+ *
+ *             Note that the state size is constant, and independent of the
+ *             number of points.
+ */
+
+void linreg_update(struct linreg *lr, double x, double y)
+{
+  lr->sum_x += x; lr->sum_x2 += x*x; lr->sum_x_y += x*y;
+  lr->sum_y += y; lr->sum_y2 += y*y;
+  lr->n++;
+}
+
+/* --- @linreg_fit@ --- *
+ *
+ * Arguments:  @const struct linreg *lr@ = linear regression state
+ *             @double *m_out, *c_out, *r_out@ = where to write outputs
+ *
+ * Returns:    ---
+ *
+ * Use:                Compute the best-fit line through the previously-specified
+ *             points.  The line has the equation %$y = m x + c$%; %$m$% and
+ *             %$c$% are written to @*m_out@ and @*c_out@ respectively, and
+ *             the correlation coefficient %$r$% is written to @*r_out@.
+ *             Any (or all, but that would be useless) of the output
+ *             pointers may be null to discard that result.
+ *
+ *             At least one point must have been given.
+ */
+
+void linreg_fit(const struct linreg *lr,
+               double *m_out, double *c_out, double *r_out)
+{
+  double E_X, E_X2, E2_X, E_X_Y, E_Y, E_Y2, E2_Y, n;
+  double cov_X_Y, var_X, var_Y, m;
+  assert(lr->n);
+
+  n = lr->n; E_X_Y = lr->sum_x_y/n;
+  E_X = lr->sum_x/n; E_X2 = lr->sum_x2/n; E2_X = E_X*E_X;
+  E_Y = lr->sum_y/n; E_Y2 = lr->sum_y2/n; E2_Y = E_Y*E_Y;
+
+  cov_X_Y = E_X_Y - E_X*E_Y; var_X = E_X2 - E2_X; var_Y = E_Y2 - E2_Y;
+
+  m = cov_X_Y/var_X;
+  if (m_out) *m_out = m;
+  if (c_out) *c_out = E_Y - m*E_X;
+  if (r_out) *r_out = cov_X_Y/sqrt(var_X*var_Y);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/utils/linreg.h b/utils/linreg.h
new file mode 100644 (file)
index 0000000..2adb65f
--- /dev/null
@@ -0,0 +1,165 @@
+/* -*-c-*-
+ *
+ * Simple linear regression
+ *
+ * (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_LINREG_H
+#define MLIB_LINREG_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/* Theory.  (This should be well-known.)
+ *
+ * We have a number of data points (x_i, y_i) for 0 <= i < n.  We believe
+ * they lie close to a straight line y = m x + c, for unknown constants m and
+ * c.  Let e_i = m x_i + c - y_i.  We seek the parameters which minimize
+ * E = âˆ‘_{0\le i<n} e_i^2.
+ *
+ * We begin by simplifying
+ *
+ *     E = âˆ‘ e_i^2
+ *       = âˆ‘ (m x_i + c - y_i)^2
+ *       = âˆ‘ (m^2 x_i^2 + c^2 + y_i^2 + 2 c m x_i - 2 m x_i y_i - 2 c y_i) .
+ *
+ * (where all sums are over 0 <= i < n).  Taking partial derivatives,
+ *
+ *     âˆ‚E/∂m = âˆ‘ (2 m x_i^2 + 2 c x_i - 2 x_i y_i)
+ *           = 2 (m âˆ‘ x_i^2 + c âˆ‘ x_i - âˆ‘ x_i y_i)
+ *
+ * and
+ *
+ *     âˆ‚E/∂c = âˆ‘ (2 c + 2 m x_i - 2 y_i)
+ *           = 2 (n c + m âˆ‘ x_i - âˆ‘ y_i) .
+ *
+ * Now we solve for m and c such that the partial derivatives vanish.  Note
+ * that the second partial derivatives are nonnegative, being 2 âˆ‘ x_i^2 and 2
+ * n respectively, so we shall indeed find a minimum.
+ *
+ * Restating the problem,
+ *
+ *     m âˆ‘ x_i^2 + c âˆ‘ x_i - âˆ‘ x_i y_i = 0
+ *     m âˆ‘ x_i   + c n     - âˆ‘ y_i     = 0 .
+ *
+ * Writing E[X] = (∑ x_i)/n, E[X Y] = (∑ x_i y_i)/n, etc., this gives
+ *
+ *     m n E[X^2] + c n E[X] - n E[X Y] = 0
+ *     m n E[X]   + c n      - n E[Y]   = 0 .
+ *
+ * We see a common factor of n, so we can take that out.  Now multiply the
+ * latter equation by E[X] and subtract, giving
+ *
+ *     m (E[X^2] - E[X]^2) - (E[X Y] - E[X] E[Y])
+ *
+ * whence
+ *
+ *     m = (E[X Y] - E[X] E[Y])/(E[X^2] - E[X]^2) .
+ *
+ * The numerator is known as the covariance of X and Y, written
+ *
+ *     cov(X, Y) = Ïƒ_{XY} = E[X Y] - E[X] E[Y] ;
+ *
+ * the denominator is the variance of X, written
+ *
+ *     var(X) = Ïƒ_X = E[X^2] - E[X]^2 .
+ *
+ * The second equation gives
+ *
+ *     c = E[Y] - m E[X] .
+ *
+ * Also of interest is the correlation coefficient
+ *
+ *     r = Ïƒ_{XY}/√(σ_X Ïƒ_Y) ,
+ *
+ * which I'm not going to derive here.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+/*----- Data structures ---------------------------------------------------*/
+
+struct linreg {
+  double sum_x, sum_x2, sum_y, sum_y2, sum_x_y;
+  unsigned long n;
+};
+#define LINREG_INIT { 0.0, 0.0, 0.0, 0.0, 0.0, 0 }
+
+/*----- Functions provided ------------------------------------------------*/
+
+/* --- @linreg_init@ --- *
+ *
+ * Arguments:  @struct linreg *lr@ = linear regression state
+ *
+ * Returns:    ---
+ *
+ * Use:                Initializes a linear-regression state ready for use.
+ */
+
+extern void linreg_init(struct linreg */*lr*/);
+
+/* --- @linreg_update@ --- *
+ *
+ * Arguments:  @struct linreg *lr@ = linear regression state
+ *             @double x, y@ = point coordinates
+ *
+ * Returns:    ---
+ *
+ * Use:                Informs the linear regression machinery of a point.
+ *
+ *             Note that the state size is constant, and independent of the
+ *             number of points.
+ */
+
+extern void linreg_update(struct linreg */*lr*/, double /*x*/, double /*y*/);
+
+/* --- @linreg_fit@ --- *
+ *
+ * Arguments:  @const struct linreg *lr@ = linear regression state
+ *             @double *m_out, *c_out, *r_out@ = where to write outputs
+ *
+ * Returns:    ---
+ *
+ * Use:                Compute the best-fit line through the previously-specified
+ *             points.  The line has the equation %$y = m x + c$%; %$m$% and
+ *             %$c$% are written to @*m_out@ and @*c_out@ respectively, and
+ *             the correlation coefficient %$r$% is written to @*r_out@.
+ *             Any (or all, but that would be useless) of the output
+ *             pointers may be null to discard that result.
+ *
+ *             At least one point must have been given.
+ */
+
+extern void linreg_fit(const struct linreg */*lr*/,
+                      double */*m_out*/, double */*c_out*/,
+                      double */*r_out*/);
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif
index 73230bd23cb524a9459fb4df0f329250886dfa0b..954ed0c7ed8227f130cf0a202f9c4a034d52d824 100644 (file)
 #  define EXECL_LIKE(ntrail) __attribute__((__sentinel__(ntrail)))
 #endif
 
+#if GCC_VERSION_P(2, 7)
+#  define LAUNDER(x)                                                   \
+       ({ __typeof__(x) _y; __asm__("" : "=g"(_y) : "0"(x)); _y; })
+#  define RELAX do __asm__ __volatile__("" ::: "memory"); while (0)
+#endif
+
 #if CLANG_VERSION_P(3, 3)
 
 #  define MLIB__PRAGMA_HACK(x) _Pragma(#x)
 #  define MUST_CHECK
 #endif
 
+#ifndef LAUNDER
+#  define LAUNDER
+#endif
+
+#ifndef RELAX
+#  define RELAX
+#endif
+
 #ifndef GCC_WARNING
 #  define GCC_WARNING(warn)
 #endif
index 0d0f66cdf15a0d091dd61a1ce28ae3e501ee3019..743ad0ec1e479d85089cf3c32a832dbc3437fdcb 100644 (file)
 /*----- Header files ------------------------------------------------------*/
 
 #include "bits.h"
-#include "testrig.h"
+#include "tvec.h"
 
 /*----- Main code ---------------------------------------------------------*/
 
+enum {
+  RZ, NROUT,
+  RX = NROUT, RY, NREG,
+  RN = RY
+};
+
 #define TSHIFT(OP)                                                     \
+  static void test_##OP(const struct tvec_reg *in, struct tvec_reg *out, \
+                       void *ctx)                                      \
+  {                                                                    \
+    kludge64 x;                                                                \
                                                                        \
-static int t##OP(dstr *v)                                              \
-{                                                                      \
-  kludge64 x, xx, y;                                                   \
-  unsigned s = *(unsigned *)v[1].buf;                                  \
-  int ok = 1;                                                          \
-                                                                       \
-  LOAD64_(x, v[0].buf);                                                        \
-  LOAD64_(xx, v[2].buf);                                               \
-  y = x;                                                               \
-  OP##64_(y, y, s);                                                    \
-  if (CMP64(y, !=, xx)) {                                              \
-    ok = 0;                                                            \
-    fprintf(stderr,                                                    \
-           "\nbad: %08x:%08x " #OP " %u != %08x:%08x [%08x:%08x]\n",   \
-           HI64(x), LO64(x), s, HI64(y), LO64(y), HI64(xx), LO64(xx)); \
-  }                                                                    \
-  return (ok);                                                         \
-}
+    LOAD64_(x, in[RX].v.bytes.p);                                      \
+    OP##64_(x, x, in[RN].v.u);                                         \
+    tvec_allocbytes(&out[RZ].v, 8);                                    \
+    STORE64_(out[RZ].v.bytes.p, x);                                    \
+  }
 
 #define TARITH(OP)                                                     \
+  static void test_##OP(const struct tvec_reg *in, struct tvec_reg *out, \
+                       void *ctx)                                      \
+  {                                                                    \
+    kludge64 x, y;                                                     \
                                                                        \
-static int t##OP(dstr *v)                                              \
-{                                                                      \
-  kludge64 x, y, xx, yy;                                               \
-  int ok = 1;                                                          \
-                                                                       \
-  LOAD64_(x, v[0].buf);                                                        \
-  LOAD64_(y, v[1].buf);                                                        \
-  LOAD64_(xx, v[2].buf);                                               \
-  yy = x;                                                              \
-  OP##64(yy, yy, y);                                                   \
-  if (CMP64(yy, !=, xx)) {                                             \
-    ok = 0;                                                            \
-    fprintf(stderr,                                                    \
-           "\nbad: %08x:%08x " #OP " %08x:%08x != %08x:%08x "          \
-           "[%08x:%08x]\n",                                            \
-           HI64(x), LO64(x), HI64(y), LO64(y),                         \
-           HI64(yy), LO64(yy), HI64(xx), LO64(xx));                    \
-  }                                                                    \
-  return (ok);                                                         \
-}
+    LOAD64_(x, in[RX].v.bytes.p); LOAD64_(y, in[RY].v.bytes.p);                \
+    OP##64(x, x, y);                                                   \
+    tvec_allocbytes(&out[RZ].v, 8);                                    \
+    STORE64_(out[RZ].v.bytes.p, x);                                    \
+  }
 
 TSHIFT(LSL)
 TSHIFT(LSR)
@@ -83,20 +69,35 @@ TSHIFT(ROR)
 TARITH(ADD)
 TARITH(SUB)
 
-static test_chunk tests[] = {
-  { "lsl64", tLSL, { &type_hex, &type_int, &type_hex, 0 } },
-  { "lsr64", tLSR, { &type_hex, &type_int, &type_hex, 0 } },
-  { "rol64", tROL, { &type_hex, &type_int, &type_hex, 0 } },
-  { "ror64", tROR, { &type_hex, &type_int, &type_hex, 0 } },
-  { "add64", tADD, { &type_hex, &type_hex, &type_hex, 0 } },
-  { "sub64", tSUB, { &type_hex, &type_hex, &type_hex, 0 } },
-  { 0, 0, { 0 } }
+static const struct tvec_urange ur_eight = { 8, 8 };
+static const struct tvec_urange ur_shift = { 0, 63 };
+static const struct tvec_regdef shift_regs[] = {
+  { "x", RX, &tvty_bytes, 0, { &ur_eight } },
+  { "n", RN, &tvty_uint, 0, { &ur_shift } },
+  { "z", RZ, &tvty_bytes, 0, { &ur_eight } },
+  { 0, 0, 0, 0 }
 };
+static const struct tvec_regdef arith_regs[] = {
+  { "x", RX, &tvty_bytes, 0, { &ur_eight } },
+  { "y", RY, &tvty_bytes, 0, { &ur_eight } },
+  { "z", RZ, &tvty_bytes, 0, { &ur_eight } },
+  { 0, 0, 0, 0 }
+};
+
+static const struct tvec_test tests[] = {
+  { "lsl64", shift_regs, 0, tvec_runtest, test_LSL },
+  { "lsr64", shift_regs, 0, tvec_runtest, test_LSR },
+  { "rol64", shift_regs, 0, tvec_runtest, test_ROL },
+  { "ror64", shift_regs, 0, tvec_runtest, test_ROR },
+  { "add64", arith_regs, 0, tvec_runtest, test_ADD },
+  { "sub64", arith_regs, 0, tvec_runtest, test_SUB },
+  { 0, 0, 0, 0, 0 }
+};
+
+static const struct tvec_info testinfo =
+  { tests, NROUT, NREG, sizeof(struct tvec_reg) };
 
 int main(int argc, char *argv[])
-{
-  test_run(argc, argv, tests, SRCDIR "/t/bits.tests");
-  return (0);
-}
+  { return (tvec_main(argc, argv, &testinfo, 0)); }
 
 /*----- That's all, folks -------------------------------------------------*/
index c43f4962e8b196565ffb3cd783fb3783261b373e..baceba574d7c3a3f567e3c866489efe52b6c3a16 100644 (file)
@@ -13,45 +13,83 @@ NVEC = 64
 WD = 64
 LIMIT = 1 << WD
 MASK = LIMIT - 1
+FMT = "%%0%dx" % (WD/4)
 
-ARGS = SYS.argv[1:]; ARGS.reverse()
-def arg(default = None):
-  if len(ARGS): return ARGS.pop()
-  else: return default
+def rol(x, n): return ((x << n) | (x >> (WD - n))) & MASK
+def ror(x, n): return ((x >> n) | (x << (WD - n))) & MASK
 
-R.seed(None)
-seed = arg()
-if seed is None: SEED = R.randrange(0, 1 << 32)
-else: SEED = int(seed, 0)
-R.seed(SEED)
+class BaseVector (object):
+  def __init__(me):
+    me._all = []
+  def newseed(me, seed):
+    me._curr = []
+    me._all.append((seed, me._curr))
+  def _append(me, *args):
+    if len(args) != len(me._REGS):
+      raise TypeError("expected %d arguments" % len(me._REGS))
+    me._curr.append(args)
+  def write(me):
+    print("")
+    print("[%s]" % me._NAME)
+    for seed, vv in me._all:
+      print("")
+      print(";;; seed = 0x%08x" % seed)
+      for v in vv:
+        print("")
+        for r, x in zip(me._REGS, v): print("%s = %s" % (r, me._RFMT[r] % x))
 
-print('### Test vectors for 64-bit arithmetic macros')
-print('###   [generated; seed = 0x%08x]' % SEED)
+class ShiftVector (BaseVector):
+  _REGS = ["x", "n", "z"]
+  _RFMT = { "x": FMT, "n": "%d", "z": FMT }
+  def add(me, r):
+    for i in xrange(NVEC):
+      x = r.randrange(LIMIT)
+      n = r.randrange(70)%WD
+      z = me._op(x, n)
+      me._append(x, n, z)
+    for i in xrange(4):
+      x = r.randrange(LIMIT)
+      z = me._op(x, WD/2)
+      me._append(x, WD/2, z)
+class LSLVector (ShiftVector):
+  _NAME = "lsl64"
+  def _op(me, x, n): return (x << n)&MASK
+class LSRVector (ShiftVector):
+  _NAME = "lsr64"
+  def _op(me, x, n): return (x >> n)&MASK
+class ROLVector (ShiftVector):
+  _NAME = "rol64"
+  def _op(me, x, n): return rol(x, n)
+class RORVector (ShiftVector):
+  _NAME = "ror64"
+  def _op(me, x, n): return ror(x, n)
 
-def rol(x, n): return ((x << n) | (x >> (WD - n))) & MASK
-def ror(x, n): return ((x >> n) | (x << (WD - n))) & MASK
-def put(x): return '%0*x' % (WD//4, x)
-
-for name, func in [('lsl', lambda x, n: x << n),
-                   ('lsr', lambda x, n: x >> n),
-                   ('rol', rol),
-                   ('ror', ror)]:
-  print('\n%s64 {' % name)
-  for i in xrange(NVEC):
-    x = R.randrange(LIMIT)
-    sh = R.randrange(0, 70) & 63
-    print('  %s %2d %s;' % (put(x), sh, put(func(x, sh) & MASK)))
-  for i in xrange(4):
-    x = R.randrange(LIMIT)
-    sh = 32
-    print('  %s %2d %s;' % (put(x), sh, put(func(x, sh) & MASK)))
-  print('}')
-
-for name, func in [('add', lambda x, y: x + y),
-                   ('sub', lambda x, y: x - y)]:
-  print('\n%s64 {' % name)
-  for i in xrange(NVEC):
-    x = R.randrange(LIMIT)
-    y = R.randrange(LIMIT)
-    print('  %s %s %s;' % (put(x), put(y), put(func(x, y) & MASK)))
-  print('}')
+class ArithVector (BaseVector):
+  _REGS = ["x", "y", "z"]
+  _RFMT = { "x": FMT, "y": FMT, "z": FMT }
+  def add(me, r):
+    for i in xrange(NVEC):
+      x = r.randrange(LIMIT)
+      y = r.randrange(LIMIT)
+      z = me._op(x, y)
+      me._append(x, y, z)
+class AddVector (ArithVector):
+  _NAME = "add64"
+  def _op(me, x, y): return (x + y)&MASK
+class SubVector (ArithVector):
+  _NAME = "sub64"
+  def _op(me, x, y): return (x - y)&MASK
+
+VECS = [LSLVector(), LSRVector(), ROLVector(), RORVector(),
+        AddVector(), SubVector()]
+
+for arg in SYS.argv[1:]:
+  if arg == "-": seed = R.SystemRandom().randrange(1 << 32)
+  else: seed = int(arg, 0)
+  r = R.Random(seed)
+  for v in VECS: v.newseed(seed); v.add(r)
+
+print(";;; -*-conf-*- Test vectors for 64-bit arithmetic macros")
+print(";;;   [generated]")
+
+for v in VECS: v.write()
index 0f47e26662e8ccb012ae37dcaef7623716291e75..772ddd29a03fdcf6361e931d39ca22e96e11dcc9 100644 (file)
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
 #include "control.h"
+#include "report.h"
+#include "tvec.h"
 
 /*----- Main code ---------------------------------------------------------*/
 
-static int step = 0;
-static int rc = 0;
-
-#define STEP(s) check_step(s, __FILE__ ": " STR(__LINE__))
-#define MISSTEP STEP(-1)
-static void check_step(int s, const char *where)
+static struct tvec_state tvstate;
+static int step;
+
+#define TESTGROUP(name)                                                        \
+       TVEC_TESTGROUP(tg, &tvstate, name)                              \
+       MC_BEFORE(init, { step = 0; })
+#define TEST TVEC_TEST(test, &tvstate)
+#define STEP(s) do {                                                   \
+         tvec_claim(&tvstate, s == step, __FILE__, __LINE__,           \
+                    "found %d /= expected %d", s, step);               \
+         step++;                                                       \
+       } while (0)
+#define MISSTEP do {                                                   \
+         tvec_claim(&tvstate, 0, __FILE__, __LINE__,                   \
+                    "shouldn't have reached here");                    \
+         step++;                                                       \
+       } while (0)
+
+int main(int argc, char *argv[])
 {
-  if (step != s) {
-    fprintf(stderr, "misstep at %s: expected %d but found %d\n",
-           where, step, s);
-    rc = 2;
+  struct tvec_test test;
+  int argpos;
+  int i;
+
+  tvec_parseargs(argc, argv, &tvstate, &argpos, &tvec_adhocinfo);
+  if (argpos < argc) die(2, "no input files expected");
+  tvec_adhoc(&tvstate, &test);
+
+  TESTGROUP("before-after") {
+    MC_BEFORE(before0, STEP(0)) STEP(1);
+    MC_AFTER(after0, STEP(3)) STEP(2);
+    STEP(4);
   }
-  step++;
-}
 
-#define LASTSTEP(s) laststep(s, __FILE__ ": " STR(__LINE__))
-static void laststep(int s, const char *where)
-  { check_step(s, where); step = 0; }
+  TESTGROUP("wrap") {
+    MC_WRAP(wrap0, STEP(0), STEP(2), MISSTEP)
+      STEP(1);
+    MC_WRAP(wrap1, STEP(3), MISSTEP, STEP(5))
+      { STEP(4); break; }
+    STEP(6);
+  }
 
-#define FORELSE(head)                                                  \
-                       MC_GOTO(top)                                    \
-  MC_LABEL(out)                MC_ACT(;)                                       \
-  MC_LABEL(top)                ALLOWELSE(els)                                  \
-                       AFTER(outer, GOELSE(els))                       \
-                       for (head)                                      \
-                         WRAP(inner, { ; },                            \
-                                     { ; },                            \
-                                     { MC_GOTO(out); })
+  TESTGROUP("loop") {
+    for (;;) {
+      MC_AFTER(after1, STEP(1); break) STEP(0);
+      MISSTEP; break;
+    }
+    STEP(2);
+  }
 
-#define FOR_FIZZBUZZ(var, base, limit)                                 \
-                       MC_GOTO(top)                                    \
-  MC_LABEL(out)                MC_ACT({ ; })                                   \
-  MC_LABEL(top)                DECL(bounds,                                    \
-                            int _i = base COMMA _limit = limit)        \
-                       for (; _i < _limit; _i++)                       \
-                         DECL(buf, char _buf[24])                      \
-                         DECL(var, const char *var)                    \
-                         WRAP(wrap, {                                  \
-                           switch (_i%15) {                            \
-                             case 0:                                   \
-                               var = "fizzbuzz";                       \
-                               break;                                  \
-                             case 3: case 6: case 9: case 12:          \
-                               var = "fizz";                           \
-                               break;                                  \
-                             case 5: case 10:                          \
-                               var = "buzz";                           \
-                               break;                                  \
-                             default:                                  \
-                               sprintf(_buf, "%d", _i); var = _buf;    \
-                               break;                                  \
-                           }                                           \
-                         },                                            \
-                         { ; },                                        \
-                         { MC_GOTO(out); })
-
-int main(void)
-{
-  int i;
+#define FORELSE(head) MC_LOOPELSE(forelse, for (head))
+
+  TESTGROUP("for-else") {
+    FORELSE (i = 0; i < 10; i++) {
+      STEP(i);
+      if (i == 7) break;
+    } else
+      MISSTEP;
+    STEP(8);
+  }
+
+  TESTGROUP("for-else-break") {
+    FORELSE (i = 0; i < 10; i++) {
+      STEP(i);
+      if (i == 12) break;
+    } else
+      STEP(10);
+    STEP(11);
+  }
 
-  BEFORE(before0, STEP(0)) STEP(1);
-  AFTER(after0, STEP(3)) STEP(2);
-  LASTSTEP(4);
+#undef FORELSE
 
-  WRAP(wrap0, STEP(0), STEP(2), MISSTEP) STEP(1);
-  WRAP(wrap1, STEP(3), MISSTEP, STEP(5)) { STEP(4); break; }
-  LASTSTEP(6);
+  TESTGROUP("loop-between") {
+    MC_LOOPBETWEEN(x, i = 0, i < 5, i++) STEP(2*i);
+    else STEP(2*i - 1);
+    STEP(9);
+  }
+
+  TESTGROUP("loop-between-continue-break") {
+    MC_LOOPBETWEEN(x, i = 0, i < 5, i++) {
+      if (i == 1) { STEP(2); continue; }
+      STEP(2*i);
+      if (i == 3) break;
+    } else
+      STEP(2*i - 1);
+    STEP(7);
+  }
 
-  STEP(0);
-  for (;;) {
-    AFTER(after1, STEP(2); break) STEP(1);
-    MISSTEP; break;
+#define WRAPELSE_TEST                                                  \
+       MC_TARGET(done_plain, { STEP(4); MC_GOELSE(elsie); })           \
+       MC_WRAP(outer_wrap, { STEP(0); },                               \
+                           { STEP(6); },                               \
+                           { MISSTEP; })                               \
+       MC_ALLOWELSE(elsie)                                             \
+       MC_WRAP(inner_wrap, { STEP(1); },                               \
+                           { STEP(3); MC_GOTARGET(done_plain); },      \
+                           { MISSTEP; })
+
+  TESTGROUP("wrap-else") {
+    WRAPELSE_TEST STEP(2);
+    else STEP(5);
+    STEP(7);
   }
-  LASTSTEP(3);
-
-  FORELSE (i = 0; i < 10; i++) {
-    STEP(i);
-    if (i == 7) break;
-  } else
-    MISSTEP;
-  LASTSTEP(8);
-
-  FORELSE (i = 0; i < 10; i++) {
-    STEP(i);
-    if (i == 12) break;
-  } else
-    STEP(10);
-  LASTSTEP(11);
-
-#define TEST                                                           \
-                       MC_ACT(STEP(0); MC_GOTO(in_plain))              \
-  MC_LABEL(done_plain) MC_ACT(STEP(5); GOELSE(elsie))                  \
-  MC_LABEL(in_plain)   WRAP(outer_wrap, { STEP(1); },                  \
-                                        { STEP(7); },                  \
-                                        { MISSTEP; })                  \
-                       ALLOWELSE(elsie)                                \
-                         WRAP(inner_wrap, { STEP(2); },                \
-                                          { STEP(4);                   \
-                                            MC_GOTO(done_plain); },    \
-                                          { MISSTEP; })                \
-                         STEP(3);                                      \
-                       else                                            \
-                         STEP(6);                                      \
-                       LASTSTEP(8);
-  TEST
-#undef TEST
 
+#undef WRAPELSE_TEST
+
+  TESTGROUP("decl") {
 #if __STDC_VERSION__ >= 199901 || defined(__cplusplus)
-  STEP(0);
-  DECL(decl0, int j = 1) STEP(j);
-  LASTSTEP(2);
+    STEP(0);
+    MC_DECL(decl0, int j = 1) STEP(j);
+    STEP(2);
+#else
+    tvec_skipgroup(&tvstate, "`MC_DECL' not supported on C89");
 #endif
+  }
+
+#define FIZZBUZZ_DECLS(var)                                            \
+       int _i, _limit;                                                 \
+       char _buf[24];                                                  \
+       const char *var
+#define FOR_FIZZBUZZ(var, base, limit)                                 \
+       MC_TARGET(out, { ; })                                           \
+       MC_BEFORE(bounds, { _i = base; _limit = limit; })               \
+       for (; _i < _limit; _i++)                                       \
+         MC_WRAP(wrap,                                                 \
+           { switch (_i%15) {                                          \
+               case 0: var = "fizzbuzz"; break;                        \
+               case 3: case 6: case 9: case 12: var = "fizz"; break;   \
+               case 5: case 10: var = "buzz"; break;                   \
+               default: sprintf(_buf, "%d", _i); var = _buf; break;    \
+             } },                                                      \
+           { ; },                                                      \
+           { MC_GOTARGET(out); })
+
+  TESTGROUP("fizzbuzz") {
+    FIZZBUZZ_DECLS(fb);
+    unsigned i;
+    static const char *const ref[] = {
+      "19", "buzz", "fizz", "22", "23", "fizz", "buzz",
+      "26", "fizz", "28", "29", "fizzbuzz", "31", 0
+    };
+
+    i = 0;
+    FOR_FIZZBUZZ(fb, 19, 32)
+      TEST
+       if (TVEC_CLAIM(&tvstate, ref[i]))
+         { TVEC_CLAIMEQ_STRZ(&tvstate, fb, ref[i]); i++; }
+    TVEC_CLAIM(&tvstate, !ref[i]);
+  }
 
-  FOR_FIZZBUZZ(fb, 19, 32) printf("%s\n", fb);
+#undef FIZZBUZZ_DECLS
+#undef FOR_FIZZBUZZ
 
-  return (rc);
+  return (tvec_end(&tvstate));
 }
 
 /*----- That's all, folks -------------------------------------------------*/
index 0927f46ba473e61f68276b9634dd18654d49976e..789565d14b81d596701ec9f65b1c9017b38b4ebc 100644 (file)
  * MA 02111-1307, USA.
  */
 
-#include "testrig.h"
+#include "tvec.h"
 #include "versioncmp.h"
 
-static int t_vcmp(dstr *v)
+enum {
+  RRC, NROUT,
+  RV0 = NROUT, RV1, NREG
+};
+
+static void test_versioncmp(const struct tvec_reg *in, struct tvec_reg *out,
+                           void *ctx)
+  { out[RRC].v.i = versioncmp(in[RV0].v.str.p, in[RV1].v.str.p); }
+
+static void swap_test(struct tvec_state *tv)
 {
-  int ok = 1;
-  int rc = versioncmp(v[0].buf, v[1].buf);
-  if (rc != *(int *)v[2].buf) {
-    fprintf(stderr, "\nversioncmp(%s, %s) returns %d != %d\n",
-           v[0].buf, v[1].buf, rc, *(int *)v[2].buf);
-    ok = 0;
-  }
-  return (ok);
+  struct tvec_reg rt;
+
+  tv->st = '.';
+  tv->test->fn(tv->in, tv->out, 0); tvec_check(tv, "vanilla");
+  rt = tv->in[RV0]; tv->in[RV0] = tv->in[RV1]; tv->in[RV1] = rt;
+  tv->in[RRC].v.i = -tv->in[RRC].v.i;
+  tv->test->fn(tv->in, tv->out, 0); tvec_check(tv, "swapped");
 }
 
-static const test_chunk tests[] = {
-  { "versioncmp", t_vcmp, { &type_string, &type_string, &type_int } },
-  { 0 }
+static const struct tvec_irange cmp_range = { -1, +1 };
+static const struct tvec_regdef versioncmp_regs[] = {
+  { "v0", RV0, &tvty_string, 0 },
+  { "v1", RV1, &tvty_string, 0 },
+  { "rc", RRC, &tvty_int, 0, { &cmp_range } },
+  { 0, 0, 0 }
 };
 
+static const struct tvec_test tests[] = {
+  { "versioncmp", versioncmp_regs, 0, swap_test, test_versioncmp },
+  { 0, 0, 0, 0 }
+};
+
+static const struct tvec_info testinfo =
+  { tests, NROUT, NREG, sizeof(struct tvec_reg) };
+
 int main(int argc, char *argv[])
-  { test_run(argc, argv, tests, SRCDIR "/versioncmp.in"); return (1); }
+  { return (tvec_main(argc, argv, &testinfo, 0)); }
 
 /*----- That's all, folks -------------------------------------------------*/
index 421974a4b044d0a168ad950f528af6696e9b880a..f8924176afea5718dda53491287e820173abd049 100644 (file)
@@ -1,14 +1,54 @@
-## test for versioncmp
-
-versioncmp {
-  1.2 1.2 0;
-  1.1 1.2 -1;
-  1.1 1.0 +1;
-  1.0 1.a -1;
-  1:2.0 2:0.4 -1;
-  1.2 1.2~pre0 +1;
-  1~~ 1~~a -1;
-  1~~a 1~ -1;
-  1~ 1 -1;
-  1 1a -1;
-}
+;;; -*-conf-*-
+;;; test for versioncmp
+
+[versioncmp]
+
+;; some softball tests
+
+v0 = 1.2
+v1 = 1.2
+rc = 0
+
+v0 = 1.1
+v1 = 1.2
+rc = -1
+
+v0 = 1.1
+v1 = 1.0
+rc = +1
+
+;; compare numeric to alphabetic
+
+v0 = 1.0
+v1 = 1.a
+rc = -1
+
+;; compare epochs
+
+v0 = 1:2.0
+v1 = 2:0.4
+rc = -1
+
+;; exercise `~'
+
+v0 = 1.2
+v1 = 1.2~pre0
+rc = +1
+
+v0 = 1~~
+v1 = 1~~a
+rc = -1
+
+v0 = 1~~a
+v1 = 1~
+rc = -1
+
+v0 = 1~
+v1 = 1
+rc = -1
+
+;; juxtaposed alphabetics
+
+v0 = 1
+v1 = 1a
+rc = -1
index 41e887795767db696fef233d61fc7f964475e2f8..d7cd6c3d68fc3494d4d00ceb4218d96944162b33 100644 (file)
 ## bits
 AT_SETUP([utilities: bits])
 AT_KEYWORDS([utils bits])
-for seed in 0xaca98e08 0x0b6e95fb ""; do
-  $PYTHON SRCDIR/t/bits-testgen.py $seed >bits.tests
-  AT_CHECK([BUILDDIR/t/bits.t -f bits.tests], [0], [ignore], [ignore])
-done
+$PYTHON SRCDIR/t/bits-testgen.py 0xaca98e08 0x0b6e95fb - >bits.tests
+AT_CHECK([BUILDDIR/t/bits.t bits.tests], [0], [ignore], [ignore])
 AT_CLEANUP
 
 ## control
 AT_SETUP([utilities: control])
 AT_KEYWORDS([utils control])
-AT_DATA([expout],
-[19
-buzz
-fizz
-22
-23
-fizz
-buzz
-26
-fizz
-28
-29
-fizzbuzz
-31
-])
-AT_CHECK([BUILDDIR/t/control.t], [0], [expout])
+AT_CHECK([BUILDDIR/t/control.t], [0], [ignore], [ignore])
 AT_CLEANUP
 
 ## exc
@@ -76,7 +59,7 @@ AT_CLEANUP
 ## versioncmp
 AT_SETUP([utilities: versioncmp])
 AT_KEYWORDS([utils versioncmp])
-AT_CHECK([BUILDDIR/t/versioncmp.t -f SRCDIR/t/versioncmp.tests],
+AT_CHECK([BUILDDIR/t/versioncmp.t SRCDIR/t/versioncmp.tests],
         [0], [ignore], [ignore])
 AT_CLEANUP