Auto merge of #128056 - jieyouxu:rollup-zb1y27e, r=jieyouxu

Rollup of 8 pull requests

Successful merges:

 - #127177 (Distribute rustc_codegen_cranelift for arm64 macOS)
 - #127415 (Add missing try_new_uninit_slice_in and try_new_zeroed_slice_in)
 - #127510 (Rewrite `test-float-parse` in Rust)
 - #127977 (Update wasi-sdk in CI to latest release)
 - #127985 (Migrate `test-benches`, `c-unwind-abi-catch-panic` and `compiler-lookup-paths-2` `run-make` tests to rmake)
 - #127996 (Clean up warnings + `unsafe_op_in_unsafe_fn` when building std for armv6k-nintendo-3ds)
 - #128035 (Add test for #125837)
 - #128054 (mw triagebot vacation)

r? `@ghost`
`@rustbot` modify labels: rollup
This commit is contained in:
bors 2024-07-22 08:49:00 +00:00
commit aee3dc4c6c
58 changed files with 2734 additions and 602 deletions

View File

@ -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"

View File

@ -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
# <https://github.com/rust-lang/cargo/issues/9330>
# lto = true
[patch.crates-io]
# See comments in `library/rustc-std-workspace-core/README.md` for what's going on
# here

View File

@ -70,7 +70,7 @@ For more docs on how to build and test see [build_system/usage.txt](build_system
|FreeBSD|✅[^no-rustup]|❓|❓|❓|
|AIX|❌[^xcoff]|N/A|N/A|❌[^xcoff]|
|Other unixes|❓|❓|❓|❓|
|macOS|✅|✅[^no-rustup]|N/A|N/A|
|macOS|✅|✅|N/A|N/A|
|Windows|✅[^no-rustup]|❌|N/A|N/A|
✅: Fully supported and tested

View File

@ -704,7 +704,7 @@ impl<T> Box<[T]> {
}
/// Constructs a new boxed slice with uninitialized contents. Returns an error if
/// the allocation fails
/// the allocation fails.
///
/// # Examples
///
@ -739,7 +739,7 @@ impl<T> Box<[T]> {
}
/// Constructs a new boxed slice with uninitialized contents, with the memory
/// being filled with `0` bytes. Returns an error if the allocation fails
/// being filled with `0` bytes. Returns an error if the allocation fails.
///
/// See [`MaybeUninit::zeroed`][zeroed] for examples of correct and incorrect usage
/// of this method.
@ -831,6 +831,85 @@ impl<T, A: Allocator> Box<[T], A> {
pub fn new_zeroed_slice_in(len: usize, alloc: A) -> Box<[mem::MaybeUninit<T>], A> {
unsafe { RawVec::with_capacity_zeroed_in(len, alloc).into_box(len) }
}
/// Constructs a new boxed slice with uninitialized contents in the provided allocator. Returns an error if
/// the allocation fails.
///
/// # Examples
///
/// ```
/// #![feature(allocator_api, new_uninit)]
///
/// use std::alloc::System;
///
/// let mut values = Box::<[u32], _>::try_new_uninit_slice_in(3, System)?;
/// let values = unsafe {
/// // Deferred initialization:
/// values[0].as_mut_ptr().write(1);
/// values[1].as_mut_ptr().write(2);
/// values[2].as_mut_ptr().write(3);
/// values.assume_init()
/// };
///
/// assert_eq!(*values, [1, 2, 3]);
/// # Ok::<(), std::alloc::AllocError>(())
/// ```
#[unstable(feature = "allocator_api", issue = "32838")]
#[inline]
pub fn try_new_uninit_slice_in(
len: usize,
alloc: A,
) -> Result<Box<[mem::MaybeUninit<T>], A>, AllocError> {
let ptr = if T::IS_ZST || len == 0 {
NonNull::dangling()
} else {
let layout = match Layout::array::<mem::MaybeUninit<T>>(len) {
Ok(l) => l,
Err(_) => return Err(AllocError),
};
alloc.allocate(layout)?.cast()
};
unsafe { Ok(RawVec::from_raw_parts_in(ptr.as_ptr(), len, alloc).into_box(len)) }
}
/// Constructs a new boxed slice with uninitialized contents in the provided allocator, with the memory
/// being filled with `0` bytes. Returns an error if the allocation fails.
///
/// See [`MaybeUninit::zeroed`][zeroed] for examples of correct and incorrect usage
/// of this method.
///
/// # Examples
///
/// ```
/// #![feature(allocator_api, new_uninit)]
///
/// use std::alloc::System;
///
/// let values = Box::<[u32], _>::try_new_zeroed_slice_in(3, System)?;
/// let values = unsafe { values.assume_init() };
///
/// assert_eq!(*values, [0, 0, 0]);
/// # Ok::<(), std::alloc::AllocError>(())
/// ```
///
/// [zeroed]: mem::MaybeUninit::zeroed
#[unstable(feature = "allocator_api", issue = "32838")]
#[inline]
pub fn try_new_zeroed_slice_in(
len: usize,
alloc: A,
) -> Result<Box<[mem::MaybeUninit<T>], A>, AllocError> {
let ptr = if T::IS_ZST || len == 0 {
NonNull::dangling()
} else {
let layout = match Layout::array::<mem::MaybeUninit<T>>(len) {
Ok(l) => l,
Err(_) => return Err(AllocError),
};
alloc.allocate_zeroed(layout)?.cast()
};
unsafe { Ok(RawVec::from_raw_parts_in(ptr.as_ptr(), len, alloc).into_box(len)) }
}
}
impl<T, A: Allocator> Box<mem::MaybeUninit<T>, A> {

View File

@ -1,5 +1,6 @@
//! Definitions for Horizon OS
#![forbid(unsafe_op_in_unsafe_fn)]
#![stable(feature = "raw_ext", since = "1.1.0")]
pub mod fs;

View File

@ -38,6 +38,7 @@ pub type time_t = libc::time_t;
#[repr(C)]
#[derive(Clone)]
#[stable(feature = "raw_ext", since = "1.1.0")]
#[allow(dead_code)] // This exists for parity with other `raw` modules, but isn't actually used.
pub struct stat {
#[stable(feature = "raw_ext", since = "1.1.0")]
pub st_dev: dev_t,

View File

@ -67,7 +67,7 @@ cfg_if::cfg_if! {
))] {
#[inline]
unsafe fn aligned_malloc(layout: &Layout) -> *mut u8 {
libc::memalign(layout.align(), layout.size()) as *mut u8
unsafe { libc::memalign(layout.align(), layout.size()) as *mut u8 }
}
} else {
#[inline]

View File

@ -1,4 +1,3 @@
use crate::fmt;
use crate::io;
use crate::num::NonZero;
use crate::sys::pal::unix::unsupported::*;

View File

@ -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 \

View File

@ -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);

View File

@ -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";
);

