mirror of
https://github.com/rust-lang/rust.git
synced 2024-11-22 06:44:35 +00:00
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:
commit
aee3dc4c6c
75
Cargo.lock
75
Cargo.lock
@ -2598,12 +2598,76 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@ -5630,6 +5694,17 @@ dependencies = [
|
||||
"std",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test-float-parse"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"num",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.1"
|
||||
|
13
Cargo.toml
13
Cargo.toml
@ -4,6 +4,7 @@ members = [
|
||||
"compiler/rustc",
|
||||
"library/std",
|
||||
"library/sysroot",
|
||||
"src/etc/test-float-parse",
|
||||
"src/rustdoc-json-types",
|
||||
"src/tools/build_helper",
|
||||
"src/tools/cargotest",
|
||||
@ -109,6 +110,18 @@ strip = true
|
||||
debug = 0
|
||||
strip = true
|
||||
|
||||
# Bigint libraries are slow without optimization, speed up testing
|
||||
[profile.dev.package.test-float-parse]
|
||||
opt-level = 3
|
||||
|
||||
# Speed up the binary as much as possible
|
||||
[profile.release.package.test-float-parse]
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
# FIXME: LTO cannot be enabled for binaries in a workspace
|
||||
# <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
|
||||
|
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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]
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::fmt;
|
||||
use crate::io;
|
||||
use crate::num::NonZero;
|
||||
use crate::sys::pal::unix::unsupported::*;
|
||||
|
@ -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 \
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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";
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 \
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
55
src/etc/test-float-parse/README.md
Normal file
55
src/etc/test-float-parse/README.md
Normal 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.
|
@ -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()
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
use test_float_parse::validate;
|
||||
|
||||
fn main() {
|
||||
for i in 0..(1 << 19) {
|
||||
validate(&i.to_string());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
43
src/etc/test-float-parse/src/gen/exhaustive.rs
Normal file
43
src/etc/test-float-parse/src/gen/exhaustive.rs
Normal 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()?))
|
||||
}
|
||||
}
|
95
src/etc/test-float-parse/src/gen/exponents.rs
Normal file
95
src/etc/test-float-parse/src/gen/exponents.rs
Normal 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()
|
||||
}
|
||||
}
|
88
src/etc/test-float-parse/src/gen/fuzz.rs
Normal file
88
src/etc/test-float-parse/src/gen/fuzz.rs
Normal 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))
|
||||
}
|
||||
}
|
104
src/etc/test-float-parse/src/gen/integers.rs
Normal file
104
src/etc/test-float-parse/src/gen/integers.rs
Normal 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()
|
||||
}
|
||||
}
|
58
src/etc/test-float-parse/src/gen/long_fractions.rs
Normal file
58
src/etc/test-float-parse/src/gen/long_fractions.rs
Normal 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())
|
||||
}
|
||||
}
|
84
src/etc/test-float-parse/src/gen/many_digits.rs
Normal file
84
src/etc/test-float-parse/src/gen/many_digits.rs
Normal 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)
|
||||
}
|
||||
}
|
100
src/etc/test-float-parse/src/gen/sparse.rs
Normal file
100
src/etc/test-float-parse/src/gen/sparse.rs
Normal 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))
|
||||
}
|
||||
}
|
101
src/etc/test-float-parse/src/gen/spot_checks.rs
Normal file
101
src/etc/test-float-parse/src/gen/spot_checks.rs
Normal 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()
|
||||
}
|
||||
}
|
103
src/etc/test-float-parse/src/gen/subnorm.rs
Normal file
103
src/etc/test-float-parse/src/gen/subnorm.rs
Normal 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()?))
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
129
src/etc/test-float-parse/src/main.rs
Normal file
129
src/etc/test-float-parse/src/main.rs
Normal 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)
|
||||
}
|
202
src/etc/test-float-parse/src/traits.rs
Normal file
202
src/etc/test-float-parse/src/traits.rs
Normal 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>;
|
132
src/etc/test-float-parse/src/ui.rs
Normal file
132
src/etc/test-float-parse/src/ui.rs
Normal 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);
|
||||
}));
|
||||
}
|
364
src/etc/test-float-parse/src/validate.rs
Normal file
364
src/etc/test-float-parse/src/validate.rs
Normal 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;
|
149
src/etc/test-float-parse/src/validate/tests.rs
Normal file
149
src/etc/test-float-parse/src/validate/tests.rs
Normal 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));
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
18
tests/run-make/c-unwind-abi-catch-panic/rmake.rs
Normal file
18
tests/run-make/c-unwind-abi-catch-panic/rmake.rs
Normal 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");
|
||||
}
|
@ -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
|
20
tests/run-make/compiler-lookup-paths-2/rmake.rs
Normal file
20
tests/run-make/compiler-lookup-paths-2/rmake.rs
Normal 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();
|
||||
}
|
@ -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)
|
22
tests/run-make/test-benches/rmake.rs
Normal file
22
tests/run-make/test-benches/rmake.rs
Normal 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");
|
||||
}
|
@ -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());
|
||||
|
@ -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());
|
||||
|
17
tests/ui/mir/ice-mir-const-qualif-125837.rs
Normal file
17
tests/ui/mir/ice-mir-const-qualif-125837.rs
Normal 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() {}
|
41
tests/ui/mir/ice-mir-const-qualif-125837.stderr
Normal file
41
tests/ui/mir/ice-mir-const-qualif-125837.stderr
Normal 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`.
|
@ -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 = [
|
||||
|
Loading…
Reference in New Issue
Block a user