From 5b0b4ff03c83f03defc3b2a4919aaf5d1ce893ea Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 19 Jul 2024 10:05:30 -0700 Subject: [PATCH 01/14] Update wasi-sdk in CI to latest release This commit updates the `wasi-sdk` download used by the `wasm32-wasi*` targets. The motivation for this commit is generally just "keep things up to date" and is not intended to cause any issues or differences from before, just a routine update. --- src/ci/docker/host-x86_64/dist-various-2/Dockerfile | 4 ++-- src/ci/docker/host-x86_64/test-various/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ci/docker/host-x86_64/dist-various-2/Dockerfile b/src/ci/docker/host-x86_64/dist-various-2/Dockerfile index 962484593b4be..2621e9a603185 100644 --- a/src/ci/docker/host-x86_64/dist-various-2/Dockerfile +++ b/src/ci/docker/host-x86_64/dist-various-2/Dockerfile @@ -85,9 +85,9 @@ RUN /tmp/build-solaris-toolchain.sh sparcv9 sparcv9 solaris-sparc sun COPY host-x86_64/dist-various-2/build-x86_64-fortanix-unknown-sgx-toolchain.sh /tmp/ RUN /tmp/build-x86_64-fortanix-unknown-sgx-toolchain.sh -RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz | \ +RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-23/wasi-sdk-23.0-x86_64-linux.tar.gz | \ tar -xz -ENV WASI_SDK_PATH=/tmp/wasi-sdk-22.0 +ENV WASI_SDK_PATH=/tmp/wasi-sdk-23.0-x86_64-linux COPY scripts/freebsd-toolchain.sh /tmp/ RUN /tmp/freebsd-toolchain.sh i686 diff --git a/src/ci/docker/host-x86_64/test-various/Dockerfile b/src/ci/docker/host-x86_64/test-various/Dockerfile index f874b2ed475fd..c2f5a87b1234f 100644 --- a/src/ci/docker/host-x86_64/test-various/Dockerfile +++ b/src/ci/docker/host-x86_64/test-various/Dockerfile @@ -40,9 +40,9 @@ WORKDIR / COPY scripts/sccache.sh /scripts/ RUN sh /scripts/sccache.sh -RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz | \ +RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-23/wasi-sdk-23.0-x86_64-linux.tar.gz | \ tar -xz -ENV WASI_SDK_PATH=/wasi-sdk-22.0 +ENV WASI_SDK_PATH=/wasi-sdk-23.0-x86_64-linux ENV RUST_CONFIGURE_ARGS \ --musl-root-x86_64=/usr/local/x86_64-linux-musl \ From 006c884480d2d0ecf727660cbd6535b0e6479dfd Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 19 Jul 2024 10:27:14 -0700 Subject: [PATCH 02/14] Fix two new failing tests The updated wasi-sdk has debuginfo by default so be sure to strip the debuginfo by default when testing the size of new executables. --- tests/run-make/wasm-panic-small/rmake.rs | 9 ++++++++- tests/run-make/wasm-stringify-ints-small/rmake.rs | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/run-make/wasm-panic-small/rmake.rs b/tests/run-make/wasm-panic-small/rmake.rs index 8d0944ed98d5b..e69fbac96356a 100644 --- a/tests/run-make/wasm-panic-small/rmake.rs +++ b/tests/run-make/wasm-panic-small/rmake.rs @@ -13,7 +13,14 @@ fn main() { fn test(cfg: &str) { eprintln!("running cfg {cfg:?}"); - rustc().input("foo.rs").target("wasm32-wasip1").arg("-Clto").opt().cfg(cfg).run(); + rustc() + .input("foo.rs") + .target("wasm32-wasip1") + .arg("-Clto") + .arg("-Cstrip=debuginfo") + .opt() + .cfg(cfg) + .run(); let bytes = rfs::read("foo.wasm"); println!("{}", bytes.len()); diff --git a/tests/run-make/wasm-stringify-ints-small/rmake.rs b/tests/run-make/wasm-stringify-ints-small/rmake.rs index 93eb38b098766..c0448c59c037b 100644 --- a/tests/run-make/wasm-stringify-ints-small/rmake.rs +++ b/tests/run-make/wasm-stringify-ints-small/rmake.rs @@ -4,7 +4,13 @@ use run_make_support::{rfs, rustc}; fn main() { - rustc().input("foo.rs").target("wasm32-wasip1").arg("-Clto").opt().run(); + rustc() + .input("foo.rs") + .target("wasm32-wasip1") + .arg("-Clto") + .arg("-Cstrip=debuginfo") + .opt() + .run(); let bytes = rfs::read("foo.wasm"); println!("{}", bytes.len()); From 59429e67f91e9db38ea2dca8f2a99ec42aea68d7 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 20 Jul 2024 11:04:47 -0500 Subject: [PATCH 03/14] Rewrite `test-float-parse` in Rust The existing implementation uses Python to launch a set of Rust-written binaries. Unfortunately, this is currently broken; it seems that some updates meant it no longer compiles. There is also a problem that support for more float types (`f16`, `f128`) would be difficult to add since this is very specialized to `f32` and `f64`. Because of these sortcomings, migrate to a version written in Rust. This version should be significantly faster; test generators can execute in parallel, and test cases are chunked and parallelized. This should also resolve the preexisting "... the worker processes are leaked and stick around forever" comment. This change also introduces genericism over float types and properties, meaning it will be much easier to extend support to newly added types. `num::BigRational` is used in place of Python's fractions for infinite-precision calculations. --- src/etc/test-float-parse/Cargo.toml | 9 +- src/etc/test-float-parse/README.md | 55 ++ src/etc/test-float-parse/runtests.py | 394 ------------- src/etc/test-float-parse/src/bin/few-ones.rs | 15 - .../test-float-parse/src/bin/huge-pow10.rs | 9 - .../src/bin/long-fractions.rs | 15 - .../test-float-parse/src/bin/many-digits.rs | 25 - src/etc/test-float-parse/src/bin/rand-f64.rs | 18 - .../src/bin/short-decimals.rs | 17 - src/etc/test-float-parse/src/bin/subnorm.rs | 11 - .../test-float-parse/src/bin/tiny-pow10.rs | 9 - src/etc/test-float-parse/src/bin/u32-small.rs | 7 - src/etc/test-float-parse/src/bin/u64-pow2.rs | 15 - .../test-float-parse/src/gen/exhaustive.rs | 43 ++ src/etc/test-float-parse/src/gen/exponents.rs | 95 +++ src/etc/test-float-parse/src/gen/fuzz.rs | 88 +++ src/etc/test-float-parse/src/gen/integers.rs | 104 ++++ .../src/gen/long_fractions.rs | 58 ++ .../test-float-parse/src/gen/many_digits.rs | 84 +++ src/etc/test-float-parse/src/gen/sparse.rs | 100 ++++ .../test-float-parse/src/gen/spot_checks.rs | 101 ++++ src/etc/test-float-parse/src/gen/subnorm.rs | 103 ++++ src/etc/test-float-parse/src/lib.rs | 540 +++++++++++++++++- src/etc/test-float-parse/src/main.rs | 129 +++++ src/etc/test-float-parse/src/traits.rs | 202 +++++++ src/etc/test-float-parse/src/ui.rs | 132 +++++ src/etc/test-float-parse/src/validate.rs | 364 ++++++++++++ .../test-float-parse/src/validate/tests.rs | 149 +++++ 28 files changed, 2337 insertions(+), 554 deletions(-) create mode 100644 src/etc/test-float-parse/README.md delete mode 100755 src/etc/test-float-parse/runtests.py delete mode 100644 src/etc/test-float-parse/src/bin/few-ones.rs delete mode 100644 src/etc/test-float-parse/src/bin/huge-pow10.rs delete mode 100644 src/etc/test-float-parse/src/bin/long-fractions.rs delete mode 100644 src/etc/test-float-parse/src/bin/many-digits.rs delete mode 100644 src/etc/test-float-parse/src/bin/rand-f64.rs delete mode 100644 src/etc/test-float-parse/src/bin/short-decimals.rs delete mode 100644 src/etc/test-float-parse/src/bin/subnorm.rs delete mode 100644 src/etc/test-float-parse/src/bin/tiny-pow10.rs delete mode 100644 src/etc/test-float-parse/src/bin/u32-small.rs delete mode 100644 src/etc/test-float-parse/src/bin/u64-pow2.rs create mode 100644 src/etc/test-float-parse/src/gen/exhaustive.rs create mode 100644 src/etc/test-float-parse/src/gen/exponents.rs create mode 100644 src/etc/test-float-parse/src/gen/fuzz.rs create mode 100644 src/etc/test-float-parse/src/gen/integers.rs create mode 100644 src/etc/test-float-parse/src/gen/long_fractions.rs create mode 100644 src/etc/test-float-parse/src/gen/many_digits.rs create mode 100644 src/etc/test-float-parse/src/gen/sparse.rs create mode 100644 src/etc/test-float-parse/src/gen/spot_checks.rs create mode 100644 src/etc/test-float-parse/src/gen/subnorm.rs create mode 100644 src/etc/test-float-parse/src/main.rs create mode 100644 src/etc/test-float-parse/src/traits.rs create mode 100644 src/etc/test-float-parse/src/ui.rs create mode 100644 src/etc/test-float-parse/src/validate.rs create mode 100644 src/etc/test-float-parse/src/validate/tests.rs 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)); +} From 51827ce4e462d65e806be1f98e9630e446c33756 Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 20 Jul 2024 11:13:27 -0500 Subject: [PATCH 04/14] Move `test-float-parse` to the global workspace Since `test-float-parse` is now implemented in Rust, we can move it into the global workspace and check dependency licenses. --- Cargo.lock | 75 ++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 +++++++ src/tools/tidy/src/deps.rs | 2 +- 3 files changed, 89 insertions(+), 1 deletion(-) 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/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 From 6062059ab0f9a805ae8327a80e55bc6f47330a7b Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 20 Jul 2024 11:15:53 -0500 Subject: [PATCH 05/14] Expose `test-float-parse` via bootstrap With updates to `test-float-parse`, it is now possible to run as another Rust tool. Enable check, clippy, and test. Test runs the unit tests, as well as shorter parsing tests (takes approximately 1 minute). --- src/bootstrap/src/core/build_steps/check.rs | 1 + src/bootstrap/src/core/build_steps/clippy.rs | 1 + src/bootstrap/src/core/build_steps/test.rs | 77 ++++++++++++++++++++ src/bootstrap/src/core/builder.rs | 3 + 4 files changed, 82 insertions(+) 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 From ee034f4912f21291c523b4525d4ecafce8a92cfa Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 20 Jul 2024 22:28:13 +0200 Subject: [PATCH 06/14] Fix stab display in doc blocks --- src/librustdoc/html/static/css/rustdoc.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 28ed94432c86e..caa9bd563b11e 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -831,6 +831,10 @@ pre, .rustdoc.src .example-wrap { background: var(--table-alt-row-background-color); } +.docblock .stab, .docblock-short .stab { + display: inline-block; +} + /* "where ..." clauses with block display are also smaller */ div.where { white-space: pre-wrap; From 3862095bd214a3ebdfb3fbaa4a13eac2ad8de331 Mon Sep 17 00:00:00 2001 From: Michael Goulet Date: Sat, 20 Jul 2024 17:58:05 -0400 Subject: [PATCH 07/14] Just totally fully deny late-bound consts --- compiler/rustc_ast_passes/messages.ftl | 3 +++ compiler/rustc_ast_passes/src/errors.rs | 7 +++++++ compiler/rustc_ast_passes/src/feature_gate.rs | 16 ++++++++++++++++ .../src/collect/resolve_bound_vars.rs | 6 +----- compiler/rustc_resolve/src/late.rs | 6 +++++- tests/crashes/127009.rs | 11 ----------- tests/ui/closures/binder/const-bound.rs | 3 ++- tests/ui/closures/binder/const-bound.stderr | 8 +++++++- ...no-entry-found-for-key-ice-gce-nlb-113133.rs | 3 ++- ...ntry-found-for-key-ice-gce-nlb-113133.stderr | 8 +++++++- .../bad-suggestion-on-missing-assoc.rs | 3 ++- .../bad-suggestion-on-missing-assoc.stderr | 8 +++++++- .../binder-defaults-112547.rs | 5 +++-- .../binder-defaults-112547.stderr | 12 ++++++++++-- .../binder-defaults-119489.rs | 5 +++-- .../binder-defaults-119489.stderr | 8 +++++++- .../non_lifetime_binders/late-const-param-wf.rs | 11 +++++++++++ .../late-const-param-wf.stderr | 17 +++++++++++++++++ 18 files changed, 110 insertions(+), 30 deletions(-) delete mode 100644 tests/crashes/127009.rs create mode 100644 tests/ui/traits/non_lifetime_binders/late-const-param-wf.rs create mode 100644 tests/ui/traits/non_lifetime_binders/late-const-param-wf.stderr diff --git a/compiler/rustc_ast_passes/messages.ftl b/compiler/rustc_ast_passes/messages.ftl index 8f7dd77420709..ca0b7f2ac3a67 100644 --- a/compiler/rustc_ast_passes/messages.ftl +++ b/compiler/rustc_ast_passes/messages.ftl @@ -120,6 +120,9 @@ ast_passes_fn_without_body = ast_passes_forbidden_bound = bounds cannot be used in this context +ast_passes_forbidden_const_param = + late-bound const parameters cannot be used currently + ast_passes_forbidden_default = `default` is only allowed on items in trait impls .label = `default` because of this diff --git a/compiler/rustc_ast_passes/src/errors.rs b/compiler/rustc_ast_passes/src/errors.rs index 783bca6b6958d..215ccd2ab4d9e 100644 --- a/compiler/rustc_ast_passes/src/errors.rs +++ b/compiler/rustc_ast_passes/src/errors.rs @@ -69,6 +69,13 @@ pub struct ForbiddenBound { pub spans: Vec, } +#[derive(Diagnostic)] +#[diag(ast_passes_forbidden_const_param)] +pub struct ForbiddenConstParam { + #[primary_span] + pub const_param_spans: Vec, +} + #[derive(Diagnostic)] #[diag(ast_passes_fn_param_too_many)] pub struct FnParamTooMany { diff --git a/compiler/rustc_ast_passes/src/feature_gate.rs b/compiler/rustc_ast_passes/src/feature_gate.rs index 2178b65727d09..e91dfb2776662 100644 --- a/compiler/rustc_ast_passes/src/feature_gate.rs +++ b/compiler/rustc_ast_passes/src/feature_gate.rs @@ -162,6 +162,22 @@ impl<'a> PostExpansionVisitor<'a> { crate::fluent_generated::ast_passes_forbidden_non_lifetime_param ); + // FIXME(non_lifetime_binders): Const bound params are pretty broken. + // Let's keep users from using this feature accidentally. + if self.features.non_lifetime_binders { + let const_param_spans: Vec<_> = params + .iter() + .filter_map(|param| match param.kind { + ast::GenericParamKind::Const { .. } => Some(param.ident.span), + _ => None, + }) + .collect(); + + if !const_param_spans.is_empty() { + self.sess.dcx().emit_err(errors::ForbiddenConstParam { const_param_spans }); + } + } + for param in params { if !param.bounds.is_empty() { let spans: Vec<_> = param.bounds.iter().map(|b| b.span()).collect(); diff --git a/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs b/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs index 7930f54038daf..349dc9ad00ed3 100644 --- a/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs +++ b/compiler/rustc_hir_analysis/src/collect/resolve_bound_vars.rs @@ -2094,11 +2094,7 @@ pub fn deny_non_region_late_bound( format!("late-bound {what} parameter not allowed on {where_}"), ); - let guar = if tcx.features().non_lifetime_binders && first { - diag.emit() - } else { - diag.delay_as_bug() - }; + let guar = diag.emit_unless(!tcx.features().non_lifetime_binders || !first); first = false; *arg = ResolvedArg::Error(guar); diff --git a/compiler/rustc_resolve/src/late.rs b/compiler/rustc_resolve/src/late.rs index dc7200465d97e..51414d785963d 100644 --- a/compiler/rustc_resolve/src/late.rs +++ b/compiler/rustc_resolve/src/late.rs @@ -2763,7 +2763,11 @@ impl<'a: 'ast, 'b, 'ast, 'tcx> LateResolutionVisitor<'a, 'b, 'ast, 'tcx> { let res = match kind { RibKind::Item(..) | RibKind::AssocItem => Res::Def(def_kind, def_id.to_def_id()), RibKind::Normal => { - if self.r.tcx.features().non_lifetime_binders { + // FIXME(non_lifetime_binders): Stop special-casing + // const params to error out here. + if self.r.tcx.features().non_lifetime_binders + && matches!(param.kind, GenericParamKind::Type { .. }) + { Res::Def(def_kind, def_id.to_def_id()) } else { Res::Err diff --git a/tests/crashes/127009.rs b/tests/crashes/127009.rs deleted file mode 100644 index 74ca14393e4e9..0000000000000 --- a/tests/crashes/127009.rs +++ /dev/null @@ -1,11 +0,0 @@ -//@ known-bug: #127009 - -#![feature(non_lifetime_binders)] - -fn b() -where - for [(); C]: Copy, -{ -} - -fn main() {} diff --git a/tests/ui/closures/binder/const-bound.rs b/tests/ui/closures/binder/const-bound.rs index b1c79db137510..10d869fcc8512 100644 --- a/tests/ui/closures/binder/const-bound.rs +++ b/tests/ui/closures/binder/const-bound.rs @@ -3,5 +3,6 @@ fn main() { for || -> () {}; - //~^ ERROR late-bound const parameter not allowed on closures + //~^ ERROR late-bound const parameters cannot be used currently + //~| ERROR late-bound const parameter not allowed on closures } diff --git a/tests/ui/closures/binder/const-bound.stderr b/tests/ui/closures/binder/const-bound.stderr index 9c4fd95ed76ed..b805879f7fab5 100644 --- a/tests/ui/closures/binder/const-bound.stderr +++ b/tests/ui/closures/binder/const-bound.stderr @@ -1,3 +1,9 @@ +error: late-bound const parameters cannot be used currently + --> $DIR/const-bound.rs:5:15 + | +LL | for || -> () {}; + | ^ + warning: the feature `non_lifetime_binders` is incomplete and may not be safe to use and/or cause compiler crashes --> $DIR/const-bound.rs:1:37 | @@ -13,5 +19,5 @@ error: late-bound const parameter not allowed on closures LL | for || -> () {}; | ^^^^^^^^^^^^ -error: aborting due to 1 previous error; 1 warning emitted +error: aborting due to 2 previous errors; 1 warning emitted diff --git a/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.rs b/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.rs index 5673f1dd0738f..ffa9d960e048b 100644 --- a/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.rs +++ b/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.rs @@ -7,7 +7,8 @@ pub fn foo() where for ():, - //~^ ERROR defaults for generic parameters are not allowed in `for<...>` binders + //~^ ERROR late-bound const parameters cannot be used currently + //~| ERROR defaults for generic parameters are not allowed in `for<...>` binders {} fn main() {} diff --git a/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.stderr b/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.stderr index 5924a673da944..814022f26b90f 100644 --- a/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.stderr +++ b/tests/ui/const-generics/generic_const_exprs/no-entry-found-for-key-ice-gce-nlb-113133.stderr @@ -1,8 +1,14 @@ +error: late-bound const parameters cannot be used currently + --> $DIR/no-entry-found-for-key-ice-gce-nlb-113133.rs:9:15 + | +LL | for ():, + | ^ + error: defaults for generic parameters are not allowed in `for<...>` binders --> $DIR/no-entry-found-for-key-ice-gce-nlb-113133.rs:9:9 | LL | for ():, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: aborting due to 1 previous error +error: aborting due to 2 previous errors diff --git a/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.rs b/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.rs index fc64381b961d4..b61a21eab419c 100644 --- a/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.rs +++ b/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.rs @@ -18,7 +18,8 @@ trait TraitC {} fn foo() where for T: TraitA>, - //~^ ERROR defaults for generic parameters are not allowed in `for<...>` binders + //~^ ERROR late-bound const parameters cannot be used currently + //~| ERROR defaults for generic parameters are not allowed in `for<...>` binders //~| ERROR `impl Trait` is not allowed in bounds { } diff --git a/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.stderr b/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.stderr index a4a79413a9be6..e891df3f0c092 100644 --- a/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.stderr +++ b/tests/ui/traits/non_lifetime_binders/bad-suggestion-on-missing-assoc.stderr @@ -1,3 +1,9 @@ +error: late-bound const parameters cannot be used currently + --> $DIR/bad-suggestion-on-missing-assoc.rs:20:15 + | +LL | for T: TraitA>, + | ^ + warning: the feature `generic_const_exprs` is incomplete and may not be safe to use and/or cause compiler crashes --> $DIR/bad-suggestion-on-missing-assoc.rs:1:12 | @@ -29,6 +35,6 @@ LL | for T: TraitA` binders (||1usize)() }> V: IntoIterator -//~^^^ ERROR defaults for generic parameters are not allowed in `for<...>` binders -//~^^ ERROR cannot find type `V` in this scope +//~^ ERROR cannot find type `V` in this scope { } diff --git a/tests/ui/traits/non_lifetime_binders/binder-defaults-112547.stderr b/tests/ui/traits/non_lifetime_binders/binder-defaults-112547.stderr index edc55a3c8e68f..d9e77dec794a7 100644 --- a/tests/ui/traits/non_lifetime_binders/binder-defaults-112547.stderr +++ b/tests/ui/traits/non_lifetime_binders/binder-defaults-112547.stderr @@ -1,5 +1,5 @@ error[E0412]: cannot find type `V` in this scope - --> $DIR/binder-defaults-112547.rs:8:4 + --> $DIR/binder-defaults-112547.rs:10:4 | LL | }> V: IntoIterator | ^ not found in this scope @@ -9,6 +9,12 @@ help: you might be missing a type parameter LL | pub fn bar() | +++ +error: late-bound const parameters cannot be used currently + --> $DIR/binder-defaults-112547.rs:6:15 + | +LL | for $DIR/binder-defaults-112547.rs:1:12 | @@ -23,10 +29,12 @@ error: defaults for generic parameters are not allowed in `for<...>` binders | LL | for V: IntoIterator | |_^ -error: aborting due to 2 previous errors; 1 warning emitted +error: aborting due to 3 previous errors; 1 warning emitted For more information about this error, try `rustc --explain E0412`. diff --git a/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.rs b/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.rs index f33da416ad8ae..bdfe41ca11b04 100644 --- a/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.rs +++ b/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.rs @@ -5,8 +5,9 @@ fn fun() where for ():, -//~^ ERROR defaults for generic parameters are not allowed in `for<...>` binders -//~| ERROR defaults for generic parameters are not allowed in `for<...>` binders + //~^ ERROR late-bound const parameters cannot be used currently + //~| ERROR defaults for generic parameters are not allowed in `for<...>` binders + //~| ERROR defaults for generic parameters are not allowed in `for<...>` binders {} fn main() {} diff --git a/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.stderr b/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.stderr index 7fe82f1f097c4..947dd3a73bf12 100644 --- a/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.stderr +++ b/tests/ui/traits/non_lifetime_binders/binder-defaults-119489.stderr @@ -1,3 +1,9 @@ +error: late-bound const parameters cannot be used currently + --> $DIR/binder-defaults-119489.rs:7:23 + | +LL | for ():, + | ^ + warning: the feature `non_lifetime_binders` is incomplete and may not be safe to use and/or cause compiler crashes --> $DIR/binder-defaults-119489.rs:1:12 | @@ -27,5 +33,5 @@ error: defaults for generic parameters are not allowed in `for<...>` binders LL | for ():, | ^^^^^^^^^^^^^^^^^^ -error: aborting due to 2 previous errors; 2 warnings emitted +error: aborting due to 3 previous errors; 2 warnings emitted diff --git a/tests/ui/traits/non_lifetime_binders/late-const-param-wf.rs b/tests/ui/traits/non_lifetime_binders/late-const-param-wf.rs new file mode 100644 index 0000000000000..2d44388f875f9 --- /dev/null +++ b/tests/ui/traits/non_lifetime_binders/late-const-param-wf.rs @@ -0,0 +1,11 @@ +#![feature(non_lifetime_binders)] +//~^ WARN the feature `non_lifetime_binders` is incomplete + +fn b() +where + for [(); C]: Copy, + //~^ ERROR late-bound const parameters cannot be used currently +{ +} + +fn main() {} diff --git a/tests/ui/traits/non_lifetime_binders/late-const-param-wf.stderr b/tests/ui/traits/non_lifetime_binders/late-const-param-wf.stderr new file mode 100644 index 0000000000000..136d533a03c40 --- /dev/null +++ b/tests/ui/traits/non_lifetime_binders/late-const-param-wf.stderr @@ -0,0 +1,17 @@ +error: late-bound const parameters cannot be used currently + --> $DIR/late-const-param-wf.rs:6:15 + | +LL | for [(); C]: Copy, + | ^ + +warning: the feature `non_lifetime_binders` is incomplete and may not be safe to use and/or cause compiler crashes + --> $DIR/late-const-param-wf.rs:1:12 + | +LL | #![feature(non_lifetime_binders)] + | ^^^^^^^^^^^^^^^^^^^^ + | + = note: see issue #108185 for more information + = note: `#[warn(incomplete_features)]` on by default + +error: aborting due to 1 previous error; 1 warning emitted + From 95335444f7171c4582fd56f20ade6f84da84c984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Kijewski?= Date: Sun, 21 Jul 2024 02:28:04 +0200 Subject: [PATCH 08/14] rustdoc: short descr. cause word-breaks in tables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `.item-table` class is used to display name+description lists, e.g. the exported functions, as a table. If the names are long and the descriptions are short, then the width of the table does not expand to the whole size, but only uses a fraction. This causes a some names to break inside a word. This change makes the table always use 100% of its parent width. The `.width-limiter` wrapper already ensures that the used width still does not become excessive. Signed-off-by: René Kijewski --- src/librustdoc/html/static/css/rustdoc.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 28ed94432c86e..867275cf4fbb6 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -953,6 +953,7 @@ table, display: table; padding: 0; margin: 0; + width: 100%; } .item-table > li { display: table-row; From e4d701b1d3f1cc04574c9b92abfa33e2a4c0e9c3 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sat, 20 Jul 2024 22:28:27 +0200 Subject: [PATCH 09/14] Add regression test for stab display in doc blocks --- tests/rustdoc-gui/src/test_docs/lib.rs | 8 ++++---- tests/rustdoc-gui/stab-in-doc.goml | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 tests/rustdoc-gui/stab-in-doc.goml diff --git a/tests/rustdoc-gui/src/test_docs/lib.rs b/tests/rustdoc-gui/src/test_docs/lib.rs index 7e34178e56f03..360ad3edefa1f 100644 --- a/tests/rustdoc-gui/src/test_docs/lib.rs +++ b/tests/rustdoc-gui/src/test_docs/lib.rs @@ -20,10 +20,10 @@ Also, stop using `bar` as it's deprecated Also, stop using `bar` as it's deprecated. Also, stop using `bar` as it's deprecated. -Finally, you can use `quz` only on Unix or x86-64 -. -Finally, you can use `quz` only on Unix or x86-64 -. +Finally, you can use `quz` only on Unix or x86-64 +. +Finally, you can use `quz` only on Unix or x86-64 +. */ use std::convert::AsRef; diff --git a/tests/rustdoc-gui/stab-in-doc.goml b/tests/rustdoc-gui/stab-in-doc.goml new file mode 100644 index 0000000000000..6a03a51fe9f27 --- /dev/null +++ b/tests/rustdoc-gui/stab-in-doc.goml @@ -0,0 +1,9 @@ +// This test ensure that `stab` elements if used in doc blocks are not breaking the text layout. +go-to: "file://" + |DOC_PATH| + "/test_docs/index.html" +// We make the window wide enough for the two stabs who are looking into to be on the same line. +set-window-size: (1100, 600) +compare-elements-position: ( + ".top-doc .docblock span[data-span='1']", + ".top-doc .docblock span[data-span='2']", + ["y"], +) From 84db684f49d9e1e4250fe24a90b5a5d2d4737df0 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Sun, 21 Jul 2024 12:47:34 +0200 Subject: [PATCH 10/14] Update `source-code-page-code-scroll.goml` GUI test --- tests/rustdoc-gui/source-code-page-code-scroll.goml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rustdoc-gui/source-code-page-code-scroll.goml b/tests/rustdoc-gui/source-code-page-code-scroll.goml index 35f338ea32834..31ab281d6ce09 100644 --- a/tests/rustdoc-gui/source-code-page-code-scroll.goml +++ b/tests/rustdoc-gui/source-code-page-code-scroll.goml @@ -2,7 +2,7 @@ go-to: "file://" + |DOC_PATH| + "/src/test_docs/lib.rs.html" set-window-size: (800, 1000) // "scrollWidth" should be superior than "clientWidth". -assert-property: ("body", {"scrollWidth": 1047, "clientWidth": 800}) +assert-property: ("body", {"scrollWidth": 1114, "clientWidth": 800}) // Both properties should be equal (ie, no scroll on the code block). -assert-property: (".example-wrap .rust", {"scrollWidth": 933, "clientWidth": 933}) +assert-property: (".example-wrap .rust", {"scrollWidth": 1000, "clientWidth": 1000}) From ae42efc522a5ed43631b95f1cce80520c7929e15 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:42:54 +0200 Subject: [PATCH 11/14] Deal with invalid UTF-8 from `gai_strerror` When the system is using a non-UTF-8 locale, the value will indeed not be UTF-8. That sucks for everyone involved, but is no reason for panic. We can "handle" this gracefully by just using from lossy, replacing the invalid UTF-8 with the ? and keeping the accidentally valid UTF-8. Good luck when debugging, but at least it's not a crash. We already do this for `strerror_r`. --- library/std/src/sys/pal/unix/net.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/std/src/sys/pal/unix/net.rs b/library/std/src/sys/pal/unix/net.rs index b8dc1538a6378..bedb06043a7b4 100644 --- a/library/std/src/sys/pal/unix/net.rs +++ b/library/std/src/sys/pal/unix/net.rs @@ -4,7 +4,6 @@ use crate::io::{self, BorrowedBuf, BorrowedCursor, IoSlice, IoSliceMut}; use crate::mem; use crate::net::{Shutdown, SocketAddr}; use crate::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, RawFd}; -use crate::str; use crate::sys::fd::FileDesc; use crate::sys::pal::unix::IsMinusOne; use crate::sys_common::net::{getsockopt, setsockopt, sockaddr_to_addr}; @@ -47,7 +46,9 @@ pub fn cvt_gai(err: c_int) -> io::Result<()> { #[cfg(not(target_os = "espidf"))] let detail = unsafe { - str::from_utf8(CStr::from_ptr(libc::gai_strerror(err)).to_bytes()).unwrap().to_owned() + // We can't always expect a UTF-8 environment. When we don't get that luxury, + // it's better to give a low-quality error message than none at all. + CStr::from_ptr(libc::gai_strerror(err)).to_string_lossy() }; #[cfg(target_os = "espidf")] From 710add58e2063616770ec8893e41b3179ad23d31 Mon Sep 17 00:00:00 2001 From: Nadrieril Date: Sun, 21 Jul 2024 15:17:29 +0200 Subject: [PATCH 12/14] Tweak `collect_non_exhaustive_tys` --- .../src/thir/pattern/check_match.rs | 32 +++++++++---------- compiler/rustc_pattern_analysis/src/rustc.rs | 8 ++++- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/compiler/rustc_mir_build/src/thir/pattern/check_match.rs b/compiler/rustc_mir_build/src/thir/pattern/check_match.rs index 70065b5a2c329..38f425581c100 100644 --- a/compiler/rustc_mir_build/src/thir/pattern/check_match.rs +++ b/compiler/rustc_mir_build/src/thir/pattern/check_match.rs @@ -16,8 +16,8 @@ use rustc_middle::ty::print::with_no_trimmed_paths; use rustc_middle::ty::{self, AdtDef, Ty, TyCtxt}; use rustc_pattern_analysis::errors::Uncovered; use rustc_pattern_analysis::rustc::{ - Constructor, DeconstructedPat, MatchArm, RustcPatCtxt as PatCtxt, Usefulness, UsefulnessReport, - WitnessPat, + Constructor, DeconstructedPat, MatchArm, RevealedTy, RustcPatCtxt as PatCtxt, Usefulness, + UsefulnessReport, WitnessPat, }; use rustc_session::lint::builtin::{ BINDINGS_WITH_VARIANT_NAME, IRREFUTABLE_LET_PATTERNS, UNREACHABLE_PATTERNS, @@ -998,26 +998,26 @@ fn report_non_exhaustive_match<'p, 'tcx>( err.note(format!("the matched value is of type `{}`", scrut_ty)); if !is_empty_match { - let mut non_exhaustive_tys = FxIndexSet::default(); + let mut special_tys = FxIndexSet::default(); // Look at the first witness. - collect_non_exhaustive_tys(cx, &witnesses[0], &mut non_exhaustive_tys); + collect_special_tys(cx, &witnesses[0], &mut special_tys); - for ty in non_exhaustive_tys { + for ty in special_tys { if ty.is_ptr_sized_integral() { - if ty == cx.tcx.types.usize { + if ty.inner() == cx.tcx.types.usize { err.note(format!( "`{ty}` does not have a fixed maximum value, so half-open ranges are necessary to match \ exhaustively", )); - } else if ty == cx.tcx.types.isize { + } else if ty.inner() == cx.tcx.types.isize { err.note(format!( "`{ty}` does not have fixed minimum and maximum values, so half-open ranges are necessary to match \ exhaustively", )); } - } else if ty == cx.tcx.types.str_ { + } else if ty.inner() == cx.tcx.types.str_ { err.note("`&str` cannot be matched exhaustively, so a wildcard `_` is necessary"); - } else if cx.is_foreign_non_exhaustive_enum(cx.reveal_opaque_ty(ty)) { + } else if cx.is_foreign_non_exhaustive_enum(ty) { err.note(format!("`{ty}` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively")); } } @@ -1168,22 +1168,22 @@ fn joined_uncovered_patterns<'p, 'tcx>( } } -fn collect_non_exhaustive_tys<'tcx>( +/// Collect types that require specific explanations when they show up in witnesses. +fn collect_special_tys<'tcx>( cx: &PatCtxt<'_, 'tcx>, pat: &WitnessPat<'_, 'tcx>, - non_exhaustive_tys: &mut FxIndexSet>, + special_tys: &mut FxIndexSet>, ) { - if matches!(pat.ctor(), Constructor::NonExhaustive) { - non_exhaustive_tys.insert(pat.ty().inner()); + if matches!(pat.ctor(), Constructor::NonExhaustive | Constructor::Never) { + special_tys.insert(*pat.ty()); } if let Constructor::IntRange(range) = pat.ctor() { if cx.is_range_beyond_boundaries(range, *pat.ty()) { // The range denotes the values before `isize::MIN` or the values after `usize::MAX`/`isize::MAX`. - non_exhaustive_tys.insert(pat.ty().inner()); + special_tys.insert(*pat.ty()); } } - pat.iter_fields() - .for_each(|field_pat| collect_non_exhaustive_tys(cx, field_pat, non_exhaustive_tys)) + pat.iter_fields().for_each(|field_pat| collect_special_tys(cx, field_pat, special_tys)) } fn report_adt_defined_here<'tcx>( diff --git a/compiler/rustc_pattern_analysis/src/rustc.rs b/compiler/rustc_pattern_analysis/src/rustc.rs index d17ee8bff503e..6ef2d69273ee7 100644 --- a/compiler/rustc_pattern_analysis/src/rustc.rs +++ b/compiler/rustc_pattern_analysis/src/rustc.rs @@ -40,9 +40,15 @@ pub type WitnessPat<'p, 'tcx> = crate::pat::WitnessPat>; /// /// Use `.inner()` or deref to get to the `Ty<'tcx>`. #[repr(transparent)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct RevealedTy<'tcx>(Ty<'tcx>); +impl<'tcx> fmt::Display for RevealedTy<'tcx> { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(fmt) + } +} + impl<'tcx> fmt::Debug for RevealedTy<'tcx> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(fmt) From 8a49d83db730f3dfdd7f0c04df823549bf571b33 Mon Sep 17 00:00:00 2001 From: Nadrieril Date: Sun, 21 Jul 2024 15:18:10 +0200 Subject: [PATCH 13/14] Explain why we require `_` for empty patterns --- compiler/rustc_mir_build/src/thir/pattern/check_match.rs | 4 ++++ .../pattern/usefulness/empty-types.min_exh_pats.stderr | 9 +++++++++ .../slice_of_empty.min_exhaustive_patterns.stderr | 1 + 3 files changed, 14 insertions(+) diff --git a/compiler/rustc_mir_build/src/thir/pattern/check_match.rs b/compiler/rustc_mir_build/src/thir/pattern/check_match.rs index 38f425581c100..95799cec94b04 100644 --- a/compiler/rustc_mir_build/src/thir/pattern/check_match.rs +++ b/compiler/rustc_mir_build/src/thir/pattern/check_match.rs @@ -1019,6 +1019,10 @@ fn report_non_exhaustive_match<'p, 'tcx>( err.note("`&str` cannot be matched exhaustively, so a wildcard `_` is necessary"); } else if cx.is_foreign_non_exhaustive_enum(ty) { err.note(format!("`{ty}` is marked as non-exhaustive, so a wildcard `_` is necessary to match exhaustively")); + } else if cx.is_uninhabited(ty.inner()) && cx.tcx.features().min_exhaustive_patterns { + // The type is uninhabited yet there is a witness: we must be in the `MaybeInvalid` + // case. + err.note(format!("`{ty}` is uninhabited but is not being matched by value, so a wildcard `_` is required")); } } } diff --git a/tests/ui/pattern/usefulness/empty-types.min_exh_pats.stderr b/tests/ui/pattern/usefulness/empty-types.min_exh_pats.stderr index 6e50dfe6a263c..9b57c895eea32 100644 --- a/tests/ui/pattern/usefulness/empty-types.min_exh_pats.stderr +++ b/tests/ui/pattern/usefulness/empty-types.min_exh_pats.stderr @@ -204,6 +204,7 @@ note: `Option` defined here | = note: not covered = note: the matched value is of type `Option` + = note: `Void` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ None => {}, @@ -349,6 +350,7 @@ LL | match slice_never { | ^^^^^^^^^^^ pattern `&[_, ..]` not covered | = note: the matched value is of type `&[!]` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ [] => {}, @@ -484,6 +486,7 @@ note: `Option` defined here | = note: not covered = note: the matched value is of type `&Option` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ &None => {}, @@ -502,6 +505,7 @@ note: `Option` defined here | = note: not covered = note: the matched value is of type `Option` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ None => {}, @@ -520,6 +524,7 @@ note: `Result` defined here | = note: not covered = note: the matched value is of type `Result` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ Ok(_) => {}, @@ -538,6 +543,7 @@ note: `Result` defined here | = note: not covered = note: the matched value is of type `Result` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ Ok(_a) => {}, @@ -589,6 +595,7 @@ LL | match ref_never { | ^^^^^^^^^ pattern `&_` not covered | = note: the matched value is of type `&!` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required = note: references are always considered inhabited = note: match arms with guards don't count towards exhaustivity help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown @@ -609,6 +616,7 @@ note: `Result` defined here | = note: not covered = note: the matched value is of type `Result` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ Err(_) => {}, @@ -627,6 +635,7 @@ note: `Option>` defined here | = note: not covered = note: the matched value is of type `Option>` + = note: `Result` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ None => {}, diff --git a/tests/ui/pattern/usefulness/slice_of_empty.min_exhaustive_patterns.stderr b/tests/ui/pattern/usefulness/slice_of_empty.min_exhaustive_patterns.stderr index a1239466c9c2c..f24ce154d149d 100644 --- a/tests/ui/pattern/usefulness/slice_of_empty.min_exhaustive_patterns.stderr +++ b/tests/ui/pattern/usefulness/slice_of_empty.min_exhaustive_patterns.stderr @@ -5,6 +5,7 @@ LL | match nevers { | ^^^^^^ pattern `&[_, ..]` not covered | = note: the matched value is of type `&[!]` + = note: `!` is uninhabited but is not being matched by value, so a wildcard `_` is required help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown | LL ~ &[] => (), From d12387835f32ee333bc41b85a43234f39f7e32ef Mon Sep 17 00:00:00 2001 From: Trevor Gross Date: Sat, 20 Jul 2024 11:17:37 -0500 Subject: [PATCH 14/14] Run `test-float-parse` as part of CI With the previous improvements, it is now possible to run float parsing tests as part of CI. Enable it here. This only runs a subset of tests, which takes about one minute. --- src/bootstrap/mk/Makefile.in | 1 + 1 file changed, 1 insertion(+) 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 \