View File

@ -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);
}
}

View File

@ -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

View File

@ -203,7 +203,9 @@ pub fn target_supports_cranelift_backend(target: TargetSelection) -> bool {
|| target.contains("aarch64")
|| target.contains("s390x")
|| target.contains("riscv64gc")
} else if target.contains("darwin") || target.is_windows() {
} else if target.contains("darwin") {
target.contains("x86_64") || target.contains("aarch64")
} else if target.is_windows() {
target.contains("x86_64")
} else {
false

View File

@ -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

View File

@ -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 \

View File

@ -326,6 +326,7 @@ auto:
NO_DEBUG_ASSERTIONS: 1
NO_OVERFLOW_CHECKS: 1
DIST_REQUIRE_ALL_TOOLS: 1
CODEGEN_BACKENDS: llvm,cranelift
<<: *job-macos-m1
# This target only needs to support 11.0 and up as nothing else supports the hardware

View File

@ -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"

View File

@ -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.

View File

@ -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()

View File

@ -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());
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -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::<f64>().is_ok() {
validate(&s);
}
}
}
}

View File

@ -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<R: Rng>(n: u32, range: &mut Range<u32>, 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
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}
}

View File

@ -1,7 +0,0 @@
use test_float_parse::validate;
fn main() {
for i in 0..(1 << 19) {
validate(&i.to_string());
}
}

