about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorTrevor Gross <tmgross@umich.edu>2024-06-20 01:27:42 -0500
committerTrevor Gross <tmgross@umich.edu>2024-07-04 15:45:41 -0400
commitc5e9513aa2aee071d05bdf0acae9c110aaa040ff (patch)
treed2c265925ee985a75d9c65a8de88577a1fbcee65 /src
parenta3460e23ea42a83b48eb60d61586a8bb22e09290 (diff)
downloadrust-c5e9513aa2aee071d05bdf0acae9c110aaa040ff.tar.gz
rust-c5e9513aa2aee071d05bdf0acae9c110aaa040ff.zip
Refactor float casting tests
This is an attempt to remove the magic from a lot of the numbers tested,
which should make things easier when it is time to add `f16` and `f128`.
A nice side effect is that these tests now cover all int <-> float
conversions with the same amount of tests.

Co-authored-by: Ralf Jung <post@ralfj.de>
Diffstat (limited to 'src')
-rw-r--r--src/tools/miri/tests/pass/float.rs576
1 files changed, 390 insertions, 186 deletions
diff --git a/src/tools/miri/tests/pass/float.rs b/src/tools/miri/tests/pass/float.rs
index 5464627fa14..a4589847fc1 100644
--- a/src/tools/miri/tests/pass/float.rs
+++ b/src/tools/miri/tests/pass/float.rs
@@ -4,8 +4,11 @@
 #![feature(f128)]
 #![feature(f16)]
 #![allow(arithmetic_overflow)]
+#![allow(internal_features)]
 
-use std::fmt::Debug;
+use std::any::type_name;
+use std::cmp::min;
+use std::fmt::{Debug, Display, LowerHex};
 use std::hint::black_box;
 use std::{f32, f64};
 
@@ -29,15 +32,39 @@ fn main() {
     test_algebraic();
 }
 
