In the mean time, here is a command-line interface for examining values and storage formats of floats. It only cursorily checks that the implementation uses IEEE 754 Binary32 float: it does not detect e.g. GCC -fno-signed-zeros and such.
// SPDX-License-Identifier: CC0-1.0
// Nominal Animal, 2024
//
// Compile using for example
// gcc -Wall -O2 -std=c11 this.c -lm -o this.exe
#define _ISOC99_SOURCE
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#include <stdio.h>
#include <math.h>
#include <errno.h>
typedef union {
uint32_t u;
int32_t i;
float f;
} word32;
// Returns NULL float uses the expected storage format, error text otherwise.
static const char *verify_float_format(void) {
// Size must match uint32_t.
if (sizeof (float) != sizeof (uint32_t))
return "Invalid float size";
// Encoding.
if (((word32){ .u = 0x3f5d7b99 }).f != 0.8651672f ||
((word32){ .u = 0xc0490fdb }).f != -3.1415927f)
return "Invalid storage format";
// Subnormal reachable from zero.
if (nextafterf(0.0f, +1.0f) != ((word32){ .u = 0x00000001 }).f ||
nextafterf(0.0f, -1.0f) != ((word32){ .u = 0x80000001 }).f)
return "nextafterf() does not support subnormals";
// All tests passed.
return NULL;
}
static const struct {
const char *name;
const char *desc;
const word32 value;
} constant[] = {
{ .name = "nan", .value = { .u = 0x7F800001 }, .desc = " Not a Number" },
{ .name = "inf", .value = { .u = 0x7F800000 }, .desc = " Infinity" },
{ .name = "max", .value = { .u = 0x7F7FFFFF }, .desc = " = 3.4028235e+38, largest finite float" },
{ .name = "twopi", .value = { .u = 0x40C90FDB }, .desc = " = 6.2831855" },
{ .name = "pi", .value = { .u = 0x40490FDB }, .desc = " = 3.1415927" },
{ .name = "e", .value = { .u = 0x402DF854 }, .desc = " = 2.7182817" },
{ .name = "halfpi", .value = { .u = 0x3FC90FDB }, .desc = " = 1.5707964 = pi / 2" },
{ .name = "one", .value = { .u = 0x3F800000 }, .desc = " = 1" },
{ .name = "inve", .value = { .u = 0x3EBC5AB2 }, .desc = " = 0.36787945 = 1/e" },
{ .name = "min", .value = { .u = 0x00800000 }, .desc = " = 1.1754944e-38, smallest normal float" },
{ .name = "submax", .value = { .u = 0x007FFFFF }, .desc = " = 1.1754942e-38, largest subnormal float" },
{ .name = "submin", .value = { .u = 0x00000001 }, .desc = " = 1e-45, smallest subnormal float" },
{ .name = "zero", .value = { .u = 0x80000000 }, .desc = " = 0" },
};
#define CONSTANTS (sizeof constant / sizeof constant[0])
#ifndef WHITESPACE
#define WHITESPACE "\t\n\v\f\r "
#endif
static inline int separator_at(const char *s) {
if (!s)
return 0;
return (*s == '\0' || *s == '\t' || *s == '\n' ||
*s == '\v' || *s == '\f' || *s == '\r' ||
*s == ' ' ||
*s == '+' || *s == '-' ||
*s == '*' || *s == '/' ||
*s == '^' ||
*s == '~' || *s == '_');
}
static const char *matches(const char *from, const char *name) {
if (!from || !*from || !name || !*name)
return NULL;
while (1) {
const int f = (*from >= 'A' && *from <= 'Z') ? (*from - 'A' + 'a') : *from;
if (f == '\0')
return (separator_at(name)) ? from : NULL;
if (*name == '\0')
return (separator_at(from)) ? from : NULL;
if (f != *name)
return NULL;
from++;
name++;
}
}
static const char *parse_suffixes(const char *from, word32 *to) {
if (!from)
return NULL;
while (1)
switch (*from) {
case '~': // Post-increment
if (to)
to->f = nexttowardf(to->f, HUGE_VAL);
from++;
break;
case '_': // Post-decrement
if (to)
to->f = nexttowardf(to->f, -HUGE_VAL);
from++;
break;
default:
return from;
}
}
const char *parse_int(const char *from, int *to) {
const char *ends = NULL;
long val;
if (!from)
return NULL;
errno = 0;
val = strtol(from, (char **)&ends, 0);
if (errno)
return NULL;
if (!ends || ends == from || !separator_at(ends))
return NULL;
if ((long)((int)val) != val)
return NULL;
if (to)
*to = (int)val;
return ends;
}
const char *parse_part32(const char *from, word32 *to) {
const char *ends = NULL;
word32 tmp;
// Fail if nothing to parse.
if (!from)
return NULL;
from += strspn(from, WHITESPACE);
if (!*from)
return NULL;
// Hexadecimal storage representation?
if (from[0] == '0' && (from[1] == 'x' || from[1] == 'X')) {
do {
errno = 0;
unsigned long val = strtoul(from + 2, (char **)&ends, 16);
if (errno || !ends || ends == from + 2)
break;
if ((unsigned long)((uint32_t)val) != val)
break;
tmp.u = val;
ends = parse_suffixes(ends, &tmp);
if (!separator_at(ends))
break;
if (to)
*to = tmp;
return ends;
} while (0);
}
// A known constant?
for (size_t i = 0; i < CONSTANTS; i++) {
if (!constant[i].name)
continue;
uint32_t negative = 0x00000000;
ends = from;
while (*ends == '+' || *ends == '-')
if (*(ends++) == '-')
negative ^= 0x80000000;
ends = matches(from, constant[i].name);
if (!ends)
continue;
tmp.u = negative ^ constant[i].value.u;
ends = parse_suffixes(ends, &tmp);
if (!separator_at(ends)) {
ends = NULL;
continue;
}
if (to)
*to = tmp;
return ends;
}
ends = NULL;
// A floating-point numeric value?
errno = 0;
double val = strtod(from, (char **)&ends);
if (errno)
return NULL;
if (!ends || ends == from)
return NULL;
// Value is nonzero, but rounds to zero?
if (val != 0.0 && (float)val == 0.0f)
return NULL;
tmp.f = val;
ends = parse_suffixes(ends, &tmp);
if (!separator_at(ends))
return NULL;
if (to)
*to = tmp;
return ends;
}
int parse_word32(const char *from, word32 *const to, const char **errp) {
const char *dummy;
word32 have, temp;
int p;
// Clear error pointer
if (errp)
*errp = NULL;
else
errp = &dummy;
if (!from || !*from)
return -1; // Nothing to parse.
*errp = from; from = parse_part32(from, &have);
if (!from)
return -1; // Failed to parse.
while (1) {
// Skip whitespace.
from += strspn(from, WHITESPACE);
*errp = from;
switch (*from) {
case '\0': // Completed.
*errp = NULL;
if (to)
*to = have;
return 0;
case '+': // Addition
from = parse_part32(from + 1, &temp);
if (!from)
return -1;
have.f += temp.f;
break;
case '-': // Substraction
from = parse_part32(from + 1, &temp);
if (!from)
return -1;
have.f -= temp.f;
break;
case '*': // Multiplication
from = parse_part32(from + 1, &temp);
if (!from)
return -1;
have.f *= temp.f;
break;
case '/': // Division
from = parse_part32(from + 1, &temp);
if (!from)
return -1;
have.f /= temp.f;
break;
case '^': // Multiply by a power of two
from = parse_int(from + 1, &p);
if (!from)
return -1;
have.f = ldexpf(have.f, p);
*errp = from;
from = parse_suffixes(from, &have);
if (!from)
return -1;
break;
default:
return -1;
}
}
}
typedef struct {
char buf[256];
} buffer;
const char *word32_exact_float(buffer *const where, const word32 value) {
int len = snprintf(where->buf, sizeof where->buf, "%.128f", value.f);
if (len < 1 || (size_t)len >= sizeof where->buf)
return "(ERROR)";
while (len > 1 && where->buf[len-1] == '0' && where->buf[len-2] >= '0' && where->buf[len - 2] <= '9')
where->buf[--len] = '\0';
return (const char *)(where->buf);
}
const char *word32_pattern(buffer *const where, const word32 value) {
char *p = where->buf + sizeof where->buf;
uint32_t u = value.u;
*(--p) = '\0';
// 23 mantissa bits
for (int i = 0; i < 23; i++) {
*(--p) = '0' + (u & 1);
u >>= 1;
}
// Implicit bit.
if ((value.u & 0x7F800000) == 0x00000000)
*(--p) = '0';
else
if ((value.u & 0x7F800000) < 0x7F800000)
*(--p) = '1';
else
*(--p) = ' '; // Not shown for NaNs and Infs
*(--p) = ' ';
// 8 exponent bits
for (int i = 0; i < 8; i++) {
*(--p) = '0' + (u & 1);
u >>= 1;
}
*(--p) = ' ';
// Sign bit.
*(--p) = '0' + (u & 1);
return (const char *)p;
}
const char *word32_float(buffer *const where, const word32 value) {
// Use scientific notation for very large and very small values.
const char *format = (value.f < -100000000.0f || value.f > +100000000.0f ||
(value.f >= -0.000000001f && value.f <= 0.000000001f) ) ? "%.*g" : "%.*f";
int decimals = 1;
while (1) {
int len = snprintf(where->buf, sizeof where->buf, format, decimals, value.f);
if (len < 1 || (size_t)len >= sizeof where->buf)
return "(ERROR)";
errno = 0;
char *end = where->buf;
float val = strtod(end, &end);
if (errno || end == where->buf || *end)
return "(ERROR)";
if (val == value.f)
return (const char *)(where->buf);
decimals++;
if (decimals >= (int)sizeof where->buf)
return "(ERROR)";
}
}
int usage(const char *arg0, int status) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
fprintf(stderr, " %s FLOAT [ FLOAT ... ]\n", arg0);
fprintf(stderr, "Where each FLOAT can be:\n");
fprintf(stderr, " VALUE [ OP VALUE ]\n");
fprintf(stderr, "where OP is\n");
fprintf(stderr, " + for addition,\n");
fprintf(stderr, " - for substraction,\n");
fprintf(stderr, " * for multiplication,\n");
fprintf(stderr, " / for division, and\n");
fprintf(stderr, " ^ for multiplying with a power of two,\n");
fprintf(stderr, "and each VALUE may be suffixed with zero or more of\n");
fprintf(stderr, " _ (underscore) to decrement by 1 ULP\n");
fprintf(stderr, " ~ (tilde) to increment by 1 ULP\n");
fprintf(stderr, "possibly repeated. Order of operations is strictly left to right.\n");
fprintf(stderr, "\n");
fprintf(stderr, "Known constants:\n");
for (size_t i = 0; i < CONSTANTS; i++)
fprintf(stderr, "%15s %s\n", constant[i].name, constant[i].desc);
fprintf(stderr, "\n");
exit(status);
}
int main(int argc, char *argv[]) {
const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
const char *bad_format = verify_float_format();
if (bad_format) {
fprintf(stderr, "Detected non-IEEE 754 Binary32 'float' format: %s.\n", bad_format);
exit(EXIT_FAILURE);
}
// If run without arguments, display usage.
if (argc < 2)
usage(arg0, EXIT_SUCCESS);
// If any of the arguments asks for help, display usage.
for (int arg = 1; arg < argc; arg++)
if (!strcmp(argv[arg], "-h") || !strcmp(argv[arg], "--help"))
usage(arg0, EXIT_SUCCESS);
// Describe input float values.
for (int arg = 1; arg < argc; arg++) {
const char *err = NULL;
buffer buf;
word32 val;
if (parse_word32(argv[arg], &val, &err)) {
fprintf(stderr, "%s: Invalid expression.\n", (err) ? err : argv[arg]);
exit(EXIT_FAILURE);
}
const int fpc = fpclassify(val.f);
printf("Input: %s\n", argv[arg]);
printf("Value: %s\n", word32_float(&buf, val));
printf("Exact: %s\n", word32_exact_float(&buf, val));
printf("Class:");
if (signbit(val.f))
printf(" negative");
if (isfinite(val.f))
printf(" finite");
if (isnormal(val.f))
printf(" normal");
if (fpc == FP_ZERO)
printf(" zero");
if (fpc == FP_NAN)
printf(" nan");
if (fpc == FP_INFINITE)
printf(" inf");
if (fpc == FP_SUBNORMAL)
printf(" subnormal");
printf("\n");
printf("Bits: %s\n", word32_pattern(&buf, val));
printf("Hex: 0x%08x\n\n", (unsigned int)(val.u));
fflush(stdout);
}
return EXIT_SUCCESS;
}
For example, to find out what is the smallest float greater than zero, run it with "1~" as an argument, and it should print
Input: 1~
Value: 1.0000001
Exact: 1.00000011920928955078125
Class: finite normal
Bits: 0 01111111 100000000000000000000001
Hex: 0x3f800001
Note that the Bits: row includes the implicit mantissa bit for finite (normal and subnormal and zero) values; it is omitted for infinities and NaNs.
Run without parameters to see usage information.
I use this to examine the storage representation of specific float values (Bits: and Hex: lines), to see the minimum decimal representation matching them (Value: line), and examining the results of simple arithmetic operations.
The reason this is a command-line tool and not say a HTML+CSS tool one can use in a browser, is that browsers don't have IEEE-754 Binary32 environment (we'd have to synthesize one, yuk), but most desktops and servers and SBCs do.