View File

@ -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());
}
}

View File

@ -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<F: Float> {
iter: RangeInclusive<F::Int>,
}
impl<F: Float> Generator<F> for Exhaustive<F>
where
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
{
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<F: Float> Iterator for Exhaustive<F>
where
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
{
type Item = F;
fn next(&mut self) -> Option<Self::Item> {
Some(F::from_bits(self.iter.next()?))
}
}

View File

@ -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<i32> = (-SMALL_COEFF_MAX)..=SMALL_COEFF_MAX;
const SMALL_EXP_RANGE: RangeInclusive<i32> = (-SMALL_EXP_MAX)..=SMALL_EXP_MAX;
const LARGE_COEFF_RANGE: RangeInclusive<u32> = 0..=100_000;
const LARGE_EXP_RANGE: RangeInclusive<u32> = 300..=350;
/// Check exponential values around zero.
pub struct SmallExponents<F: Float> {
iter: BoxGenIter<Self, F>,
}
impl<F: Float> Generator<F> for SmallExponents<F> {
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<F: Float> Iterator for SmallExponents<F> {
type Item = (i32, i32);
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}
/// Check exponential values further from zero.
pub struct LargeExponents<F: Float> {
iter: BoxGenIter<Self, F>,
}
impl<F: Float> Generator<F> for LargeExponents<F> {
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<F: Float> Iterator for LargeExponents<F> {
type Item = (u32, u32, bool);
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}

View File

@ -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<BTreeMap<TypeId, u64>> = Mutex::new(BTreeMap::new());
/// Generic fuzzer; just tests deterministic random bit patterns N times.
pub struct Fuzz<F> {
iter: Range<u64>,
rng: ChaCha8Rng,
/// Allow us to use generics in `Iterator`.
marker: PhantomData<F>,
}
impl<F: Float> Fuzz<F> {
/// 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<u64>) {
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::<F>(), count);
}
}
impl<F: Float> Generator<F> for Fuzz<F>
where
Standard: Distribution<<F as Float>::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::<F>())
.unwrap_or_else(|| panic!("missing fuzz count for {}", type_name::<F>()))
}
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<F: Float> Iterator for Fuzz<F>
where
Standard: Distribution<<F as Float>::Int>,
{
type Item = <Self as Generator<F>>::WriteCtx;
fn next(&mut self) -> Option<Self::Item> {
let _ = self.iter.next()?;
let i: F::Int = self.rng.gen();
Some(F::from_bits(i))
}
}

View File

@ -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<i32> = {
let max = 1i32 << SMALL_MAX_POW2;
(-max)..=max
};
/// Large values only get tested around powers of two
const LARGE_POWERS: Range<u32> = SMALL_MAX_POW2..128;
/// We perturbe each large value around these ranges
const LARGE_PERTURBATIONS: RangeInclusive<i128> = -256..=256;
/// Test all integers up to `2 ^ MAX_POW2`
pub struct SmallInt {
iter: RangeInclusive<i32>,
}
impl<F: Float> Generator<F> 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::Item> {
self.iter.next()
}
}
/// Test much bigger integers than [`SmallInt`].
pub struct LargeInt<F: Float> {
iter: BoxGenIter<Self, F>,
}
impl<F: Float> LargeInt<F> {
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<F: Float> Generator<F> for LargeInt<F> {
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<F: Float> Iterator for LargeInt<F> {
type Item = i128;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}

View File

@ -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<F: Float> Generator<F> 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<Self::Item> {
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())
}
}

View File