-// Helper function to avoid promotion so that this tests "run-time" casts, not CTFE.
-// Doesn't make a big difference when running this in Miri, but it means we can compare this
-// with the LLVM backend by running `rustc -Zmir-opt-level=0 -Zsaturating-float-casts`.
-#[track_caller]
-#[inline(never)]
-fn assert_eq<T: PartialEq + Debug>(x: T, y: T) {
-    assert_eq!(x, y);
+trait Float: Copy + PartialEq + Debug {
+    /// The unsigned integer with the same bit width as this float
+    type Int: Copy + PartialEq + LowerHex + Debug;
+    const BITS: u32 = size_of::<Self>() as u32 * 8;
+    const EXPONENT_BITS: u32 = Self::BITS - Self::SIGNIFICAND_BITS - 1;
+    const SIGNIFICAND_BITS: u32;
+
+    /// The saturated (all ones) value of the exponent (infinity representation)
+    const EXPONENT_SAT: u32 = (1 << Self::EXPONENT_BITS) - 1;
+
+    /// The exponent bias value (max representable positive exponent)
+    const EXPONENT_BIAS: u32 = Self::EXPONENT_SAT >> 1;
+
+    fn to_bits(self) -> Self::Int;
+}
+
+macro_rules! impl_float {
+    ($ty:ty, $ity:ty) => {
+        impl Float for $ty {
+            type Int = $ity;
+            // Just get this from std's value, which includes the implicit digit
+            const SIGNIFICAND_BITS: u32 = <$ty>::MANTISSA_DIGITS - 1;
+
+            fn to_bits(self) -> Self::Int {
+                self.to_bits()
+            }
+        }
+    };
 }
 
+impl_float!(f32, u32);
+impl_float!(f64, u64);
+
 trait FloatToInt<Int>: Copy {
     fn cast(self) -> Int;
     unsafe fn cast_unchecked(self) -> Int;
@@ -64,13 +91,53 @@ float_to_int!(f64 => i8, u8, i16, u16, i32, u32, i64, u64, i128, u128);
 /// Test this cast both via `as` and via `approx_unchecked` (i.e., it must not saturate).
 #[track_caller]
 #[inline(never)]
-fn test_both_cast<F, I>(x: F, y: I)
+fn test_both_cast<F, I>(x: F, y: I, msg: impl Display)
 where
     F: FloatToInt<I>,
     I: PartialEq + Debug,
 {
-    assert_eq!(x.cast(), y);
-    assert_eq!(unsafe { x.cast_unchecked() }, y);
+    let f_tname = type_name::<F>();
+    let i_tname = type_name::<I>();
+    assert_eq!(x.cast(), y, "{f_tname} -> {i_tname}: {msg}");
+    assert_eq!(unsafe { x.cast_unchecked() }, y, "{f_tname} -> {i_tname}: {msg}",);
+}
+
+/// Helper function to avoid promotion so that this tests "run-time" casts, not CTFE.
+/// Doesn't make a big difference when running this in Miri, but it means we can compare this
+/// with the LLVM backend by running `rustc -Zmir-opt-level=0 -Zsaturating-float-casts`.
+#[track_caller]
+#[inline(never)]
+fn assert_eq<T: PartialEq + Debug>(x: T, y: T) {
+    assert_eq!(x, y);
+}
+
+/// The same as `assert_eq` except prints a specific message on failure
+#[track_caller]
+#[inline(never)]
+fn assert_eq_msg<T: PartialEq + Debug>(x: T, y: T, msg: impl Display) {
+    assert_eq!(x, y, "{msg}");
+}
+
+/// Check that floats have bitwise equality
+fn assert_biteq<F: Float>(a: F, b: F, msg: impl Display) {
+    let ab = a.to_bits();
+    let bb = b.to_bits();
+    let tname = type_name::<F>();
+    let width = (2 + F::BITS / 4) as usize;
+    assert_eq_msg::<F::Int>(
+        ab,
+        bb,
+        format_args!("({ab:#0width$x} != {bb:#0width$x}) {tname}: {msg}"),
+    );
+}
+
+/// Check that two floats have equality
+fn assert_feq<F: Float>(a: F, b: F, msg: impl Display) {
+    let ab = a.to_bits();
+    let bb = b.to_bits();
+    let tname = type_name::<F>();
+    let width = (2 + F::BITS / 4) as usize;
+    assert_eq_msg::<F>(a, b, format_args!("({ab:#0width$x} != {bb:#0width$x}) {tname}: {msg}"));
 }
 
 fn basic() {
@@ -148,155 +215,316 @@ fn basic() {
     assert_eq!(34.2f64.abs(), 34.2f64);
 }
 
-/// Many of these test values are taken from
+/// Test casts from floats to ints and back
+macro_rules! test_ftoi_itof {
+    (
+        f: $fty:ty,
+        i: $ity:ty,
+        // Int min and max as float literals
+        imin_f: $imin_f:literal,
+        imax_f: $imax_f:literal $(,)?
+    ) => {{
+        /// By default we test float to int `as` casting as well as to_int_unchecked
+        fn assert_ftoi(f: $fty, i: $ity, msg: &str) {
+            #[allow(unused_comparisons)]
+            if <$ity>::MIN >= 0 && f < 0.0 {
+                // If `ity` is signed and `f` is negative, it is unrepresentable so skip
+                // unchecked casts.
+                assert_ftoi_unrep(f, i, msg);
+            } else {
+                test_both_cast::<$fty, $ity>(f, i, msg);
+            }
+        }
+
+        /// Unrepresentable values only get tested with `as` casting, not unchecked
+        fn assert_ftoi_unrep(f: $fty, i: $ity, msg: &str) {
+            assert_eq_msg::<$ity>(
+                f as $ity,
+                i,
+                format_args!("{} -> {}: {msg}", stringify!($fty), stringify!($ity)),
+            );
+        }
+
+        /// Int to float checks
+        fn assert_itof(i: $ity, f: $fty, msg: &str) {
+            assert_eq_msg::<$fty>(
+                i as $fty,
+                f,
+                format_args!("{} -> {}: {msg}", stringify!($ity), stringify!($fty)),
+            );
+        }
+
+        /// Check both float to int and int to float
+        fn assert_bidir(f: $fty, i: $ity, msg: &str) {
+            assert_ftoi(f, i, msg);
+            assert_itof(i, f, msg);
+        }
+
+        /// Check both float to int and int to float for unrepresentable numbers
+        fn assert_bidir_unrep(f: $fty, i: $ity, msg: &str) {
+            assert_ftoi_unrep(f, i, msg);
+            assert_itof(i, f, msg);
+        }
+
+        let fbits = <$fty>::BITS;
+        let fsig_bits = <$fty>::SIGNIFICAND_BITS;
+        let ibits = <$ity>::BITS;
+        let imax: $ity = <$ity>::MAX;
+        let imin: $ity = <$ity>::MIN;
+        let izero: $ity = 0;
+        #[allow(unused_comparisons)]
+        let isigned = <$ity>::MIN < 0;
+
+        #[allow(overflowing_literals)]
+        let imin_f: $fty = $imin_f;
+        #[allow(overflowing_literals)]
+        let imax_f: $fty = $imax_f;
+
+        // If an integer can fit entirely in the mantissa (counting the hidden bit), every value
+        // can be represented exactly.
+        let all_ints_exact_rep = ibits <= fsig_bits + 1;
+
+        // We can represent the full range of the integer (but possibly not every value) without
+        // saturating to infinity if `1 << (I::BITS - 1)` (single one in the MSB position) is
+        // within the float's dynamic range.
+        let int_range_rep = ibits - 1 < <$fty>::EXPONENT_BIAS;
+
+        // Skip unchecked cast when int min/max would be unrepresentable
+        let assert_ftoi_big = if all_ints_exact_rep { assert_ftoi } else { assert_ftoi_unrep };
+        let assert_bidir_big = if all_ints_exact_rep { assert_bidir } else { assert_bidir_unrep };
+
+        // Near zero representations
+        assert_bidir(0.0, 0, "zero");
+        assert_ftoi(-0.0, 0, "negative zero");
+        assert_ftoi(1.0, 1, "one");
+        assert_ftoi(-1.0, izero.saturating_sub(1), "negative one");
+        assert_ftoi(1.0 - <$fty>::EPSILON, 0, "1.0 - ε");
+        assert_ftoi(1.0 + <$fty>::EPSILON, 1, "1.0 + ε");
+        assert_ftoi(-1.0 + <$fty>::EPSILON, 0, "-1.0 + ε");
+        assert_ftoi(-1.0 - <$fty>::EPSILON, izero.saturating_sub(1), "-1.0 - ε");
+        assert_ftoi(<$fty>::from_bits(0x1), 0, "min subnormal");
+        assert_ftoi(<$fty>::from_bits(0x1 | 1 << (fbits - 1)), 0, "min neg subnormal");
+
+        // Spot checks. Use `saturating_sub` to create negative integers so that unsigned
+        // integers stay at zero.
+        assert_ftoi(0.9, 0, "0.9");
+        assert_ftoi(-0.9, 0, "-0.9");
+        assert_ftoi(1.1, 1, "1.1");
+        assert_ftoi(-1.1, izero.saturating_sub(1), "-1.1");
+        assert_ftoi(1.9, 1, "1.9");
+        assert_ftoi(-1.9, izero.saturating_sub(1), "-1.9");
+        assert_ftoi(5.0, 5, "5.0");
+        assert_ftoi(-5.0, izero.saturating_sub(5), "-5.0");
+        assert_ftoi(5.9, 5, "5.0");
+        assert_ftoi(-5.9, izero.saturating_sub(5), "-5.0");
+
+        // Exercise the middle of the integer's bit range. A power of two fits as long as the
+        // exponent can fit its log2, so cap at the maximum representable power of two (which
+        // is the exponent's bias).
+        let half_i_max: $ity = 1 << min(ibits / 2, <$fty>::EXPONENT_BIAS);
+        let half_i_min = izero.saturating_sub(half_i_max);
+        assert_bidir(half_i_max as $fty, half_i_max, "half int max");
+        assert_bidir(half_i_min as $fty, half_i_min, "half int min");
+
+        // Integer limits
+        assert_bidir_big(imax_f, imax, "i max");
+        assert_bidir_big(imin_f, imin, "i min");
+
+        // We need a small perturbation to test against that does not round up to the next
+        // integer. `f16` needs a smaller perturbation since it only has resolution for ~1 decimal
+        // place near 10^3.
+        let perturb = if fbits < 32 { 0.9 } else { 0.99 };
+        assert_ftoi_big(imax_f + perturb, <$ity>::MAX, "slightly above i max");
+        assert_ftoi_big(imin_f - perturb, <$ity>::MIN, "slightly below i min");
+
+        // Tests for when we can represent the integer's magnitude
+        if int_range_rep {
+            // If the float can represent values larger than the integer, float extremes
+            // will saturate.
+            assert_ftoi_unrep(<$fty>::MAX, imax, "f max");
+            assert_ftoi_unrep(<$fty>::MIN, imin, "f min");
+
+            // Max representable power of 10
+            let pow10_max = (10 as $ity).pow(imax.ilog10());
+
+            // If the power of 10 should be representable (fits in a mantissa), check it
+            if ibits - pow10_max.leading_zeros() - pow10_max.trailing_zeros() <= fsig_bits + 1 {
+                assert_bidir(pow10_max as $fty, pow10_max, "pow10 max");
+            }
+        }
+
+        // Test rounding the largest and smallest integers, but skip this when
+        // all integers have an exact representation (it's less interesting then and the arithmetic gets more complicated).
+        if int_range_rep && !all_ints_exact_rep {
+            // The maximum representable integer is a saturated mantissa (including the implicit
+            // bit), shifted into the int's leftmost position.
+            //
+            // Positive signed integers never use their top bit, so shift by one bit fewer.
+            let sat_mantissa: $ity = (1 << (fsig_bits + 1)) - 1;
+            let adj = if isigned { 1 } else { 0 };
+            let max_rep = sat_mantissa << (sat_mantissa.leading_zeros() - adj);
+
+            // This value should roundtrip exactly
+            assert_bidir(max_rep as $fty, max_rep, "max representable int");
+
+            // The cutoff for where to round to `imax` is halfway between the maximum exactly
+            // representable integer and `imax`. This should round down (to `max_rep`),
+            // i.e., `max_rep as $fty == max_non_sat as $fty`.
+            let max_non_sat = max_rep + ((imax - max_rep) / 2);
+            assert_bidir(max_non_sat as $fty, max_rep, "max non saturating int");
+
+            // So the next value up should round up to the maximum value of the integer
+            assert_bidir_unrep((max_non_sat + 1) as $fty, imax, "min infinite int");
+
+            if isigned {
+                // Floats can always represent the minimum signed number if they can fit the
+                // exponent, because it is just a `1` in the MSB. So, no negative int -> float
+                // conversion will round to negative infinity (if the exponent fits).
+                //
+                // Since `imin` is thus the minimum representable value, we test rounding near
+                // the next value. This happens to be the opposite of the maximum representable
+                // value, and it should roundtrip exactly.
+                let next_min_rep = max_rep.wrapping_neg();
+                assert_bidir(next_min_rep as $fty, next_min_rep, "min representable above imin");
+
+                // Following a similar pattern as for positive numbers, halfway between this value
+                // and `imin` should round back to `next_min_rep`.
+                let min_non_sat = imin - ((imin - next_min_rep) / 2) + 1;
+                assert_bidir(
+                    min_non_sat as $fty,
+                    next_min_rep,
+                    "min int that does not round to imin",
+                );
+
+                // And then anything else saturates to the minimum value.
+                assert_bidir_unrep(
+                    (min_non_sat - 1) as $fty,
+                    imin,
+                    "max negative int that rounds to imin",
+                );
+            }
+        }
+
+        // Check potentially saturating int ranges. (`imax_f` here will be `$fty::INFINITY` if
+        // it cannot be represented as a finite value.)
+        assert_itof(imax, imax_f, "imax");
+        assert_itof(imin, imin_f, "imin");
+
+        // Float limits
+        assert_ftoi_unrep(<$fty>::INFINITY, imax, "f inf");
+        assert_ftoi_unrep(<$fty>::NEG_INFINITY, imin, "f neg inf");
+        assert_ftoi_unrep(<$fty>::NAN, 0, "f nan");
+        assert_ftoi_unrep(-<$fty>::NAN, 0, "f neg nan");
+    }};
+}
+
+/// Test casts from one float to another
+macro_rules! test_ftof {
+    (
+        f1: $f1:ty,
+        f2: $f2:ty $(,)?
+    ) => {{
+        type F2Int = <$f2 as Float>::Int;
+
+        let f1zero: $f1 = 0.0;
+        let f2zero: $f2 = 0.0;
+        let f1five: $f1 = 5.0;
+        let f2five: $f2 = 5.0;
+
+        assert_biteq((f1zero as $f2), f2zero, "0.0");
+        assert_biteq(((-f1zero) as $f2), (-f2zero), "-0.0");
+        assert_biteq((f1five as $f2), f2five, "5.0");
+        assert_biteq(((-f1five) as $f2), (-f2five), "-5.0");
+
+        assert_feq(<$f1>::INFINITY as $f2, <$f2>::INFINITY, "max -> inf");
+        assert_feq(<$f1>::NEG_INFINITY as $f2, <$f2>::NEG_INFINITY, "max -> inf");
+        assert!((<$f1>::NAN as $f2).is_nan(), "{} -> {} nan", stringify!($f1), stringify!($f2));
+
+        let min_sub_casted = <$f1>::from_bits(0x1) as $f2;
+        let min_neg_sub_casted = <$f1>::from_bits(0x1 | 1 << (<$f1>::BITS - 1)) as $f2;
+
+        if <$f1>::BITS > <$f2>::BITS {
+            assert_feq(<$f1>::MAX as $f2, <$f2>::INFINITY, "max -> inf");
+            assert_feq(<$f1>::MIN as $f2, <$f2>::NEG_INFINITY, "max -> inf");
+            assert_biteq(min_sub_casted, f2zero, "min subnormal -> 0.0");
+            assert_biteq(min_neg_sub_casted, -f2zero, "min neg subnormal -> -0.0");
+        } else {
+            // When increasing precision, the minimum subnormal will just roll to the next
+            // exponent. This exponent will be the current exponent (with bias), plus
+            // `sig_bits - 1` to account for the implicit change in exponent (since the
+            // mantissa starts with 0).
+            let sub_casted = <$f2>::from_bits(
+                ((<$f2>::EXPONENT_BIAS - (<$f1>::EXPONENT_BIAS + <$f1>::SIGNIFICAND_BITS - 1))
+                    as F2Int)
+                    << <$f2>::SIGNIFICAND_BITS,
+            );
+            assert_biteq(min_sub_casted, sub_casted, "min subnormal");
+            assert_biteq(min_neg_sub_casted, -sub_casted, "min neg subnormal");
+        }
+    }};
+}
+
+/// Many of these test patterns were adapted from the values in
 /// https://github.com/WebAssembly/testsuite/blob/master/conversions.wast.
 fn casts() {
-    // f32 -> i8
-    test_both_cast::<f32, i8>(127.99, 127);
-    test_both_cast::<f32, i8>(-128.99, -128);
-
-    // f32 -> i32
-    test_both_cast::<f32, i32>(0.0, 0);
-    test_both_cast::<f32, i32>(-0.0, 0);
-    test_both_cast::<f32, i32>(/*0x1p-149*/ f32::from_bits(0x00000001), 0);
-    test_both_cast::<f32, i32>(/*-0x1p-149*/ f32::from_bits(0x80000001), 0);
-    test_both_cast::<f32, i32>(/*0x1.19999ap+0*/ f32::from_bits(0x3f8ccccd), 1);
-    test_both_cast::<f32, i32>(/*-0x1.19999ap+0*/ f32::from_bits(0xbf8ccccd), -1);
-    test_both_cast::<f32, i32>(1.9, 1);
-    test_both_cast::<f32, i32>(-1.9, -1);
-    test_both_cast::<f32, i32>(5.0, 5);
-    test_both_cast::<f32, i32>(-5.0, -5);
-    test_both_cast::<f32, i32>(2147483520.0, 2147483520);
-    test_both_cast::<f32, i32>(-2147483648.0, -2147483648);
-    // unrepresentable casts
-    assert_eq::<i32>(2147483648.0f32 as i32, i32::MAX);
-    assert_eq::<i32>(-2147483904.0f32 as i32, i32::MIN);
-    assert_eq::<i32>(f32::MAX as i32, i32::MAX);
-    assert_eq::<i32>(f32::MIN as i32, i32::MIN);
-    assert_eq::<i32>(f32::INFINITY as i32, i32::MAX);
-    assert_eq::<i32>(f32::NEG_INFINITY as i32, i32::MIN);
-    assert_eq::<i32>(f32::NAN as i32, 0);
-    assert_eq::<i32>((-f32::NAN) as i32, 0);
-
-    // f32 -> u32
-    test_both_cast::<f32, u32>(0.0, 0);
-    test_both_cast::<f32, u32>(-0.0, 0);
-    test_both_cast::<f32, u32>(-0.9999999, 0);
-    test_both_cast::<f32, u32>(/*0x1p-149*/ f32::from_bits(0x1), 0);
-    test_both_cast::<f32, u32>(/*-0x1p-149*/ f32::from_bits(0x80000001), 0);
-    test_both_cast::<f32, u32>(/*0x1.19999ap+0*/ f32::from_bits(0x3f8ccccd), 1);
-    test_both_cast::<f32, u32>(1.9, 1);
-    test_both_cast::<f32, u32>(5.0, 5);
-    test_both_cast::<f32, u32>(2147483648.0, 0x8000_0000);
-    test_both_cast::<f32, u32>(4294967040.0, 0u32.wrapping_sub(256));
-    test_both_cast::<f32, u32>(/*-0x1.ccccccp-1*/ f32::from_bits(0xbf666666), 0);
-    test_both_cast::<f32, u32>(/*-0x1.fffffep-1*/ f32::from_bits(0xbf7fffff), 0);
-    test_both_cast::<f32, u32>((u32::MAX - 128) as f32, u32::MAX - 255); // rounding loss
-    // unrepresentable casts
-    assert_eq::<u32>((u32::MAX - 127) as f32 as u32, u32::MAX); // rounds up and then becomes unrepresentable
-    assert_eq::<u32>(4294967296.0f32 as u32, u32::MAX);
-    assert_eq::<u32>(-5.0f32 as u32, 0);
-    assert_eq::<u32>(f32::MAX as u32, u32::MAX);
-    assert_eq::<u32>(f32::MIN as u32, 0);
-    assert_eq::<u32>(f32::INFINITY as u32, u32::MAX);
-    assert_eq::<u32>(f32::NEG_INFINITY as u32, 0);
-    assert_eq::<u32>(f32::NAN as u32, 0);
-    assert_eq::<u32>((-f32::NAN) as u32, 0);
-
-    // f32 -> i64
-    test_both_cast::<f32, i64>(4294967296.0, 4294967296);
-    test_both_cast::<f32, i64>(-4294967296.0, -4294967296);
-    test_both_cast::<f32, i64>(9223371487098961920.0, 9223371487098961920);
-    test_both_cast::<f32, i64>(-9223372036854775808.0, -9223372036854775808);
-
-    // f64 -> i8
-    test_both_cast::<f64, i8>(127.99, 127);
-    test_both_cast::<f64, i8>(-128.99, -128);
-
-    // f64 -> i32
-    test_both_cast::<f64, i32>(0.0, 0);
-    test_both_cast::<f64, i32>(-0.0, 0);
-    test_both_cast::<f64, i32>(/*0x1.199999999999ap+0*/ f64::from_bits(0x3ff199999999999a), 1);
-    test_both_cast::<f64, i32>(
-        /*-0x1.199999999999ap+0*/ f64::from_bits(0xbff199999999999a),
-        -1,
-    );
-    test_both_cast::<f64, i32>(1.9, 1);
-    test_both_cast::<f64, i32>(-1.9, -1);
-    test_both_cast::<f64, i32>(1e8, 100_000_000);
-    test_both_cast::<f64, i32>(2147483647.0, 2147483647);
-    test_both_cast::<f64, i32>(-2147483648.0, -2147483648);
-    // unrepresentable casts
-    assert_eq::<i32>(2147483648.0f64 as i32, i32::MAX);
-    assert_eq::<i32>(-2147483649.0f64 as i32, i32::MIN);
-
-    // f64 -> i64
-    test_both_cast::<f64, i64>(0.0, 0);
-    test_both_cast::<f64, i64>(-0.0, 0);
-    test_both_cast::<f64, i64>(/*0x0.0000000000001p-1022*/ f64::from_bits(0x1), 0);
-    test_both_cast::<f64, i64>(
-        /*-0x0.0000000000001p-1022*/ f64::from_bits(0x8000000000000001),
-        0,
-    );
-    test_both_cast::<f64, i64>(/*0x1.199999999999ap+0*/ f64::from_bits(0x3ff199999999999a), 1);
-    test_both_cast::<f64, i64>(
-        /*-0x1.199999999999ap+0*/ f64::from_bits(0xbff199999999999a),
-        -1,
-    );
-    test_both_cast::<f64, i64>(5.0, 5);
-    test_both_cast::<f64, i64>(5.9, 5);
-    test_both_cast::<f64, i64>(-5.0, -5);
-    test_both_cast::<f64, i64>(-5.9, -5);
-    test_both_cast::<f64, i64>(4294967296.0, 4294967296);
-    test_both_cast::<f64, i64>(-4294967296.0, -4294967296);
-    test_both_cast::<f64, i64>(9223372036854774784.0, 9223372036854774784);
-    test_both_cast::<f64, i64>(-9223372036854775808.0, -9223372036854775808);
-    // unrepresentable casts
-    assert_eq::<i64>(9223372036854775808.0f64 as i64, i64::MAX);
-    assert_eq::<i64>(-9223372036854777856.0f64 as i64, i64::MIN);
-    assert_eq::<i64>(f64::MAX as i64, i64::MAX);
-    assert_eq::<i64>(f64::MIN as i64, i64::MIN);
-    assert_eq::<i64>(f64::INFINITY as i64, i64::MAX);
-    assert_eq::<i64>(f64::NEG_INFINITY as i64, i64::MIN);
-    assert_eq::<i64>(f64::NAN as i64, 0);
-    assert_eq::<i64>((-f64::NAN) as i64, 0);
-
-    // f64 -> u64
-    test_both_cast::<f64, u64>(0.0, 0);
-    test_both_cast::<f64, u64>(-0.0, 0);
-    test_both_cast::<f64, u64>(-0.99999999999, 0);
-    test_both_cast::<f64, u64>(5.0, 5);
-    test_both_cast::<f64, u64>(1e16, 10000000000000000);
-    test_both_cast::<f64, u64>((u64::MAX - 1024) as f64, u64::MAX - 2047); // rounding loss
-    test_both_cast::<f64, u64>(9223372036854775808.0, 9223372036854775808);
-    // unrepresentable casts
-    assert_eq::<u64>(-5.0f64 as u64, 0);
-    assert_eq::<u64>((u64::MAX - 1023) as f64 as u64, u64::MAX); // rounds up and then becomes unrepresentable
-    assert_eq::<u64>(18446744073709551616.0f64 as u64, u64::MAX);
-    assert_eq::<u64>(f64::MAX as u64, u64::MAX);
-    assert_eq::<u64>(f64::MIN as u64, 0);
-    assert_eq::<u64>(f64::INFINITY as u64, u64::MAX);
-    assert_eq::<u64>(f64::NEG_INFINITY as u64, 0);
-    assert_eq::<u64>(f64::NAN as u64, 0);
-    assert_eq::<u64>((-f64::NAN) as u64, 0);
-
-    // f64 -> i128
-    assert_eq::<i128>(f64::MAX as i128, i128::MAX);
-    assert_eq::<i128>(f64::MIN as i128, i128::MIN);
-
-    // f64 -> u128
-    assert_eq::<u128>(f64::MAX as u128, u128::MAX);
-    assert_eq::<u128>(f64::MIN as u128, 0);
+    /* int <-> float generic tests */
+
+    test_ftoi_itof! { f: f32, i: i8, imin_f: -128.0, imax_f: 127.0 };
+    test_ftoi_itof! { f: f32, i: u8, imin_f: 0.0, imax_f: 255.0 };
+    test_ftoi_itof! { f: f32, i: i16, imin_f: -32_768.0, imax_f: 32_767.0 };
+    test_ftoi_itof! { f: f32, i: u16, imin_f: 0.0, imax_f: 65_535.0 };
+    test_ftoi_itof! { f: f32, i: i32, imin_f: -2_147_483_648.0, imax_f: 2_147_483_647.0 };
+    test_ftoi_itof! { f: f32, i: u32, imin_f: 0.0, imax_f: 4_294_967_295.0 };
+    test_ftoi_itof! {
+        f: f32,
+        i: i64,
+        imin_f: -9_223_372_036_854_775_808.0,
+        imax_f: 9_223_372_036_854_775_807.0
+    };
+    test_ftoi_itof! { f: f32, i: u64, imin_f: 0.0, imax_f: 18_446_744_073_709_551_615.0 };
+    test_ftoi_itof! {
+        f: f32,
+        i: i128,
+        imin_f: -170_141_183_460_469_231_731_687_303_715_884_105_728.0,
+        imax_f: 170_141_183_460_469_231_731_687_303_715_884_105_727.0,
+    };
+    test_ftoi_itof! {
+        f: f32,
+        i: u128,
+        imin_f: 0.0,
+        imax_f: 340_282_366_920_938_463_463_374_607_431_768_211_455.0
+    };
+
+    test_ftoi_itof! { f: f64, i: i8, imin_f: -128.0, imax_f: 127.0 };
+    test_ftoi_itof! { f: f64, i: u8, imin_f: 0.0, imax_f: 255.0 };
+    test_ftoi_itof! { f: f64, i: i16, imin_f: -32_768.0, imax_f: 32_767.0 };
+    test_ftoi_itof! { f: f64, i: u16, imin_f: 0.0, imax_f: 65_535.0 };
+    test_ftoi_itof! { f: f64, i: i32, imin_f: -2_147_483_648.0, imax_f: 2_147_483_647.0 };
+    test_ftoi_itof! { f: f64, i: u32, imin_f: 0.0, imax_f: 4_294_967_295.0 };
+    test_ftoi_itof! {
+        f: f64,
+        i: i64,
+        imin_f: -9_223_372_036_854_775_808.0,
+        imax_f: 9_223_372_036_854_775_807.0
+    };
+    test_ftoi_itof! { f: f64, i: u64, imin_f: 0.0, imax_f: 18_446_744_073_709_551_615.0 };
+    test_ftoi_itof! {
+        f: f64,
+        i: i128,
+        imin_f: -170_141_183_460_469_231_731_687_303_715_884_105_728.0,
+        imax_f: 170_141_183_460_469_231_731_687_303_715_884_105_727.0,
+    };
+    test_ftoi_itof! {
+        f: f64,
+        i: u128,
+        imin_f: 0.0,
+        imax_f: 340_282_366_920_938_463_463_374_607_431_768_211_455.0
+    };
+
+    /* int <-> float spot checks */
 
     // int -> f32
-    assert_eq::<f32>(127i8 as f32, 127.0);
-    assert_eq::<f32>(2147483647i32 as f32, 2147483648.0);
-    assert_eq::<f32>((-2147483648i32) as f32, -2147483648.0);
     assert_eq::<f32>(1234567890i32 as f32, /*0x1.26580cp+30*/ f32::from_bits(0x4e932c06));
-    assert_eq::<f32>(16777217i32 as f32, 16777216.0);
-    assert_eq::<f32>((-16777217i32) as f32, -16777216.0);
-    assert_eq::<f32>(16777219i32 as f32, 16777220.0);
-    assert_eq::<f32>((-16777219i32) as f32, -16777220.0);
     assert_eq::<f32>(
         0x7fffff4000000001i64 as f32,
         /*0x1.fffffep+62*/ f32::from_bits(0x5effffff),
@@ -313,36 +541,23 @@ fn casts() {
         0xffdfffffdfffffffu64 as i64 as f32,
         /*-0x1.000002p+53*/ f32::from_bits(0xda000001),
     );
-    assert_eq::<f32>(i128::MIN as f32, -170141183460469231731687303715884105728.0f32);
-    assert_eq::<f32>(u128::MAX as f32, f32::INFINITY); // saturation
 
     // int -> f64
-    assert_eq::<f64>(127i8 as f64, 127.0);
-    assert_eq::<f64>(i16::MIN as f64, -32768.0f64);
-    assert_eq::<f64>(2147483647i32 as f64, 2147483647.0);
-    assert_eq::<f64>(-2147483648i32 as f64, -2147483648.0);
     assert_eq::<f64>(987654321i32 as f64, 987654321.0);
-    assert_eq::<f64>(9223372036854775807i64 as f64, 9223372036854775807.0);
-    assert_eq::<f64>(-9223372036854775808i64 as f64, -9223372036854775808.0);
     assert_eq::<f64>(4669201609102990i64 as f64, 4669201609102990.0); // Feigenbaum (?)
     assert_eq::<f64>(9007199254740993i64 as f64, 9007199254740992.0);
     assert_eq::<f64>(-9007199254740993i64 as f64, -9007199254740992.0);
     assert_eq::<f64>(9007199254740995i64 as f64, 9007199254740996.0);
     assert_eq::<f64>(-9007199254740995i64 as f64, -9007199254740996.0);
-    assert_eq::<f64>(u128::MAX as f64, 340282366920938463463374607431768211455.0f64); // even that fits...
+
+    /* float -> float generic tests */
+
+    test_ftof! { f1: f32, f2: f64 };
+    test_ftof! { f1: f64, f2: f32 };
+
+    /* float -> float spot checks */
 
     // f32 -> f64
-    assert_eq::<u64>((0.0f32 as f64).to_bits(), 0.0f64.to_bits());
-    assert_eq::<u64>(((-0.0f32) as f64).to_bits(), (-0.0f64).to_bits());
-    assert_eq::<f64>(5.0f32 as f64, 5.0f64);
-    assert_eq::<f64>(
-        /*0x1p-149*/ f32::from_bits(0x1) as f64,
-        /*0x1p-149*/ f64::from_bits(0x36a0000000000000),
-    );
-    assert_eq::<f64>(
-        /*-0x1p-149*/ f32::from_bits(0x80000001) as f64,
-        /*-0x1p-149*/ f64::from_bits(0xb6a0000000000000),
-    );
     assert_eq::<f64>(
         /*0x1.fffffep+127*/ f32::from_bits(0x7f7fffff) as f64,
         /*0x1.fffffep+127*/ f64::from_bits(0x47efffffe0000000),
@@ -359,15 +574,8 @@ fn casts() {
         /*0x1.8f867ep+125*/ f32::from_bits(0x7e47c33f) as f64,
         6.6382536710104395e+37,
     );
-    assert_eq::<f64>(f32::INFINITY as f64, f64::INFINITY);
-    assert_eq::<f64>(f32::NEG_INFINITY as f64, f64::NEG_INFINITY);
 
     // f64 -> f32
-    assert_eq::<u32>((0.0f64 as f32).to_bits(), 0.0f32.to_bits());
-    assert_eq::<u32>(((-0.0f64) as f32).to_bits(), (-0.0f32).to_bits());
-    assert_eq::<f32>(5.0f64 as f32, 5.0f32);
-    assert_eq::<f32>(/*0x0.0000000000001p-1022*/ f64::from_bits(0x1) as f32, 0.0);
-    assert_eq::<f32>(/*-0x0.0000000000001p-1022*/ (-f64::from_bits(0x1)) as f32, -0.0);
     assert_eq::<f32>(
         /*0x1.fffffe0000000p-127*/ f64::from_bits(0x380fffffe0000000) as f32,
         /*0x1p-149*/ f32::from_bits(0x800000),
@@ -376,10 +584,6 @@ fn casts() {
         /*0x1.4eae4f7024c7p+108*/ f64::from_bits(0x46b4eae4f7024c70) as f32,
         /*0x1.4eae5p+108*/ f32::from_bits(0x75a75728),
     );
-    assert_eq::<f32>(f64::MAX as f32, f32::INFINITY);
-    assert_eq::<f32>(f64::MIN as f32, f32::NEG_INFINITY);
-    assert_eq::<f32>(f64::INFINITY as f32, f32::INFINITY);
-    assert_eq::<f32>(f64::NEG_INFINITY as f32, f32::NEG_INFINITY);
 }
 
 fn ops() {