@@@ fltfmt mess
[mLib] / utils / t / fltfmt-testgen
diff --git a/utils/t/fltfmt-testgen b/utils/t/fltfmt-testgen
new file mode 100755 (executable)
index 0000000..7f63664
--- /dev/null
@@ -0,0 +1,449 @@
+#! /usr/bin/python
+###
+### Generate exhaustive tests for floating-point conversions.
+###
+### (c) 2024 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.
+
+###--------------------------------------------------------------------------
+### Imports.
+
+import sys as SYS
+import optparse as OP
+import random as R
+if SYS.version_info >= (3,):
+  from io import StringIO
+  xrange = range
+  def iterkeys(d): return d.keys()
+else:
+  from cStringIO import StringIO
+  def iterkeys(d): return d.iterkeys()
+
+###--------------------------------------------------------------------------
+### Utilities.
+
+def bit(k): "Return an integer with just bit K set."; return 1 << k
+def mask(k): "Return an integer with bits 0 to K - 1 set."; return bit(k) - 1
+M32 = mask(32)
+
+def explore(wd, lobits, hibits):
+  """
+  Return an iterator over various WD-bit values.
+
+  Suppose that a test wants to explore various WD-bit fields, but WD might be
+  too large to do this exhaustively.  We assume (reasonably, in the case at
+  hand of floating-point formats) that the really interesting bits are those
+  at the low and high ends of the field, and test small subfields at the ends
+  exhaustively, filling in the bits in the middle with zeros, ones, or random
+  data.
+
+  So, the generator behaves as follows.  If WD <= LOBITS + HIBITS + 1 then
+  the iterator will yield all WD-bit values exhaustively.  Otherwise, it
+  yields a sequence which includes all combinations of: every LOBITS-bit
+  pattern in the least significant bits; every HIBITS-bit pattern in the most
+  significant bits; and all-bits-clear, all-bits-set, and a random pattern in
+  the bits in between.
+  """
+
+  if wd <= hibits + lobits + 1:
+    for i in xrange(bit(wd)): yield i
+  else:
+    midbit = bit(wd - hibits - lobits)
+    hishift = wd - hibits
+    m = (midbit - 1) << lobits
+    for hi in xrange(bit(hibits)):
+      top = hi << hishift
+      for lo in xrange(bit(lobits)):
+        base = lo | top
+        yield base
+        yield base | (R.randrange(midbit) << lobits)
+        yield base | m
+
+class ExploreParameters (object):
+  """
+  Simple structure for exploration parameters; see `explore' for background.
+
+  The `explo' and `exphi' attributes are the low and high subfield sizes
+  for exponent fields, and `siglo' and `sighi' are the low and high subfield
+  sizes for significand fields.
+  """
+  def __init__(me, explo = 0, exphi = 2, siglo = 1, sighi = 3):
+    me.explo, me.exphi = explo, exphi
+    me.siglo, me.sighi = siglo, sighi
+
+FMTMAP = {} # maps format names to classes
+
+def with_metaclass(meta, *supers):
+  """
+  Return an arbitrary instance of the metaclass META.
+
+  The class will have SUPERS (default just `object') as its superclasses.
+  This is intended to be used in direct-superclass lists, as a compatibility
+  hack, because the Python 2 and 3 syntaxes are wildly different.
+  """
+  return meta("#<anonymous base %s>" % meta.__name__,
+              supers or (object,), dict())
+
+class FormatClass (type):
+  """
+  Metaclass for format classes.
+
+  If the class defines a `NAME' attribute then register the class in
+  `FMTMAP'.
+  """
+  def __new__(cls, name, supers, dict):
+    c = type.__new__(cls, name, supers, dict)
+    try: FMTMAP[c.NAME] = c
+    except AttributeError: pass
+    return c
+
+class IEEEFormat (with_metaclass(FormatClass)):
+  """
+  Floating point format class.
+
+  Concrete subclasses must define the following class attributes.
+
+    * `HIDDENP' -- true if the format uses a `hidden bit' convention for
+      normal numbers.
+
+    * `EXPWD' -- exponent field width, in bits.
+
+    * `PREC' -- precision, in bits, /including/ the hidden bit if any.
+
+  Many useful quantities are derived.
+
+    * `_expbias' is the exponent bias.
+
+    * `_minexp' and `_maxexp' are the minimum and maximum representable
+      exponents.
+
+    * `_sigwd' is the width of the significand field.
+
+    * `_paywords' is the number of words required to represent a NaN payload.
+
+    * `_nbits' is the total number of bits in an encoded value.
+
+    * `_rawbytes' is the number of bytes required for an encoded value.
+  """
+
+  def __init__(me):
+    """
+    Initialize an instance.
+    """
+    me._expbias = mask(me.EXPWD - 1)
+    me._maxexp = me._expbias
+    me._minexp = 1 - me._expbias
+    if me.HIDDENP: me._sigwd = me.PREC - 1
+    else: me._sigwd = me.PREC
+
+    me._paywords = (me._sigwd + 29)//32
+
+    me._nbits = 1 + me.EXPWD + me._sigwd
+    me._rawbytes = (me._nbits + 7)//8
+
+  def decode(me, x):
+    """
+    Decode the encoded floating-point value X, represented as an integer.
+
+    Return five quantities (FLAGS, EXP, FW, FRAC, ERR), corresponding mostly
+    to the `struct floatbits' representation, characterizing the value
+    encoded in X.
+
+      * FLAGS is a list of flag tokens:
+
+         -- `NEG' if the value is negative;
+         -- `ZERO' if the value is exactly zero;
+         -- `INF' if the value is infinite;
+         -- `SNAN' if the value is a signalling NaN; and/or
+         -- `QNAN' if the value is a quiet NaN.
+
+        FLAGS will be empty if the value is a strictly positive finite
+        number.
+
+      * EXP is the exponent, as a signed integer.  This will be `None' if the
+        value is zero, infinite, or a NaN.
+
+      * FW is the length of the fraction, in 32-bit words.  This will be
+        `None' if the value is zero or infinite.
+
+      * FRAC is the fraction or payload.  This will be `None' if the value is
+        zero or infinite; otherwise it will be an integer, 0 <= FRAC <
+        2^{32FW}.  If the value is a NaN, then the FRAC represents the
+        payload, /not/ including the quiet bit, left aligned.  Otherwise,
+        FRAC is normalized so that 2^{32FW-1} <= FRAC < 2^{32FW}, and the
+        value represented is S FRAC 2^{EXP-32FW}, where S = -1 if `NEG' is in
+        FLAGS, or +1 otherwise.  The represented value is unchanged by
+        multiplying or dividing FRAC by an exact power of 2^{32} and
+        (respectively) incrementing or decrementing FW to match, but this
+        will affect the output data in a way that affects the tests.
+
+      * ERR is a list of error tokens:
+
+          -- `INVAL' if the encoded value is erroneous (though decoding
+             continues anyway).
+
+        ERR will be empty if no error occurred.
+    """
+
+    ## Extract fields.
+    sig = x&mask(me._sigwd)
+    biasedexp = (x >> me._sigwd)&mask(me.EXPWD)
+    signbit = (x >> (me._sigwd + me.EXPWD))&1
+    if not me.HIDDENP: unitbit = sig >> me.PREC - 1
+
+    ## Initialize flag lists.
+    flags = []
+    err = []
+
+    ## Capture the sign.  This is always relevant.
+    if signbit: flags.append("NEG")
+
+    ## If the exponent field is all-bits-set then we have infinity or NaN.
+    if biasedexp == mask(me.EXPWD):
+
+      ## If there's no hidden bit then the unit bit should be /set/, but is
+      ## /not/ part of the NaN payload -- or even significant for
+      ## distinguishing a NaN from an infinity.  If it's clear, signal an
+      ## error; if it's set, then clear it so that we don't have to think
+      ## about it again.
+      if not me.HIDDENP:
+        if unitbit: sig &= mask(me._sigwd - 1)
+        else: err.append("INVAL")
+
+      ## If the significand is (now) zero, we have an infinity and there's
+      ## nothing else to do.
+      if not sig:
+        flags.append("INF")
+        frac = fw = exp = None
+
+      ## Otherwise determine the NaN flavour and extract the payload.
+      else:
+        if sig&bit(me.PREC - 2): flags.append("QNAN")
+        else: flags.append("SNAN")
+        shift = 32*me._paywords + 2 - me.PREC
+        frac = (sig&mask(me.PREC - 2)) << shift
+        exp = None
+        fw = me._paywords
+
+    ## Otherwise we have a finite number.  We handle all of these together.
+    else:
+
+      ## If there's no hidden bit, then check that the unit bit matches the
+      ## exponent: it should be clear if the exponent field is all-bits-zero
+      ## (zero or subnormal numbers), and set otherwise (normal numbers).  If
+      ## this isn't the case, signal an error, but continue.  We'll normalize
+      ## the number correctly as we go.
+      if not me.HIDDENP:
+        if (not biasedexp and unitbit) or (biasedexp and not unitbit):
+          err.append("INVAL")
+
+      ## If the exponent is all-bits-zero then set it to 1; otherwise, if the
+      ## format uses a hidden bit then force the unit bit of our significand
+      ## on.  The absolute value is now exactly
+      ##
+      ##        2^{biasedexp-_expbias-PREC+1} sig
+      ##
+      ## in all cases.
+      if not biasedexp: biasedexp = 1
+      elif me.HIDDENP: sig |= bit(me._sigwd)
+
+      ## If the significand is now zero then the value must be zero.
+      if not sig:
+        flags.append("ZERO")
+        frac = fw = exp = None
+
+      ## Otherwise we have a nonzero finite value, which might need
+      ## normalization.
+      else:
+        sigwd = sig.bit_length()
+        fw = (sigwd + 31)//32
+        exp = biasedexp - me._expbias - me.PREC + sigwd + 1
+        frac = sig << (32*fw - sigwd)
+
+    ## All done.
+    return flags, exp, frac, fw, err
+
+  def _dump_as_bytes(me, var, x, wd):
+    """
+    Dump an assignment to VAR of X as a WD-byte binary string.
+
+    Print, on standard output, an assignment `VAR = ...' giving the value of
+    X, in hexadecimal, split with spaces into groups of 8 digits from the
+    right.
+    """
+
+    if not wd:
+      print("%s = #empty" % var)
+    else:
+      out = StringIO()
+      for i in xrange(wd - 1, -1, -1):
+        out.write("%02x" % ((x >> 8*i)&0xff))
+        if i and not i%4: out.write(" ")
+      print("%s = %s" % (var, out.getvalue()))
+
+  def _dump_flags(me, var, flags, zero = "0"):
+    """
+    Dump an assignment to VAR of FLAGS as a list of flags.
+
+    Print, on standard output, an assignment `VAR = ...' giving the named
+    flags.  Print ZERO (default `0') if FLAGS is empty.
+    """
+
+    if flags: print("%s = %s" % (var, " | ".join(flags)))
+    else: print("%s = %s" % (var, zero))
+
+  def genenc(me, ep = ExploreParameters()):
+    """
+    Print, on standard output, tests of encoding floating-point values.
+
+    The tests will cover positive and negative values, with the exponent and
+    signficand fields explored according to the parameters EP.
+    """
+
+    print("[enc%s]" % me.NAME)
+    for s in xrange(2):
+      for e in explore(me.EXPWD, ep.explo, ep.exphi):
+        for m in explore(me.PREC - 1, ep.siglo, ep.sighi):
+          if not me.HIDDENP and e: m |= bit(me.PREC - 1)
+          x = (s << (me.EXPWD + me._sigwd)) | (e << me._sigwd) | m
+          flags, exp, frac, fw, err = me.decode(x)
+          print("")
+          me._dump_flags("f", flags)
+          if exp is not None: print("e = %d" % exp)
+          if frac is not None:
+            while not frac&M32 and fw: frac >>= 32; fw -= 1
+            me._dump_as_bytes("m", frac, 4*fw)
+          me._dump_as_bytes("z", x, me._rawbytes)
+          if err: me._dump_flags("err", err, "OK")
+
+  def gendec(me, ep = ExploreParameters()):
+    """
+    Print, on standard output, tests of decoding floating-point values.
+
+    The tests will cover positive and negative values, with the exponent and
+    signficand fields explored according to the parameters EP.
+    """
+
+    print("[dec%s]" % me.NAME)
+    for s in xrange(2):
+      for e in explore(me.EXPWD, ep.explo, ep.exphi):
+        for m in explore(me._sigwd, ep.siglo, ep.sighi):
+          x = (s << (me.EXPWD + me._sigwd)) | (e << me._sigwd) | m
+          flags, exp, frac, fw, err = me.decode(x)
+          print("")
+          me._dump_as_bytes("x", x, me._rawbytes)
+          me._dump_flags("f", flags)
+          if exp is not None: print("e = %d" % exp)
+          if frac is not None: me._dump_as_bytes("m", frac, 4*fw)
+          if err: me._dump_flags("err", err, "OK")
+
+class MiniFloat (IEEEFormat):
+  NAME = "mini"
+  EXPWD = 4
+  PREC = 4
+  HIDDENP = True
+
+class BFloat16 (IEEEFormat):
+  NAME = "bf16"
+  EXPWD = 8
+  PREC = 8
+  HIDDENP = True
+
+class Binary16 (IEEEFormat):
+  NAME = "f16"
+  EXPWD = 5
+  PREC = 11
+  HIDDENP = True
+
+class Binary32 (IEEEFormat):
+  NAME = "f32"
+  EXPWD = 8
+  PREC = 24
+  HIDDENP = True
+
+class Binary64 (IEEEFormat):
+  NAME = "f64"
+  EXPWD = 11
+  PREC = 53
+  HIDDENP = True
+
+class Binary128 (IEEEFormat):
+  NAME = "f128"
+  EXPWD = 15
+  PREC = 113
+  HIDDENP = True
+
+class DoubleExtended80 (IEEEFormat):
+  NAME = "idblext80"
+  EXPWD = 15
+  PREC = 64
+  HIDDENP = False
+
+###--------------------------------------------------------------------------
+### Main program.
+
+op = OP.OptionParser \
+  (description = "Generate test data for IEEE format encoding and decoding",
+   usage = "usage: %prog [-E LO/HI] [-M LO/HI] [[enc|dec]FORMAT]")
+for shortopt, longopt, kw in \
+  [("-E", "--explore-exponent",
+    dict(action = "store", metavar = "LO/HI", dest = "expparam",
+         help = "exponent exploration parameters")),
+   ("-M", "--explore-significand",
+    dict(action = "store", metavar = "LO/HI", dest = "sigparam",
+         help = "significand exploration parameters"))]:
+  op.add_option(shortopt, longopt, **kw)
+opts, args = op.parse_args()
+
+ep = ExploreParameters()
+for optattr, loattr, hiattr in [("expparam", "explo", "exphi"),
+                                ("sigparam", "siglo", "sighi")]:
+  opt = getattr(opts, optattr)
+  if opt is not None:
+    ok = False
+    try: sl = opt.index("/")
+    except ValueError: pass
+    else:
+      try: lo, hi = map(int, (opt[:sl], opt[sl + 1:]))
+      except ValueError: pass
+      else:
+        setattr(ep, loattr, lo)
+        setattr(ep, hiattr, hi)
+        ok = True
+    if not ok: op.error("bad exploration parameter `%s'" % opt)
+
+if not args:
+  for fmt in iterkeys(FMTMAP):
+    args.append("enc" + fmt)
+    args.append("dec" + fmt)
+firstp = True
+for arg in args:
+  tail = fmt = None
+  if arg.startswith("enc"): tail = arg[3:]; gen = lambda f: f.genenc(ep)
+  elif arg.startswith("dec"): tail = arg[3:]; gen = lambda f: f.gendec(ep)
+  if tail is not None: fmt = FMTMAP.get(tail)
+  if not fmt: op.error("unknown test group `%s'" % arg)
+  if firstp: firstp = False
+  else: print("")
+  gen(fmt())
+
+###----- That's all, folks --------------------------------------------------