@ -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<usize> = 100..=400;
/// Range of possible exponents
const EXP_RANGE: Range<i32> = -4500..4500;
/// Try strings of random digits.
pub struct RandDigits<F> {
rng: ChaCha8Rng,
iter: Range<u64>,
uniform: Uniform<u32>,
/// Allow us to use generics in `Iterator`.
marker: PhantomData<F>,
}
impl<F: Float> Generator<F> for RandDigits<F> {
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<F: Float> Iterator for RandDigits<F> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
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)
}
}

View File

@ -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<F: Float>
where
FewOnesInt<F>: Generator<F>,
{
iter: BoxGenIter<Self, F>,
}
impl<F: Float> Generator<F> for FewOnesInt<F>
where
<F::Int as TryFrom<u128>>::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<F: Float> Iterator for FewOnesInt<F> {
type Item = F::Int;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next()
}
}
/// Similar to `FewOnesInt` except test those bit patterns as a float.
pub struct FewOnesFloat<F: Float>(FewOnesInt<F>);
impl<F: Float> Generator<F> for FewOnesFloat<F>
where
<F::Int as TryFrom<u128>>::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::<F>::total_tests()
}
fn new() -> Self {
Self(FewOnesInt::new())
}
fn write_string(s: &mut String, ctx: Self::WriteCtx) {
write!(s, "{ctx:e}").unwrap();
}
}
impl<F: Float> Iterator for FewOnesFloat<F> {
type Item = F;
fn next(&mut self) -> Option<Self::Item> {
self.0.next().map(|i| F::from_bits(i))
}
}

View File

@ -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<F: Float> Generator<F> 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::Item> {
self.iter.next().copied()
}
}
/// Strings that we know have failed in the past
const REGRESSIONS: &[&str] = &[
// From <https://github.com/rust-lang/rust/issues/31407>
"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<F: Float> Generator<F> 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::Item> {
self.iter.next().copied()
}
}

View File

