diff --git a/Cargo.lock b/Cargo.lock index 1c1607e4c1ac4..3355e1ddaef86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2598,12 +2598,76 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -5630,6 +5694,17 @@ dependencies = [ "std", ] +[[package]] +name = "test-float-parse" +version = "0.1.0" +dependencies = [ + "indicatif", + "num", + "rand", + "rand_chacha", + "rayon", +] + [[package]] name = "textwrap" version = "0.16.1" diff --git a/Cargo.toml b/Cargo.toml index ce87a8c20b77d..043f339883a52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "compiler/rustc", "library/std", "library/sysroot", + "src/etc/test-float-parse", "src/rustdoc-json-types", "src/tools/build_helper", "src/tools/cargotest", @@ -109,6 +110,18 @@ strip = true debug = 0 strip = true +# Bigint libraries are slow without optimization, speed up testing +[profile.dev.package.test-float-parse] +opt-level = 3 + +# Speed up the binary as much as possible +[profile.release.package.test-float-parse] +opt-level = 3 +codegen-units = 1 +# FIXME: LTO cannot be enabled for binaries in a workspace +# +# lto = true + [patch.crates-io] # See comments in `library/rustc-std-workspace-core/README.md` for what's going on # here diff --git a/src/bootstrap/mk/Makefile.in b/src/bootstrap/mk/Makefile.in index 7e38a0996e59b..9acd85cddde6a 100644 --- a/src/bootstrap/mk/Makefile.in +++ b/src/bootstrap/mk/Makefile.in @@ -51,6 +51,7 @@ check-aux: $(Q)$(BOOTSTRAP) test --stage 2 \ src/tools/cargo \ src/tools/cargotest \ + src/etc/test-float-parse \ $(BOOTSTRAP_ARGS) # Run standard library tests in Miri. $(Q)BOOTSTRAP_SKIP_TARGET_SANITY=1 \ diff --git a/src/bootstrap/src/core/build_steps/check.rs b/src/bootstrap/src/core/build_steps/check.rs index 8235d4634b753..ed5b9edc86d64 100644 --- a/src/bootstrap/src/core/build_steps/check.rs +++ b/src/bootstrap/src/core/build_steps/check.rs @@ -466,6 +466,7 @@ tool_check_step!(CargoMiri, "src/tools/miri/cargo-miri", SourceType::InTree); tool_check_step!(Rls, "src/tools/rls", SourceType::InTree); tool_check_step!(Rustfmt, "src/tools/rustfmt", SourceType::InTree); tool_check_step!(MiroptTestTools, "src/tools/miropt-test-tools", SourceType::InTree); +tool_check_step!(TestFloatParse, "src/etc/test-float-parse", SourceType::InTree); tool_check_step!(Bootstrap, "src/bootstrap", SourceType::InTree, false); diff --git a/src/bootstrap/src/core/build_steps/clippy.rs b/src/bootstrap/src/core/build_steps/clippy.rs index 40a2112b19254..ee7fb368a8c27 100644 --- a/src/bootstrap/src/core/build_steps/clippy.rs +++ b/src/bootstrap/src/core/build_steps/clippy.rs @@ -326,4 +326,5 @@ lint_any!( Rustfmt, "src/tools/rustfmt", "rustfmt"; RustInstaller, "src/tools/rust-installer", "rust-installer"; Tidy, "src/tools/tidy", "tidy"; + TestFloatParse, "src/etc/test-float-parse", "test-float-parse"; ); diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs index 3f0cbde64e394..cc5931c68db1f 100644 --- a/src/bootstrap/src/core/build_steps/test.rs +++ b/src/bootstrap/src/core/build_steps/test.rs @@ -3505,3 +3505,80 @@ impl Step for CodegenGCC { cargo.into_cmd().run(builder); } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TestFloatParse { + path: PathBuf, + host: TargetSelection, +} + +impl Step for TestFloatParse { + type Output = (); + const ONLY_HOSTS: bool = true; + const DEFAULT: bool = true; + + fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { + run.path("src/etc/test-float-parse") + } + + fn make_run(run: RunConfig<'_>) { + for path in run.paths { + let path = path.assert_single_path().path.clone(); + run.builder.ensure(Self { path, host: run.target }); + } + } + + fn run(self, builder: &Builder<'_>) { + let bootstrap_host = builder.config.build; + let compiler = builder.compiler(0, bootstrap_host); + let path = self.path.to_str().unwrap(); + let crate_name = self.path.components().last().unwrap().as_os_str().to_str().unwrap(); + + builder.ensure(compile::Std::new(compiler, self.host)); + + // Run any unit tests in the crate + let cargo_test = tool::prepare_tool_cargo( + builder, + compiler, + Mode::ToolStd, + bootstrap_host, + "test", + path, + SourceType::InTree, + &[], + ); + + run_cargo_test( + cargo_test, + &[], + &[], + crate_name, + crate_name, + compiler, + bootstrap_host, + builder, + ); + + // Run the actual parse tests. + let mut cargo_run = tool::prepare_tool_cargo( + builder, + compiler, + Mode::ToolStd, + bootstrap_host, + "run", + path, + SourceType::InTree, + &[], + ); + + cargo_run.arg("--"); + if builder.config.args().is_empty() { + // By default, exclude tests that take longer than ~1m. + cargo_run.arg("--skip-huge"); + } else { + cargo_run.args(builder.config.args()); + } + + cargo_run.into_cmd().run(builder); + } +} diff --git a/src/bootstrap/src/core/builder.rs b/src/bootstrap/src/core/builder.rs index 6d6df650b149b..78fbea2e8107c 100644 --- a/src/bootstrap/src/core/builder.rs +++ b/src/bootstrap/src/core/builder.rs @@ -826,6 +826,7 @@ impl<'a> Builder<'a> { clippy::Rustdoc, clippy::Rustfmt, clippy::RustInstaller, + clippy::TestFloatParse, clippy::Tidy, ), Kind::Check | Kind::Fix => describe!( @@ -840,6 +841,7 @@ impl<'a> Builder<'a> { check::Rls, check::Rustfmt, check::RustAnalyzer, + check::TestFloatParse, check::Bootstrap, ), Kind::Test => describe!( @@ -901,6 +903,7 @@ impl<'a> Builder<'a> { test::RustdocJson, test::HtmlCheck, test::RustInstaller, + test::TestFloatParse, // Run bootstrap close to the end as it's unlikely to fail test::Bootstrap, // Run run-make last, since these won't pass without make on Windows diff --git a/src/etc/test-float-parse/Cargo.toml b/src/etc/test-float-parse/Cargo.toml index a045be956acd1..56cb5cddeea52 100644 --- a/src/etc/test-float-parse/Cargo.toml +++ b/src/etc/test-float-parse/Cargo.toml @@ -4,11 +4,12 @@ version = "0.1.0" edition = "2021" publish = false -[workspace] -resolver = "1" - [dependencies] -rand = "0.8" +indicatif = { version = "0.17.8", default-features = false } +num = "0.4.3" +rand = "0.8.5" +rand_chacha = "0.3" +rayon = "1" [lib] name = "test_float_parse" diff --git a/src/etc/test-float-parse/README.md b/src/etc/test-float-parse/README.md new file mode 100644 index 0000000000000..21b20d0a072d7 --- /dev/null +++ b/src/etc/test-float-parse/README.md @@ -0,0 +1,55 @@ +# Float Parsing Tests + +These are tests designed to test decimal to float conversions (`dec2flt`) used +by the standard library. + +It consistes of a collection of test generators that each generate a set of +patterns intended to test a specific property. In addition, there are exhaustive +tests (for <= `f32`) and fuzzers (for anything that can't be run exhaustively). + +The generators work as follows: + +- Each generator is a struct that lives somewhere in the `gen` module. Usually + it is generic over a float type. +- These generators must implement `Iterator`, which should return a context type + that can be used to construct a test string (but usually not the string + itself). +- They must also implement the `Generator` trait, which provides a method to + write test context to a string as a test case, as well as some extra metadata. + + The split between context generation and string construction is so that we can + reuse string allocations. +- Each generator gets registered once for each float type. Each of these + generators then get their iterator called, and each test case checked against + the float type's parse implementation. + +Some generators produce decimal strings, others create bit patterns that need to +be bitcasted to the float type, which then uses its `Display` implementation to +write to a string. For these, float to decimal (`flt2dec`) conversions also get +tested, if unintentionally. + +For each test case, the following is done: + +- The test string is parsed to the float type using the standard library's + implementation. +- The test string is parsed separately to a `BigRational`, which acts as a + representation with infinite precision. +- The rational value then gets checked that it is within the float's + representable values (absolute value greater than the smallest number to round + to zero, but less less than the first value to round to infinity). If these + limits are exceeded, check that the parsed float reflects that. +- For real nonzero numbers, the parsed float is converted into a rational using + `significand * 2^exponent`. It is then checked against the actual rational + value, and verified to be within half a bit's precision of the parsed value. + Also it is checked that ties round to even. + +This is all highly parallelized with `rayon`; test generators can run in +parallel, and their tests get chunked and run in parallel. + +There is a simple command line that allows filtering which tests are run, +setting the number of iterations for fuzzing tests, limiting failures, setting +timeouts, etc. See `main.rs` or run with `--help` for options. + +Note that when running via `./x`, only tests that take less than a few minutes +are run by default. Navigate to the crate (or pass `-C` to Cargo) and run it +directly to run all tests or pass specific arguments. diff --git a/src/etc/test-float-parse/runtests.py b/src/etc/test-float-parse/runtests.py deleted file mode 100755 index cc5e31a051fc2..0000000000000 --- a/src/etc/test-float-parse/runtests.py +++ /dev/null @@ -1,394 +0,0 @@ -#!/usr/bin/env python3 - -""" -Testing dec2flt -=============== -These are *really* extensive tests. Expect them to run for hours. Due to the -nature of the problem (the input is a string of arbitrary length), exhaustive -testing is not really possible. Instead, there are exhaustive tests for some -classes of inputs for which that is feasible and a bunch of deterministic and -random non-exhaustive tests for covering everything else. - -The actual tests (generating decimal strings and feeding them to dec2flt) is -performed by a set of stand-along rust programs. This script compiles, runs, -and supervises them. The programs report the strings they generate and the -floating point numbers they converted those strings to, and this script -checks that the results are correct. - -You can run specific tests rather than all of them by giving their names -(without .rs extension) as command line parameters. - -Verification ------------- -The tricky part is not generating those inputs but verifying the outputs. -Comparing with the result of Python's float() does not cut it because -(and this is apparently undocumented) although Python includes a version of -Martin Gay's code including the decimal-to-float part, it doesn't actually use -it for float() (only for round()) instead relying on the system scanf() which -is not necessarily completely accurate. - -Instead, we take the input and compute the true value with bignum arithmetic -(as a fraction, using the ``fractions`` module). - -Given an input string and the corresponding float computed via Rust, simply -decode the float into f * 2^k (for integers f, k) and the ULP. -We can now easily compute the error and check if it is within 0.5 ULP as it -should be. Zero and infinites are handled similarly: - -- If the approximation is 0.0, the exact value should be *less or equal* - half the smallest denormal float: the smallest denormal floating point - number has an odd mantissa (00...001) and thus half of that is rounded - to 00...00, i.e., zero. -- If the approximation is Inf, the exact value should be *greater or equal* - to the largest finite float + 0.5 ULP: the largest finite float has an odd - mantissa (11...11), so that plus half an ULP is rounded up to the nearest - even number, which overflows. - -Implementation details ----------------------- -This directory contains a set of single-file Rust programs that perform -tests with a particular class of inputs. Each is compiled and run without -parameters, outputs (f64, f32, decimal) pairs to verify externally, and -in any case either exits gracefully or with a panic. - -If a test binary writes *anything at all* to stderr or exits with an -exit code that's not 0, the test fails. -The output on stdout is treated as (f64, f32, decimal) record, encoded thusly: - -- First, the bits of the f64 encoded as an ASCII hex string. -- Second, the bits of the f32 encoded as an ASCII hex string. -- Then the corresponding string input, in ASCII -- The record is terminated with a newline. - -Incomplete records are an error. Not-a-Number bit patterns are invalid too. - -The tests run serially but the validation for a single test is parallelized -with ``multiprocessing``. Each test is launched as a subprocess. -One thread supervises it: Accepts and enqueues records to validate, observe -stderr, and waits for the process to exit. A set of worker processes perform -the validation work for the outputs enqueued there. Another thread listens -for progress updates from the workers. - -Known issues ------------- -Some errors (e.g., NaN outputs) aren't handled very gracefully. -Also, if there is an exception or the process is interrupted (at least on -Windows) the worker processes are leaked and stick around forever. -They're only a few megabytes each, but still, this script should not be run -if you aren't prepared to manually kill a lot of orphaned processes. -""" -from __future__ import print_function -import sys -import os.path -import time -import struct -from fractions import Fraction -from collections import namedtuple -from subprocess import Popen, check_call, PIPE -from glob import glob -import multiprocessing -import threading -import ctypes -import binascii - -try: # Python 3 - import queue as Queue -except ImportError: # Python 2 - import Queue - -NUM_WORKERS = 2 -UPDATE_EVERY_N = 50000 -INF = namedtuple('INF', '')() -NEG_INF = namedtuple('NEG_INF', '')() -ZERO = namedtuple('ZERO', '')() -MAILBOX = None # The queue for reporting errors to the main process. -STDOUT_LOCK = threading.Lock() -test_name = None -child_processes = [] -exit_status = 0 - -def msg(*args): - with STDOUT_LOCK: - print("[" + test_name + "]", *args) - sys.stdout.flush() - - -def write_errors(): - global exit_status - f = open("errors.txt", 'w') - have_seen_error = False - while True: - args = MAILBOX.get() - if args is None: - f.close() - break - print(*args, file=f) - f.flush() - if not have_seen_error: - have_seen_error = True - msg("Something is broken:", *args) - msg("Future errors will be logged to errors.txt") - exit_status = 101 - - -def cargo(): - print("compiling tests") - sys.stdout.flush() - check_call(['cargo', 'build', '--release']) - - -def run(test): - global test_name - test_name = test - - t0 = time.perf_counter() - msg("setting up supervisor") - command = ['cargo', 'run', '--bin', test, '--release'] - proc = Popen(command, bufsize=1<<20 , stdin=PIPE, stdout=PIPE, stderr=PIPE) - done = multiprocessing.Value(ctypes.c_bool) - queue = multiprocessing.Queue(maxsize=5)#(maxsize=1024) - workers = [] - for n in range(NUM_WORKERS): - worker = multiprocessing.Process(name='Worker-' + str(n + 1), - target=init_worker, - args=[test, MAILBOX, queue, done]) - workers.append(worker) - child_processes.append(worker) - for worker in workers: - worker.start() - msg("running test") - interact(proc, queue) - with done.get_lock(): - done.value = True - for worker in workers: - worker.join() - msg("python is done") - assert queue.empty(), "did not validate everything" - dt = time.perf_counter() - t0 - msg("took", round(dt, 3), "seconds") - - -def interact(proc, queue): - n = 0 - while proc.poll() is None: - line = proc.stdout.readline() - if not line: - continue - assert line.endswith(b'\n'), "incomplete line: " + repr(line) - queue.put(line) - n += 1 - if n % UPDATE_EVERY_N == 0: - msg("got", str(n // 1000) + "k", "records") - msg("rust is done. exit code:", proc.returncode) - rest, stderr = proc.communicate() - if stderr: - msg("rust stderr output:", stderr) - for line in rest.split(b'\n'): - if not line: - continue - queue.put(line) - - -def main(): - global MAILBOX - files = glob('src/bin/*.rs') - basenames = [os.path.basename(i) for i in files] - all_tests = [os.path.splitext(f)[0] for f in basenames if not f.startswith('_')] - args = sys.argv[1:] - if args: - tests = [test for test in all_tests if test in args] - else: - tests = all_tests - if not tests: - print("Error: No tests to run") - sys.exit(1) - # Compile first for quicker feedback - cargo() - # Set up mailbox once for all tests - MAILBOX = multiprocessing.Queue() - mailman = threading.Thread(target=write_errors) - mailman.daemon = True - mailman.start() - for test in tests: - run(test) - MAILBOX.put(None) - mailman.join() - - -# ---- Worker thread code ---- - - -POW2 = { e: Fraction(2) ** e for e in range(-1100, 1100) } -HALF_ULP = { e: (Fraction(2) ** e)/2 for e in range(-1100, 1100) } -DONE_FLAG = None - - -def send_error_to_supervisor(*args): - MAILBOX.put(args) - - -def init_worker(test, mailbox, queue, done): - global test_name, MAILBOX, DONE_FLAG - test_name = test - MAILBOX = mailbox - DONE_FLAG = done - do_work(queue) - - -def is_done(): - with DONE_FLAG.get_lock(): - return DONE_FLAG.value - - -def do_work(queue): - while True: - try: - line = queue.get(timeout=0.01) - except Queue.Empty: - if queue.empty() and is_done(): - return - else: - continue - bin64, bin32, text = line.rstrip().split() - validate(bin64, bin32, text.decode('utf-8')) - - -def decode_binary64(x): - """ - Turn a IEEE 754 binary64 into (mantissa, exponent), except 0.0 and - infinity (positive and negative), which return ZERO, INF, and NEG_INF - respectively. - """ - x = binascii.unhexlify(x) - assert len(x) == 8, repr(x) - [bits] = struct.unpack(b'>Q', x) - if bits == 0: - return ZERO - exponent = (bits >> 52) & 0x7FF - negative = bits >> 63 - low_bits = bits & 0xFFFFFFFFFFFFF - if exponent == 0: - mantissa = low_bits - exponent += 1 - if mantissa == 0: - return ZERO - elif exponent == 0x7FF: - assert low_bits == 0, "NaN" - if negative: - return NEG_INF - else: - return INF - else: - mantissa = low_bits | (1 << 52) - exponent -= 1023 + 52 - if negative: - mantissa = -mantissa - return (mantissa, exponent) - - -def decode_binary32(x): - """ - Turn a IEEE 754 binary32 into (mantissa, exponent), except 0.0 and - infinity (positive and negative), which return ZERO, INF, and NEG_INF - respectively. - """ - x = binascii.unhexlify(x) - assert len(x) == 4, repr(x) - [bits] = struct.unpack(b'>I', x) - if bits == 0: - return ZERO - exponent = (bits >> 23) & 0xFF - negative = bits >> 31 - low_bits = bits & 0x7FFFFF - if exponent == 0: - mantissa = low_bits - exponent += 1 - if mantissa == 0: - return ZERO - elif exponent == 0xFF: - if negative: - return NEG_INF - else: - return INF - else: - mantissa = low_bits | (1 << 23) - exponent -= 127 + 23 - if negative: - mantissa = -mantissa - return (mantissa, exponent) - - -MIN_SUBNORMAL_DOUBLE = Fraction(2) ** -1074 -MIN_SUBNORMAL_SINGLE = Fraction(2) ** -149 # XXX unsure -MAX_DOUBLE = (2 - Fraction(2) ** -52) * (2 ** 1023) -MAX_SINGLE = (2 - Fraction(2) ** -23) * (2 ** 127) -MAX_ULP_DOUBLE = 1023 - 52 -MAX_ULP_SINGLE = 127 - 23 -DOUBLE_ZERO_CUTOFF = MIN_SUBNORMAL_DOUBLE / 2 -DOUBLE_INF_CUTOFF = MAX_DOUBLE + 2 ** (MAX_ULP_DOUBLE - 1) -SINGLE_ZERO_CUTOFF = MIN_SUBNORMAL_SINGLE / 2 -SINGLE_INF_CUTOFF = MAX_SINGLE + 2 ** (MAX_ULP_SINGLE - 1) - -def validate(bin64, bin32, text): - try: - double = decode_binary64(bin64) - except AssertionError: - print(bin64, bin32, text) - raise - single = decode_binary32(bin32) - real = Fraction(text) - - if double is ZERO: - if real > DOUBLE_ZERO_CUTOFF: - record_special_error(text, "f64 zero") - elif double is INF: - if real < DOUBLE_INF_CUTOFF: - record_special_error(text, "f64 inf") - elif double is NEG_INF: - if -real < DOUBLE_INF_CUTOFF: - record_special_error(text, "f64 -inf") - elif len(double) == 2: - sig, k = double - validate_normal(text, real, sig, k, "f64") - else: - assert 0, "didn't handle binary64" - if single is ZERO: - if real > SINGLE_ZERO_CUTOFF: - record_special_error(text, "f32 zero") - elif single is INF: - if real < SINGLE_INF_CUTOFF: - record_special_error(text, "f32 inf") - elif single is NEG_INF: - if -real < SINGLE_INF_CUTOFF: - record_special_error(text, "f32 -inf") - elif len(single) == 2: - sig, k = single - validate_normal(text, real, sig, k, "f32") - else: - assert 0, "didn't handle binary32" - -def record_special_error(text, descr): - send_error_to_supervisor(text.strip(), "wrongly rounded to", descr) - - -def validate_normal(text, real, sig, k, kind): - approx = sig * POW2[k] - error = abs(approx - real) - if error > HALF_ULP[k]: - record_normal_error(text, error, k, kind) - - -def record_normal_error(text, error, k, kind): - one_ulp = HALF_ULP[k + 1] - assert one_ulp == 2 * HALF_ULP[k] - relative_error = error / one_ulp - text = text.strip() - try: - err_repr = float(relative_error) - except ValueError: - err_repr = str(err_repr).replace('/', ' / ') - send_error_to_supervisor(err_repr, "ULP error on", text, "(" + kind + ")") - - -if __name__ == '__main__': - main() diff --git a/src/etc/test-float-parse/src/bin/few-ones.rs b/src/etc/test-float-parse/src/bin/few-ones.rs deleted file mode 100644 index 6bb406a594771..0000000000000 --- a/src/etc/test-float-parse/src/bin/few-ones.rs +++ /dev/null @@ -1,15 +0,0 @@ -use test_float_parse::validate; - -fn main() { - let mut pow = vec![]; - for i in 0..63 { - pow.push(1u64 << i); - } - for a in &pow { - for b in &pow { - for c in &pow { - validate(&(a | b | c).to_string()); - } - } - } -} diff --git a/src/etc/test-float-parse/src/bin/huge-pow10.rs b/src/etc/test-float-parse/src/bin/huge-pow10.rs deleted file mode 100644 index 722a24ffcd8d9..0000000000000 --- a/src/etc/test-float-parse/src/bin/huge-pow10.rs +++ /dev/null @@ -1,9 +0,0 @@ -use test_float_parse::validate; - -fn main() { - for e in 300..310 { - for i in 0..100000 { - validate(&format!("{}e{}", i, e)); - } - } -} diff --git a/src/etc/test-float-parse/src/bin/long-fractions.rs b/src/etc/test-float-parse/src/bin/long-fractions.rs deleted file mode 100644 index c715bc1ac2bd3..0000000000000 --- a/src/etc/test-float-parse/src/bin/long-fractions.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::char; -use test_float_parse::validate; - -fn main() { - for n in 0..10 { - let digit = char::from_digit(n, 10).unwrap(); - let mut s = "0.".to_string(); - for _ in 0..400 { - s.push(digit); - if s.parse::().is_ok() { - validate(&s); - } - } - } -} diff --git a/src/etc/test-float-parse/src/bin/many-digits.rs b/src/etc/test-float-parse/src/bin/many-digits.rs deleted file mode 100644 index ba166fd56079d..0000000000000 --- a/src/etc/test-float-parse/src/bin/many-digits.rs +++ /dev/null @@ -1,25 +0,0 @@ -extern crate rand; - -use rand::distributions::{Range, Sample}; -use rand::{IsaacRng, Rng, SeedableRng}; -use std::char; -use test_float_parse::{validate, SEED}; - -fn main() { - let mut rnd = IsaacRng::from_seed(&SEED); - let mut range = Range::new(0, 10); - for _ in 0..5_000_000u64 { - let num_digits = rnd.gen_range(100, 400); - let digits = gen_digits(num_digits, &mut range, &mut rnd); - validate(&digits); - } -} - -fn gen_digits(n: u32, range: &mut Range, rnd: &mut R) -> String { - let mut s = String::new(); - for _ in 0..n { - let digit = char::from_digit(range.sample(rnd), 10).unwrap(); - s.push(digit); - } - s -} diff --git a/src/etc/test-float-parse/src/bin/rand-f64.rs b/src/etc/test-float-parse/src/bin/rand-f64.rs deleted file mode 100644 index 6991e8be15e1c..0000000000000 --- a/src/etc/test-float-parse/src/bin/rand-f64.rs +++ /dev/null @@ -1,18 +0,0 @@ -extern crate rand; - -use rand::{IsaacRng, Rng, SeedableRng}; -use std::mem::transmute; -use test_float_parse::{validate, SEED}; - -fn main() { - let mut rnd = IsaacRng::from_seed(&SEED); - let mut i = 0; - while i < 10_000_000 { - let bits = rnd.next_u64(); - let x: f64 = unsafe { transmute(bits) }; - if x.is_finite() { - validate(&format!("{:e}", x)); - i += 1; - } - } -} diff --git a/src/etc/test-float-parse/src/bin/short-decimals.rs b/src/etc/test-float-parse/src/bin/short-decimals.rs deleted file mode 100644 index 49084eb35e834..0000000000000 --- a/src/etc/test-float-parse/src/bin/short-decimals.rs +++ /dev/null @@ -1,17 +0,0 @@ -use test_float_parse::validate; - -fn main() { - // Skip e = 0 because small-u32 already does those. - for e in 1..301 { - for i in 0..10000 { - // If it ends in zeros, the parser will strip those (and adjust the exponent), - // which almost always (except for exponents near +/- 300) result in an input - // equivalent to something we already generate in a different way. - if i % 10 == 0 { - continue; - } - validate(&format!("{}e{}", i, e)); - validate(&format!("{}e-{}", i, e)); - } - } -} diff --git a/src/etc/test-float-parse/src/bin/subnorm.rs b/src/etc/test-float-parse/src/bin/subnorm.rs deleted file mode 100644 index ac88747eacd35..0000000000000 --- a/src/etc/test-float-parse/src/bin/subnorm.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::mem::transmute; -use test_float_parse::validate; - -fn main() { - for bits in 0u32..(1 << 21) { - let single: f32 = unsafe { transmute(bits) }; - validate(&format!("{:e}", single)); - let double: f64 = unsafe { transmute(bits as u64) }; - validate(&format!("{:e}", double)); - } -} diff --git a/src/etc/test-float-parse/src/bin/tiny-pow10.rs b/src/etc/test-float-parse/src/bin/tiny-pow10.rs deleted file mode 100644 index fb6ba16638044..0000000000000 --- a/src/etc/test-float-parse/src/bin/tiny-pow10.rs +++ /dev/null @@ -1,9 +0,0 @@ -use test_float_parse::validate; - -fn main() { - for e in 301..327 { - for i in 0..100000 { - validate(&format!("{}e-{}", i, e)); - } - } -} diff --git a/src/etc/test-float-parse/src/bin/u32-small.rs b/src/etc/test-float-parse/src/bin/u32-small.rs deleted file mode 100644 index 5ec9d1eea5fbe..0000000000000 --- a/src/etc/test-float-parse/src/bin/u32-small.rs +++ /dev/null @@ -1,7 +0,0 @@ -use test_float_parse::validate; - -fn main() { - for i in 0..(1 << 19) { - validate(&i.to_string()); - } -} diff --git a/src/etc/test-float-parse/src/bin/u64-pow2.rs b/src/etc/test-float-parse/src/bin/u64-pow2.rs deleted file mode 100644 index 984e49200cda3..0000000000000 --- a/src/etc/test-float-parse/src/bin/u64-pow2.rs +++ /dev/null @@ -1,15 +0,0 @@ -use test_float_parse::validate; - -fn main() { - for exp in 19..64 { - let power: u64 = 1 << exp; - validate(&power.to_string()); - for offset in 1..123 { - validate(&(power + offset).to_string()); - validate(&(power - offset).to_string()); - } - } - for offset in 0..123 { - validate(&(u64::MAX - offset).to_string()); - } -} diff --git a/src/etc/test-float-parse/src/gen/exhaustive.rs b/src/etc/test-float-parse/src/gen/exhaustive.rs new file mode 100644 index 0000000000000..5d4b6df8e59b9 --- /dev/null +++ b/src/etc/test-float-parse/src/gen/exhaustive.rs @@ -0,0 +1,43 @@ +use std::fmt::Write; +use std::ops::RangeInclusive; + +use crate::{Float, Generator, Int}; + +/// Test every possible bit pattern. This is infeasible to run on any float types larger than +/// `f32` (which takes about an hour). +pub struct Exhaustive { + iter: RangeInclusive, +} + +impl Generator for Exhaustive +where + RangeInclusive: Iterator, +{ + const NAME: &'static str = "exhaustive"; + const SHORT_NAME: &'static str = "exhaustive"; + + type WriteCtx = F; + + fn total_tests() -> u64 { + F::Int::MAX.try_into().unwrap_or(u64::MAX) + } + + fn new() -> Self { + Self { iter: F::Int::ZERO..=F::Int::MAX } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for Exhaustive +where + RangeInclusive: Iterator, +{ + type Item = F; + + fn next(&mut self) -> Option { + Some(F::from_bits(self.iter.next()?)) + } +} diff --git a/src/etc/test-float-parse/src/gen/exponents.rs b/src/etc/test-float-parse/src/gen/exponents.rs new file mode 100644 index 0000000000000..3748e9d380c1e --- /dev/null +++ b/src/etc/test-float-parse/src/gen/exponents.rs @@ -0,0 +1,95 @@ +use std::fmt::Write; +use std::ops::RangeInclusive; + +use crate::traits::BoxGenIter; +use crate::{Float, Generator}; + +const SMALL_COEFF_MAX: i32 = 10_000; +const SMALL_EXP_MAX: i32 = 300; + +const SMALL_COEFF_RANGE: RangeInclusive = (-SMALL_COEFF_MAX)..=SMALL_COEFF_MAX; +const SMALL_EXP_RANGE: RangeInclusive = (-SMALL_EXP_MAX)..=SMALL_EXP_MAX; + +const LARGE_COEFF_RANGE: RangeInclusive = 0..=100_000; +const LARGE_EXP_RANGE: RangeInclusive = 300..=350; + +/// Check exponential values around zero. +pub struct SmallExponents { + iter: BoxGenIter, +} + +impl Generator for SmallExponents { + const NAME: &'static str = "small exponents"; + const SHORT_NAME: &'static str = "small exp"; + + /// `(coefficient, exponent)` + type WriteCtx = (i32, i32); + + fn total_tests() -> u64 { + ((1 + SMALL_COEFF_RANGE.end() - SMALL_COEFF_RANGE.start()) + * (1 + SMALL_EXP_RANGE.end() - SMALL_EXP_RANGE.start())) + .try_into() + .unwrap() + } + + fn new() -> Self { + let iter = SMALL_EXP_RANGE.flat_map(|exp| SMALL_COEFF_RANGE.map(move |coeff| (coeff, exp))); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + let (coeff, exp) = ctx; + write!(s, "{coeff}e{exp}").unwrap(); + } +} + +impl Iterator for SmallExponents { + type Item = (i32, i32); + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +/// Check exponential values further from zero. +pub struct LargeExponents { + iter: BoxGenIter, +} + +impl Generator for LargeExponents { + const NAME: &'static str = "large positive exponents"; + const SHORT_NAME: &'static str = "large exp"; + + /// `(coefficient, exponent, is_positive)` + type WriteCtx = (u32, u32, bool); + + fn total_tests() -> u64 { + ((1 + LARGE_EXP_RANGE.end() - LARGE_EXP_RANGE.start()) + * (1 + LARGE_COEFF_RANGE.end() - LARGE_COEFF_RANGE.start()) + * 2) + .into() + } + + fn new() -> Self { + let iter = LARGE_EXP_RANGE + .flat_map(|exp| LARGE_COEFF_RANGE.map(move |coeff| (coeff, exp))) + .flat_map(|(coeff, exp)| [(coeff, exp, false), (coeff, exp, true)]); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + let (coeff, exp, is_positive) = ctx; + let sign = if is_positive { "" } else { "-" }; + write!(s, "{sign}{coeff}e{exp}").unwrap(); + } +} + +impl Iterator for LargeExponents { + type Item = (u32, u32, bool); + + fn next(&mut self) -> Option { + self.iter.next() + } +} diff --git a/src/etc/test-float-parse/src/gen/fuzz.rs b/src/etc/test-float-parse/src/gen/fuzz.rs new file mode 100644 index 0000000000000..213bcfc64af0c --- /dev/null +++ b/src/etc/test-float-parse/src/gen/fuzz.rs @@ -0,0 +1,88 @@ +use std::any::{type_name, TypeId}; +use std::collections::BTreeMap; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::Range; +use std::sync::Mutex; + +use rand::distributions::{Distribution, Standard}; +use rand::Rng; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; + +use crate::{Float, Generator, Int, SEED}; + +/// Mapping of float types to the number of iterations that should be run. +/// +/// We could probably make `Generator::new` take an argument instead of the global state, +/// but we only load this once so it works. +static FUZZ_COUNTS: Mutex> = Mutex::new(BTreeMap::new()); + +/// Generic fuzzer; just tests deterministic random bit patterns N times. +pub struct Fuzz { + iter: Range, + rng: ChaCha8Rng, + /// Allow us to use generics in `Iterator`. + marker: PhantomData, +} + +impl Fuzz { + /// Register how many iterations the fuzzer should run for a type. Uses some logic by + /// default, but if `from_cfg` is `Some`, that will be used instead. + pub fn set_iterations(from_cfg: Option) { + let count = if let Some(cfg_count) = from_cfg { + cfg_count + } else if F::BITS <= crate::MAX_BITS_FOR_EXHAUUSTIVE { + // If we run exhaustively, still fuzz but only do half as many bits. The only goal here is + // to catch failures from e.g. high bit patterns before exhaustive tests would get to them. + (F::Int::MAX >> (F::BITS / 2)).try_into().unwrap() + } else { + // Eveything bigger gets a fuzz test with as many iterations as `f32` exhaustive. + u32::MAX.into() + }; + + let _ = FUZZ_COUNTS.lock().unwrap().insert(TypeId::of::(), count); + } +} + +impl Generator for Fuzz +where + Standard: Distribution<::Int>, +{ + const NAME: &'static str = "fuzz"; + const SHORT_NAME: &'static str = "fuzz"; + + type WriteCtx = F; + + fn total_tests() -> u64 { + *FUZZ_COUNTS + .lock() + .unwrap() + .get(&TypeId::of::()) + .unwrap_or_else(|| panic!("missing fuzz count for {}", type_name::())) + } + + fn new() -> Self { + let rng = ChaCha8Rng::from_seed(SEED); + + Self { iter: 0..Self::total_tests(), rng, marker: PhantomData } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for Fuzz +where + Standard: Distribution<::Int>, +{ + type Item = >::WriteCtx; + + fn next(&mut self) -> Option { + let _ = self.iter.next()?; + let i: F::Int = self.rng.gen(); + + Some(F::from_bits(i)) + } +} diff --git a/src/etc/test-float-parse/src/gen/integers.rs b/src/etc/test-float-parse/src/gen/integers.rs new file mode 100644 index 0000000000000..070d188e88c4a --- /dev/null +++ b/src/etc/test-float-parse/src/gen/integers.rs @@ -0,0 +1,104 @@ +use std::fmt::Write; +use std::ops::{Range, RangeInclusive}; + +use crate::traits::BoxGenIter; +use crate::{Float, Generator}; + +const SMALL_MAX_POW2: u32 = 19; + +/// All values up to the max power of two +const SMALL_VALUES: RangeInclusive = { + let max = 1i32 << SMALL_MAX_POW2; + (-max)..=max +}; + +/// Large values only get tested around powers of two +const LARGE_POWERS: Range = SMALL_MAX_POW2..128; + +/// We perturbe each large value around these ranges +const LARGE_PERTURBATIONS: RangeInclusive = -256..=256; + +/// Test all integers up to `2 ^ MAX_POW2` +pub struct SmallInt { + iter: RangeInclusive, +} + +impl Generator for SmallInt { + const NAME: &'static str = "small integer values"; + const SHORT_NAME: &'static str = "int small"; + + type WriteCtx = i32; + + fn total_tests() -> u64 { + (SMALL_VALUES.end() + 1 - SMALL_VALUES.start()).try_into().unwrap() + } + + fn new() -> Self { + Self { iter: SMALL_VALUES } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx}").unwrap(); + } +} + +impl Iterator for SmallInt { + type Item = i32; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +/// Test much bigger integers than [`SmallInt`]. +pub struct LargeInt { + iter: BoxGenIter, +} + +impl LargeInt { + const EDGE_CASES: [i128; 7] = [ + i32::MIN as i128, + i32::MAX as i128, + i64::MIN as i128, + i64::MAX as i128, + u64::MAX as i128, + i128::MIN, + i128::MAX, + ]; +} + +impl Generator for LargeInt { + const NAME: &'static str = "large integer values"; + const SHORT_NAME: &'static str = "int large"; + + type WriteCtx = i128; + + fn total_tests() -> u64 { + u64::try_from( + (i128::from(LARGE_POWERS.end - LARGE_POWERS.start) + + i128::try_from(Self::EDGE_CASES.len()).unwrap()) + * (LARGE_PERTURBATIONS.end() + 1 - LARGE_PERTURBATIONS.start()), + ) + .unwrap() + } + + fn new() -> Self { + let iter = LARGE_POWERS + .map(|pow| 1i128 << pow) + .chain(Self::EDGE_CASES) + .flat_map(|base| LARGE_PERTURBATIONS.map(move |perturb| base.saturating_add(perturb))); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx}").unwrap(); + } +} +impl Iterator for LargeInt { + type Item = i128; + + fn next(&mut self) -> Option { + self.iter.next() + } +} diff --git a/src/etc/test-float-parse/src/gen/long_fractions.rs b/src/etc/test-float-parse/src/gen/long_fractions.rs new file mode 100644 index 0000000000000..b75148b779c1b --- /dev/null +++ b/src/etc/test-float-parse/src/gen/long_fractions.rs @@ -0,0 +1,58 @@ +use std::char; +use std::fmt::Write; + +use crate::{Float, Generator}; + +/// Number of decimal digits to check (all of them). +const MAX_DIGIT: u32 = 9; +/// Test with this many decimals in the string. +const MAX_DECIMALS: usize = 410; +const PREFIX: &str = "0."; + +/// Test e.g. `0.1`, `0.11`, `0.111`, `0.1111`, ..., `0.2`, `0.22`, ... +pub struct RepeatingDecimal { + digit: u32, + buf: String, +} + +impl Generator for RepeatingDecimal { + const NAME: &'static str = "repeating decimal"; + const SHORT_NAME: &'static str = "dec rep"; + + type WriteCtx = String; + + fn total_tests() -> u64 { + u64::from(MAX_DIGIT + 1) * u64::try_from(MAX_DECIMALS + 1).unwrap() + 1 + } + + fn new() -> Self { + Self { digit: 0, buf: PREFIX.to_owned() } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + *s = ctx; + } +} + +impl Iterator for RepeatingDecimal { + type Item = String; + + fn next(&mut self) -> Option { + if self.digit > MAX_DIGIT { + return None; + } + + let digit = self.digit; + let inc_digit = self.buf.len() - PREFIX.len() > MAX_DECIMALS; + + if inc_digit { + // Reset the string + self.buf.clear(); + self.digit += 1; + self.buf.write_str(PREFIX).unwrap(); + } + + self.buf.push(char::from_digit(digit, 10).unwrap()); + Some(self.buf.clone()) + } +} diff --git a/src/etc/test-float-parse/src/gen/many_digits.rs b/src/etc/test-float-parse/src/gen/many_digits.rs new file mode 100644 index 0000000000000..aab8d5d704bec --- /dev/null +++ b/src/etc/test-float-parse/src/gen/many_digits.rs @@ -0,0 +1,84 @@ +use std::char; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::{Range, RangeInclusive}; + +use rand::distributions::{Distribution, Uniform}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +use crate::{Float, Generator, SEED}; + +/// Total iterations +const ITERATIONS: u64 = 5_000_000; + +/// Possible lengths of the string, excluding decimals and exponents +const POSSIBLE_NUM_DIGITS: RangeInclusive = 100..=400; + +/// Range of possible exponents +const EXP_RANGE: Range = -4500..4500; + +/// Try strings of random digits. +pub struct RandDigits { + rng: ChaCha8Rng, + iter: Range, + uniform: Uniform, + /// Allow us to use generics in `Iterator`. + marker: PhantomData, +} + +impl Generator for RandDigits { + const NAME: &'static str = "random digits"; + + const SHORT_NAME: &'static str = "rand digits"; + + type WriteCtx = String; + + fn total_tests() -> u64 { + ITERATIONS + } + + fn new() -> Self { + let rng = ChaCha8Rng::from_seed(SEED); + let range = Uniform::from(0..10); + + Self { rng, iter: 0..ITERATIONS, uniform: range, marker: PhantomData } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + *s = ctx; + } +} + +impl Iterator for RandDigits { + type Item = String; + + fn next(&mut self) -> Option { + let _ = self.iter.next()?; + let num_digits = self.rng.gen_range(POSSIBLE_NUM_DIGITS); + let has_decimal = self.rng.gen_bool(0.2); + let has_exp = self.rng.gen_bool(0.2); + + let dec_pos = if has_decimal { Some(self.rng.gen_range(0..num_digits)) } else { None }; + + let mut s = String::with_capacity(num_digits); + + for pos in 0..num_digits { + let digit = char::from_digit(self.uniform.sample(&mut self.rng), 10).unwrap(); + s.push(digit); + + if let Some(dec_pos) = dec_pos { + if pos == dec_pos { + s.push('.'); + } + } + } + + if has_exp { + let exp = self.rng.gen_range(EXP_RANGE); + write!(s, "e{exp}").unwrap(); + } + + Some(s) + } +} diff --git a/src/etc/test-float-parse/src/gen/sparse.rs b/src/etc/test-float-parse/src/gen/sparse.rs new file mode 100644 index 0000000000000..389b71056a3e5 --- /dev/null +++ b/src/etc/test-float-parse/src/gen/sparse.rs @@ -0,0 +1,100 @@ +use std::fmt::Write; + +use crate::traits::BoxGenIter; +use crate::{Float, Generator}; + +const POWERS_OF_TWO: [u128; 128] = make_powers_of_two(); + +const fn make_powers_of_two() -> [u128; 128] { + let mut ret = [0; 128]; + let mut i = 0; + while i < 128 { + ret[i] = 1 << i; + i += 1; + } + + ret +} + +/// Can't clone this result because of lifetime errors, just use a macro. +macro_rules! pow_iter { + () => { + (0..F::BITS).map(|i| F::Int::try_from(POWERS_OF_TWO[i as usize]).unwrap()) + }; +} + +/// Test all numbers that include three 1s in the binary representation as integers. +pub struct FewOnesInt +where + FewOnesInt: Generator, +{ + iter: BoxGenIter, +} + +impl Generator for FewOnesInt +where + >::Error: std::fmt::Debug, +{ + const NAME: &'static str = "few ones int"; + const SHORT_NAME: &'static str = "few ones int"; + + type WriteCtx = F::Int; + + fn total_tests() -> u64 { + u64::from(F::BITS).pow(3) + } + + fn new() -> Self { + let iter = pow_iter!() + .flat_map(move |a| pow_iter!().map(move |b| (a, b))) + .flat_map(move |(a, b)| pow_iter!().map(move |c| (a, b, c))) + .map(|(a, b, c)| a | b | c); + + Self { iter: Box::new(iter) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx}").unwrap(); + } +} + +impl Iterator for FewOnesInt { + type Item = F::Int; + + fn next(&mut self) -> Option { + self.iter.next() + } +} + +/// Similar to `FewOnesInt` except test those bit patterns as a float. +pub struct FewOnesFloat(FewOnesInt); + +impl Generator for FewOnesFloat +where + >::Error: std::fmt::Debug, +{ + const NAME: &'static str = "few ones float"; + const SHORT_NAME: &'static str = "few ones float"; + + type WriteCtx = F; + + fn total_tests() -> u64 { + FewOnesInt::::total_tests() + } + + fn new() -> Self { + Self(FewOnesInt::new()) + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for FewOnesFloat { + type Item = F; + + fn next(&mut self) -> Option { + self.0.next().map(|i| F::from_bits(i)) + } +} diff --git a/src/etc/test-float-parse/src/gen/spot_checks.rs b/src/etc/test-float-parse/src/gen/spot_checks.rs new file mode 100644 index 0000000000000..18691f9d6cf4b --- /dev/null +++ b/src/etc/test-float-parse/src/gen/spot_checks.rs @@ -0,0 +1,101 @@ +use std::fmt::Write; + +use crate::traits::{Float, Generator}; + +const SPECIAL: &[&str] = &[ + "inf", "Inf", "iNf", "INF", "-inf", "-Inf", "-iNf", "-INF", "+inf", "+Inf", "+iNf", "+INF", + "nan", "NaN", "NAN", "nAn", "-nan", "-NaN", "-NAN", "-nAn", "+nan", "+NaN", "+NAN", "+nAn", + "1", "-1", "+1", "1e1", "-1e1", "+1e1", "1e-1", "-1e-1", "+1e-1", "1e+1", "-1e+1", "+1e+1", + "1E1", "-1E1", "+1E1", "1E-1", "-1E-1", "+1E-1", "1E+1", "-1E+1", "+1E+1", "0", "-0", "+0", +]; + +/// Check various non-numeric special strings. +pub struct Special { + iter: std::slice::Iter<'static, &'static str>, +} + +impl Generator for Special { + const NAME: &'static str = "special values"; + + const SHORT_NAME: &'static str = "special"; + + type WriteCtx = &'static str; + + fn total_tests() -> u64 { + SPECIAL.len().try_into().unwrap() + } + + fn new() -> Self { + Self { iter: SPECIAL.iter() } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + s.write_str(ctx).unwrap(); + } +} + +impl Iterator for Special { + type Item = &'static str; + + fn next(&mut self) -> Option { + self.iter.next().copied() + } +} + +/// Strings that we know have failed in the past +const REGRESSIONS: &[&str] = &[ + // From + "1234567890123456789012345678901234567890e-340", + "2.225073858507201136057409796709131975934819546351645648023426109724822222021076945516529523908135087914149158913039621106870086438694594645527657207407820621743379988141063267329253552286881372149012981122451451889849057222307285255133155755015914397476397983411801999323962548289017107081850690630666655994938275772572015763062690663332647565300009245888316433037779791869612049497390377829704905051080609940730262937128958950003583799967207254304360284078895771796150945516748243471030702609144621572289880258182545180325707018860872113128079512233426288368622321503775666622503982534335974568884423900265498198385487948292206894721689831099698365846814022854243330660339850886445804001034933970427567186443383770486037861622771738545623065874679014086723327636718749999999999999999999999999999999999999e-308", + "2.22507385850720113605740979670913197593481954635164564802342610972482222202107694551652952390813508791414915891303962110687008643869459464552765720740782062174337998814106326732925355228688137214901298112245145188984905722230728525513315575501591439747639798341180199932396254828901710708185069063066665599493827577257201576306269066333264756530000924588831643303777979186961204949739037782970490505108060994073026293712895895000358379996720725430436028407889577179615094551674824347103070260914462157228988025818254518032570701886087211312807951223342628836862232150377566662250398253433597456888442390026549819838548794829220689472168983109969836584681402285424333066033985088644580400103493397042756718644338377048603786162277173854562306587467901408672332763671875e-308", + "0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000222507385850720138309023271733240406421921598046233183055332741688720443481391819585428315901251102056406733973103581100515243416155346010885601238537771882113077799353200233047961014744258363607192156504694250373420837525080665061665815894872049117996859163964850063590877011830487479978088775374994945158045160505091539985658247081864511353793580499211598108576605199243335211435239014879569960959128889160299264151106346631339366347758651302937176204732563178148566435087212282863764204484681140761391147706280168985324411002416144742161856716615054015428508471675290190316132277889672970737312333408698898317506783884692609277397797285865965494109136909540613646756870239867831529068098461721092462539672851562500000000000000001", + "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497791.9999999999999999999999999999999999999999999999999999999999999999999999", + "2.47032822920623272e-324", + "6.631236871469758276785396630275967243399099947355303144249971758736286630139265439618068200788048744105960420552601852889715006376325666595539603330361800519107591783233358492337208057849499360899425128640718856616503093444922854759159988160304439909868291973931426625698663157749836252274523485312442358651207051292453083278116143932569727918709786004497872322193856150225415211997283078496319412124640111777216148110752815101775295719811974338451936095907419622417538473679495148632480391435931767981122396703443803335529756003353209830071832230689201383015598792184172909927924176339315507402234836120730914783168400715462440053817592702766213559042115986763819482654128770595766806872783349146967171293949598850675682115696218943412532098591327667236328125E-316", + "3.237883913302901289588352412501532174863037669423108059901297049552301970670676565786835742587799557860615776559838283435514391084153169252689190564396459577394618038928365305143463955100356696665629202017331344031730044369360205258345803431471660032699580731300954848363975548690010751530018881758184174569652173110473696022749934638425380623369774736560008997404060967498028389191878963968575439222206416981462690113342524002724385941651051293552601421155333430225237291523843322331326138431477823591142408800030775170625915670728657003151953664260769822494937951845801530895238439819708403389937873241463484205608000027270531106827387907791444918534771598750162812548862768493201518991668028251730299953143924168545708663913273994694463908672332763671875E-319", + "6.953355807847677105972805215521891690222119817145950754416205607980030131549636688806115726399441880065386399864028691275539539414652831584795668560082999889551357784961446896042113198284213107935110217162654939802416034676213829409720583759540476786936413816541621287843248433202369209916612249676005573022703244799714622116542188837770376022371172079559125853382801396219552418839469770514904192657627060319372847562301074140442660237844114174497210955449896389180395827191602886654488182452409583981389442783377001505462015745017848754574668342161759496661766020028752888783387074850773192997102997936619876226688096314989645766000479009083731736585750335262099860150896718774401964796827166283225641992040747894382698751809812609536720628966577351093292236328125E-310", + "3.339068557571188581835713701280943911923401916998521771655656997328440314559615318168849149074662609099998113009465566426808170378434065722991659642619467706034884424989741080790766778456332168200464651593995817371782125010668346652995912233993254584461125868481633343674905074271064409763090708017856584019776878812425312008812326260363035474811532236853359905334625575404216060622858633280744301892470300555678734689978476870369853549413277156622170245846166991655321535529623870646888786637528995592800436177901746286272273374471701452991433047257863864601424252024791567368195056077320885329384322332391564645264143400798619665040608077549162173963649264049738362290606875883456826586710961041737908872035803481241600376705491726170293986797332763671875E-319", + "2.4703282292062327208828439643411068618252990130716238221279284125033775363510437593264991818081799618989828234772285886546332835517796989819938739800539093906315035659515570226392290858392449105184435931802849936536152500319370457678249219365623669863658480757001585769269903706311928279558551332927834338409351978015531246597263579574622766465272827220056374006485499977096599470454020828166226237857393450736339007967761930577506740176324673600968951340535537458516661134223766678604162159680461914467291840300530057530849048765391711386591646239524912623653881879636239373280423891018672348497668235089863388587925628302755995657524455507255189313690836254779186948667994968324049705821028513185451396213837722826145437693412532098591327667236328124999e-324", + "2.4703282292062327208828439643411068618252990130716238221279284125033775363510437593264991818081799618989828234772285886546332835517796989819938739800539093906315035659515570226392290858392449105184435931802849936536152500319370457678249219365623669863658480757001585769269903706311928279558551332927834338409351978015531246597263579574622766465272827220056374006485499977096599470454020828166226237857393450736339007967761930577506740176324673600968951340535537458516661134223766678604162159680461914467291840300530057530849048765391711386591646239524912623653881879636239373280423891018672348497668235089863388587925628302755995657524455507255189313690836254779186948667994968324049705821028513185451396213837722826145437693412532098591327667236328125e-324", + "2.4703282292062327208828439643411068618252990130716238221279284125033775363510437593264991818081799618989828234772285886546332835517796989819938739800539093906315035659515570226392290858392449105184435931802849936536152500319370457678249219365623669863658480757001585769269903706311928279558551332927834338409351978015531246597263579574622766465272827220056374006485499977096599470454020828166226237857393450736339007967761930577506740176324673600968951340535537458516661134223766678604162159680461914467291840300530057530849048765391711386591646239524912623653881879636239373280423891018672348497668235089863388587925628302755995657524455507255189313690836254779186948667994968324049705821028513185451396213837722826145437693412532098591327667236328125001e-324", + "7.4109846876186981626485318930233205854758970392148714663837852375101326090531312779794975454245398856969484704316857659638998506553390969459816219401617281718945106978546710679176872575177347315553307795408549809608457500958111373034747658096871009590975442271004757307809711118935784838675653998783503015228055934046593739791790738723868299395818481660169122019456499931289798411362062484498678713572180352209017023903285791732520220528974020802906854021606612375549983402671300035812486479041385743401875520901590172592547146296175134159774938718574737870961645638908718119841271673056017045493004705269590165763776884908267986972573366521765567941072508764337560846003984904972149117463085539556354188641513168478436313080237596295773983001708984374999e-324", + "7.4109846876186981626485318930233205854758970392148714663837852375101326090531312779794975454245398856969484704316857659638998506553390969459816219401617281718945106978546710679176872575177347315553307795408549809608457500958111373034747658096871009590975442271004757307809711118935784838675653998783503015228055934046593739791790738723868299395818481660169122019456499931289798411362062484498678713572180352209017023903285791732520220528974020802906854021606612375549983402671300035812486479041385743401875520901590172592547146296175134159774938718574737870961645638908718119841271673056017045493004705269590165763776884908267986972573366521765567941072508764337560846003984904972149117463085539556354188641513168478436313080237596295773983001708984375e-324", + "7.4109846876186981626485318930233205854758970392148714663837852375101326090531312779794975454245398856969484704316857659638998506553390969459816219401617281718945106978546710679176872575177347315553307795408549809608457500958111373034747658096871009590975442271004757307809711118935784838675653998783503015228055934046593739791790738723868299395818481660169122019456499931289798411362062484498678713572180352209017023903285791732520220528974020802906854021606612375549983402671300035812486479041385743401875520901590172592547146296175134159774938718574737870961645638908718119841271673056017045493004705269590165763776884908267986972573366521765567941072508764337560846003984904972149117463085539556354188641513168478436313080237596295773983001708984375001e-324", + "94393431193180696942841837085033647913224148539854e-358", + "104308485241983990666713401708072175773165034278685682646111762292409330928739751702404658197872319129036519947435319418387839758990478549477777586673075945844895981012024387992135617064532141489278815239849108105951619997829153633535314849999674266169258928940692239684771590065027025835804863585454872499320500023126142553932654370362024104462255244034053203998964360882487378334860197725139151265590832887433736189468858614521708567646743455601905935595381852723723645799866672558576993978025033590728687206296379801363024094048327273913079612469982585674824156000783167963081616214710691759864332339239688734656548790656486646106983450809073750535624894296242072010195710276073042036425579852459556183541199012652571123898996574563824424330960027873516082763671875e-1075", + "0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247032822920623272088284396434110686182529901307162382212792841250337753635104375932649918180817996189898282347722858865463328355177969898199387398005390939063150356595155702263922908583924491051844359318028499365361525003193704576782492193656236698636584807570015857692699037063119282795585513329278343384093519780155312465972635795746227664652728272200563740064854999770965994704540208281662262378573934507363390079677619305775067401763246736009689513405355374585166611342237666786041621596804619144672918403005300575308490487653917113865916462395249126236538818796362393732804238910186723484976682350898633885879256283027559956575244555072551893136908362547791869486679949683240497058210285131854513962138377228261454376934125320985913276672363281249", + "0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000247032822920623272088284396434110686182529901307162382212792841250337753635104375932649918180817996189898282347722858865463328355177969898199387398005390939063150356595155702263922908583924491051844359318028499365361525003193704576782492193656236698636584807570015857692699037063119282795585513329278343384093519780155312465972635795746227664652728272200563740064854999770965994704540208281662262378573934507363390079677619305775067401763246736009689513405355374585166611342237666786041621596804619144672918403005300575308490487653917113865916462395249126236538818796362393732804238910186723484976682350898633885879256283027559956575244555072551893136908362547791869486679949683240497058210285131854513962138377228261454376934125320985913276672363281251", +]; + +/// Check items that failed in the past. +pub struct RegressionCheck { + iter: std::slice::Iter<'static, &'static str>, +} + +impl Generator for RegressionCheck { + const NAME: &'static str = "regression check"; + + const SHORT_NAME: &'static str = "regression"; + + type WriteCtx = &'static str; + + fn total_tests() -> u64 { + REGRESSIONS.len().try_into().unwrap() + } + + fn new() -> Self { + Self { iter: REGRESSIONS.iter() } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + s.write_str(ctx).unwrap(); + } +} + +impl Iterator for RegressionCheck { + type Item = &'static str; + + fn next(&mut self) -> Option { + self.iter.next().copied() + } +} diff --git a/src/etc/test-float-parse/src/gen/subnorm.rs b/src/etc/test-float-parse/src/gen/subnorm.rs new file mode 100644 index 0000000000000..4fe3b90a3ddf4 --- /dev/null +++ b/src/etc/test-float-parse/src/gen/subnorm.rs @@ -0,0 +1,103 @@ +use std::cmp::min; +use std::fmt::Write; +use std::ops::RangeInclusive; + +use crate::{Float, Generator, Int}; + +/// Spot check some edge cases for subnormals. +pub struct SubnormEdgeCases { + cases: [F::Int; 6], + index: usize, +} + +impl SubnormEdgeCases { + /// Shorthand + const I1: F::Int = F::Int::ONE; + + fn edge_cases() -> [F::Int; 6] { + // Comments use an 8-bit mantissa as a demo + [ + // 0b00000001 + Self::I1, + // 0b10000000 + Self::I1 << (F::MAN_BITS - 1), + // 0b00001000 + Self::I1 << ((F::MAN_BITS / 2) - 1), + // 0b00001111 + Self::I1 << ((F::MAN_BITS / 2) - 1), + // 0b00001111 + Self::I1 << ((F::MAN_BITS / 2) - 1), + // 0b11111111 + F::MAN_MASK, + ] + } +} + +impl Generator for SubnormEdgeCases { + const NAME: &'static str = "subnormal edge cases"; + const SHORT_NAME: &'static str = "subnorm edge"; + + type WriteCtx = F; + + fn new() -> Self { + Self { cases: Self::edge_cases(), index: 0 } + } + + fn total_tests() -> u64 { + Self::edge_cases().len().try_into().unwrap() + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for SubnormEdgeCases { + type Item = F; + + fn next(&mut self) -> Option { + let i = self.cases.get(self.index)?; + self.index += 1; + + Some(F::from_bits(*i)) + } +} + +/// Test all subnormals up to `1 << 22`. +pub struct SubnormComplete { + iter: RangeInclusive, +} + +impl Generator for SubnormComplete +where + RangeInclusive: Iterator, +{ + const NAME: &'static str = "subnormal"; + const SHORT_NAME: &'static str = "subnorm "; + + type WriteCtx = F; + + fn total_tests() -> u64 { + let iter = Self::new().iter; + (F::Int::ONE + *iter.end() - *iter.start()).try_into().unwrap() + } + + fn new() -> Self { + Self { iter: F::Int::ZERO..=min(F::Int::ONE << 22, F::MAN_BITS.try_into().unwrap()) } + } + + fn write_string(s: &mut String, ctx: Self::WriteCtx) { + write!(s, "{ctx:e}").unwrap(); + } +} + +impl Iterator for SubnormComplete +where + RangeInclusive: Iterator, +{ + type Item = F; + + fn next(&mut self) -> Option { + Some(F::from_bits(self.iter.next()?)) + } +} diff --git a/src/etc/test-float-parse/src/lib.rs b/src/etc/test-float-parse/src/lib.rs index 9cbad5486b485..f36e3928d26c0 100644 --- a/src/etc/test-float-parse/src/lib.rs +++ b/src/etc/test-float-parse/src/lib.rs @@ -1,16 +1,526 @@ -use std::io; -use std::io::prelude::*; -use std::mem::transmute; - -// Nothing up my sleeve: Just (PI - 3) in base 16. -#[allow(dead_code)] -pub const SEED: [u32; 3] = [0x243f_6a88, 0x85a3_08d3, 0x1319_8a2e]; - -pub fn validate(text: &str) { - let mut out = io::stdout(); - let x: f64 = text.parse().unwrap(); - let f64_bytes: u64 = unsafe { transmute(x) }; - let x: f32 = text.parse().unwrap(); - let f32_bytes: u32 = unsafe { transmute(x) }; - writeln!(&mut out, "{:016x} {:08x} {}", f64_bytes, f32_bytes, text).unwrap(); +mod traits; +mod ui; +mod validate; + +use std::any::{type_name, TypeId}; +use std::cmp::min; +use std::ops::RangeInclusive; +use std::process::ExitCode; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{mpsc, OnceLock}; +use std::{fmt, time}; + +use indicatif::{MultiProgress, ProgressBar}; +use rand::distributions::{Distribution, Standard}; +use rayon::prelude::*; +use time::{Duration, Instant}; +use traits::{Float, Generator, Int}; + +/// Test generators. +mod gen { + pub mod exhaustive; + pub mod exponents; + pub mod fuzz; + pub mod integers; + pub mod long_fractions; + pub mod many_digits; + pub mod sparse; + pub mod spot_checks; + pub mod subnorm; +} + +/// How many failures to exit after if unspecified. +const DEFAULT_MAX_FAILURES: u64 = 20; + +/// Register exhaustive tests only for <= 32 bits. No more because it would take years. +const MAX_BITS_FOR_EXHAUUSTIVE: u32 = 32; + +/// If there are more tests than this threashold, the test will be defered until after all +/// others run (so as to avoid thread pool starvation). They also can be excluded with +/// `--skip-huge`. +const HUGE_TEST_CUTOFF: u64 = 5_000_000; + +/// Seed for tests that use a deterministic RNG. +const SEED: [u8; 32] = *b"3.141592653589793238462643383279"; + +/// Global configuration +#[derive(Debug)] +pub struct Config { + pub timeout: Duration, + /// Failures per test + pub max_failures: u64, + pub disable_max_failures: bool, + /// If `None`, the default will be used + pub fuzz_count: Option, + pub skip_huge: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + timeout: Duration::from_secs(60 * 60 * 3), + max_failures: DEFAULT_MAX_FAILURES, + disable_max_failures: false, + fuzz_count: None, + skip_huge: false, + } + } +} + +/// Collect, filter, and launch all tests. +pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode { + // With default parallelism, the CPU doesn't saturate. We don't need to be nice to + // other processes, so do 1.5x to make sure we use all available resources. + let threads = std::thread::available_parallelism().map(Into::into).unwrap_or(0) * 3 / 2; + rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap(); + + let mut tests = register_tests(&cfg); + println!("registered"); + let initial_tests: Vec<_> = tests.iter().map(|t| t.name.clone()).collect(); + + let unmatched: Vec<_> = include + .iter() + .chain(exclude.iter()) + .filter(|filt| !tests.iter().any(|t| t.matches(filt))) + .collect(); + + assert!( + unmatched.is_empty(), + "filters were provided that have no matching tests: {unmatched:#?}" + ); + + tests.retain(|test| !exclude.iter().any(|exc| test.matches(exc))); + + if cfg.skip_huge { + tests.retain(|test| !test.is_huge_test()); + } + + if !include.is_empty() { + tests.retain(|test| include.iter().any(|inc| test.matches(inc))); + } + + for exc in initial_tests.iter().filter(|orig_name| !tests.iter().any(|t| t.name == **orig_name)) + { + println!("Skipping test '{exc}'"); + } + + println!("launching"); + let elapsed = launch_tests(&mut tests, &cfg); + ui::finish(&tests, elapsed, &cfg) +} + +/// Enumerate tests to run but don't actaully run them. +pub fn register_tests(cfg: &Config) -> Vec { + let mut tests = Vec::new(); + + // Register normal generators for all floats. + register_float::(&mut tests, cfg); + register_float::(&mut tests, cfg); + + tests.sort_unstable_by_key(|t| (t.float_name, t.gen_name)); + for i in 0..(tests.len() - 1) { + if tests[i].gen_name == tests[i + 1].gen_name { + panic!("dupliate test name {}", tests[i].gen_name); + } + } + + tests +} + +/// Register all generators for a single float. +fn register_float(tests: &mut Vec, cfg: &Config) +where + RangeInclusive: Iterator, + >::Error: std::fmt::Debug, + Standard: Distribution<::Int>, +{ + if F::BITS <= MAX_BITS_FOR_EXHAUUSTIVE { + // Only run exhaustive tests if there is a chance of completion. + TestInfo::register::>(tests); + } + + gen::fuzz::Fuzz::::set_iterations(cfg.fuzz_count); + + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::(tests); + TestInfo::register::(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); + TestInfo::register::(tests); + TestInfo::register::(tests); + TestInfo::register::>(tests); + TestInfo::register::>(tests); +} + +/// Configuration for a single test. +#[derive(Debug)] +pub struct TestInfo { + pub name: String, + /// Tests are identified by the type ID of `(F, G)` (tuple of the float and generator type). + /// This gives an easy way to associate messages with tests. + id: TypeId, + float_name: &'static str, + gen_name: &'static str, + /// Name for display in the progress bar. + short_name: String, + total_tests: u64, + /// Function to launch this test. + launch: fn(&mpsc::Sender, &TestInfo, &Config), + /// Progress bar to be updated. + pb: Option, + /// Once completed, this will be set. + completed: OnceLock, +} + +impl TestInfo { + /// Check if either the name or short name is a match, for filtering. + fn matches(&self, pat: &str) -> bool { + self.short_name.contains(pat) || self.name.contains(pat) + } + + /// Create a `TestInfo` for a given float and generator, then add it to a list. + fn register>(v: &mut Vec) { + let f_name = type_name::(); + let gen_name = G::NAME; + let gen_short_name = G::SHORT_NAME; + + let info = TestInfo { + id: TypeId::of::<(F, G)>(), + float_name: f_name, + gen_name, + pb: None, + name: format!("{f_name} {gen_name}"), + short_name: format!("{f_name} {gen_short_name}"), + launch: test_runner::, + total_tests: G::total_tests(), + completed: OnceLock::new(), + }; + v.push(info); + } + + /// Pad the short name to a common width for progress bar use. + fn short_name_padded(&self) -> String { + format!("{:18}", self.short_name) + } + + /// Create a progress bar for this test within a multiprogress bar. + fn register_pb(&mut self, mp: &MultiProgress, drop_bars: &mut Vec) { + self.pb = Some(ui::create_pb(mp, self.total_tests, &self.short_name_padded(), drop_bars)); + } + + /// When the test is finished, update progress bar messages and finalize. + fn finalize_pb(&self, c: &Completed) { + let pb = self.pb.as_ref().unwrap(); + ui::finalize_pb(pb, &self.short_name_padded(), c); + } + + /// True if this should be run after all others. + fn is_huge_test(&self) -> bool { + self.total_tests >= HUGE_TEST_CUTOFF + } +} + +/// A message sent from test runner threads to the UI/log thread. +#[derive(Clone, Debug)] +struct Msg { + id: TypeId, + update: Update, +} + +impl Msg { + /// Wrap an `Update` into a message for the specified type. We use the `TypeId` of `(F, G)` to + /// identify which test a message in the channel came from. + fn new>(u: Update) -> Self { + Self { id: TypeId::of::<(F, G)>(), update: u } + } + + /// Get the matching test from a list. Panics if not found. + fn find_test<'a>(&self, tests: &'a [TestInfo]) -> &'a TestInfo { + tests.iter().find(|t| t.id == self.id).unwrap() + } + + /// Update UI as needed for a single message received from the test runners. + fn handle(self, tests: &[TestInfo], mp: &MultiProgress) { + let test = self.find_test(tests); + let pb = test.pb.as_ref().unwrap(); + + match self.update { + Update::Started => { + mp.println(format!("Testing '{}'", test.name)).unwrap(); + } + Update::Progress { executed, failures } => { + pb.set_message(format! {"{failures}"}); + pb.set_position(executed); + } + Update::Failure { fail, input, float_res } => { + mp.println(format!( + "Failure in '{}': {fail}. parsing '{input}'. Parsed as: {float_res}", + test.name + )) + .unwrap(); + } + Update::Completed(c) => { + test.finalize_pb(&c); + + let prefix = match c.result { + Ok(FinishedAll) => "Completed tests for", + Err(EarlyExit::Timeout) => "Timed out", + Err(EarlyExit::MaxFailures) => "Max failures reached for", + }; + + mp.println(format!( + "{prefix} generator '{}' in {:?}. {} tests run, {} failures", + test.name, c.elapsed, c.executed, c.failures + )) + .unwrap(); + test.completed.set(c).unwrap(); + } + }; + } +} + +/// Status sent with a message. +#[derive(Clone, Debug)] +enum Update { + /// Starting a new test runner. + Started, + /// Completed a out of b tests. + Progress { executed: u64, failures: u64 }, + /// Received a failed test. + Failure { + fail: CheckFailure, + /// String for which parsing was attempted. + input: Box, + /// The parsed & decomposed `FloatRes`, aleady stringified so we don't need generics here. + float_res: Box, + }, + /// Exited with an unexpected condition. + Completed(Completed), +} + +/// Result of an input did not parsing successfully. +#[derive(Clone, Debug)] +enum CheckFailure { + /// Above the zero cutoff but got rounded to zero. + UnexpectedZero, + /// Below the infinity cutoff but got rounded to infinity. + UnexpectedInf, + /// Above the negative infinity cutoff but got rounded to negative infinity. + UnexpectedNegInf, + /// Got a `NaN` when none was expected. + UnexpectedNan, + /// Expected `NaN`, got none. + ExpectedNan, + /// Expected infinity, got finite. + ExpectedInf, + /// Expected negative infinity, got finite. + ExpectedNegInf, + /// The value exceeded its error tolerance. + InvalidReal { + /// Error from the expected value, as a float. + error_float: Option, + /// Error as a rational string (since it can't always be represented as a float). + error_str: Box, + /// True if the error was caused by not rounding to even at the midpoint between + /// two representable values. + incorrect_midpoint_rounding: bool, + }, +} + +impl fmt::Display for CheckFailure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + CheckFailure::UnexpectedZero => { + write!(f, "incorrectly rounded to 0 (expected nonzero)") + } + CheckFailure::UnexpectedInf => { + write!(f, "incorrectly rounded to +inf (expected finite)") + } + CheckFailure::UnexpectedNegInf => { + write!(f, "incorrectly rounded to -inf (expected finite)") + } + CheckFailure::UnexpectedNan => write!(f, "got a NaN where none was expected"), + CheckFailure::ExpectedNan => write!(f, "expected a NaN but did not get it"), + CheckFailure::ExpectedInf => write!(f, "expected +inf but did not get it"), + CheckFailure::ExpectedNegInf => write!(f, "expected -inf but did not get it"), + CheckFailure::InvalidReal { error_float, error_str, incorrect_midpoint_rounding } => { + if *incorrect_midpoint_rounding { + write!( + f, + "midpoint between two representable values did not correctly \ + round to even; error: {error_str}" + )?; + } else { + write!(f, "real number did not parse correctly; error: {error_str}")?; + } + + if let Some(float) = error_float { + write!(f, " ({float})")?; + } + Ok(()) + } + } + } +} + +/// Information about a completed test generator. +#[derive(Clone, Debug)] +struct Completed { + /// Finished tests (both successful and failed). + executed: u64, + /// Failed tests. + failures: u64, + /// Extra exit information if unsuccessful. + result: Result, + /// If there is something to warn about (e.g bad estimate), leave it here. + warning: Option>, + /// Total time to run the test. + elapsed: Duration, +} + +/// Marker for completing all tests (used in `Result` types). +#[derive(Clone, Debug)] +struct FinishedAll; + +/// Reasons for exiting early. +#[derive(Clone, Debug)] +enum EarlyExit { + Timeout, + MaxFailures, +} + +/// Run all tests in `tests`. +/// +/// This launches a main thread that receives messages and handlees UI updates, and uses the +/// rest of the thread pool to execute the tests. +fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration { + // Run shorter tests first + tests.sort_unstable_by_key(|test| test.total_tests); + + for test in tests.iter() { + println!("Launching test '{}'", test.name); + } + + // Configure progress bars + let mut all_progress_bars = Vec::new(); + let mp = MultiProgress::new(); + mp.set_move_cursor(true); + for test in tests.iter_mut() { + test.register_pb(&mp, &mut all_progress_bars); + } + + ui::set_panic_hook(all_progress_bars); + + let (tx, rx) = mpsc::channel::(); + let start = Instant::now(); + + rayon::scope(|scope| { + // Thread that updates the UI + scope.spawn(|_scope| { + let rx = rx; // move rx + + loop { + if tests.iter().all(|t| t.completed.get().is_some()) { + break; + } + + let msg = rx.recv().unwrap(); + msg.handle(tests, &mp); + } + + // All tests completed; finish things up + drop(mp); + assert_eq!(rx.try_recv().unwrap_err(), mpsc::TryRecvError::Empty); + }); + + // Don't let the thread pool be starved by huge tests. Run faster tests first in parallel, + // then parallelize only within the rest of the tests. + let (huge_tests, normal_tests): (Vec<_>, Vec<_>) = + tests.iter().partition(|t| t.is_huge_test()); + + // Run the actual tests + normal_tests.par_iter().for_each(|test| ((test.launch)(&tx, test, cfg))); + + huge_tests.par_iter().for_each(|test| ((test.launch)(&tx, test, cfg))); + }); + + start.elapsed() +} + +/// Test runer for a single generator. +/// +/// This calls the generator's iterator multiple times (in parallel) and validates each output. +fn test_runner>(tx: &mpsc::Sender, _info: &TestInfo, cfg: &Config) { + tx.send(Msg::new::(Update::Started)).unwrap(); + + let total = G::total_tests(); + let gen = G::new(); + let executed = AtomicU64::new(0); + let failures = AtomicU64::new(0); + + let checks_per_update = min(total, 1000); + let started = Instant::now(); + + // Function to execute for a single test iteration. + let check_one = |buf: &mut String, ctx: G::WriteCtx| { + let executed = executed.fetch_add(1, Ordering::Relaxed); + buf.clear(); + G::write_string(buf, ctx); + + match validate::validate::(buf) { + Ok(()) => (), + Err(e) => { + tx.send(Msg::new::(e)).unwrap(); + let f = failures.fetch_add(1, Ordering::Relaxed); + // End early if the limit is exceeded. + if f >= cfg.max_failures { + return Err(EarlyExit::MaxFailures); + } + } + }; + + // Send periodic updates + if executed % checks_per_update == 0 { + let failures = failures.load(Ordering::Relaxed); + + tx.send(Msg::new::(Update::Progress { executed, failures })).unwrap(); + + if started.elapsed() > cfg.timeout { + return Err(EarlyExit::Timeout); + } + } + + Ok(()) + }; + + // Run the test iterations in parallel. Each thread gets a string buffer to write + // its check values to. + let res = gen.par_bridge().try_for_each_init(|| String::with_capacity(100), check_one); + + let elapsed = started.elapsed(); + let executed = executed.into_inner(); + let failures = failures.into_inner(); + + // Warn about bad estimates if relevant. + let warning = if executed != total && res.is_ok() { + let msg = format!("executed tests != estimated ({executed} != {total}) for {}", G::NAME); + + Some(msg.into()) + } else { + None + }; + + let result = res.map(|()| FinishedAll); + tx.send(Msg::new::(Update::Completed(Completed { + executed, + failures, + result, + warning, + elapsed, + }))) + .unwrap(); } diff --git a/src/etc/test-float-parse/src/main.rs b/src/etc/test-float-parse/src/main.rs new file mode 100644 index 0000000000000..9c6cad7324f74 --- /dev/null +++ b/src/etc/test-float-parse/src/main.rs @@ -0,0 +1,129 @@ +use std::process::ExitCode; +use std::time::Duration; + +use test_float_parse as tfp; + +static HELP: &str = r#"Usage: + + ./test-float-parse [--timeout x] [--exclude x] [--max-failures x] [INCLUDE ...] + ./test-float-parse [--fuzz-count x] [INCLUDE ...] + ./test-float-parse [--skip-huge] [INCLUDE ...] + ./test-float-parse --list + +Args: + + INCLUDE Include only tests with names containing these + strings. If this argument is not specified, all tests + are run. + --timeout N Exit after this amount of time (in seconds). + --exclude FILTER Skip tests containing this string. May be specified + more than once. + --list List available tests. + --max-failures N Limit to N failures per test. Defaults to 20. Pass + "--max-failures none" to remove this limit. + --fuzz-count N Run the fuzzer with N iterations. Only has an effect + if fuzz tests are enabled. Pass `--fuzz-count none` + to remove this limit. + --skip-huge Skip tests that run for a long time. + --all Reset previous `--exclude`, `--skip-huge`, and + `INCLUDE` arguments (useful for running all tests + via `./x`). +"#; + +enum ArgMode { + Any, + Timeout, + Exclude, + FuzzCount, + MaxFailures, +} + +fn main() -> ExitCode { + if cfg!(debug_assertions) { + println!( + "WARNING: running in debug mode. Release mode is recommended to reduce test duration." + ); + std::thread::sleep(Duration::from_secs(2)); + } + + let args: Vec<_> = std::env::args().skip(1).collect(); + if args.iter().any(|arg| arg == "--help" || arg == "-h") { + println!("{HELP}"); + return ExitCode::SUCCESS; + } + + if args.iter().any(|arg| arg == "--list") { + let tests = tfp::register_tests(&tfp::Config::default()); + println!("Available tests:"); + for t in tests { + println!("{}", t.name); + } + + return ExitCode::SUCCESS; + } + + let (cfg, include, exclude) = parse_args(args); + + tfp::run(cfg, &include, &exclude) +} + +/// Simple command argument parser +fn parse_args(args: Vec) -> (tfp::Config, Vec, Vec) { + let mut cfg = tfp::Config::default(); + + let mut mode = ArgMode::Any; + let mut include = Vec::new(); + let mut exclude = Vec::new(); + + for arg in args { + mode = match mode { + ArgMode::Any if arg == "--timeout" => ArgMode::Timeout, + ArgMode::Any if arg == "--exclude" => ArgMode::Exclude, + ArgMode::Any if arg == "--max-failures" => ArgMode::MaxFailures, + ArgMode::Any if arg == "--fuzz-count" => ArgMode::FuzzCount, + ArgMode::Any if arg == "--skip-huge" => { + cfg.skip_huge = true; + ArgMode::Any + } + ArgMode::Any if arg == "--all" => { + cfg.skip_huge = false; + include.clear(); + exclude.clear(); + ArgMode::Any + } + ArgMode::Any if arg.starts_with('-') => { + panic!("Unknown argument {arg}. Usage:\n{HELP}") + } + ArgMode::Any => { + include.push(arg); + ArgMode::Any + } + ArgMode::Timeout => { + cfg.timeout = Duration::from_secs(arg.parse().unwrap()); + ArgMode::Any + } + ArgMode::MaxFailures => { + if arg == "none" { + cfg.disable_max_failures = true; + } else { + cfg.max_failures = arg.parse().unwrap(); + } + ArgMode::Any + } + ArgMode::FuzzCount => { + if arg == "none" { + cfg.fuzz_count = Some(u64::MAX); + } else { + cfg.fuzz_count = Some(arg.parse().unwrap()); + } + ArgMode::Any + } + ArgMode::Exclude => { + exclude.push(arg); + ArgMode::Any + } + } + } + + (cfg, include, exclude) +} diff --git a/src/etc/test-float-parse/src/traits.rs b/src/etc/test-float-parse/src/traits.rs new file mode 100644 index 0000000000000..dc009ea235f95 --- /dev/null +++ b/src/etc/test-float-parse/src/traits.rs @@ -0,0 +1,202 @@ +//! Interfaces used throughout this crate. + +use std::str::FromStr; +use std::{fmt, ops}; + +use num::bigint::ToBigInt; +use num::Integer; + +use crate::validate::Constants; + +/// Integer types. +#[allow(dead_code)] // Some functions only used for testing +pub trait Int: + Clone + + Copy + + fmt::Debug + + fmt::Display + + fmt::LowerHex + + ops::Add + + ops::Sub + + ops::Shl + + ops::Shr + + ops::BitAnd + + ops::BitOr + + ops::Not + + ops::AddAssign + + ops::BitAndAssign + + ops::BitOrAssign + + From + + TryFrom + + TryFrom + + TryFrom + + TryFrom + + TryInto + + TryInto + + ToBigInt + + PartialOrd + + Integer + + Send + + 'static +{ + type Signed: Int; + type Bytes: Default + AsMut<[u8]>; + + const BITS: u32; + const ZERO: Self; + const ONE: Self; + const MAX: Self; + + fn to_signed(self) -> Self::Signed; + fn wrapping_neg(self) -> Self; + fn trailing_zeros(self) -> u32; + + fn hex(self) -> String { + format!("{self:x}") + } +} + +macro_rules! impl_int { + ($($uty:ty, $sty:ty);+) => { + $( + impl Int for $uty { + type Signed = $sty; + type Bytes = [u8; Self::BITS as usize / 8]; + const BITS: u32 = Self::BITS; + const ZERO: Self = 0; + const ONE: Self = 1; + const MAX: Self = Self::MAX; + fn to_signed(self) -> Self::Signed { + self.try_into().unwrap() + } + fn wrapping_neg(self) -> Self { + self.wrapping_neg() + } + fn trailing_zeros(self) -> u32 { + self.trailing_zeros() + } + } + + impl Int for $sty { + type Signed = Self; + type Bytes = [u8; Self::BITS as usize / 8]; + const BITS: u32 = Self::BITS; + const ZERO: Self = 0; + const ONE: Self = 1; + const MAX: Self = Self::MAX; + fn to_signed(self) -> Self::Signed { + self + } + fn wrapping_neg(self) -> Self { + self.wrapping_neg() + } + fn trailing_zeros(self) -> u32 { + self.trailing_zeros() + } + } + )+ + } +} + +impl_int!(u32, i32; u64, i64); + +/// Floating point types. +pub trait Float: + Copy + fmt::Debug + fmt::LowerExp + FromStr + Sized + Send + 'static +{ + /// Unsigned integer of same width + type Int: Int; + type SInt: Int; + + /// Total bits + const BITS: u32; + + /// (Stored) bits in the mantissa) + const MAN_BITS: u32; + + /// Bits in the exponent + const EXP_BITS: u32 = Self::BITS - Self::MAN_BITS - 1; + + /// A saturated exponent (all ones) + const EXP_SAT: u32 = (1 << Self::EXP_BITS) - 1; + + /// The exponent bias, also its maximum value + const EXP_BIAS: u32 = Self::EXP_SAT >> 1; + + const MAN_MASK: Self::Int; + const SIGN_MASK: Self::Int; + + fn from_bits(i: Self::Int) -> Self; + fn to_bits(self) -> Self::Int; + + /// Rational constants associated with this float type. + fn constants() -> &'static Constants; + + fn is_sign_negative(self) -> bool { + (self.to_bits() & Self::SIGN_MASK) > Self::Int::ZERO + } + + /// Exponent without adjustment for bias. + fn exponent(self) -> u32 { + ((self.to_bits() >> Self::MAN_BITS) & Self::EXP_SAT.try_into().unwrap()).try_into().unwrap() + } + + fn mantissa(self) -> Self::Int { + self.to_bits() & Self::MAN_MASK + } +} + +macro_rules! impl_float { + ($($fty:ty, $ity:ty, $bits:literal);+) => { + $( + impl Float for $fty { + type Int = $ity; + type SInt = ::Signed; + const BITS: u32 = $bits; + const MAN_BITS: u32 = Self::MANTISSA_DIGITS - 1; + const MAN_MASK: Self::Int = (Self::Int::ONE << Self::MAN_BITS) - Self::Int::ONE; + const SIGN_MASK: Self::Int = Self::Int::ONE << (Self::BITS-1); + fn from_bits(i: Self::Int) -> Self { Self::from_bits(i) } + fn to_bits(self) -> Self::Int { self.to_bits() } + fn constants() -> &'static Constants { + use std::sync::LazyLock; + static CONSTANTS: LazyLock = LazyLock::new(Constants::new::<$fty>); + &CONSTANTS + } + } + )+ + } +} + +impl_float!(f32, u32, 32; f64, u64, 64); + +/// A test generator. Should provide an iterator that produces unique patterns to parse. +/// +/// The iterator needs to provide a `WriteCtx` (could be anything), which is then used to +/// write the string at a later step. This is done separately so that we can reuse string +/// allocations (which otherwise turn out to be a pretty expensive part of these tests). +pub trait Generator: Iterator + Send + 'static { + /// Full display and filtering name + const NAME: &'static str; + + /// Name for display with the progress bar + const SHORT_NAME: &'static str; + + /// The context needed to create a test string. + type WriteCtx: Send; + + /// Number of tests that will be run. + fn total_tests() -> u64; + + /// Constructor for this test generator. + fn new() -> Self; + + /// Create a test string given write context, which was produced as a step from the iterator. + /// + /// `s` will be provided empty. + fn write_string(s: &mut String, ctx: Self::WriteCtx); +} + +/// For tests that use iterator combinators, it is easier to just to box the iterator than trying +/// to specify its type. This is a shorthand for the usual type. +pub type BoxGenIter = Box>::WriteCtx> + Send>; diff --git a/src/etc/test-float-parse/src/ui.rs b/src/etc/test-float-parse/src/ui.rs new file mode 100644 index 0000000000000..f333bd4a55dc9 --- /dev/null +++ b/src/etc/test-float-parse/src/ui.rs @@ -0,0 +1,132 @@ +//! Progress bars and such. + +use std::io::{self, Write}; +use std::process::ExitCode; +use std::time::Duration; + +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; + +use crate::{Completed, Config, EarlyExit, FinishedAll, TestInfo}; + +/// Templates for progress bars. +const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME ({pos}/{len}, {msg} f, {per_sec}, eta {eta})"; +const PB_TEMPLATE_FINAL: &str = + "[{elapsed:3} {percent:3}%] NAME ({pos}/{len}, {msg:.COLOR}, {per_sec}, {elapsed_precise})"; + +/// Create a new progress bar within a multiprogress bar. +pub fn create_pb( + mp: &MultiProgress, + total_tests: u64, + short_name_padded: &str, + all_bars: &mut Vec, +) -> ProgressBar { + let pb = mp.add(ProgressBar::new(total_tests)); + let pb_style = ProgressStyle::with_template(&PB_TEMPLATE.replace("NAME", short_name_padded)) + .unwrap() + .progress_chars("##-"); + + pb.set_style(pb_style.clone()); + pb.set_message("0"); + all_bars.push(pb.clone()); + pb +} + +/// Removes the status bar and replace it with a message. +pub fn finalize_pb(pb: &ProgressBar, short_name_padded: &str, c: &Completed) { + let f = c.failures; + + // Use a tuple so we can use colors + let (color, msg, finish_pb): (&str, String, fn(&ProgressBar, String)) = match &c.result { + Ok(FinishedAll) if f > 0 => { + ("red", format!("{f} f (finished with errors)",), ProgressBar::finish_with_message) + } + Ok(FinishedAll) => { + ("green", format!("{f} f (finished successfully)",), ProgressBar::finish_with_message) + } + Err(EarlyExit::Timeout) => { + ("red", format!("{f} f (timed out)"), ProgressBar::abandon_with_message) + } + Err(EarlyExit::MaxFailures) => { + ("red", format!("{f} f (failure limit)"), ProgressBar::abandon_with_message) + } + }; + + let pb_style = ProgressStyle::with_template( + &PB_TEMPLATE_FINAL.replace("NAME", short_name_padded).replace("COLOR", color), + ) + .unwrap(); + + pb.set_style(pb_style); + finish_pb(pb, msg); +} + +/// Print final messages after all tests are complete. +pub fn finish(tests: &[TestInfo], total_elapsed: Duration, cfg: &Config) -> ExitCode { + println!("\n\nResults:"); + + let mut failed_generators = 0; + let mut stopped_generators = 0; + + for t in tests { + let Completed { executed, failures, elapsed, warning, result } = t.completed.get().unwrap(); + + let stat = if result.is_err() { + stopped_generators += 1; + "STOPPED" + } else if *failures > 0 { + failed_generators += 1; + "FAILURE" + } else { + "SUCCESS" + }; + + println!( + " {stat} for generator '{name}'. {passed}/{executed} passed in {elapsed:?}", + name = t.name, + passed = executed - failures, + ); + + if let Some(warning) = warning { + println!(" warning: {warning}"); + } + + match result { + Ok(FinishedAll) => (), + Err(EarlyExit::Timeout) => { + println!(" exited early; exceded {:?} timeout", cfg.timeout) + } + Err(EarlyExit::MaxFailures) => { + println!(" exited early; exceeded {:?} max failures", cfg.max_failures) + } + } + } + + println!( + "{passed}/{} tests succeeded in {total_elapsed:?} ({passed} passed, {} failed, {} stopped)", + tests.len(), + failed_generators, + stopped_generators, + passed = tests.len() - failed_generators - stopped_generators, + ); + + if failed_generators > 0 || stopped_generators > 0 { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} + +/// indicatif likes to eat panic messages. This workaround isn't ideal, but it improves things. +/// . +pub fn set_panic_hook(drop_bars: Vec) { + let hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + for bar in &drop_bars { + bar.abandon(); + println!(); + io::stdout().flush().unwrap(); + io::stderr().flush().unwrap(); + } + hook(info); + })); +} diff --git a/src/etc/test-float-parse/src/validate.rs b/src/etc/test-float-parse/src/validate.rs new file mode 100644 index 0000000000000..1eb3699cfb9f7 --- /dev/null +++ b/src/etc/test-float-parse/src/validate.rs @@ -0,0 +1,364 @@ +//! Everything related to verifying that parsed outputs are correct. + +use std::any::type_name; +use std::collections::BTreeMap; +use std::ops::RangeInclusive; +use std::str::FromStr; +use std::sync::LazyLock; + +use num::bigint::ToBigInt; +use num::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive}; + +use crate::{CheckFailure, Float, Int, Update}; + +/// Powers of two that we store for constants. Account for binary128 which has a 15-bit exponent. +const POWERS_OF_TWO_RANGE: RangeInclusive = (-(2 << 15))..=(2 << 15); + +/// Powers of ten that we cache. Account for binary128, which can fit +4932/-4931 +const POWERS_OF_TEN_RANGE: RangeInclusive = -5_000..=5_000; + +/// Cached powers of 10 so we can look them up rather than recreating. +static POWERS_OF_TEN: LazyLock> = LazyLock::new(|| { + POWERS_OF_TEN_RANGE.map(|exp| (exp, BigRational::from_u32(10).unwrap().pow(exp))).collect() +}); + +/// Rational property-related constants for a specific float type. +#[allow(dead_code)] +#[derive(Debug)] +pub struct Constants { + /// The minimum positive value (a subnormal). + min_subnormal: BigRational, + /// The maximum possible finite value. + max: BigRational, + /// Cutoff between rounding to zero and rounding to the minimum value (min subnormal). + zero_cutoff: BigRational, + /// Cutoff between rounding to the max value and rounding to infinity. + inf_cutoff: BigRational, + /// Opposite of `inf_cutoff` + neg_inf_cutoff: BigRational, + /// The powers of two for all relevant integers. + powers_of_two: BTreeMap, + /// Half of each power of two. ULP = "unit in last position". + /// + /// This is a mapping from integers to half the precision available at that exponent. In other + /// words, `0.5 * 2^n` = `2^(n-1)`, which is half the distance between `m * 2^n` and + /// `(m + 1) * 2^n`, m ∈ ℤ. + /// + /// So, this is the maximum error from a real number to its floating point representation, + /// assuming the float type can represent the exponent. + half_ulp: BTreeMap, + /// Handy to have around so we don't need to reallocate for it + two: BigInt, +} + +impl Constants { + pub fn new() -> Self { + let two_int = &BigInt::from_u32(2).unwrap(); + let two = &BigRational::from_integer(2.into()); + + // The minimum subnormal (aka minimum positive) value. Most negative power of two is the + // minimum exponent (bias - 1) plus the extra from shifting within the mantissa bits. + let min_subnormal = two.pow(-(F::EXP_BIAS + F::MAN_BITS - 1).to_signed()); + + // The maximum value is the maximum exponent with a fully saturated mantissa. This + // is easiest to calculate by evaluating what the next value up would be if representable + // (zeroed mantissa, exponent increments by one, i.e. `2^(bias + 1)`), and subtracting + // a single LSB (`2 ^ (-mantissa_bits)`). + let max = (two - two.pow(-F::MAN_BITS.to_signed())) * (two.pow(F::EXP_BIAS.to_signed())); + let zero_cutoff = &min_subnormal / two_int; + + let inf_cutoff = &max + two_int.pow(F::EXP_BIAS - F::MAN_BITS - 1); + let neg_inf_cutoff = -&inf_cutoff; + + let powers_of_two: BTreeMap = + (POWERS_OF_TWO_RANGE).map(|n| (n, two.pow(n))).collect(); + let mut half_ulp = powers_of_two.clone(); + half_ulp.iter_mut().for_each(|(_k, v)| *v = &*v / two_int); + + Self { + min_subnormal, + max, + zero_cutoff, + inf_cutoff, + neg_inf_cutoff, + powers_of_two, + half_ulp, + two: two_int.clone(), + } + } +} + +/// Validate that a string parses correctly +pub fn validate(input: &str) -> Result<(), Update> { + let parsed: F = input + .parse() + .unwrap_or_else(|e| panic!("parsing failed for {}: {e}. Input: {input}", type_name::())); + + // Parsed float, decoded into significand and exponent + let decoded = decode(parsed); + + // Float parsed separately into a rational + let rational = Rational::parse(input); + + // Verify that the values match + decoded.check(rational, input) +} + +/// The result of parsing a string to a float type. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FloatRes { + Inf, + NegInf, + Zero, + Nan, + /// A real number with significand and exponent. Value is `sig * 2 ^ exp`. + Real { + sig: F::SInt, + exp: i32, + }, +} + +impl FloatRes { + /// Given a known exact rational, check that this representation is accurate within the + /// limits of the float representation. If not, construct a failure `Update` to send. + fn check(self, expected: Rational, input: &str) -> Result<(), Update> { + let consts = F::constants(); + // let bool_helper = |cond: bool, err| cond.then_some(()).ok_or(err); + + let res = match (expected, self) { + // Easy correct cases + (Rational::Inf, FloatRes::Inf) + | (Rational::NegInf, FloatRes::NegInf) + | (Rational::Nan, FloatRes::Nan) => Ok(()), + + // Easy incorrect cases + ( + Rational::Inf, + FloatRes::NegInf | FloatRes::Zero | FloatRes::Nan | FloatRes::Real { .. }, + ) => Err(CheckFailure::ExpectedInf), + ( + Rational::NegInf, + FloatRes::Inf | FloatRes::Zero | FloatRes::Nan | FloatRes::Real { .. }, + ) => Err(CheckFailure::ExpectedNegInf), + ( + Rational::Nan, + FloatRes::Inf | FloatRes::NegInf | FloatRes::Zero | FloatRes::Real { .. }, + ) => Err(CheckFailure::ExpectedNan), + (Rational::Finite(_), FloatRes::Nan) => Err(CheckFailure::UnexpectedNan), + + // Cases near limits + (Rational::Finite(r), FloatRes::Zero) => { + if r <= consts.zero_cutoff { + Ok(()) + } else { + Err(CheckFailure::UnexpectedZero) + } + } + (Rational::Finite(r), FloatRes::Inf) => { + if r >= consts.inf_cutoff { + Ok(()) + } else { + Err(CheckFailure::UnexpectedInf) + } + } + (Rational::Finite(r), FloatRes::NegInf) => { + if r <= consts.neg_inf_cutoff { + Ok(()) + } else { + Err(CheckFailure::UnexpectedNegInf) + } + } + + // Actual numbers + (Rational::Finite(r), FloatRes::Real { sig, exp }) => Self::validate_real(r, sig, exp), + }; + + res.map_err(|fail| Update::Failure { + fail, + input: input.into(), + float_res: format!("{self:?}").into(), + }) + } + + /// Check that `sig * 2^exp` is the same as `rational`, within the float's error margin. + fn validate_real(rational: BigRational, sig: F::SInt, exp: i32) -> Result<(), CheckFailure> { + let consts = F::constants(); + + // `2^exp`. Use cached powers of two to be faster. + let two_exp = consts + .powers_of_two + .get(&exp) + .unwrap_or_else(|| panic!("missing exponent {exp} for {}", type_name::())); + + // Rational from the parsed value, `sig * 2^exp` + let parsed_rational = two_exp * sig.to_bigint().unwrap(); + let error = (parsed_rational - &rational).abs(); + + // Determine acceptable error at this exponent, which is halfway between this value + // (`sig * 2^exp`) and the next value up (`(sig+1) * 2^exp`). + let half_ulp = consts.half_ulp.get(&exp).unwrap(); + + // If we are within one error value (but not equal) then we rounded correctly. + if &error < half_ulp { + return Ok(()); + } + + // For values where we are exactly between two representable values, meaning that the error + // is exactly one half of the precision at that exponent, we need to round to an even + // binary value (i.e. mantissa ends in 0). + let incorrect_midpoint_rounding = if &error == half_ulp { + if sig & F::SInt::ONE == F::SInt::ZERO { + return Ok(()); + } + + // We rounded to odd rather than even; failing based on midpoint rounding. + true + } else { + // We are out of spec for some other reason. + false + }; + + let one_ulp = consts.half_ulp.get(&(exp + 1)).unwrap(); + assert_eq!(one_ulp, &(half_ulp * &consts.two), "ULP values are incorrect"); + + let relative_error = error / one_ulp; + + Err(CheckFailure::InvalidReal { + error_float: relative_error.to_f64(), + error_str: relative_error.to_string().into(), + incorrect_midpoint_rounding, + }) + } + + /// Remove trailing zeros in the significand and adjust the exponent + #[cfg(test)] + fn normalize(self) -> Self { + use std::cmp::min; + + match self { + Self::Real { sig, exp } => { + // If there are trailing zeroes, remove them and increment the exponent instead + let shift = min(sig.trailing_zeros(), exp.wrapping_neg().try_into().unwrap()); + Self::Real { sig: sig >> shift, exp: exp + i32::try_from(shift).unwrap() } + } + _ => self, + } + } +} + +/// Decompose a float into its integral components. This includes the implicit bit. +/// +/// If `allow_nan` is `false`, panic if `NaN` values are reached. +fn decode(f: F) -> FloatRes { + let ione = F::SInt::ONE; + let izero = F::SInt::ZERO; + + let mut exponent_biased = f.exponent(); + let mut mantissa = f.mantissa().to_signed(); + + if exponent_biased == 0 { + if mantissa == izero { + return FloatRes::Zero; + } + + exponent_biased += 1; + } else if exponent_biased == F::EXP_SAT { + if mantissa != izero { + return FloatRes::Nan; + } + + if f.is_sign_negative() { + return FloatRes::NegInf; + } + + return FloatRes::Inf; + } else { + // Set implicit bit + mantissa |= ione << F::MAN_BITS; + } + + let mut exponent = i32::try_from(exponent_biased).unwrap(); + + // Adjust for bias and the rnage of the mantissa + exponent -= i32::try_from(F::EXP_BIAS + F::MAN_BITS).unwrap(); + + if f.is_sign_negative() { + mantissa = mantissa.wrapping_neg(); + } + + FloatRes::Real { sig: mantissa, exp: exponent } +} + +/// A rational or its unrepresentable values. +#[derive(Clone, Debug, PartialEq)] +enum Rational { + Inf, + NegInf, + Nan, + Finite(BigRational), +} + +impl Rational { + /// Turn a string into a rational. `None` if `NaN`. + fn parse(s: &str) -> Rational { + let mut s = s; // lifetime rules + + if s.strip_prefix('+').unwrap_or(s).eq_ignore_ascii_case("nan") + || s.eq_ignore_ascii_case("-nan") + { + return Rational::Nan; + } + + if s.strip_prefix('+').unwrap_or(s).eq_ignore_ascii_case("inf") { + return Rational::Inf; + } + + if s.eq_ignore_ascii_case("-inf") { + return Rational::NegInf; + } + + // Fast path; no decimals or exponents ot parse + if s.bytes().all(|b| b.is_ascii_digit() || b == b'-') { + return Rational::Finite(BigRational::from_str(s).unwrap()); + } + + let mut ten_exp: i32 = 0; + + // Remove and handle e.g. `e-4`, `e+10`, `e5` suffixes + if let Some(pos) = s.bytes().position(|b| b == b'e' || b == b'E') { + let (dec, exp) = s.split_at(pos); + s = dec; + ten_exp = exp[1..].parse().unwrap(); + } + + // Remove the decimal and instead change our exponent + // E.g. "12.3456" becomes "123456 * 10^-4" + let mut s_owned; + if let Some(pos) = s.bytes().position(|b| b == b'.') { + ten_exp = ten_exp.checked_sub((s.len() - pos - 1).try_into().unwrap()).unwrap(); + s_owned = s.to_owned(); + s_owned.remove(pos); + s = &s_owned; + } + + // let pow = BigRational::from_u32(10).unwrap().pow(ten_exp); + let pow = + POWERS_OF_TEN.get(&ten_exp).unwrap_or_else(|| panic!("missing power of ten {ten_exp}")); + let r = pow + * BigInt::from_str(s) + .unwrap_or_else(|e| panic!("`BigInt::from_str(\"{s}\")` failed with {e}")); + Rational::Finite(r) + } + + #[cfg(test)] + fn expect_finite(self) -> BigRational { + let Self::Finite(r) = self else { + panic!("got non rational: {self:?}"); + }; + + r + } +} + +#[cfg(test)] +mod tests; diff --git a/src/etc/test-float-parse/src/validate/tests.rs b/src/etc/test-float-parse/src/validate/tests.rs new file mode 100644 index 0000000000000..ab0e7d8a7ba91 --- /dev/null +++ b/src/etc/test-float-parse/src/validate/tests.rs @@ -0,0 +1,149 @@ +use num::ToPrimitive; + +use super::*; + +#[test] +fn test_parse_rational() { + assert_eq!(Rational::parse("1234").expect_finite(), BigRational::new(1234.into(), 1.into())); + assert_eq!( + Rational::parse("-1234").expect_finite(), + BigRational::new((-1234).into(), 1.into()) + ); + assert_eq!(Rational::parse("1e+6").expect_finite(), BigRational::new(1000000.into(), 1.into())); + assert_eq!(Rational::parse("1e-6").expect_finite(), BigRational::new(1.into(), 1000000.into())); + assert_eq!( + Rational::parse("10.4e6").expect_finite(), + BigRational::new(10400000.into(), 1.into()) + ); + assert_eq!( + Rational::parse("10.4e+6").expect_finite(), + BigRational::new(10400000.into(), 1.into()) + ); + assert_eq!( + Rational::parse("10.4e-6").expect_finite(), + BigRational::new(13.into(), 1250000.into()) + ); + assert_eq!( + Rational::parse("10.4243566462342456234124").expect_finite(), + BigRational::new(104243566462342456234124_i128.into(), 10000000000000000000000_i128.into()) + ); + assert_eq!(Rational::parse("inf"), Rational::Inf); + assert_eq!(Rational::parse("+inf"), Rational::Inf); + assert_eq!(Rational::parse("-inf"), Rational::NegInf); + assert_eq!(Rational::parse("NaN"), Rational::Nan); +} + +#[test] +fn test_decode() { + assert_eq!(decode(0f32), FloatRes::Zero); + assert_eq!(decode(f32::INFINITY), FloatRes::Inf); + assert_eq!(decode(f32::NEG_INFINITY), FloatRes::NegInf); + assert_eq!(decode(1.0f32).normalize(), FloatRes::Real { sig: 1, exp: 0 }); + assert_eq!(decode(-1.0f32).normalize(), FloatRes::Real { sig: -1, exp: 0 }); + assert_eq!(decode(100.0f32).normalize(), FloatRes::Real { sig: 100, exp: 0 }); + assert_eq!(decode(100.5f32).normalize(), FloatRes::Real { sig: 201, exp: -1 }); + assert_eq!(decode(-4.004f32).normalize(), FloatRes::Real { sig: -8396997, exp: -21 }); + assert_eq!(decode(0.0004f32).normalize(), FloatRes::Real { sig: 13743895, exp: -35 }); + assert_eq!(decode(f32::from_bits(0x1)).normalize(), FloatRes::Real { sig: 1, exp: -149 }); +} + +#[test] +fn test_validate() { + validate::("0").unwrap(); + validate::("-0").unwrap(); + validate::("1").unwrap(); + validate::("-1").unwrap(); + validate::("1.1").unwrap(); + validate::("-1.1").unwrap(); + validate::("1e10").unwrap(); + validate::("1e1000").unwrap(); + validate::("-1e1000").unwrap(); + validate::("1e-1000").unwrap(); + validate::("-1e-1000").unwrap(); +} + +#[test] +fn test_validate_real() { + // Most of the arbitrary values come from checking against . + let r = &BigRational::from_float(10.0).unwrap(); + FloatRes::::validate_real(r.clone(), 10, 0).unwrap(); + FloatRes::::validate_real(r.clone(), 10, -1).unwrap_err(); + FloatRes::::validate_real(r.clone(), 10, 1).unwrap_err(); + + let r = &BigRational::from_float(0.25).unwrap(); + FloatRes::::validate_real(r.clone(), 1, -2).unwrap(); + FloatRes::::validate_real(r.clone(), 2, -2).unwrap_err(); + + let r = &BigRational::from_float(1234.5678).unwrap(); + FloatRes::::validate_real(r.clone(), 0b100110100101001000101011, -13).unwrap(); + FloatRes::::validate_real(r.clone(), 0b100110100101001000101010, -13).unwrap_err(); + FloatRes::::validate_real(r.clone(), 0b100110100101001000101100, -13).unwrap_err(); + + let r = &BigRational::from_float(-1234.5678).unwrap(); + FloatRes::::validate_real(r.clone(), -0b100110100101001000101011, -13).unwrap(); + FloatRes::::validate_real(r.clone(), -0b100110100101001000101010, -13).unwrap_err(); + FloatRes::::validate_real(r.clone(), -0b100110100101001000101100, -13).unwrap_err(); +} + +#[test] +#[allow(unused)] +fn test_validate_real_rounding() { + // Check that we catch when values don't round to even. + + // For f32, the cutoff between 1.0 and the next value up (1.0000001) is + // 1.000000059604644775390625. Anything below it should round down, anything above it should + // round up, and the value itself should round _down_ because `1.0` has an even significand but + // 1.0000001 is odd. + let v1_low_down = Rational::parse("1.00000005960464477539062499999").expect_finite(); + let v1_mid_down = Rational::parse("1.000000059604644775390625").expect_finite(); + let v1_high_up = Rational::parse("1.00000005960464477539062500001").expect_finite(); + + let exp = -(f32::MAN_BITS as i32); + let v1_down_sig = 1 << f32::MAN_BITS; + let v1_up_sig = (1 << f32::MAN_BITS) | 0b1; + + FloatRes::::validate_real(v1_low_down.clone(), v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(v1_mid_down.clone(), v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(v1_high_up.clone(), v1_up_sig, exp).unwrap(); + FloatRes::::validate_real(-v1_low_down.clone(), -v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(-v1_mid_down.clone(), -v1_down_sig, exp).unwrap(); + FloatRes::::validate_real(-v1_high_up.clone(), -v1_up_sig, exp).unwrap(); + + // 1.000000178813934326171875 is between 1.0000001 and the next value up, 1.0000002. The middle + // value here should round _up_ since 1.0000002 has an even mantissa. + let v2_low_down = Rational::parse("1.00000017881393432617187499999").expect_finite(); + let v2_mid_up = Rational::parse("1.000000178813934326171875").expect_finite(); + let v2_high_up = Rational::parse("1.00000017881393432617187500001").expect_finite(); + + let v2_down_sig = v1_up_sig; + let v2_up_sig = (1 << f32::MAN_BITS) | 0b10; + + FloatRes::::validate_real(v2_low_down.clone(), v2_down_sig, exp).unwrap(); + FloatRes::::validate_real(v2_mid_up.clone(), v2_up_sig, exp).unwrap(); + FloatRes::::validate_real(v2_high_up.clone(), v2_up_sig, exp).unwrap(); + FloatRes::::validate_real(-v2_low_down.clone(), -v2_down_sig, exp).unwrap(); + FloatRes::::validate_real(-v2_mid_up.clone(), -v2_up_sig, exp).unwrap(); + FloatRes::::validate_real(-v2_high_up.clone(), -v2_up_sig, exp).unwrap(); + + // Rounding the wrong direction should error + for res in [ + FloatRes::::validate_real(v1_mid_down.clone(), v1_up_sig, exp), + FloatRes::::validate_real(v2_mid_up.clone(), v2_down_sig, exp), + FloatRes::::validate_real(-v1_mid_down.clone(), -v1_up_sig, exp), + FloatRes::::validate_real(-v2_mid_up.clone(), -v2_down_sig, exp), + ] { + let e = res.unwrap_err(); + let CheckFailure::InvalidReal { incorrect_midpoint_rounding: true, .. } = e else { + panic!("{e:?}"); + }; + } +} + +/// Just a quick check that the constants are what we expect. +#[test] +fn check_constants() { + assert_eq!(f32::constants().max.to_f32().unwrap(), f32::MAX); + assert_eq!(f32::constants().min_subnormal.to_f32().unwrap(), f32::from_bits(0x1)); + assert_eq!(f64::constants().max.to_f64().unwrap(), f64::MAX); + assert_eq!(f64::constants().min_subnormal.to_f64().unwrap(), f64::from_bits(0x1)); +} diff --git a/src/tools/tidy/src/deps.rs b/src/tools/tidy/src/deps.rs index f9bf04626f785..286cfbf26292f 100644 --- a/src/tools/tidy/src/deps.rs +++ b/src/tools/tidy/src/deps.rs @@ -65,7 +65,7 @@ pub(crate) const WORKSPACES: &[(&str, ExceptionList, Option<(&[&str], &[&str])>) //("library/stdarch", EXCEPTIONS_STDARCH, None), // FIXME uncomment once rust-lang/stdarch#1462 has been synced back to the rust repo ("src/bootstrap", EXCEPTIONS_BOOTSTRAP, None), ("src/ci/docker/host-x86_64/test-various/uefi_qemu_test", EXCEPTIONS_UEFI_QEMU_TEST, None), - //("src/etc/test-float-parse", &[], None), // FIXME uncomment once all deps are vendored + ("src/etc/test-float-parse", EXCEPTIONS, None), ("src/tools/cargo", EXCEPTIONS_CARGO, None), //("src/tools/miri/test-cargo-miri", &[], None), // FIXME uncomment once all deps are vendored //("src/tools/miri/test_dependencies", &[], None), // FIXME uncomment once all deps are vendored