blob: 24ea6ec4f7c4ecf24158d0f18325a8f4531b1727 [file] [edit]
#!/usr/bin/env python3
# ===----------------------------------------------------------------------===#
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# ===----------------------------------------------------------------------===#
"""Convert CORE-MATH .wc (worst case) test files to raw IEEE 754 binary format.
This script parses text .wc files containing hexadecimal floating-point numbers,
handles comments, special values (NaNs, sNaNs, inf, signed zeros), and
outputs raw binary IEEE-754 values (by default 64-bit double precision,
little-endian).
Why binary?
- CORE-MATH .wc files for binary64 functions often contain >1,000,000 cases,
taking 25+ MB of text.
- Raw binary representation reduces the file size by ~3x (e.g., 25 MB -> 8.7 MB).
- Test suites can memory-map or read the entire binary file with a single fread()
call in sub-milliseconds, avoiding expensive text parsing during test startup.
Supported Input Formats:
- Hexadecimal floating point: '0x1.005023d32fee5p+1'
- Standard decimal floats: '1.2345'
- Special values / NaNs: 'snan', '-snan', 'qnan', 'nan', 'inf', '-inf'
- Signed zeros & integers: '+0', '-0', '1', '-1'
- Univariate inputs: One value per line.
- Bivariate inputs: Values separated by commas, e.g. '0x1.0p+0, 0x1.8p+0'
- Comments: Lines starting with '#' or inline '#' comments.
Example Usages:
1. Convert a binary64 worst-case file to binary:
$ python3 wc_to_bin.py sin.wc sin.bin
2. Convert a binary32 (single-precision float) file:
$ python3 wc_to_bin.py -t binary32 sinf.wc sinf.bin
3. Use with stdin and stdout pipes:
$ cat cos.wc | python3 wc_to_bin.py > cos.bin
4. Convert and display summary statistics:
$ python3 wc_to_bin.py --summary cos.wc cos.bin
TODO: Add a CI check to ensure that the .wc and .bin files in
libc/test/src/math/exhaustive/ are kept in sync.
"""
import argparse
import struct
import sys
def parse_token_binary64(s: str) -> int:
"""Parse a string token representing a binary64 number to a 64-bit unsigned integer.
Returns the exact 64-bit IEEE 754 bit-pattern as an integer, or None if the token
is empty.
Handles:
- Signaling NaNs ('snan', '+snan', '-snan')
- Quiet NaNs ('qnan', 'nan', '+nan', '-nan', '-qnan')
- Infinities ('inf', '+inf', '-inf', 'infinity', '-infinity')
- Signed zeros ('+0', '-0', '0.0', '-0.0')
- Common integer values ('1', '+1', '-1')
- Hexadecimal floats ('0x1.0p+0') and standard floats
"""
s = s.strip()
if not s:
return None
# Handle signaling NaNs (quiet bit is 0, payload non-zero)
if s in ("snan", "+snan"):
return 0x7FF4000000000000
if s == "-snan":
return 0xFFF4000000000000
# Handle quiet NaNs (quiet bit is 1)
if s in ("qnan", "+qnan", "nan", "+nan"):
return 0x7FF8000000000000
if s in ("-nan", "-qnan"):
return 0xFFF8000000000000
# Handle infinities (exponent all 1s, mantissa 0)
if s in ("inf", "+inf", "infinity", "+infinity"):
return 0x7FF0000000000000
if s in ("-inf", "-infinity"):
return 0xFFF0000000000000
# Handle zeros (preserve negative sign bit: 0x8000000000000000)
if s in ("+0", "0", "+0.0", "0.0"):
return 0x0000000000000000
if s in ("-0", "-0.0"):
return 0x8000000000000000
# Handle common integer constants
if s in ("+1", "1", "+1.0", "1.0"):
return 0x3FF0000000000000
if s in ("-1", "-1.0"):
return 0xBFF0000000000000
# Parse hexadecimal or standard decimal floating-point representations.
try:
val = float.fromhex(s)
except ValueError:
val = float(s)
# Pack as IEEE 754 double (little-endian) and unpack as uint64
return struct.unpack("<Q", struct.pack("<d", val))[0]
def parse_token_binary32(s: str) -> int:
"""Parse a string token representing a binary32 number to a 32-bit unsigned integer.
Returns the exact 32-bit IEEE 754 bit-pattern as an integer, or None if the token
is empty.
Handles:
- Signaling NaNs ('snan', '+snan', '-snan')
- Quiet NaNs ('qnan', 'nan', '+nan', '-nan', '-qnan')
- Infinities ('inf', '+inf', '-inf', 'infinity', '-infinity')
- Signed zeros ('+0', '-0', '0.0', '-0.0')
- Common integer values ('1', '+1', '-1')
- Hexadecimal floats ('0x1.0p+0') and standard floats
"""
s = s.strip()
if not s:
return None
# Handle signaling NaNs (quiet bit is 0, payload non-zero)
if s in ("snan", "+snan"):
return 0x7FA00000
if s == "-snan":
return 0xFFA00000
# Handle quiet NaNs (quiet bit is 1)
if s in ("qnan", "+qnan", "nan", "+nan"):
return 0x7FC00000
if s in ("-nan", "-qnan"):
return 0xFFC00000
# Handle infinities (exponent all 1s, mantissa 0)
if s in ("inf", "+inf", "infinity", "+infinity"):
return 0x7F800000
if s in ("-inf", "-infinity"):
return 0xFF800000
# Handle zeros (preserve negative sign bit: 0x80000000)
if s in ("+0", "0", "+0.0", "0.0"):
return 0x00000000
if s in ("-0", "-0.0"):
return 0x80000000
# Handle common integer constants
if s in ("+1", "1", "+1.0", "1.0"):
return 0x3F800000
if s in ("-1", "-1.0"):
return 0xBF800000
# Parse hexadecimal or standard decimal floating-point representations.
try:
val = float.fromhex(s)
except ValueError:
val = float(s)
# Pack as IEEE 754 single float (little-endian) and unpack as uint32
return struct.unpack("<I", struct.pack("<f", val))[0]
def convert_wc_to_bin(in_stream, out_stream, float_type="binary64") -> int:
"""Convert an input text stream of .wc lines to a raw binary stream.
Args:
in_stream: An iterable text stream or file-like object yielding lines.
out_stream: A writable binary stream (file or sys.stdout.buffer).
float_type: 'binary64'/'double' (8 bytes) or 'binary32'/'float' (4 bytes).
Returns:
The total count of floating-point values successfully converted and written.
"""
if float_type in ("binary64", "double", "f64"):
parse_func = parse_token_binary64
pack_format = "<Q"
elif float_type in ("binary32", "float", "f32"):
parse_func = parse_token_binary32
pack_format = "<I"
else:
raise ValueError(f"Unsupported float type: {float_type}")
count = 0
buffer = bytearray()
# Use a 1 MB write buffer to optimize I/O throughput on large files (>1M lines).
CHUNK_BYTES = 1024 * 1024
for line in in_stream:
# Strip comments beginning with '#' and trim whitespace
line = line.split("#")[0].strip()
if not line:
continue
# Handle comma-separated values (for bivariate functions like pow, hypot)
parts = line.split(",")
for part in parts:
# Take the first token in each part, ignoring any trailing remarks
tokens = part.split()
if not tokens:
continue
token = tokens[0]
bit_pattern = parse_func(token)
if bit_pattern is not None:
buffer.extend(struct.pack(pack_format, bit_pattern))
count += 1
# Flush the buffer when it reaches the chunk size to bound memory use
if len(buffer) >= CHUNK_BYTES:
out_stream.write(buffer)
buffer.clear()
# Flush any remaining bytes in the buffer
if buffer:
out_stream.write(buffer)
buffer.clear()
return count
def main():
examples = """Examples:
Convert sin.wc to binary format (binary64 by default):
%(prog)s sin.wc sin.bin
Convert a binary32 worst case file:
%(prog)s -t binary32 sinf.wc sinf.bin
Stream from stdin to stdout:
cat cos.wc | %(prog)s > cos.bin
Convert and print summary statistics:
%(prog)s --summary cos.wc cos.bin
"""
parser = argparse.ArgumentParser(
description="Convert CORE-MATH .wc files to raw binary IEEE 754 data.",
epilog=examples,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"input",
nargs="?",
default="-",
help="Input .wc file path (default: stdin)",
)
parser.add_argument(
"output",
nargs="?",
default="-",
help="Output .bin file path (default: stdout)",
)
parser.add_argument(
"-t",
"--type",
choices=["binary64", "double", "binary32", "float"],
default="binary64",
help="Target floating point type (default: binary64)",
)
parser.add_argument(
"-s",
"--summary",
action="store_true",
help="Print summary statistics to stderr",
)
args = parser.parse_args()
# Open input stream: defaults to standard input if '-' or unspecified
if args.input == "-":
in_stream = sys.stdin
else:
in_stream = open(args.input, "r", encoding="utf-8")
# Open output stream: defaults to standard output if '-' or unspecified
if args.output == "-":
out_stream = sys.stdout.buffer
else:
out_stream = open(args.output, "wb")
try:
count = convert_wc_to_bin(in_stream, out_stream, args.type)
finally:
if in_stream is not sys.stdin:
in_stream.close()
if out_stream is not sys.stdout.buffer:
out_stream.close()
# Print summary if requested or when writing to an explicit output file
if args.summary or args.output != "-":
bytes_written = count * (8 if "64" in args.type or "double" in args.type else 4)
sys.stderr.write(
f"Converted {count} test cases ({bytes_written} bytes) to {args.output}\n"
)
if __name__ == "__main__":
main()