@ -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<F: Float> {
cases: [F::Int; 6],
index: usize,
}
impl<F: Float> SubnormEdgeCases<F> {
/// 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<F: Float> Generator<F> for SubnormEdgeCases<F> {
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<F: Float> Iterator for SubnormEdgeCases<F> {
type Item = F;
fn next(&mut self) -> Option<Self::Item> {
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<F: Float> {
iter: RangeInclusive<F::Int>,
}
impl<F: Float> Generator<F> for SubnormComplete<F>
where
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
{
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<F: Float> Iterator for SubnormComplete<F>
where
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
{
type Item = F;
fn next(&mut self) -> Option<Self::Item> {
Some(F::from_bits(self.iter.next()?))
}
}

View File

@ -1,16 +1,526 @@
use std::io;
use std::io::prelude::*;
use std::mem::transmute;
mod traits;
mod ui;
mod validate;
// Nothing up my sleeve: Just (PI - 3) in base 16.
#[allow(dead_code)]
pub const SEED: [u32; 3] = [0x243f_6a88, 0x85a3_08d3, 0x1319_8a2e];
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};
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();
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<u64>,
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<TestInfo> {
let mut tests = Vec::new();
// Register normal generators for all floats.
register_float::<f32>(&mut tests, cfg);
register_float::<f64>(&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<F: Float>(tests: &mut Vec<TestInfo>, cfg: &Config)
where
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
<F::Int as TryFrom<u128>>::Error: std::fmt::Debug,
Standard: Distribution<<F as traits::Float>::Int>,
{
if F::BITS <= MAX_BITS_FOR_EXHAUUSTIVE {
// Only run exhaustive tests if there is a chance of completion.
TestInfo::register::<F, gen::exhaustive::Exhaustive<F>>(tests);
}
gen::fuzz::Fuzz::<F>::set_iterations(cfg.fuzz_count);
TestInfo::register::<F, gen::exponents::LargeExponents<F>>(tests);
TestInfo::register::<F, gen::exponents::SmallExponents<F>>(tests);
TestInfo::register::<F, gen::fuzz::Fuzz<F>>(tests);
TestInfo::register::<F, gen::integers::LargeInt<F>>(tests);
TestInfo::register::<F, gen::integers::SmallInt>(tests);
TestInfo::register::<F, gen::long_fractions::RepeatingDecimal>(tests);
TestInfo::register::<F, gen::many_digits::RandDigits<F>>(tests);
TestInfo::register::<F, gen::sparse::FewOnesFloat<F>>(tests);
TestInfo::register::<F, gen::sparse::FewOnesInt<F>>(tests);
TestInfo::register::<F, gen::spot_checks::RegressionCheck>(tests);
TestInfo::register::<F, gen::spot_checks::Special>(tests);
TestInfo::register::<F, gen::subnorm::SubnormComplete<F>>(tests);
TestInfo::register::<F, gen::subnorm::SubnormEdgeCases<F>>(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<Msg>, &TestInfo, &Config),
/// Progress bar to be updated.
pb: Option<ProgressBar>,
/// Once completed, this will be set.
completed: OnceLock<Completed>,
}
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<F: Float, G: Generator<F>>(v: &mut Vec<Self>) {
let f_name = type_name::<F>();
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::<F, G>,
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<ProgressBar>) {
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<F: Float, G: Generator<F>>(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<str>,
/// The parsed & decomposed `FloatRes`, aleady stringified so we don't need generics here.
float_res: Box<str>,
},
/// 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<f64>,
/// Error as a rational string (since it can't always be represented as a float).
error_str: Box<str>,
/// 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<FinishedAll, EarlyExit>,
/// If there is something to warn about (e.g bad estimate), leave it here.
warning: Option<Box<str>>,
/// 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::<Msg>();
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<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestInfo, cfg: &Config) {
tx.send(Msg::new::<F, G>(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::<F>(buf) {
Ok(()) => (),
Err(e) => {
tx.send(Msg::new::<F, G>(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::<F, G>(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::<F, G>(Update::Completed(Completed {
executed,
failures,
result,
warning,
elapsed,
})))
.unwrap();
}

View File

@ -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<String>) -> (tfp::Config, Vec<String>, Vec<String>) {
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)
}

View File

@ -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<Output = Self>
+ ops::Sub<Output = Self>
+ ops::Shl<u32, Output = Self>
+ ops::Shr<u32, Output = Self>
+ ops::BitAnd<Output = Self>
+ ops::BitOr<Output = Self>
+ ops::Not<Output = Self>
+ ops::AddAssign
+ ops::BitAndAssign
+ ops::BitOrAssign
+ From<u8>
+ TryFrom<i8>
+ TryFrom<u32, Error: fmt::Debug>
+ TryFrom<u64, Error: fmt::Debug>
+ TryFrom<u128, Error: fmt::Debug>
+ TryInto<u64, Error: fmt::Debug>
+ TryInto<u32, Error: fmt::Debug>
+ 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<Err: fmt::Display> + Sized + Send + 'static
{
/// Unsigned integer of same width
type Int: Int<Signed = Self::SInt>;
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 = <Self::Int as Int>::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<Constants> = 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<F: Float>: Iterator<Item = Self::WriteCtx> + 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<This, F> = Box<dyn Iterator<Item = <This as Generator<F>>::WriteCtx> + Send>;

View File

@ -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>,
) -> 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.
/// <https://github.com/console-rs/indicatif/issues/121>.
pub fn set_panic_hook(drop_bars: Vec<ProgressBar>) {
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);
}));
}

View File

@ -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<i32> = (-(2 << 15))..=(2 << 15);
/// Powers of ten that we cache. Account for binary128, which can fit +4932/-4931
const POWERS_OF_TEN_RANGE: RangeInclusive<i32> = -5_000..=5_000;
/// Cached powers of 10 so we can look them up rather than recreating.
static POWERS_OF_TEN: LazyLock<BTreeMap<i32, BigRational>> = 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<i32, BigRational>,
/// 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<i32, BigRational>,
/// Handy to have around so we don't need to reallocate for it
two: BigInt,
}
impl Constants {
pub fn new<F: Float>() -> 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<i32, _> =
(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<F: Float>(input: &str) -> Result<(), Update> {
let parsed: F = input
.parse()
.unwrap_or_else(|e| panic!("parsing failed for {}: {e}. Input: {input}", type_name::<F>()));
// 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<F: Float> {
Inf,
NegInf,
Zero,
Nan,
/// A real number with significand and exponent. Value is `sig * 2 ^ exp`.
Real {
sig: F::SInt,
exp: i32,
},
}
impl<F: Float> FloatRes<F> {
/// 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::<F>()));
// 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: Float>(f: F) -> FloatRes<F> {
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;

View File

@ -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::<f32>("0").unwrap();
validate::<f32>("-0").unwrap();
validate::<f32>("1").unwrap();
validate::<f32>("-1").unwrap();
validate::<f32>("1.1").unwrap();
validate::<f32>("-1.1").unwrap();
validate::<f32>("1e10").unwrap();
validate::<f32>("1e1000").unwrap();
validate::<f32>("-1e1000").unwrap();
validate::<f32>("1e-1000").unwrap();
validate::<f32>("-1e-1000").unwrap();
}
#[test]
fn test_validate_real() {
// Most of the arbitrary values come from checking against <http://weitz.de/ieee/>.
let r = &BigRational::from_float(10.0).unwrap();
FloatRes::<f32>::validate_real(r.clone(), 10, 0).unwrap();
FloatRes::<f32>::validate_real(r.clone(), 10, -1).unwrap_err();
FloatRes::<f32>::validate_real(r.clone(), 10, 1).unwrap_err();
let r = &BigRational::from_float(0.25).unwrap();
FloatRes::<f32>::validate_real(r.clone(), 1, -2).unwrap();
FloatRes::<f32>::validate_real(r.clone(), 2, -2).unwrap_err();
let r = &BigRational::from_float(1234.5678).unwrap();
FloatRes::<f32>::validate_real(r.clone(), 0b100110100101001000101011, -13).unwrap();
FloatRes::<f32>::validate_real(r.clone(), 0b100110100101001000101010, -13).unwrap_err();
FloatRes::<f32>::validate_real(r.clone(), 0b100110100101001000101100, -13).unwrap_err();
let r = &BigRational::from_float(-1234.5678).unwrap();
FloatRes::<f32>::validate_real(r.clone(), -0b100110100101001000101011, -13).unwrap();
FloatRes::<f32>::validate_real(r.clone(), -0b100110100101001000101010, -13).unwrap_err();
FloatRes::<f32>::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::<f32>::validate_real(v1_low_down.clone(), v1_down_sig, exp).unwrap();
FloatRes::<f32>::validate_real(v1_mid_down.clone(), v1_down_sig, exp).unwrap();
FloatRes::<f32>::validate_real(v1_high_up.clone(), v1_up_sig, exp).unwrap();
FloatRes::<f32>::validate_real(-v1_low_down.clone(), -v1_down_sig, exp).unwrap();
FloatRes::<f32>::validate_real(-v1_mid_down.clone(), -v1_down_sig, exp).unwrap();
FloatRes::<f32>::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::<f32>::validate_real(v2_low_down.clone(), v2_down_sig, exp).unwrap();
FloatRes::<f32>::validate_real(v2_mid_up.clone(), v2_up_sig, exp).unwrap();
FloatRes::<f32>::validate_real(v2_high_up.clone(), v2_up_sig, exp).unwrap();
FloatRes::<f32>::validate_real(-v2_low_down.clone(), -v2_down_sig, exp).unwrap();
FloatRes::<f32>::validate_real(-v2_mid_up.clone(), -v2_up_sig, exp).unwrap();
FloatRes::<f32>::validate_real(-v2_high_up.clone(), -v2_up_sig, exp).unwrap();
// Rounding the wrong direction should error
for res in [
FloatRes::<f32>::validate_real(v1_mid_down.clone(), v1_up_sig, exp),
FloatRes::<f32>::validate_real(v2_mid_up.clone(), v2_down_sig, exp),
FloatRes::<f32>::validate_real(-v1_mid_down.clone(), -v1_up_sig, exp),
FloatRes::<f32>::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));
}

View File

@ -2,10 +2,8 @@ run-make/branch-protection-check-IBT/Makefile
run-make/c-dynamic-dylib/Makefile
run-make/c-dynamic-rlib/Makefile
run-make/c-unwind-abi-catch-lib-panic/Makefile
run-make/c-unwind-abi-catch-panic/Makefile
run-make/cat-and-grep-sanity-check/Makefile
run-make/cdylib-dylib-linkage/Makefile
run-make/compiler-lookup-paths-2/Makefile
run-make/compiler-rt-works-on-mingw/Makefile
run-make/cross-lang-lto-clang/Makefile
run-make/cross-lang-lto-pgo-smoketest/Makefile
@ -90,7 +88,6 @@ run-make/staticlib-dylib-linkage/Makefile
run-make/symbol-mangling-hashed/Makefile
run-make/symbol-visibility/Makefile
run-make/sysroot-crates-are-unstable/Makefile
run-make/test-benches/Makefile
run-make/thumb-none-cortex-m/Makefile
run-make/thumb-none-qemu/Makefile
run-make/translation/Makefile

View File

@ -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

View File

@ -1,10 +0,0 @@
# Exercise unwinding a panic. This catches a panic across an FFI boundary and downcasts it into an integer. The Rust code that panics is in the same directory.
# See https://github.com/rust-lang/rust/commit/baf227ea0c1e07fc54395a51e4b3881d701180cb
# ignore-cross-compile
# needs-unwind
include ../tools.mk
all: $(call NATIVE_STATICLIB,add)
$(RUSTC) main.rs
$(call RUN,main) || exit 1

View File

@ -0,0 +1,18 @@
// A test for calling `C-unwind` functions across foreign function boundaries (FFI).
// This test triggers a panic when calling a foreign function that calls *back* into Rust.
// This catches a panic across an FFI boundary and downcasts it into an integer.
// The Rust code that panics is in the same directory, unlike `c-unwind-abi-catch-lib-panic`.
// See https://github.com/rust-lang/rust/pull/76570
//@ ignore-cross-compile
// Reason: the compiled binary is executed
//@ needs-unwind
// Reason: this test exercises panic unwinding
use run_make_support::{build_native_static_lib, run, rustc};
fn main() {
build_native_static_lib("add");
rustc().input("main.rs").run();
run("main");
}

View File

@ -1,11 +0,0 @@
# This test checks that extern crate declarations in Cargo without a corresponding declaration in the manifest of a dependency are NOT allowed.
# See https://github.com/rust-lang/rust/pull/21113
include ../tools.mk
all:
mkdir -p $(TMPDIR)/a $(TMPDIR)/b
$(RUSTC) a.rs && mv $(TMPDIR)/liba.rlib $(TMPDIR)/a
$(RUSTC) b.rs -L $(TMPDIR)/a && mv $(TMPDIR)/libb.rlib $(TMPDIR)/b
$(RUSTC) c.rs -L crate=$(TMPDIR)/b -L dependency=$(TMPDIR)/a \
&& exit 1 || exit 0

View File

@ -0,0 +1,20 @@
// This test checks that extern crate declarations in Cargo without a corresponding declaration
// in the manifest of a dependency are NOT allowed. The last rustc call does it anyways, which
// should result in a compilation failure.
// See https://github.com/rust-lang/rust/pull/21113
use run_make_support::{path, rfs, rust_lib_name, rustc};
fn main() {
rfs::create_dir("a");
rfs::create_dir("b");
rustc().input("a.rs").run();
rfs::rename(rust_lib_name("a"), path("a").join(rust_lib_name("a")));
rustc().input("b.rs").library_search_path("a").run();
rfs::rename(rust_lib_name("b"), path("b").join(rust_lib_name("b")));
rustc()
.input("c.rs")
.library_search_path("crate=b")
.library_search_path("dependency=a")
.run_fail();
}

View File

@ -1,12 +0,0 @@
include ../tools.mk
# ignore-cross-compile
# needs-unwind #[bench] and -Zpanic-abort-tests can't be combined
all:
# Smoke-test that `#[bench]` isn't entirely broken.
$(RUSTC) --test smokebench.rs -O
$(call RUN,smokebench --bench)
$(call RUN,smokebench --bench noiter)
$(call RUN,smokebench --bench yesiter)
$(call RUN,smokebench)

View File

@ -0,0 +1,22 @@
// #[bench] is a Rust feature to run benchmarks on performance-critical
// code, which previously experienced a runtime panic bug in #103794.
// In order to ensure future breakages of this feature are detected, this
// smoke test was created, using the benchmarking feature with various
// runtime flags.
// See https://github.com/rust-lang/rust/issues/103794
//@ ignore-cross-compile
// Reason: the compiled binary is executed
//@ needs-unwind
// Reason: #[bench] and -Zpanic-abort-tests can't be combined
use run_make_support::{run, run_with_args, rustc};
fn main() {
// Smoke-test that #[bench] isn't entirely broken.
rustc().arg("--test").input("smokebench.rs").opt().run();
run_with_args("smokebench", &["--bench"]);
run_with_args("smokebench", &["--bench", "noiter"]);
run_with_args("smokebench", &["--bench", "yesiter"]);
run("smokebench");
}

View File

@ -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());

View File

@ -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());

View File

@ -0,0 +1,17 @@
// Test for ICE: mir_const_qualif: index out of bounds: the len is 0 but the index is 0
// https://github.com/rust-lang/rust/issues/125837
use std::fmt::Debug;
trait Foo<Item> {}
impl<Item, D: Debug + Clone> Foo for D {
//~^ ERROR missing generics for trait `Foo`
fn foo<'a>(&'a self) -> impl Debug {
//~^ ERROR method `foo` is not a member of trait `Foo`
const { return }
//~^ ERROR return statement outside of function body
}
}
pub fn main() {}

View File

@ -0,0 +1,41 @@
error[E0407]: method `foo` is not a member of trait `Foo`
--> $DIR/ice-mir-const-qualif-125837.rs:10:5
|
LL | / fn foo<'a>(&'a self) -> impl Debug {
LL | |
LL | | const { return }
LL | |
LL | | }
| |_____^ not a member of trait `Foo`
error[E0107]: missing generics for trait `Foo`
--> $DIR/ice-mir-const-qualif-125837.rs:8:30
|
LL | impl<Item, D: Debug + Clone> Foo for D {
| ^^^ expected 1 generic argument
|
note: trait defined here, with 1 generic parameter: `Item`
--> $DIR/ice-mir-const-qualif-125837.rs:6:7
|
LL | trait Foo<Item> {}
| ^^^ ----
help: add missing generic argument
|
LL | impl<Item, D: Debug + Clone> Foo<Item> for D {
| ++++++
error[E0572]: return statement outside of function body
--> $DIR/ice-mir-const-qualif-125837.rs:12:17
|
LL | / fn foo<'a>(&'a self) -> impl Debug {
LL | |
LL | | const { return }
| | --^^^^^^-- the return is part of this body...
LL | |
LL | | }
| |_____- ...not the enclosing function body
error: aborting due to 3 previous errors
Some errors have detailed explanations: E0107, E0407, E0572.
For more information about an error, try `rustc --explain E0107`.

View File

@ -905,7 +905,7 @@ cc = ["@kobzol"]
[assign]
warn_non_default_branch = true
contributing_url = "https://rustc-dev-guide.rust-lang.org/getting-started.html"
users_on_vacation = ["jyn514", "jhpratt", "oli-obk"]
users_on_vacation = ["jyn514", "jhpratt", "oli-obk", "michaelwoerister"]
[assign.adhoc_groups]
compiler-team = [