@@@ fltfmt wip
[mLib] / utils / t / fltfmt-testgen
CommitLineData
b1a20bee
MW
1#! /usr/bin/python
2###
3### Generate exhaustive tests for floating-point conversions.
4###
5### (c) 2024 Straylight/Edgeware
6###
7
8###----- Licensing notice ---------------------------------------------------
9###
10### This file is part of the mLib utilities library.
11###
12### mLib is free software: you can redistribute it and/or modify it under
13### the terms of the GNU Library General Public License as published by
14### the Free Software Foundation; either version 2 of the License, or (at
15### your option) any later version.
16###
17### mLib is distributed in the hope that it will be useful, but WITHOUT
18### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19### FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
20### License for more details.
21###
22### You should have received a copy of the GNU Library General Public
23### License along with mLib. If not, write to the Free Software
24### Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
25### USA.
26
27###--------------------------------------------------------------------------
28### Imports.
29
30import sys as SYS
31import optparse as OP
32import random as R
33if SYS.version_info >= (3,):
34 from io import StringIO
35 xrange = range
36 def iterkeys(d): return d.keys()
37else:
38 from cStringIO import StringIO
39 def iterkeys(d): return d.iterkeys()
40
41###--------------------------------------------------------------------------
42### Utilities.
43
44def bit(k): "Return an integer with just bit K set."; return 1 << k
45def mask(k): "Return an integer with bits 0 to K - 1 set."; return bit(k) - 1
46M32 = mask(32)
47
48def explore(wd, lobits, hibits):
49 """
50 Return an iterator over various WD-bit values.
51
52 Suppose that a test wants to explore various WD-bit fields, but WD might be
53 too large to do this exhaustively. We assume (reasonably, in the case at
54 hand of floating-point formats) that the really interesting bits are those
55 at the low and high ends of the field, and test small subfields at the ends
56 exhaustively, filling in the bits in the middle with zeros, ones, or random
57 data.
58
59 So, the generator behaves as follows. If WD <= LOBITS + HIBITS + 1 then
60 the iterator will yield all WD-bit values exhaustively. Otherwise, it
61 yields a sequence which includes all combinations of: every LOBITS-bit
62 pattern in the least significant bits; every HIBITS-bit pattern in the most
63 significant bits; and all-bits-clear, all-bits-set, and a random pattern in
64 the bits in between.
65 """
66
67 if wd <= hibits + lobits + 1:
68 for i in xrange(bit(wd)): yield i
69 else:
70 midbit = bit(wd - hibits - lobits)
71 hishift = wd - hibits
72 m = (midbit - 1) << lobits
73 for hi in xrange(bit(hibits)):
74 top = hi << hishift
75 for lo in xrange(bit(lobits)):
dc6eea4e
MW
76 while True:
77 fill = R.randrange(midbit)
78 if fill != 0 and fill != midbit - 1: break
b1a20bee
MW
79 base = lo | top
80 yield base
dc6eea4e 81 yield base | (fill << lobits)
b1a20bee
MW
82 yield base | m
83
84class ExploreParameters (object):
85 """
86 Simple structure for exploration parameters; see `explore' for background.
87
88 The `explo' and `exphi' attributes are the low and high subfield sizes
89 for exponent fields, and `siglo' and `sighi' are the low and high subfield
90 sizes for significand fields.
91 """
92 def __init__(me, explo = 0, exphi = 2, siglo = 1, sighi = 3):
93 me.explo, me.exphi = explo, exphi
94 me.siglo, me.sighi = siglo, sighi
95
96FMTMAP = {} # maps format names to classes
97
98def with_metaclass(meta, *supers):
99 """
100 Return an arbitrary instance of the metaclass META.
101
102 The class will have SUPERS (default just `object') as its superclasses.
103 This is intended to be used in direct-superclass lists, as a compatibility
104 hack, because the Python 2 and 3 syntaxes are wildly different.
105 """
106 return meta("#<anonymous base %s>" % meta.__name__,
107 supers or (object,), dict())
108
109class FormatClass (type):
110 """
111 Metaclass for format classes.
112
113 If the class defines a `NAME' attribute then register the class in
114 `FMTMAP'.
115 """
116 def __new__(cls, name, supers, dict):
117 c = type.__new__(cls, name, supers, dict)
118 try: FMTMAP[c.NAME] = c
119 except AttributeError: pass
120 return c
121
122class IEEEFormat (with_metaclass(FormatClass)):
123 """
124 Floating point format class.
125
126 Concrete subclasses must define the following class attributes.
127
128 * `HIDDENP' -- true if the format uses a `hidden bit' convention for
129 normal numbers.
130
131 * `EXPWD' -- exponent field width, in bits.
132
133 * `PREC' -- precision, in bits, /including/ the hidden bit if any.
134
135 Many useful quantities are derived.
136
137 * `_expbias' is the exponent bias.
138
139 * `_minexp' and `_maxexp' are the minimum and maximum representable
140 exponents.
141
142 * `_sigwd' is the width of the significand field.
143
144 * `_paywords' is the number of words required to represent a NaN payload.
145
146 * `_nbits' is the total number of bits in an encoded value.
147
148 * `_rawbytes' is the number of bytes required for an encoded value.
149 """
150
151 def __init__(me):
152 """
153 Initialize an instance.
154 """
155 me._expbias = mask(me.EXPWD - 1)
156 me._maxexp = me._expbias
157 me._minexp = 1 - me._expbias
158 if me.HIDDENP: me._sigwd = me.PREC - 1
159 else: me._sigwd = me.PREC
160
161 me._paywords = (me._sigwd + 29)//32
162
163 me._nbits = 1 + me.EXPWD + me._sigwd
164 me._rawbytes = (me._nbits + 7)//8
165
166 def decode(me, x):
167 """
168 Decode the encoded floating-point value X, represented as an integer.
169
170 Return five quantities (FLAGS, EXP, FW, FRAC, ERR), corresponding mostly
171 to the `struct floatbits' representation, characterizing the value
172 encoded in X.
173
174 * FLAGS is a list of flag tokens:
175
176 -- `NEG' if the value is negative;
177 -- `ZERO' if the value is exactly zero;
178 -- `INF' if the value is infinite;
179 -- `SNAN' if the value is a signalling NaN; and/or
180 -- `QNAN' if the value is a quiet NaN.
181
182 FLAGS will be empty if the value is a strictly positive finite
183 number.
184
185 * EXP is the exponent, as a signed integer. This will be `None' if the
186 value is zero, infinite, or a NaN.
187
188 * FW is the length of the fraction, in 32-bit words. This will be
189 `None' if the value is zero or infinite.
190
191 * FRAC is the fraction or payload. This will be `None' if the value is
192 zero or infinite; otherwise it will be an integer, 0 <= FRAC <
193 2^{32FW}. If the value is a NaN, then the FRAC represents the
194 payload, /not/ including the quiet bit, left aligned. Otherwise,
195 FRAC is normalized so that 2^{32FW-1} <= FRAC < 2^{32FW}, and the
196 value represented is S FRAC 2^{EXP-32FW}, where S = -1 if `NEG' is in
197 FLAGS, or +1 otherwise. The represented value is unchanged by
198 multiplying or dividing FRAC by an exact power of 2^{32} and
199 (respectively) incrementing or decrementing FW to match, but this
200 will affect the output data in a way that affects the tests.
201
202 * ERR is a list of error tokens:
203
204 -- `INVAL' if the encoded value is erroneous (though decoding
205 continues anyway).
206
207 ERR will be empty if no error occurred.
208 """
209
210 ## Extract fields.
211 sig = x&mask(me._sigwd)
212 biasedexp = (x >> me._sigwd)&mask(me.EXPWD)
213 signbit = (x >> (me._sigwd + me.EXPWD))&1
214 if not me.HIDDENP: unitbit = sig >> me.PREC - 1
215
216 ## Initialize flag lists.
217 flags = []
218 err = []
219
220 ## Capture the sign. This is always relevant.
221 if signbit: flags.append("NEG")
222
223 ## If the exponent field is all-bits-set then we have infinity or NaN.
224 if biasedexp == mask(me.EXPWD):
225
226 ## If there's no hidden bit then the unit bit should be /set/, but is
227 ## /not/ part of the NaN payload -- or even significant for
228 ## distinguishing a NaN from an infinity. If it's clear, signal an
229 ## error; if it's set, then clear it so that we don't have to think
230 ## about it again.
231 if not me.HIDDENP:
232 if unitbit: sig &= mask(me._sigwd - 1)
233 else: err.append("INVAL")
234
235 ## If the significand is (now) zero, we have an infinity and there's
236 ## nothing else to do.
237 if not sig:
238 flags.append("INF")
239 frac = fw = exp = None
240
241 ## Otherwise determine the NaN flavour and extract the payload.
242 else:
243 if sig&bit(me.PREC - 2): flags.append("QNAN")
244 else: flags.append("SNAN")
245 shift = 32*me._paywords + 2 - me.PREC
246 frac = (sig&mask(me.PREC - 2)) << shift
247 exp = None
248 fw = me._paywords
249
250 ## Otherwise we have a finite number. We handle all of these together.
251 else:
252
253 ## If there's no hidden bit, then check that the unit bit matches the
254 ## exponent: it should be clear if the exponent field is all-bits-zero
255 ## (zero or subnormal numbers), and set otherwise (normal numbers). If
256 ## this isn't the case, signal an error, but continue. We'll normalize
257 ## the number correctly as we go.
258 if not me.HIDDENP:
259 if (not biasedexp and unitbit) or (biasedexp and not unitbit):
260 err.append("INVAL")
261
262 ## If the exponent is all-bits-zero then set it to 1; otherwise, if the
263 ## format uses a hidden bit then force the unit bit of our significand
264 ## on. The absolute value is now exactly
265 ##
266 ## 2^{biasedexp-_expbias-PREC+1} sig
267 ##
268 ## in all cases.
269 if not biasedexp: biasedexp = 1
270 elif me.HIDDENP: sig |= bit(me._sigwd)
271
272 ## If the significand is now zero then the value must be zero.
273 if not sig:
274 flags.append("ZERO")
275 frac = fw = exp = None
276
277 ## Otherwise we have a nonzero finite value, which might need
278 ## normalization.
279 else:
280 sigwd = sig.bit_length()
281 fw = (sigwd + 31)//32
282 exp = biasedexp - me._expbias - me.PREC + sigwd + 1
283 frac = sig << (32*fw - sigwd)
284
285 ## All done.
286 return flags, exp, frac, fw, err
287
288 def _dump_as_bytes(me, var, x, wd):
289 """
290 Dump an assignment to VAR of X as a WD-byte binary string.
291
292 Print, on standard output, an assignment `VAR = ...' giving the value of
293 X, in hexadecimal, split with spaces into groups of 8 digits from the
294 right.
295 """
296
297 if not wd:
298 print("%s = #empty" % var)
299 else:
300 out = StringIO()
301 for i in xrange(wd - 1, -1, -1):
302 out.write("%02x" % ((x >> 8*i)&0xff))
303 if i and not i%4: out.write(" ")
304 print("%s = %s" % (var, out.getvalue()))
305
306 def _dump_flags(me, var, flags, zero = "0"):
307 """
308 Dump an assignment to VAR of FLAGS as a list of flags.
309
310 Print, on standard output, an assignment `VAR = ...' giving the named
311 flags. Print ZERO (default `0') if FLAGS is empty.
312 """
313
314 if flags: print("%s = %s" % (var, " | ".join(flags)))
315 else: print("%s = %s" % (var, zero))
316
317 def genenc(me, ep = ExploreParameters()):
318 """
319 Print, on standard output, tests of encoding floating-point values.
320
321 The tests will cover positive and negative values, with the exponent and
322 signficand fields explored according to the parameters EP.
323 """
324
325 print("[enc%s]" % me.NAME)
326 for s in xrange(2):
327 for e in explore(me.EXPWD, ep.explo, ep.exphi):
328 for m in explore(me.PREC - 1, ep.siglo, ep.sighi):
329 if not me.HIDDENP and e: m |= bit(me.PREC - 1)
330 x = (s << (me.EXPWD + me._sigwd)) | (e << me._sigwd) | m
331 flags, exp, frac, fw, err = me.decode(x)
332 print("")
333 me._dump_flags("f", flags)
334 if exp is not None: print("e = %d" % exp)
335 if frac is not None:
336 while not frac&M32 and fw: frac >>= 32; fw -= 1
337 me._dump_as_bytes("m", frac, 4*fw)
338 me._dump_as_bytes("z", x, me._rawbytes)
339 if err: me._dump_flags("err", err, "OK")
340
341 def gendec(me, ep = ExploreParameters()):
342 """
343 Print, on standard output, tests of decoding floating-point values.
344
345 The tests will cover positive and negative values, with the exponent and
346 signficand fields explored according to the parameters EP.
347 """
348
349 print("[dec%s]" % me.NAME)
350 for s in xrange(2):
351 for e in explore(me.EXPWD, ep.explo, ep.exphi):
352 for m in explore(me._sigwd, ep.siglo, ep.sighi):
353 x = (s << (me.EXPWD + me._sigwd)) | (e << me._sigwd) | m
354 flags, exp, frac, fw, err = me.decode(x)
355 print("")
356 me._dump_as_bytes("x", x, me._rawbytes)
357 me._dump_flags("f", flags)
358 if exp is not None: print("e = %d" % exp)
359 if frac is not None: me._dump_as_bytes("m", frac, 4*fw)
360 if err: me._dump_flags("err", err, "OK")
361
362class MiniFloat (IEEEFormat):
363 NAME = "mini"
364 EXPWD = 4
365 PREC = 4
366 HIDDENP = True
367
368class BFloat16 (IEEEFormat):
369 NAME = "bf16"
370 EXPWD = 8
371 PREC = 8
372 HIDDENP = True
373
374class Binary16 (IEEEFormat):
375 NAME = "f16"
376 EXPWD = 5
377 PREC = 11
378 HIDDENP = True
379
380class Binary32 (IEEEFormat):
381 NAME = "f32"
382 EXPWD = 8
383 PREC = 24
384 HIDDENP = True
385
386class Binary64 (IEEEFormat):
387 NAME = "f64"
388 EXPWD = 11
389 PREC = 53
390 HIDDENP = True
391
392class Binary128 (IEEEFormat):
393 NAME = "f128"
394 EXPWD = 15
395 PREC = 113
396 HIDDENP = True
397
398class DoubleExtended80 (IEEEFormat):
399 NAME = "idblext80"
400 EXPWD = 15
401 PREC = 64
402 HIDDENP = False
403
404###--------------------------------------------------------------------------
405### Main program.
406
407op = OP.OptionParser \
408 (description = "Generate test data for IEEE format encoding and decoding",
409 usage = "usage: %prog [-E LO/HI] [-M LO/HI] [[enc|dec]FORMAT]")
410for shortopt, longopt, kw in \
411 [("-E", "--explore-exponent",
412 dict(action = "store", metavar = "LO/HI", dest = "expparam",
413 help = "exponent exploration parameters")),
414 ("-M", "--explore-significand",
415 dict(action = "store", metavar = "LO/HI", dest = "sigparam",
416 help = "significand exploration parameters"))]:
417 op.add_option(shortopt, longopt, **kw)
418opts, args = op.parse_args()
419
420ep = ExploreParameters()
421for optattr, loattr, hiattr in [("expparam", "explo", "exphi"),
422 ("sigparam", "siglo", "sighi")]:
423 opt = getattr(opts, optattr)
424 if opt is not None:
425 ok = False
426 try: sl = opt.index("/")
427 except ValueError: pass
428 else:
429 try: lo, hi = map(int, (opt[:sl], opt[sl + 1:]))
430 except ValueError: pass
431 else:
432 setattr(ep, loattr, lo)
433 setattr(ep, hiattr, hi)
434 ok = True
435 if not ok: op.error("bad exploration parameter `%s'" % opt)
436
437if not args:
438 for fmt in iterkeys(FMTMAP):
439 args.append("enc" + fmt)
440 args.append("dec" + fmt)
441firstp = True
442for arg in args:
443 tail = fmt = None
444 if arg.startswith("enc"): tail = arg[3:]; gen = lambda f: f.genenc(ep)
445 elif arg.startswith("dec"): tail = arg[3:]; gen = lambda f: f.gendec(ep)
446 if tail is not None: fmt = FMTMAP.get(tail)
447 if not fmt: op.error("unknown test group `%s'" % arg)
448 if firstp: firstp = False
449 else: print("")
450 gen(fmt())
451
452###----- That's all, folks --------------------------------------------------