diff --git a/Cargo.lock b/Cargo.lock
index 1c1607e4c1ac4..3355e1ddaef86 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2598,12 +2598,76 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "num-conv"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
 
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -5630,6 +5694,17 @@ dependencies = [
  "std",
 ]
 
+[[package]]
+name = "test-float-parse"
+version = "0.1.0"
+dependencies = [
+ "indicatif",
+ "num",
+ "rand",
+ "rand_chacha",
+ "rayon",
+]
+
 [[package]]
 name = "textwrap"
 version = "0.16.1"
diff --git a/Cargo.toml b/Cargo.toml
index ce87a8c20b77d..043f339883a52 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ members = [
   "compiler/rustc",
   "library/std",
   "library/sysroot",
+  "src/etc/test-float-parse",
   "src/rustdoc-json-types",
   "src/tools/build_helper",
   "src/tools/cargotest",
@@ -109,6 +110,18 @@ strip = true
 debug = 0
 strip = true
 
+# Bigint libraries are slow without optimization, speed up testing
+[profile.dev.package.test-float-parse]
+opt-level = 3
+
+# Speed up the binary as much as possible
+[profile.release.package.test-float-parse]
+opt-level = 3
+codegen-units = 1
+# FIXME: LTO cannot be enabled for binaries in a workspace
+# <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
diff --git a/compiler/rustc_codegen_cranelift/Readme.md b/compiler/rustc_codegen_cranelift/Readme.md
index eb21e027dd0e0..3b3c86a1bd17b 100644
--- a/compiler/rustc_codegen_cranelift/Readme.md
+++ b/compiler/rustc_codegen_cranelift/Readme.md
@@ -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
diff --git a/src/bootstrap/mk/Makefile.in b/src/bootstrap/mk/Makefile.in
index 7e38a0996e59b..125a3c8d206d8 100644
--- a/src/bootstrap/mk/Makefile.in
+++ b/src/bootstrap/mk/Makefile.in
@@ -51,6 +51,7 @@ check-aux:
 	$(Q)$(BOOTSTRAP) test --stage 2 \
 		src/tools/cargo \
 		src/tools/cargotest \
+		etc/etc/test-float-parse \
 		$(BOOTSTRAP_ARGS)
 	# Run standard library tests in Miri.
 	$(Q)BOOTSTRAP_SKIP_TARGET_SANITY=1 \
diff --git a/src/bootstrap/src/core/build_steps/check.rs b/src/bootstrap/src/core/build_steps/check.rs
index 8235d4634b753..ed5b9edc86d64 100644
--- a/src/bootstrap/src/core/build_steps/check.rs
+++ b/src/bootstrap/src/core/build_steps/check.rs
@@ -466,6 +466,7 @@ tool_check_step!(CargoMiri, "src/tools/miri/cargo-miri", SourceType::InTree);
 tool_check_step!(Rls, "src/tools/rls", SourceType::InTree);
 tool_check_step!(Rustfmt, "src/tools/rustfmt", SourceType::InTree);
 tool_check_step!(MiroptTestTools, "src/tools/miropt-test-tools", SourceType::InTree);
+tool_check_step!(TestFloatParse, "src/etc/test-float-parse", SourceType::InTree);
 
 tool_check_step!(Bootstrap, "src/bootstrap", SourceType::InTree, false);
 
diff --git a/src/bootstrap/src/core/build_steps/clippy.rs b/src/bootstrap/src/core/build_steps/clippy.rs
index 40a2112b19254..ee7fb368a8c27 100644
--- a/src/bootstrap/src/core/build_steps/clippy.rs
+++ b/src/bootstrap/src/core/build_steps/clippy.rs
@@ -326,4 +326,5 @@ lint_any!(
     Rustfmt, "src/tools/rustfmt", "rustfmt";
     RustInstaller, "src/tools/rust-installer", "rust-installer";
     Tidy, "src/tools/tidy", "tidy";
+    TestFloatParse, "src/etc/test-float-parse", "test-float-parse";
 );
diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs
index 3f0cbde64e394..cc5931c68db1f 100644
--- a/src/bootstrap/src/core/build_steps/test.rs
+++ b/src/bootstrap/src/core/build_steps/test.rs
@@ -3505,3 +3505,80 @@ impl Step for CodegenGCC {
         cargo.into_cmd().run(builder);
     }
 }
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct TestFloatParse {
+    path: PathBuf,
+    host: TargetSelection,
+}
+
+impl Step for TestFloatParse {
+    type Output = ();
+    const ONLY_HOSTS: bool = true;
+    const DEFAULT: bool = true;
+
+    fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> {
+        run.path("src/etc/test-float-parse")
+    }
+
+    fn make_run(run: RunConfig<'_>) {
+        for path in run.paths {
+            let path = path.assert_single_path().path.clone();
+            run.builder.ensure(Self { path, host: run.target });
+        }
+    }
+
+    fn run(self, builder: &Builder<'_>) {
+        let bootstrap_host = builder.config.build;
+        let compiler = builder.compiler(0, bootstrap_host);
+        let path = self.path.to_str().unwrap();
+        let crate_name = self.path.components().last().unwrap().as_os_str().to_str().unwrap();
+
+        builder.ensure(compile::Std::new(compiler, self.host));
+
+        // Run any unit tests in the crate
+        let cargo_test = tool::prepare_tool_cargo(
+            builder,
+            compiler,
+            Mode::ToolStd,
+            bootstrap_host,
+            "test",
+            path,
+            SourceType::InTree,
+            &[],
+        );
+
+        run_cargo_test(
+            cargo_test,
+            &[],
+            &[],
+            crate_name,
+            crate_name,
+            compiler,
+            bootstrap_host,
+            builder,
+        );
+
+        // Run the actual parse tests.
+        let mut cargo_run = tool::prepare_tool_cargo(
+            builder,
+            compiler,
+            Mode::ToolStd,
+            bootstrap_host,
+            "run",
+            path,
+            SourceType::InTree,
+            &[],
+        );
+
+        cargo_run.arg("--");
+        if builder.config.args().is_empty() {
+            // By default, exclude tests that take longer than ~1m.
+            cargo_run.arg("--skip-huge");
+        } else {
+            cargo_run.args(builder.config.args());
+        }
+
+        cargo_run.into_cmd().run(builder);
+    }
+}
diff --git a/src/bootstrap/src/core/builder.rs b/src/bootstrap/src/core/builder.rs
index 6d6df650b149b..78fbea2e8107c 100644
--- a/src/bootstrap/src/core/builder.rs
+++ b/src/bootstrap/src/core/builder.rs
@@ -826,6 +826,7 @@ impl<'a> Builder<'a> {
                 clippy::Rustdoc,
                 clippy::Rustfmt,
                 clippy::RustInstaller,
+                clippy::TestFloatParse,
                 clippy::Tidy,
             ),
             Kind::Check | Kind::Fix => describe!(
@@ -840,6 +841,7 @@ impl<'a> Builder<'a> {
                 check::Rls,
                 check::Rustfmt,
                 check::RustAnalyzer,
+                check::TestFloatParse,
                 check::Bootstrap,
             ),
             Kind::Test => describe!(
@@ -901,6 +903,7 @@ impl<'a> Builder<'a> {
                 test::RustdocJson,
                 test::HtmlCheck,
                 test::RustInstaller,
+                test::TestFloatParse,
                 // Run bootstrap close to the end as it's unlikely to fail
                 test::Bootstrap,
                 // Run run-make last, since these won't pass without make on Windows
diff --git a/src/bootstrap/src/utils/helpers.rs b/src/bootstrap/src/utils/helpers.rs
index 773a873e47cc0..690d7318f9420 100644
--- a/src/bootstrap/src/utils/helpers.rs
+++ b/src/bootstrap/src/utils/helpers.rs
@@ -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
diff --git a/src/ci/docker/host-x86_64/dist-various-2/Dockerfile b/src/ci/docker/host-x86_64/dist-various-2/Dockerfile
index 962484593b4be..2621e9a603185 100644
--- a/src/ci/docker/host-x86_64/dist-various-2/Dockerfile
+++ b/src/ci/docker/host-x86_64/dist-various-2/Dockerfile
@@ -85,9 +85,9 @@ RUN /tmp/build-solaris-toolchain.sh sparcv9 sparcv9 solaris-sparc sun
 COPY host-x86_64/dist-various-2/build-x86_64-fortanix-unknown-sgx-toolchain.sh /tmp/
 RUN /tmp/build-x86_64-fortanix-unknown-sgx-toolchain.sh
 
-RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz | \
+RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-23/wasi-sdk-23.0-x86_64-linux.tar.gz | \
   tar -xz
-ENV WASI_SDK_PATH=/tmp/wasi-sdk-22.0
+ENV WASI_SDK_PATH=/tmp/wasi-sdk-23.0-x86_64-linux
 
 COPY scripts/freebsd-toolchain.sh /tmp/
 RUN /tmp/freebsd-toolchain.sh i686
diff --git a/src/ci/docker/host-x86_64/test-various/Dockerfile b/src/ci/docker/host-x86_64/test-various/Dockerfile
index f874b2ed475fd..c2f5a87b1234f 100644
--- a/src/ci/docker/host-x86_64/test-various/Dockerfile
+++ b/src/ci/docker/host-x86_64/test-various/Dockerfile
@@ -40,9 +40,9 @@ WORKDIR /
 COPY scripts/sccache.sh /scripts/
 RUN sh /scripts/sccache.sh
 
-RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-22/wasi-sdk-22.0-linux.tar.gz | \
+RUN curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-23/wasi-sdk-23.0-x86_64-linux.tar.gz | \
   tar -xz
-ENV WASI_SDK_PATH=/wasi-sdk-22.0
+ENV WASI_SDK_PATH=/wasi-sdk-23.0-x86_64-linux
 
 ENV RUST_CONFIGURE_ARGS \
   --musl-root-x86_64=/usr/local/x86_64-linux-musl \
diff --git a/src/ci/github-actions/jobs.yml b/src/ci/github-actions/jobs.yml
index a6e12c6ff954d..3372655d04444 100644
--- a/src/ci/github-actions/jobs.yml
+++ b/src/ci/github-actions/jobs.yml
@@ -285,7 +285,7 @@ auto:
 
   - image: dist-apple-various
     env:
-      SCRIPT: ./x.py dist bootstrap --include-default-paths --host='' --target=aarch64-apple-ios,x86_64-apple-ios,aarch64-apple-ios-sim
+      SCRIPT: ./x.py dist bootstrap --include-default-paths --host='' --target=aarch64-apple-ios,x86_64-apple-ios,aarch64-apple-ios-sim,aarch64-apple-ios-macabi,x86_64-apple-ios-macabi
       RUST_CONFIGURE_ARGS: --enable-sanitizers --enable-profiler --set rust.jemalloc
       RUSTC_RETRY_LINKER_ON_SEGFAULT: 1
       MACOSX_DEPLOYMENT_TARGET: 10.12
@@ -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
diff --git a/src/doc/rustc/src/platform-support.md b/src/doc/rustc/src/platform-support.md
index 370dbed50fa1a..98af93506f8d7 100644
--- a/src/doc/rustc/src/platform-support.md
+++ b/src/doc/rustc/src/platform-support.md
@@ -135,6 +135,7 @@ so Rustup may install the documentation for a similar tier 1 target instead.
 target | std | notes
 -------|:---:|-------
 [`aarch64-apple-ios`](platform-support/apple-ios.md) | ✓ | ARM64 iOS
+[`aarch64-apple-ios-macabi`](platform-support/apple-ios-macabi.md) | ✓ | Mac Catalyst on ARM64
 [`aarch64-apple-ios-sim`](platform-support/apple-ios.md) | ✓ | Apple iOS Simulator on ARM64
 `aarch64-fuchsia` | ✓ | Alias for `aarch64-unknown-fuchsia`
 [`aarch64-unknown-fuchsia`](platform-support/fuchsia.md) | ✓ | ARM64 Fuchsia
@@ -195,6 +196,7 @@ target | std | notes
 [`wasm32-wasip1`](platform-support/wasm32-wasip1.md) | ✓ | WebAssembly with WASI
 [`wasm32-wasip1-threads`](platform-support/wasm32-wasip1-threads.md) | ✓ | WebAssembly with WASI Preview 1 and threads
 [`x86_64-apple-ios`](platform-support/apple-ios.md) | ✓ | 64-bit x86 iOS
+[`x86_64-apple-ios-macabi`](platform-support/apple-ios-macabi.md) | ✓ | Mac Catalyst on x86_64
 [`x86_64-fortanix-unknown-sgx`](platform-support/x86_64-fortanix-unknown-sgx.md) | ✓ | [Fortanix ABI] for 64-bit Intel SGX
 `x86_64-fuchsia` | ✓ | Alias for `x86_64-unknown-fuchsia`
 [`x86_64-unknown-fuchsia`](platform-support/fuchsia.md) | ✓ | 64-bit x86 Fuchsia
@@ -244,7 +246,6 @@ target | std | host | notes
 -------|:---:|:----:|-------
 [`arm64e-apple-ios`](platform-support/arm64e-apple-ios.md) | ✓ | | ARM64e Apple iOS
 [`arm64e-apple-darwin`](platform-support/arm64e-apple-darwin.md)  | ✓ | ✓ | ARM64e Apple Darwin
-[`aarch64-apple-ios-macabi`](platform-support/apple-ios-macabi.md) | ✓ |  | Apple Catalyst on ARM64
 [`aarch64-apple-tvos`](platform-support/apple-tvos.md) | ✓ |  | ARM64 tvOS
 [`aarch64-apple-tvos-sim`](platform-support/apple-tvos.md) | ✓ |  | ARM64 tvOS Simulator
 [`aarch64-apple-watchos`](platform-support/apple-watchos.md) | ✓ |  | ARM64 Apple WatchOS
@@ -370,7 +371,6 @@ target | std | host | notes
 `thumbv7neon-unknown-linux-musleabihf` | ? |  | Thumb2-mode Armv7-A Linux with NEON, musl 1.2.3
 [`wasm32-wasip2`](platform-support/wasm32-wasip2.md) | ✓ |  | WebAssembly
 [`wasm64-unknown-unknown`](platform-support/wasm64-unknown-unknown.md) | ? |  | WebAssembly
-[`x86_64-apple-ios-macabi`](platform-support/apple-ios-macabi.md) | ✓ |  | Apple Catalyst on x86_64
 [`x86_64-apple-tvos`](platform-support/apple-tvos.md) | ✓ |  | x86 64-bit tvOS
 [`x86_64-apple-watchos-sim`](platform-support/apple-watchos.md) | ✓ |  | x86 64-bit Apple WatchOS simulator
 [`x86_64-pc-nto-qnx710`](platform-support/nto-qnx.md) | ✓ |  | x86 64-bit QNX Neutrino 7.1 RTOS |
diff --git a/src/doc/rustc/src/platform-support/apple-ios-macabi.md b/src/doc/rustc/src/platform-support/apple-ios-macabi.md
index 15ba31e0f0645..c34c47ef12c9e 100644
--- a/src/doc/rustc/src/platform-support/apple-ios-macabi.md
+++ b/src/doc/rustc/src/platform-support/apple-ios-macabi.md
@@ -2,7 +2,7 @@
 
 Apple Mac Catalyst targets.
 
-**Tier: 3**
+**Tier: 2 (without Host Tools)**
 
 - `aarch64-apple-ios-macabi`: Mac Catalyst on ARM64.
 - `x86_64-apple-ios-macabi`: Mac Catalyst on 64-bit x86.
diff --git a/src/etc/test-float-parse/Cargo.toml b/src/etc/test-float-parse/Cargo.toml
index a045be956acd1..56cb5cddeea52 100644
--- a/src/etc/test-float-parse/Cargo.toml
+++ b/src/etc/test-float-parse/Cargo.toml
@@ -4,11 +4,12 @@ version = "0.1.0"
 edition = "2021"
 publish = false
 
-[workspace]
-resolver = "1"
-
 [dependencies]
-rand = "0.8"
+indicatif = { version = "0.17.8", default-features = false }
+num = "0.4.3"
+rand = "0.8.5"
+rand_chacha = "0.3"
+rayon = "1"
 
 [lib]
 name = "test_float_parse"
diff --git a/src/etc/test-float-parse/README.md b/src/etc/test-float-parse/README.md
new file mode 100644
index 0000000000000..21b20d0a072d7
--- /dev/null
+++ b/src/etc/test-float-parse/README.md
@@ -0,0 +1,55 @@
+# Float Parsing Tests
+
+These are tests designed to test decimal to float conversions (`dec2flt`) used
+by the standard library.
+
+It consistes of a collection of test generators that each generate a set of
+patterns intended to test a specific property. In addition, there are exhaustive
+tests (for <= `f32`) and fuzzers (for anything that can't be run exhaustively).
+
+The generators work as follows:
+
+- Each generator is a struct that lives somewhere in the `gen` module. Usually
+  it is generic over a float type.
+- These generators must implement `Iterator`, which should return a context type
+  that can be used to construct a test string (but usually not the string
+  itself).
+- They must also implement the `Generator` trait, which provides a method to
+  write test context to a string as a test case, as well as some extra metadata.
+
+  The split between context generation and string construction is so that we can
+  reuse string allocations.
+- Each generator gets registered once for each float type. Each of these
+  generators then get their iterator called, and each test case checked against
+  the float type's parse implementation.
+
+Some generators produce decimal strings, others create bit patterns that need to
+be bitcasted to the float type, which then uses its `Display` implementation to
+write to a string. For these, float to decimal (`flt2dec`) conversions also get
+tested, if unintentionally.
+
+For each test case, the following is done:
+
+- The test string is parsed to the float type using the standard library's
+  implementation.
+- The test string is parsed separately to a `BigRational`, which acts as a
+  representation with infinite precision.
+- The rational value then gets checked that it is within the float's
+  representable values (absolute value greater than the smallest number to round
+  to zero, but less less than the first value to round to infinity). If these
+  limits are exceeded, check that the parsed float reflects that.
+- For real nonzero numbers, the parsed float is converted into a rational using
+  `significand * 2^exponent`. It is then checked against the actual rational
+  value, and verified to be within half a bit's precision of the parsed value.
+  Also it is checked that ties round to even.
+
+This is all highly parallelized with `rayon`; test generators can run in
+parallel, and their tests get chunked and run in parallel.
+
+There is a simple command line that allows filtering which tests are run,
+setting the number of iterations for fuzzing tests, limiting failures, setting
+timeouts, etc. See `main.rs` or run with `--help` for options.
+
+Note that when running via `./x`, only tests that take less than a few minutes
+are run by default. Navigate to the crate (or pass `-C` to Cargo) and run it
+directly to run all tests or pass specific arguments.
diff --git a/src/etc/test-float-parse/runtests.py b/src/etc/test-float-parse/runtests.py
deleted file mode 100755
index cc5e31a051fc2..0000000000000
--- a/src/etc/test-float-parse/runtests.py
+++ /dev/null
@@ -1,394 +0,0 @@
-#!/usr/bin/env python3
-
-"""
-Testing dec2flt
-===============
-These are *really* extensive tests. Expect them to run for hours. Due to the
-nature of the problem (the input is a string of arbitrary length), exhaustive
-testing is not really possible. Instead, there are exhaustive tests for some
-classes of inputs for which that is feasible and a bunch of deterministic and
-random non-exhaustive tests for covering everything else.
-
-The actual tests (generating decimal strings and feeding them to dec2flt) is
-performed by a set of stand-along rust programs. This script compiles, runs,
-and supervises them. The programs report the strings they generate and the
-floating point numbers they converted those strings to, and this script
-checks that the results are correct.
-
-You can run specific tests rather than all of them by giving their names
-(without .rs extension) as command line parameters.
-
-Verification
-------------
-The tricky part is not generating those inputs but verifying the outputs.
-Comparing with the result of Python's float() does not cut it because
-(and this is apparently undocumented) although Python includes a version of
-Martin Gay's code including the decimal-to-float part, it doesn't actually use
-it for float() (only for round()) instead relying on the system scanf() which
-is not necessarily completely accurate.
-
-Instead, we take the input and compute the true value with bignum arithmetic
-(as a fraction, using the ``fractions`` module).
-
-Given an input string and the corresponding float computed via Rust, simply
-decode the float into f * 2^k (for integers f, k) and the ULP.
-We can now easily compute the error and check if it is within 0.5 ULP as it
-should be. Zero and infinites are handled similarly:
-
-- If the approximation is 0.0, the exact value should be *less or equal*
-  half the smallest denormal float: the smallest denormal floating point
-  number has an odd mantissa (00...001) and thus half of that is rounded
-  to 00...00, i.e., zero.
-- If the approximation is Inf, the exact value should be *greater or equal*
-  to the largest finite float + 0.5 ULP: the largest finite float has an odd
-  mantissa (11...11), so that plus half an ULP is rounded up to the nearest
-  even number, which overflows.
-
-Implementation details
-----------------------
-This directory contains a set of single-file Rust programs that perform
-tests with a particular class of inputs. Each is compiled and run without
-parameters, outputs (f64, f32, decimal) pairs to verify externally, and
-in any case either exits gracefully or with a panic.
-
-If a test binary writes *anything at all* to stderr or exits with an
-exit code that's not 0, the test fails.
-The output on stdout is treated as (f64, f32, decimal) record, encoded thusly:
-
-- First, the bits of the f64 encoded as an ASCII hex string.
-- Second, the bits of the f32 encoded as an ASCII hex string.
-- Then the corresponding string input, in ASCII
-- The record is terminated with a newline.
-
-Incomplete records are an error. Not-a-Number bit patterns are invalid too.
-
-The tests run serially but the validation for a single test is parallelized
-with ``multiprocessing``. Each test is launched as a subprocess.
-One thread supervises it: Accepts and enqueues records to validate, observe
-stderr, and waits for the process to exit. A set of worker processes perform
-the validation work for the outputs enqueued there. Another thread listens
-for progress updates from the workers.
-
-Known issues
-------------
-Some errors (e.g., NaN outputs) aren't handled very gracefully.
-Also, if there is an exception or the process is interrupted (at least on
-Windows) the worker processes are leaked and stick around forever.
-They're only a few megabytes each, but still, this script should not be run
-if you aren't prepared to manually kill a lot of orphaned processes.
-"""
-from __future__ import print_function
-import sys
-import os.path
-import time
-import struct
-from fractions import Fraction
-from collections import namedtuple
-from subprocess import Popen, check_call, PIPE
-from glob import glob
-import multiprocessing
-import threading
-import ctypes
-import binascii
-
-try:  # Python 3
-    import queue as Queue
-except ImportError:  # Python 2
-    import Queue
-
-NUM_WORKERS = 2
-UPDATE_EVERY_N = 50000
-INF = namedtuple('INF', '')()
-NEG_INF = namedtuple('NEG_INF', '')()
-ZERO = namedtuple('ZERO', '')()
-MAILBOX = None  # The queue for reporting errors to the main process.
-STDOUT_LOCK = threading.Lock()
-test_name = None
-child_processes = []
-exit_status = 0
-
-def msg(*args):
-    with STDOUT_LOCK:
-        print("[" + test_name + "]", *args)
-        sys.stdout.flush()
-
-
-def write_errors():
-    global exit_status
-    f = open("errors.txt", 'w')
-    have_seen_error = False
-    while True:
-        args = MAILBOX.get()
-        if args is None:
-            f.close()
-            break
-        print(*args, file=f)
-        f.flush()
-        if not have_seen_error:
-            have_seen_error = True
-            msg("Something is broken:", *args)
-            msg("Future errors will be logged to errors.txt")
-            exit_status = 101
-
-
-def cargo():
-    print("compiling tests")
-    sys.stdout.flush()
-    check_call(['cargo', 'build', '--release'])
-
-
-def run(test):
-    global test_name
-    test_name = test
-
-    t0 = time.perf_counter()
-    msg("setting up supervisor")
-    command = ['cargo', 'run', '--bin', test, '--release']
-    proc = Popen(command, bufsize=1<<20 , stdin=PIPE, stdout=PIPE, stderr=PIPE)
-    done = multiprocessing.Value(ctypes.c_bool)
-    queue = multiprocessing.Queue(maxsize=5)#(maxsize=1024)
-    workers = []
-    for n in range(NUM_WORKERS):
-        worker = multiprocessing.Process(name='Worker-' + str(n + 1),
-                                         target=init_worker,
-                                         args=[test, MAILBOX, queue, done])
-        workers.append(worker)
-        child_processes.append(worker)
-    for worker in workers:
-        worker.start()
-    msg("running test")
-    interact(proc, queue)
-    with done.get_lock():
-        done.value = True
-    for worker in workers:
-        worker.join()
-    msg("python is done")
-    assert queue.empty(), "did not validate everything"
-    dt = time.perf_counter() - t0
-    msg("took", round(dt, 3), "seconds")
-
-
-def interact(proc, queue):
-    n = 0
-    while proc.poll() is None:
-        line = proc.stdout.readline()
-        if not line:
-            continue
-        assert line.endswith(b'\n'), "incomplete line: " + repr(line)
-        queue.put(line)
-        n += 1
-        if n % UPDATE_EVERY_N == 0:
-            msg("got", str(n // 1000) + "k", "records")
-    msg("rust is done. exit code:", proc.returncode)
-    rest, stderr = proc.communicate()
-    if stderr:
-        msg("rust stderr output:", stderr)
-    for line in rest.split(b'\n'):
-        if not line:
-            continue
-        queue.put(line)
-
-
-def main():
-    global MAILBOX
-    files = glob('src/bin/*.rs')
-    basenames = [os.path.basename(i) for i in files]
-    all_tests = [os.path.splitext(f)[0] for f in basenames if not f.startswith('_')]
-    args = sys.argv[1:]
-    if args:
-        tests = [test for test in all_tests if test in args]
-    else:
-        tests = all_tests
-    if not tests:
-        print("Error: No tests to run")
-        sys.exit(1)
-    # Compile first for quicker feedback
-    cargo()
-    # Set up mailbox once for all tests
-    MAILBOX = multiprocessing.Queue()
-    mailman = threading.Thread(target=write_errors)
-    mailman.daemon = True
-    mailman.start()
-    for test in tests:
-        run(test)
-    MAILBOX.put(None)
-    mailman.join()
-
-
-# ---- Worker thread code ----
-
-
-POW2 = { e: Fraction(2) ** e for e in range(-1100, 1100) }
-HALF_ULP = { e: (Fraction(2) ** e)/2 for e in range(-1100, 1100) }
-DONE_FLAG = None
-
-
-def send_error_to_supervisor(*args):
-    MAILBOX.put(args)
-
-
-def init_worker(test, mailbox, queue, done):
-    global test_name, MAILBOX, DONE_FLAG
-    test_name = test
-    MAILBOX = mailbox
-    DONE_FLAG = done
-    do_work(queue)
-
-
-def is_done():
-    with DONE_FLAG.get_lock():
-        return DONE_FLAG.value
-
-
-def do_work(queue):
-    while True:
-        try:
-            line = queue.get(timeout=0.01)
-        except Queue.Empty:
-            if queue.empty() and is_done():
-                return
-            else:
-                continue
-        bin64, bin32, text = line.rstrip().split()
-        validate(bin64, bin32, text.decode('utf-8'))
-
-
-def decode_binary64(x):
-    """
-    Turn a IEEE 754 binary64 into (mantissa, exponent), except 0.0 and
-    infinity (positive and negative), which return ZERO, INF, and NEG_INF
-    respectively.
-    """
-    x = binascii.unhexlify(x)
-    assert len(x) == 8, repr(x)
-    [bits] = struct.unpack(b'>Q', x)
-    if bits == 0:
-        return ZERO
-    exponent = (bits >> 52) & 0x7FF
-    negative = bits >> 63
-    low_bits = bits & 0xFFFFFFFFFFFFF
-    if exponent == 0:
-        mantissa = low_bits
-        exponent += 1
-        if mantissa == 0:
-            return ZERO
-    elif exponent == 0x7FF:
-        assert low_bits == 0, "NaN"
-        if negative:
-            return NEG_INF
-        else:
-            return INF
-    else:
-        mantissa = low_bits | (1 << 52)
-    exponent -= 1023 + 52
-    if negative:
-        mantissa = -mantissa
-    return (mantissa, exponent)
-
-
-def decode_binary32(x):
-    """
-    Turn a IEEE 754 binary32 into (mantissa, exponent), except 0.0 and
-    infinity (positive and negative), which return ZERO, INF, and NEG_INF
-    respectively.
-    """
-    x = binascii.unhexlify(x)
-    assert len(x) == 4, repr(x)
-    [bits] = struct.unpack(b'>I', x)
-    if bits == 0:
-        return ZERO
-    exponent = (bits >> 23) & 0xFF
-    negative = bits >> 31
-    low_bits = bits & 0x7FFFFF
-    if exponent == 0:
-        mantissa = low_bits
-        exponent += 1
-        if mantissa == 0:
-            return ZERO
-    elif exponent == 0xFF:
-        if negative:
-            return NEG_INF
-        else:
-            return INF
-    else:
-        mantissa = low_bits | (1 << 23)
-    exponent -= 127 + 23
-    if negative:
-        mantissa = -mantissa
-    return (mantissa, exponent)
-
-
-MIN_SUBNORMAL_DOUBLE = Fraction(2) ** -1074
-MIN_SUBNORMAL_SINGLE = Fraction(2) ** -149  # XXX unsure
-MAX_DOUBLE = (2 - Fraction(2) ** -52) * (2 ** 1023)
-MAX_SINGLE = (2 - Fraction(2) ** -23) * (2 ** 127)
-MAX_ULP_DOUBLE = 1023 - 52
-MAX_ULP_SINGLE = 127 - 23
-DOUBLE_ZERO_CUTOFF = MIN_SUBNORMAL_DOUBLE / 2
-DOUBLE_INF_CUTOFF = MAX_DOUBLE + 2 ** (MAX_ULP_DOUBLE - 1)
-SINGLE_ZERO_CUTOFF = MIN_SUBNORMAL_SINGLE / 2
-SINGLE_INF_CUTOFF = MAX_SINGLE + 2 ** (MAX_ULP_SINGLE - 1)
-
-def validate(bin64, bin32, text):
-    try:
-        double = decode_binary64(bin64)
-    except AssertionError:
-        print(bin64, bin32, text)
-        raise
-    single = decode_binary32(bin32)
-    real = Fraction(text)
-
-    if double is ZERO:
-        if real > DOUBLE_ZERO_CUTOFF:
-            record_special_error(text, "f64 zero")
-    elif double is INF:
-        if real < DOUBLE_INF_CUTOFF:
-            record_special_error(text, "f64 inf")
-    elif double is NEG_INF:
-        if -real < DOUBLE_INF_CUTOFF:
-            record_special_error(text, "f64 -inf")
-    elif len(double) == 2:
-        sig, k = double
-        validate_normal(text, real, sig, k, "f64")
-    else:
-        assert 0, "didn't handle binary64"
-    if single is ZERO:
-        if real > SINGLE_ZERO_CUTOFF:
-            record_special_error(text, "f32 zero")
-    elif single is INF:
-        if real < SINGLE_INF_CUTOFF:
-            record_special_error(text, "f32 inf")
-    elif single is NEG_INF:
-        if -real < SINGLE_INF_CUTOFF:
-            record_special_error(text, "f32 -inf")
-    elif len(single) == 2:
-        sig, k = single
-        validate_normal(text, real, sig, k, "f32")
-    else:
-        assert 0, "didn't handle binary32"
-
-def record_special_error(text, descr):
-    send_error_to_supervisor(text.strip(), "wrongly rounded to", descr)
-
-
-def validate_normal(text, real, sig, k, kind):
-    approx = sig * POW2[k]
-    error = abs(approx - real)
-    if error > HALF_ULP[k]:
-        record_normal_error(text, error, k, kind)
-
-
-def record_normal_error(text, error, k, kind):
-    one_ulp = HALF_ULP[k + 1]
-    assert one_ulp == 2 * HALF_ULP[k]
-    relative_error = error / one_ulp
-    text = text.strip()
-    try:
-        err_repr = float(relative_error)
-    except ValueError:
-        err_repr = str(err_repr).replace('/', ' / ')
-    send_error_to_supervisor(err_repr, "ULP error on", text, "(" + kind + ")")
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/etc/test-float-parse/src/bin/few-ones.rs b/src/etc/test-float-parse/src/bin/few-ones.rs
deleted file mode 100644
index 6bb406a594771..0000000000000
--- a/src/etc/test-float-parse/src/bin/few-ones.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use test_float_parse::validate;
-
-fn main() {
-    let mut pow = vec![];
-    for i in 0..63 {
-        pow.push(1u64 << i);
-    }
-    for a in &pow {
-        for b in &pow {
-            for c in &pow {
-                validate(&(a | b | c).to_string());
-            }
-        }
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/huge-pow10.rs b/src/etc/test-float-parse/src/bin/huge-pow10.rs
deleted file mode 100644
index 722a24ffcd8d9..0000000000000
--- a/src/etc/test-float-parse/src/bin/huge-pow10.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use test_float_parse::validate;
-
-fn main() {
-    for e in 300..310 {
-        for i in 0..100000 {
-            validate(&format!("{}e{}", i, e));
-        }
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/long-fractions.rs b/src/etc/test-float-parse/src/bin/long-fractions.rs
deleted file mode 100644
index c715bc1ac2bd3..0000000000000
--- a/src/etc/test-float-parse/src/bin/long-fractions.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use std::char;
-use test_float_parse::validate;
-
-fn main() {
-    for n in 0..10 {
-        let digit = char::from_digit(n, 10).unwrap();
-        let mut s = "0.".to_string();
-        for _ in 0..400 {
-            s.push(digit);
-            if s.parse::<f64>().is_ok() {
-                validate(&s);
-            }
-        }
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/many-digits.rs b/src/etc/test-float-parse/src/bin/many-digits.rs
deleted file mode 100644
index ba166fd56079d..0000000000000
--- a/src/etc/test-float-parse/src/bin/many-digits.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-extern crate rand;
-
-use rand::distributions::{Range, Sample};
-use rand::{IsaacRng, Rng, SeedableRng};
-use std::char;
-use test_float_parse::{validate, SEED};
-
-fn main() {
-    let mut rnd = IsaacRng::from_seed(&SEED);
-    let mut range = Range::new(0, 10);
-    for _ in 0..5_000_000u64 {
-        let num_digits = rnd.gen_range(100, 400);
-        let digits = gen_digits(num_digits, &mut range, &mut rnd);
-        validate(&digits);
-    }
-}
-
-fn gen_digits<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
-}
diff --git a/src/etc/test-float-parse/src/bin/rand-f64.rs b/src/etc/test-float-parse/src/bin/rand-f64.rs
deleted file mode 100644
index 6991e8be15e1c..0000000000000
--- a/src/etc/test-float-parse/src/bin/rand-f64.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-extern crate rand;
-
-use rand::{IsaacRng, Rng, SeedableRng};
-use std::mem::transmute;
-use test_float_parse::{validate, SEED};
-
-fn main() {
-    let mut rnd = IsaacRng::from_seed(&SEED);
-    let mut i = 0;
-    while i < 10_000_000 {
-        let bits = rnd.next_u64();
-        let x: f64 = unsafe { transmute(bits) };
-        if x.is_finite() {
-            validate(&format!("{:e}", x));
-            i += 1;
-        }
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/short-decimals.rs b/src/etc/test-float-parse/src/bin/short-decimals.rs
deleted file mode 100644
index 49084eb35e834..0000000000000
--- a/src/etc/test-float-parse/src/bin/short-decimals.rs
+++ /dev/null
@@ -1,17 +0,0 @@
-use test_float_parse::validate;
-
-fn main() {
-    // Skip e = 0 because small-u32 already does those.
-    for e in 1..301 {
-        for i in 0..10000 {
-            // If it ends in zeros, the parser will strip those (and adjust the exponent),
-            // which almost always (except for exponents near +/- 300) result in an input
-            // equivalent to something we already generate in a different way.
-            if i % 10 == 0 {
-                continue;
-            }
-            validate(&format!("{}e{}", i, e));
-            validate(&format!("{}e-{}", i, e));
-        }
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/subnorm.rs b/src/etc/test-float-parse/src/bin/subnorm.rs
deleted file mode 100644
index ac88747eacd35..0000000000000
--- a/src/etc/test-float-parse/src/bin/subnorm.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use std::mem::transmute;
-use test_float_parse::validate;
-
-fn main() {
-    for bits in 0u32..(1 << 21) {
-        let single: f32 = unsafe { transmute(bits) };
-        validate(&format!("{:e}", single));
-        let double: f64 = unsafe { transmute(bits as u64) };
-        validate(&format!("{:e}", double));
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/tiny-pow10.rs b/src/etc/test-float-parse/src/bin/tiny-pow10.rs
deleted file mode 100644
index fb6ba16638044..0000000000000
--- a/src/etc/test-float-parse/src/bin/tiny-pow10.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-use test_float_parse::validate;
-
-fn main() {
-    for e in 301..327 {
-        for i in 0..100000 {
-            validate(&format!("{}e-{}", i, e));
-        }
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/u32-small.rs b/src/etc/test-float-parse/src/bin/u32-small.rs
deleted file mode 100644
index 5ec9d1eea5fbe..0000000000000
--- a/src/etc/test-float-parse/src/bin/u32-small.rs
+++ /dev/null
@@ -1,7 +0,0 @@
-use test_float_parse::validate;
-
-fn main() {
-    for i in 0..(1 << 19) {
-        validate(&i.to_string());
-    }
-}
diff --git a/src/etc/test-float-parse/src/bin/u64-pow2.rs b/src/etc/test-float-parse/src/bin/u64-pow2.rs
deleted file mode 100644
index 984e49200cda3..0000000000000
--- a/src/etc/test-float-parse/src/bin/u64-pow2.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use test_float_parse::validate;
-
-fn main() {
-    for exp in 19..64 {
-        let power: u64 = 1 << exp;
-        validate(&power.to_string());
-        for offset in 1..123 {
-            validate(&(power + offset).to_string());
-            validate(&(power - offset).to_string());
-        }
-    }
-    for offset in 0..123 {
-        validate(&(u64::MAX - offset).to_string());
-    }
-}
diff --git a/src/etc/test-float-parse/src/gen/exhaustive.rs b/src/etc/test-float-parse/src/gen/exhaustive.rs
new file mode 100644
index 0000000000000..5d4b6df8e59b9
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/exhaustive.rs
@@ -0,0 +1,43 @@
+use std::fmt::Write;
+use std::ops::RangeInclusive;
+
+use crate::{Float, Generator, Int};
+
+/// Test every possible bit pattern. This is infeasible to run on any float types larger than
+/// `f32` (which takes about an hour).
+pub struct Exhaustive<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()?))
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/exponents.rs b/src/etc/test-float-parse/src/gen/exponents.rs
new file mode 100644
index 0000000000000..3748e9d380c1e
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/exponents.rs
@@ -0,0 +1,95 @@
+use std::fmt::Write;
+use std::ops::RangeInclusive;
+
+use crate::traits::BoxGenIter;
+use crate::{Float, Generator};
+
+const SMALL_COEFF_MAX: i32 = 10_000;
+const SMALL_EXP_MAX: i32 = 300;
+
+const SMALL_COEFF_RANGE: RangeInclusive<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()
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/fuzz.rs b/src/etc/test-float-parse/src/gen/fuzz.rs
new file mode 100644
index 0000000000000..213bcfc64af0c
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/fuzz.rs
@@ -0,0 +1,88 @@
+use std::any::{type_name, TypeId};
+use std::collections::BTreeMap;
+use std::fmt::Write;
+use std::marker::PhantomData;
+use std::ops::Range;
+use std::sync::Mutex;
+
+use rand::distributions::{Distribution, Standard};
+use rand::Rng;
+use rand_chacha::rand_core::SeedableRng;
+use rand_chacha::ChaCha8Rng;
+
+use crate::{Float, Generator, Int, SEED};
+
+/// Mapping of float types to the number of iterations that should be run.
+///
+/// We could probably make `Generator::new` take an argument instead of the global state,
+/// but we only load this once so it works.
+static FUZZ_COUNTS: Mutex<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))
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/integers.rs b/src/etc/test-float-parse/src/gen/integers.rs
new file mode 100644
index 0000000000000..070d188e88c4a
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/integers.rs
@@ -0,0 +1,104 @@
+use std::fmt::Write;
+use std::ops::{Range, RangeInclusive};
+
+use crate::traits::BoxGenIter;
+use crate::{Float, Generator};
+
+const SMALL_MAX_POW2: u32 = 19;
+
+/// All values up to the max power of two
+const SMALL_VALUES: RangeInclusive<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()
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/long_fractions.rs b/src/etc/test-float-parse/src/gen/long_fractions.rs
new file mode 100644
index 0000000000000..b75148b779c1b
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/long_fractions.rs
@@ -0,0 +1,58 @@
+use std::char;
+use std::fmt::Write;
+
+use crate::{Float, Generator};
+
+/// Number of decimal digits to check (all of them).
+const MAX_DIGIT: u32 = 9;
+/// Test with this many decimals in the string.
+const MAX_DECIMALS: usize = 410;
+const PREFIX: &str = "0.";
+
+/// Test e.g. `0.1`, `0.11`, `0.111`, `0.1111`, ..., `0.2`, `0.22`, ...
+pub struct RepeatingDecimal {
+    digit: u32,
+    buf: String,
+}
+
+impl<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())
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/many_digits.rs b/src/etc/test-float-parse/src/gen/many_digits.rs
new file mode 100644
index 0000000000000..aab8d5d704bec
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/many_digits.rs
@@ -0,0 +1,84 @@
+use std::char;
+use std::fmt::Write;
+use std::marker::PhantomData;
+use std::ops::{Range, RangeInclusive};
+
+use rand::distributions::{Distribution, Uniform};
+use rand::{Rng, SeedableRng};
+use rand_chacha::ChaCha8Rng;
+
+use crate::{Float, Generator, SEED};
+
+/// Total iterations
+const ITERATIONS: u64 = 5_000_000;
+
+/// Possible lengths of the string, excluding decimals and exponents
+const POSSIBLE_NUM_DIGITS: RangeInclusive<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)
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/sparse.rs b/src/etc/test-float-parse/src/gen/sparse.rs
new file mode 100644
index 0000000000000..389b71056a3e5
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/sparse.rs
@@ -0,0 +1,100 @@
+use std::fmt::Write;
+
+use crate::traits::BoxGenIter;
+use crate::{Float, Generator};
+
+const POWERS_OF_TWO: [u128; 128] = make_powers_of_two();
+
+const fn make_powers_of_two() -> [u128; 128] {
+    let mut ret = [0; 128];
+    let mut i = 0;
+    while i < 128 {
+        ret[i] = 1 << i;
+        i += 1;
+    }
+
+    ret
+}
+
+/// Can't clone this result because of lifetime errors, just use a macro.
+macro_rules! pow_iter {
+    () => {
+        (0..F::BITS).map(|i| F::Int::try_from(POWERS_OF_TWO[i as usize]).unwrap())
+    };
+}
+
+/// Test all numbers that include three 1s in the binary representation as integers.
+pub struct FewOnesInt<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))
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/spot_checks.rs b/src/etc/test-float-parse/src/gen/spot_checks.rs
new file mode 100644
index 0000000000000..18691f9d6cf4b
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/spot_checks.rs
@@ -0,0 +1,101 @@
+use std::fmt::Write;
+
+use crate::traits::{Float, Generator};
+
+const SPECIAL: &[&str] = &[
+    "inf", "Inf", "iNf", "INF", "-inf", "-Inf", "-iNf", "-INF", "+inf", "+Inf", "+iNf", "+INF",
+    "nan", "NaN", "NAN", "nAn", "-nan", "-NaN", "-NAN", "-nAn", "+nan", "+NaN", "+NAN", "+nAn",
+    "1", "-1", "+1", "1e1", "-1e1", "+1e1", "1e-1", "-1e-1", "+1e-1", "1e+1", "-1e+1", "+1e+1",
+    "1E1", "-1E1", "+1E1", "1E-1", "-1E-1", "+1E-1", "1E+1", "-1E+1", "+1E+1", "0", "-0", "+0",
+];
+
+/// Check various non-numeric special strings.
+pub struct Special {
+    iter: std::slice::Iter<'static, &'static str>,
+}
+
+impl<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()
+    }
+}
diff --git a/src/etc/test-float-parse/src/gen/subnorm.rs b/src/etc/test-float-parse/src/gen/subnorm.rs
new file mode 100644
index 0000000000000..4fe3b90a3ddf4
--- /dev/null
+++ b/src/etc/test-float-parse/src/gen/subnorm.rs
@@ -0,0 +1,103 @@
+use std::cmp::min;
+use std::fmt::Write;
+use std::ops::RangeInclusive;
+
+use crate::{Float, Generator, Int};
+
+/// Spot check some edge cases for subnormals.
+pub struct SubnormEdgeCases<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()?))
+    }
+}
diff --git a/src/etc/test-float-parse/src/lib.rs b/src/etc/test-float-parse/src/lib.rs
index 9cbad5486b485..f36e3928d26c0 100644
--- a/src/etc/test-float-parse/src/lib.rs
+++ b/src/etc/test-float-parse/src/lib.rs
@@ -1,16 +1,526 @@
-use std::io;
-use std::io::prelude::*;
-use std::mem::transmute;
-
-// Nothing up my sleeve: Just (PI - 3) in base 16.
-#[allow(dead_code)]
-pub const SEED: [u32; 3] = [0x243f_6a88, 0x85a3_08d3, 0x1319_8a2e];
-
-pub fn validate(text: &str) {
-    let mut out = io::stdout();
-    let x: f64 = text.parse().unwrap();
-    let f64_bytes: u64 = unsafe { transmute(x) };
-    let x: f32 = text.parse().unwrap();
-    let f32_bytes: u32 = unsafe { transmute(x) };
-    writeln!(&mut out, "{:016x} {:08x} {}", f64_bytes, f32_bytes, text).unwrap();
+mod traits;
+mod ui;
+mod validate;
+
+use std::any::{type_name, TypeId};
+use std::cmp::min;
+use std::ops::RangeInclusive;
+use std::process::ExitCode;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{mpsc, OnceLock};
+use std::{fmt, time};
+
+use indicatif::{MultiProgress, ProgressBar};
+use rand::distributions::{Distribution, Standard};
+use rayon::prelude::*;
+use time::{Duration, Instant};
+use traits::{Float, Generator, Int};
+
+/// Test generators.
+mod gen {
+    pub mod exhaustive;
+    pub mod exponents;
+    pub mod fuzz;
+    pub mod integers;
+    pub mod long_fractions;
+    pub mod many_digits;
+    pub mod sparse;
+    pub mod spot_checks;
+    pub mod subnorm;
+}
+
+/// How many failures to exit after if unspecified.
+const DEFAULT_MAX_FAILURES: u64 = 20;
+
+/// Register exhaustive tests only for <= 32 bits. No more because it would take years.
+const MAX_BITS_FOR_EXHAUUSTIVE: u32 = 32;
+
+/// If there are more tests than this threashold, the test will be defered until after all
+/// others run (so as to avoid thread pool starvation). They also can be excluded with
+/// `--skip-huge`.
+const HUGE_TEST_CUTOFF: u64 = 5_000_000;
+
+/// Seed for tests that use a deterministic RNG.
+const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
+
+/// Global configuration
+#[derive(Debug)]
+pub struct Config {
+    pub timeout: Duration,
+    /// Failures per test
+    pub max_failures: u64,
+    pub disable_max_failures: bool,
+    /// If `None`, the default will be used
+    pub fuzz_count: Option<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();
 }
diff --git a/src/etc/test-float-parse/src/main.rs b/src/etc/test-float-parse/src/main.rs
new file mode 100644
index 0000000000000..9c6cad7324f74
--- /dev/null
+++ b/src/etc/test-float-parse/src/main.rs
@@ -0,0 +1,129 @@
+use std::process::ExitCode;
+use std::time::Duration;
+
+use test_float_parse as tfp;
+
+static HELP: &str = r#"Usage:
+
+  ./test-float-parse [--timeout x] [--exclude x] [--max-failures x] [INCLUDE ...]
+  ./test-float-parse [--fuzz-count x] [INCLUDE ...]
+  ./test-float-parse [--skip-huge] [INCLUDE ...]
+  ./test-float-parse --list
+
+Args:
+
+  INCLUDE                  Include only tests with names containing these
+                           strings. If this argument is not specified, all tests
+                           are run.
+  --timeout N              Exit after this amount of time (in seconds).
+  --exclude FILTER         Skip tests containing this string. May be specified
+                           more than once.
+  --list                   List available tests.
+  --max-failures N         Limit to N failures per test. Defaults to 20. Pass
+                           "--max-failures none" to remove this limit.
+  --fuzz-count N           Run the fuzzer with N iterations. Only has an effect
+                           if fuzz tests are enabled. Pass `--fuzz-count none`
+                           to remove this limit.
+  --skip-huge              Skip tests that run for a long time.
+  --all                    Reset previous `--exclude`, `--skip-huge`, and
+                           `INCLUDE` arguments (useful for running all tests
+                           via `./x`).
+"#;
+
+enum ArgMode {
+    Any,
+    Timeout,
+    Exclude,
+    FuzzCount,
+    MaxFailures,
+}
+
+fn main() -> ExitCode {
+    if cfg!(debug_assertions) {
+        println!(
+            "WARNING: running in debug mode. Release mode is recommended to reduce test duration."
+        );
+        std::thread::sleep(Duration::from_secs(2));
+    }
+
+    let args: Vec<_> = std::env::args().skip(1).collect();
+    if args.iter().any(|arg| arg == "--help" || arg == "-h") {
+        println!("{HELP}");
+        return ExitCode::SUCCESS;
+    }
+
+    if args.iter().any(|arg| arg == "--list") {
+        let tests = tfp::register_tests(&tfp::Config::default());
+        println!("Available tests:");
+        for t in tests {
+            println!("{}", t.name);
+        }
+
+        return ExitCode::SUCCESS;
+    }
+
+    let (cfg, include, exclude) = parse_args(args);
+
+    tfp::run(cfg, &include, &exclude)
+}
+
+/// Simple command argument parser
+fn parse_args(args: Vec<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)
+}
diff --git a/src/etc/test-float-parse/src/traits.rs b/src/etc/test-float-parse/src/traits.rs
new file mode 100644
index 0000000000000..dc009ea235f95
--- /dev/null
+++ b/src/etc/test-float-parse/src/traits.rs
@@ -0,0 +1,202 @@
+//! Interfaces used throughout this crate.
+
+use std::str::FromStr;
+use std::{fmt, ops};
+
+use num::bigint::ToBigInt;
+use num::Integer;
+
+use crate::validate::Constants;
+
+/// Integer types.
+#[allow(dead_code)] // Some functions only used for testing
+pub trait Int:
+    Clone
+    + Copy
+    + fmt::Debug
+    + fmt::Display
+    + fmt::LowerHex
+    + ops::Add<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>;
diff --git a/src/etc/test-float-parse/src/ui.rs b/src/etc/test-float-parse/src/ui.rs
new file mode 100644
index 0000000000000..f333bd4a55dc9
--- /dev/null
+++ b/src/etc/test-float-parse/src/ui.rs
@@ -0,0 +1,132 @@
+//! Progress bars and such.
+
+use std::io::{self, Write};
+use std::process::ExitCode;
+use std::time::Duration;
+
+use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
+
+use crate::{Completed, Config, EarlyExit, FinishedAll, TestInfo};
+
+/// Templates for progress bars.
+const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME ({pos}/{len}, {msg} f, {per_sec}, eta {eta})";
+const PB_TEMPLATE_FINAL: &str =
+    "[{elapsed:3} {percent:3}%] NAME ({pos}/{len}, {msg:.COLOR}, {per_sec}, {elapsed_precise})";
+
+/// Create a new progress bar within a multiprogress bar.
+pub fn create_pb(
+    mp: &MultiProgress,
+    total_tests: u64,
+    short_name_padded: &str,
+    all_bars: &mut Vec<ProgressBar>,
+) -> 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);
+    }));
+}
diff --git a/src/etc/test-float-parse/src/validate.rs b/src/etc/test-float-parse/src/validate.rs
new file mode 100644
index 0000000000000..1eb3699cfb9f7
--- /dev/null
+++ b/src/etc/test-float-parse/src/validate.rs
@@ -0,0 +1,364 @@
+//! Everything related to verifying that parsed outputs are correct.
+
+use std::any::type_name;
+use std::collections::BTreeMap;
+use std::ops::RangeInclusive;
+use std::str::FromStr;
+use std::sync::LazyLock;
+
+use num::bigint::ToBigInt;
+use num::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive};
+
+use crate::{CheckFailure, Float, Int, Update};
+
+/// Powers of two that we store for constants. Account for binary128 which has a 15-bit exponent.
+const POWERS_OF_TWO_RANGE: RangeInclusive<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;
diff --git a/src/etc/test-float-parse/src/validate/tests.rs b/src/etc/test-float-parse/src/validate/tests.rs
new file mode 100644
index 0000000000000..ab0e7d8a7ba91
--- /dev/null
+++ b/src/etc/test-float-parse/src/validate/tests.rs
@@ -0,0 +1,149 @@
+use num::ToPrimitive;
+
+use super::*;
+
+#[test]
+fn test_parse_rational() {
+    assert_eq!(Rational::parse("1234").expect_finite(), BigRational::new(1234.into(), 1.into()));
+    assert_eq!(
+        Rational::parse("-1234").expect_finite(),
+        BigRational::new((-1234).into(), 1.into())
+    );
+    assert_eq!(Rational::parse("1e+6").expect_finite(), BigRational::new(1000000.into(), 1.into()));
+    assert_eq!(Rational::parse("1e-6").expect_finite(), BigRational::new(1.into(), 1000000.into()));
+    assert_eq!(
+        Rational::parse("10.4e6").expect_finite(),
+        BigRational::new(10400000.into(), 1.into())
+    );
+    assert_eq!(
+        Rational::parse("10.4e+6").expect_finite(),
+        BigRational::new(10400000.into(), 1.into())
+    );
+    assert_eq!(
+        Rational::parse("10.4e-6").expect_finite(),
+        BigRational::new(13.into(), 1250000.into())
+    );
+    assert_eq!(
+        Rational::parse("10.4243566462342456234124").expect_finite(),
+        BigRational::new(104243566462342456234124_i128.into(), 10000000000000000000000_i128.into())
+    );
+    assert_eq!(Rational::parse("inf"), Rational::Inf);
+    assert_eq!(Rational::parse("+inf"), Rational::Inf);
+    assert_eq!(Rational::parse("-inf"), Rational::NegInf);
+    assert_eq!(Rational::parse("NaN"), Rational::Nan);
+}
+
+#[test]
+fn test_decode() {
+    assert_eq!(decode(0f32), FloatRes::Zero);
+    assert_eq!(decode(f32::INFINITY), FloatRes::Inf);
+    assert_eq!(decode(f32::NEG_INFINITY), FloatRes::NegInf);
+    assert_eq!(decode(1.0f32).normalize(), FloatRes::Real { sig: 1, exp: 0 });
+    assert_eq!(decode(-1.0f32).normalize(), FloatRes::Real { sig: -1, exp: 0 });
+    assert_eq!(decode(100.0f32).normalize(), FloatRes::Real { sig: 100, exp: 0 });
+    assert_eq!(decode(100.5f32).normalize(), FloatRes::Real { sig: 201, exp: -1 });
+    assert_eq!(decode(-4.004f32).normalize(), FloatRes::Real { sig: -8396997, exp: -21 });
+    assert_eq!(decode(0.0004f32).normalize(), FloatRes::Real { sig: 13743895, exp: -35 });
+    assert_eq!(decode(f32::from_bits(0x1)).normalize(), FloatRes::Real { sig: 1, exp: -149 });
+}
+
+#[test]
+fn test_validate() {
+    validate::<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));
+}
diff --git a/src/tools/build-manifest/src/main.rs b/src/tools/build-manifest/src/main.rs
index a2e7907b532e4..ce44593e6e50c 100644
--- a/src/tools/build-manifest/src/main.rs
+++ b/src/tools/build-manifest/src/main.rs
@@ -54,6 +54,7 @@ static TARGETS: &[&str] = &[
     "arm64e-apple-darwin",
     "aarch64-apple-ios",
     "arm64e-apple-ios",
+    "aarch64-apple-ios-macabi",
     "aarch64-apple-ios-sim",
     "aarch64-unknown-fuchsia",
     "aarch64-linux-android",
@@ -159,6 +160,7 @@ static TARGETS: &[&str] = &[
     "wasm32-wasip1-threads",
     "x86_64-apple-darwin",
     "x86_64-apple-ios",
+    "x86_64-apple-ios-macabi",
     "x86_64-fortanix-unknown-sgx",
     "x86_64-unknown-fuchsia",
     "x86_64-linux-android",
diff --git a/src/tools/tidy/src/allowed_run_make_makefiles.txt b/src/tools/tidy/src/allowed_run_make_makefiles.txt
index 4b50bcb38fd01..abcd42a93bee1 100644
--- a/src/tools/tidy/src/allowed_run_make_makefiles.txt
+++ b/src/tools/tidy/src/allowed_run_make_makefiles.txt
@@ -4,10 +4,8 @@ run-make/c-dynamic-rlib/Makefile
 run-make/c-static-dylib/Makefile
 run-make/c-static-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
@@ -33,11 +31,8 @@ run-make/foreign-rust-exceptions/Makefile
 run-make/incr-add-rust-src-component/Makefile
 run-make/incr-foreign-head-span/Makefile
 run-make/interdependent-c-libraries/Makefile
-run-make/issue-107094/Makefile
-run-make/issue-14698/Makefile
 run-make/issue-15460/Makefile
 run-make/issue-28595/Makefile
-run-make/issue-33329/Makefile
 run-make/issue-35164/Makefile
 run-make/issue-36710/Makefile
 run-make/issue-47551/Makefile
@@ -99,7 +94,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
diff --git a/src/tools/tidy/src/deps.rs b/src/tools/tidy/src/deps.rs
index f9bf04626f785..286cfbf26292f 100644
--- a/src/tools/tidy/src/deps.rs
+++ b/src/tools/tidy/src/deps.rs
@@ -65,7 +65,7 @@ pub(crate) const WORKSPACES: &[(&str, ExceptionList, Option<(&[&str], &[&str])>)
     //("library/stdarch", EXCEPTIONS_STDARCH, None), // FIXME uncomment once rust-lang/stdarch#1462 has been synced back to the rust repo
     ("src/bootstrap", EXCEPTIONS_BOOTSTRAP, None),
     ("src/ci/docker/host-x86_64/test-various/uefi_qemu_test", EXCEPTIONS_UEFI_QEMU_TEST, None),
-    //("src/etc/test-float-parse", &[], None), // FIXME uncomment once all deps are vendored
+    ("src/etc/test-float-parse", EXCEPTIONS, None),
     ("src/tools/cargo", EXCEPTIONS_CARGO, None),
     //("src/tools/miri/test-cargo-miri", &[], None), // FIXME uncomment once all deps are vendored
     //("src/tools/miri/test_dependencies", &[], None), // FIXME uncomment once all deps are vendored
diff --git a/tests/run-make/c-unwind-abi-catch-panic/Makefile b/tests/run-make/c-unwind-abi-catch-panic/Makefile
deleted file mode 100644
index 0a38d838e32a7..0000000000000
--- a/tests/run-make/c-unwind-abi-catch-panic/Makefile
+++ /dev/null
@@ -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
diff --git a/tests/run-make/c-unwind-abi-catch-panic/rmake.rs b/tests/run-make/c-unwind-abi-catch-panic/rmake.rs
new file mode 100644
index 0000000000000..a99dbd18ec619
--- /dev/null
+++ b/tests/run-make/c-unwind-abi-catch-panic/rmake.rs
@@ -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");
+}
diff --git a/tests/run-make/compiler-lookup-paths-2/Makefile b/tests/run-make/compiler-lookup-paths-2/Makefile
deleted file mode 100644
index ecc0577384ab6..0000000000000
--- a/tests/run-make/compiler-lookup-paths-2/Makefile
+++ /dev/null
@@ -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
diff --git a/tests/run-make/compiler-lookup-paths-2/rmake.rs b/tests/run-make/compiler-lookup-paths-2/rmake.rs
new file mode 100644
index 0000000000000..99efb157b5378
--- /dev/null
+++ b/tests/run-make/compiler-lookup-paths-2/rmake.rs
@@ -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();
+}
diff --git a/tests/run-make/issue-14698/foo.rs b/tests/run-make/invalid-tmpdir-env-var/foo.rs
similarity index 100%
rename from tests/run-make/issue-14698/foo.rs
rename to tests/run-make/invalid-tmpdir-env-var/foo.rs
diff --git a/tests/run-make/invalid-tmpdir-env-var/rmake.rs b/tests/run-make/invalid-tmpdir-env-var/rmake.rs
new file mode 100644
index 0000000000000..db44debb319eb
--- /dev/null
+++ b/tests/run-make/invalid-tmpdir-env-var/rmake.rs
@@ -0,0 +1,20 @@
+// When the TMP (on Windows) or TMPDIR (on Unix) variable is set to an invalid
+// or non-existing directory, this used to cause an internal compiler error (ICE). After the
+// addition of proper error handling in #28430, this test checks that the expected message is
+// printed.
+// See https://github.com/rust-lang/rust/issues/14698
+
+use run_make_support::{is_windows, rustc};
+
+// NOTE: This is not a UI test despite its simplicity, as the error message contains a path
+// with some variability that is difficult to normalize
+
+fn main() {
+    let mut rustc = rustc();
+    if is_windows() {
+        rustc.env("TMP", "fake");
+    } else {
+        rustc.env("TMPDIR", "fake");
+    }
+    rustc.input("foo.rs").run_fail().assert_stderr_contains("couldn't create a temp dir");
+}
diff --git a/tests/run-make/issue-107094/Makefile b/tests/run-make/issue-107094/Makefile
deleted file mode 100644
index d614e3e1055c0..0000000000000
--- a/tests/run-make/issue-107094/Makefile
+++ /dev/null
@@ -1,7 +0,0 @@
-# needs-git-hash
-
-include ../tools.mk
-
-all:
-	$(BARE_RUSTC) --version --verbose | $(CGREP) -i -e "commit-hash: [0-9a-f]{40}" "commit-date: [0-9]{4}-[0-9]{2}-[0-9]{2}"
-	$(BARE_RUSTDOC) --version --verbose | $(CGREP) -i -e "commit-hash: [0-9a-f]{40}" "commit-date: [0-9]{4}-[0-9]{2}-[0-9]{2}"
diff --git a/tests/run-make/issue-14698/Makefile b/tests/run-make/issue-14698/Makefile
deleted file mode 100644
index a1cfb5abab541..0000000000000
--- a/tests/run-make/issue-14698/Makefile
+++ /dev/null
@@ -1,4 +0,0 @@
-include ../tools.mk
-
-all:
-	TMP=fake TMPDIR=fake $(RUSTC) foo.rs 2>&1 | $(CGREP) "couldn't create a temp dir:"
diff --git a/tests/run-make/issue-33329/Makefile b/tests/run-make/issue-33329/Makefile
deleted file mode 100644
index 9c149440d8ec4..0000000000000
--- a/tests/run-make/issue-33329/Makefile
+++ /dev/null
@@ -1,5 +0,0 @@
-include ../tools.mk
-
-all:
-	$(RUSTC) --target x86_64_unknown-linux-musl main.rs 2>&1 | $(CGREP) \
-		"error: Error loading target specification: Could not find specification for target"
diff --git a/tests/run-make/issue-33329/main.rs b/tests/run-make/issue-33329/main.rs
deleted file mode 100644
index f328e4d9d04c3..0000000000000
--- a/tests/run-make/issue-33329/main.rs
+++ /dev/null
@@ -1 +0,0 @@
-fn main() {}
diff --git a/tests/run-make/test-benches/Makefile b/tests/run-make/test-benches/Makefile
deleted file mode 100644
index 11aed2e4c79a6..0000000000000
--- a/tests/run-make/test-benches/Makefile
+++ /dev/null
@@ -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)
diff --git a/tests/run-make/test-benches/rmake.rs b/tests/run-make/test-benches/rmake.rs
new file mode 100644
index 0000000000000..1458fb8c9904a
--- /dev/null
+++ b/tests/run-make/test-benches/rmake.rs
@@ -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");
+}
diff --git a/tests/run-make/version-verbose-commit-hash/rmake.rs b/tests/run-make/version-verbose-commit-hash/rmake.rs
new file mode 100644
index 0000000000000..733c0e2cdb14f
--- /dev/null
+++ b/tests/run-make/version-verbose-commit-hash/rmake.rs
@@ -0,0 +1,20 @@
+// `--version --verbose` should display the git-commit hashes of rustc and rustdoc, but this
+// functionality was lost due to #104184. After this feature was returned by #109981, this
+// test ensures it will not be broken again.
+// See https://github.com/rust-lang/rust/issues/107094
+
+//@ needs-git-hash
+
+use run_make_support::{bare_rustc, bare_rustdoc, regex};
+
+fn main() {
+    let out_rustc =
+        bare_rustc().arg("--version").arg("--verbose").run().stdout_utf8().to_lowercase();
+    let out_rustdoc =
+        bare_rustdoc().arg("--version").arg("--verbose").run().stdout_utf8().to_lowercase();
+    let re =
+        regex::Regex::new(r#"commit-hash: [0-9a-f]{40}\ncommit-date: [0-9]{4}-[0-9]{2}-[0-9]{2}"#)
+            .unwrap();
+    assert!(re.is_match(&out_rustc));
+    assert!(re.is_match(&out_rustdoc));
+}
diff --git a/tests/run-make/wasm-panic-small/rmake.rs b/tests/run-make/wasm-panic-small/rmake.rs
index 8d0944ed98d5b..e69fbac96356a 100644
--- a/tests/run-make/wasm-panic-small/rmake.rs
+++ b/tests/run-make/wasm-panic-small/rmake.rs
@@ -13,7 +13,14 @@ fn main() {
 fn test(cfg: &str) {
     eprintln!("running cfg {cfg:?}");
 
-    rustc().input("foo.rs").target("wasm32-wasip1").arg("-Clto").opt().cfg(cfg).run();
+    rustc()
+        .input("foo.rs")
+        .target("wasm32-wasip1")
+        .arg("-Clto")
+        .arg("-Cstrip=debuginfo")
+        .opt()
+        .cfg(cfg)
+        .run();
 
     let bytes = rfs::read("foo.wasm");
     println!("{}", bytes.len());
diff --git a/tests/run-make/wasm-stringify-ints-small/rmake.rs b/tests/run-make/wasm-stringify-ints-small/rmake.rs
index 93eb38b098766..c0448c59c037b 100644
--- a/tests/run-make/wasm-stringify-ints-small/rmake.rs
+++ b/tests/run-make/wasm-stringify-ints-small/rmake.rs
@@ -4,7 +4,13 @@
 use run_make_support::{rfs, rustc};
 
 fn main() {
-    rustc().input("foo.rs").target("wasm32-wasip1").arg("-Clto").opt().run();
+    rustc()
+        .input("foo.rs")
+        .target("wasm32-wasip1")
+        .arg("-Clto")
+        .arg("-Cstrip=debuginfo")
+        .opt()
+        .run();
 
     let bytes = rfs::read("foo.wasm");
     println!("{}", bytes.len());
diff --git a/tests/ui/errors/wrong-target-spec.rs b/tests/ui/errors/wrong-target-spec.rs
new file mode 100644
index 0000000000000..bc9038c1fab54
--- /dev/null
+++ b/tests/ui/errors/wrong-target-spec.rs
@@ -0,0 +1,8 @@
+// The attentive may note the underscores in the target triple, making it invalid. This test
+// checks that such invalid target specs are rejected by the compiler.
+// See https://github.com/rust-lang/rust/issues/33329
+
+//@ needs-llvm-components: x86
+//@ compile-flags: --target x86_64_unknown-linux-musl
+
+fn main() {}
diff --git a/tests/ui/errors/wrong-target-spec.stderr b/tests/ui/errors/wrong-target-spec.stderr
new file mode 100644
index 0000000000000..8b06f404078d0
--- /dev/null
+++ b/tests/ui/errors/wrong-target-spec.stderr
@@ -0,0 +1,2 @@
+error: Error loading target specification: Could not find specification for target "x86_64_unknown-linux-musl". Run `rustc --print target-list` for a list of built-in targets
+