Rollup merge of #152512 - okaneco:exact_integer, r=tgross35

core: Implement feature `float_exact_integer_constants`

Accepted ACP - https://github.com/rust-lang/libs-team/issues/713#issuecomment-3880122239
Tracking issue - https://github.com/rust-lang/rust/issues/152466

Implement accepted ACP for `MAX_EXACT_INTEGER` and `MIN_EXACT_INTEGER` on `f16`, `f32`, `f64`, and `f128`
Add tests to `coretests/tests/floats/mod.rs`
This commit is contained in:
Stuart Cook 2026-02-17 13:02:22 +11:00 committed by GitHub
commit 331a785f81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 339 additions and 1 deletions

View file

@ -275,6 +275,70 @@ impl f128 {
#[unstable(feature = "f128", issue = "116909")]
pub const NEG_INFINITY: f128 = -1.0_f128 / 0.0_f128;
/// Maximum integer that can be represented exactly in an [`f128`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i128`] and [`f128`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f128`] and back to
/// [`i128`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f128`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// [`MAX_EXACT_INTEGER`]: f128::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f128::MIN_EXACT_INTEGER
/// ```
/// #![feature(f128)]
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// # #[cfg(target_has_reliable_f128)] {
/// let max_exact_int = f128::MAX_EXACT_INTEGER;
/// assert_eq!(max_exact_int, max_exact_int as f128 as i128);
/// assert_eq!(max_exact_int + 1, (max_exact_int + 1) as f128 as i128);
/// assert_ne!(max_exact_int + 2, (max_exact_int + 2) as f128 as i128);
///
/// // Beyond `f128::MAX_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((max_exact_int + 1) as f128, (max_exact_int + 2) as f128);
/// # }}
/// ```
// #[unstable(feature = "f128", issue = "116909")]
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MAX_EXACT_INTEGER: i128 = (1 << Self::MANTISSA_DIGITS) - 1;
/// Minimum integer that can be represented exactly in an [`f128`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i128`] and [`f128`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f128`] and back to
/// [`i128`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f128`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// This constant is equivalent to `-MAX_EXACT_INTEGER`.
///
/// [`MAX_EXACT_INTEGER`]: f128::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f128::MIN_EXACT_INTEGER
/// ```
/// #![feature(f128)]
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// # #[cfg(target_has_reliable_f128)] {
/// let min_exact_int = f128::MIN_EXACT_INTEGER;
/// assert_eq!(min_exact_int, min_exact_int as f128 as i128);
/// assert_eq!(min_exact_int - 1, (min_exact_int - 1) as f128 as i128);
/// assert_ne!(min_exact_int - 2, (min_exact_int - 2) as f128 as i128);
///
/// // Below `f128::MIN_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((min_exact_int - 1) as f128, (min_exact_int - 2) as f128);
/// # }}
/// ```
// #[unstable(feature = "f128", issue = "116909")]
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MIN_EXACT_INTEGER: i128 = -Self::MAX_EXACT_INTEGER;
/// Sign bit
pub(crate) const SIGN_MASK: u128 = 0x8000_0000_0000_0000_0000_0000_0000_0000;

View file

@ -269,6 +269,70 @@ impl f16 {
#[unstable(feature = "f16", issue = "116909")]
pub const NEG_INFINITY: f16 = -1.0_f16 / 0.0_f16;
/// Maximum integer that can be represented exactly in an [`f16`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i16`] and [`f16`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f16`] and back to
/// [`i16`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f16`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// [`MAX_EXACT_INTEGER`]: f16::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f16::MIN_EXACT_INTEGER
/// ```
/// #![feature(f16)]
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// # #[cfg(target_has_reliable_f16)] {
/// let max_exact_int = f16::MAX_EXACT_INTEGER;
/// assert_eq!(max_exact_int, max_exact_int as f16 as i16);
/// assert_eq!(max_exact_int + 1, (max_exact_int + 1) as f16 as i16);
/// assert_ne!(max_exact_int + 2, (max_exact_int + 2) as f16 as i16);
///
/// // Beyond `f16::MAX_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((max_exact_int + 1) as f16, (max_exact_int + 2) as f16);
/// # }}
/// ```
// #[unstable(feature = "f16", issue = "116909")]
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MAX_EXACT_INTEGER: i16 = (1 << Self::MANTISSA_DIGITS) - 1;
/// Minimum integer that can be represented exactly in an [`f16`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i16`] and [`f16`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f16`] and back to
/// [`i16`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f16`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// This constant is equivalent to `-MAX_EXACT_INTEGER`.
///
/// [`MAX_EXACT_INTEGER`]: f16::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f16::MIN_EXACT_INTEGER
/// ```
/// #![feature(f16)]
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// # #[cfg(target_has_reliable_f16)] {
/// let min_exact_int = f16::MIN_EXACT_INTEGER;
/// assert_eq!(min_exact_int, min_exact_int as f16 as i16);
/// assert_eq!(min_exact_int - 1, (min_exact_int - 1) as f16 as i16);
/// assert_ne!(min_exact_int - 2, (min_exact_int - 2) as f16 as i16);
///
/// // Below `f16::MIN_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((min_exact_int - 1) as f16, (min_exact_int - 2) as f16);
/// # }}
/// ```
// #[unstable(feature = "f16", issue = "116909")]
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MIN_EXACT_INTEGER: i16 = -Self::MAX_EXACT_INTEGER;
/// Sign bit
pub(crate) const SIGN_MASK: u16 = 0x8000;

View file

@ -513,6 +513,64 @@ impl f32 {
#[stable(feature = "assoc_int_consts", since = "1.43.0")]
pub const NEG_INFINITY: f32 = -1.0_f32 / 0.0_f32;
/// Maximum integer that can be represented exactly in an [`f32`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i32`] and [`f32`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f32`] and back to
/// [`i32`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f32`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// [`MAX_EXACT_INTEGER`]: f32::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f32::MIN_EXACT_INTEGER
/// ```
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// let max_exact_int = f32::MAX_EXACT_INTEGER;
/// assert_eq!(max_exact_int, max_exact_int as f32 as i32);
/// assert_eq!(max_exact_int + 1, (max_exact_int + 1) as f32 as i32);
/// assert_ne!(max_exact_int + 2, (max_exact_int + 2) as f32 as i32);
///
/// // Beyond `f32::MAX_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((max_exact_int + 1) as f32, (max_exact_int + 2) as f32);
/// # }
/// ```
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MAX_EXACT_INTEGER: i32 = (1 << Self::MANTISSA_DIGITS) - 1;
/// Minimum integer that can be represented exactly in an [`f32`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i32`] and [`f32`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f32`] and back to
/// [`i32`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f32`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// This constant is equivalent to `-MAX_EXACT_INTEGER`.
///
/// [`MAX_EXACT_INTEGER`]: f32::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f32::MIN_EXACT_INTEGER
/// ```
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// let min_exact_int = f32::MIN_EXACT_INTEGER;
/// assert_eq!(min_exact_int, min_exact_int as f32 as i32);
/// assert_eq!(min_exact_int - 1, (min_exact_int - 1) as f32 as i32);
/// assert_ne!(min_exact_int - 2, (min_exact_int - 2) as f32 as i32);
///
/// // Below `f32::MIN_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((min_exact_int - 1) as f32, (min_exact_int - 2) as f32);
/// # }
/// ```
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MIN_EXACT_INTEGER: i32 = -Self::MAX_EXACT_INTEGER;
/// Sign bit
pub(crate) const SIGN_MASK: u32 = 0x8000_0000;

View file

@ -512,6 +512,64 @@ impl f64 {
#[stable(feature = "assoc_int_consts", since = "1.43.0")]
pub const NEG_INFINITY: f64 = -1.0_f64 / 0.0_f64;
/// Maximum integer that can be represented exactly in an [`f64`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i64`] and [`f64`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f64`] and back to
/// [`i64`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f64`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// [`MAX_EXACT_INTEGER`]: f64::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f64::MIN_EXACT_INTEGER
/// ```
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// let max_exact_int = f64::MAX_EXACT_INTEGER;
/// assert_eq!(max_exact_int, max_exact_int as f64 as i64);
/// assert_eq!(max_exact_int + 1, (max_exact_int + 1) as f64 as i64);
/// assert_ne!(max_exact_int + 2, (max_exact_int + 2) as f64 as i64);
///
/// // Beyond `f64::MAX_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((max_exact_int + 1) as f64, (max_exact_int + 2) as f64);
/// # }
/// ```
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MAX_EXACT_INTEGER: i64 = (1 << Self::MANTISSA_DIGITS) - 1;
/// Minimum integer that can be represented exactly in an [`f64`] value,
/// with no other integer converting to the same floating point value.
///
/// For an integer `x` which satisfies `MIN_EXACT_INTEGER <= x <= MAX_EXACT_INTEGER`,
/// there is a "one-to-one" mapping between [`i64`] and [`f64`] values.
/// `MAX_EXACT_INTEGER + 1` also converts losslessly to [`f64`] and back to
/// [`i64`], but `MAX_EXACT_INTEGER + 2` converts to the same [`f64`] value
/// (and back to `MAX_EXACT_INTEGER + 1` as an integer) so there is not a
/// "one-to-one" mapping.
///
/// This constant is equivalent to `-MAX_EXACT_INTEGER`.
///
/// [`MAX_EXACT_INTEGER`]: f64::MAX_EXACT_INTEGER
/// [`MIN_EXACT_INTEGER`]: f64::MIN_EXACT_INTEGER
/// ```
/// #![feature(float_exact_integer_constants)]
/// # // FIXME(#152635): Float rounding on `i586` does not adhere to IEEE 754
/// # #[cfg(not(all(target_arch = "x86", not(target_feature = "sse"))))] {
/// let min_exact_int = f64::MIN_EXACT_INTEGER;
/// assert_eq!(min_exact_int, min_exact_int as f64 as i64);
/// assert_eq!(min_exact_int - 1, (min_exact_int - 1) as f64 as i64);
/// assert_ne!(min_exact_int - 2, (min_exact_int - 2) as f64 as i64);
///
/// // Below `f64::MIN_EXACT_INTEGER`, multiple integers can map to one float value
/// assert_eq!((min_exact_int - 1) as f64, (min_exact_int - 2) as f64);
/// # }
/// ```
#[unstable(feature = "float_exact_integer_constants", issue = "152466")]
pub const MIN_EXACT_INTEGER: i64 = -Self::MAX_EXACT_INTEGER;
/// Sign bit
pub(crate) const SIGN_MASK: u64 = 0x8000_0000_0000_0000;

View file

@ -5,6 +5,8 @@ trait TestableFloat: Sized {
const BITS: u32;
/// Unsigned int with the same size, for converting to/from bits.
type Int;
/// Signed int with the same size.
type SInt;
/// Set the default tolerance for float comparison based on the type.
const APPROX: Self;
/// Allow looser tolerance for f32 on miri
@ -61,6 +63,7 @@ trait TestableFloat: Sized {
impl TestableFloat for f16 {
const BITS: u32 = 16;
type Int = u16;
type SInt = i16;
const APPROX: Self = 1e-3;
const POWF_APPROX: Self = 5e-1;
const _180_TO_RADIANS_APPROX: Self = 1e-2;
@ -101,6 +104,7 @@ impl TestableFloat for f16 {
impl TestableFloat for f32 {
const BITS: u32 = 32;
type Int = u32;
type SInt = i32;
const APPROX: Self = 1e-6;
/// Miri adds some extra errors to float functions; make sure the tests still pass.
/// These values are purely used as a canary to test against and are thus not a stable guarantee Rust provides.
@ -143,6 +147,7 @@ impl TestableFloat for f32 {
impl TestableFloat for f64 {
const BITS: u32 = 64;
type Int = u64;
type SInt = i64;
const APPROX: Self = 1e-6;
const GAMMA_APPROX_LOOSE: Self = 1e-4;
const LNGAMMA_APPROX_LOOSE: Self = 1e-4;
@ -170,6 +175,7 @@ impl TestableFloat for f64 {
impl TestableFloat for f128 {
const BITS: u32 = 128;
type Int = u128;
type SInt = i128;
const APPROX: Self = 1e-9;
const EXP_APPROX: Self = 1e-12;
const LN_APPROX: Self = 1e-12;
@ -2003,6 +2009,93 @@ float_test! {
}
}
// Test the `float_exact_integer_constants` feature
float_test! {
name: max_exact_integer_constant,
attrs: {
f16: #[cfg(any(miri, target_has_reliable_f16))],
f128: #[cfg(any(miri, target_has_reliable_f128))],
},
test<Float> {
// The maximum integer that converts to a unique floating point
// value.
const MAX_EXACT_INTEGER: <Float as TestableFloat>::SInt = Float::MAX_EXACT_INTEGER;
let max_minus_one = (MAX_EXACT_INTEGER - 1) as Float as <Float as TestableFloat>::SInt;
let max_plus_one = (MAX_EXACT_INTEGER + 1) as Float as <Float as TestableFloat>::SInt;
let max_plus_two = (MAX_EXACT_INTEGER + 2) as Float as <Float as TestableFloat>::SInt;
// This does an extra round trip back to float for the second operand in
// order to print the results if there is a mismatch
assert_biteq!((MAX_EXACT_INTEGER - 1) as Float, max_minus_one as Float);
assert_biteq!(MAX_EXACT_INTEGER as Float, MAX_EXACT_INTEGER as Float as <Float as TestableFloat>::SInt as Float);
assert_biteq!((MAX_EXACT_INTEGER + 1) as Float, max_plus_one as Float);
// The first non-unique conversion, where `max_plus_two` roundtrips to
// `max_plus_one`
assert_biteq!((MAX_EXACT_INTEGER + 1) as Float, (MAX_EXACT_INTEGER + 2) as Float);
assert_biteq!((MAX_EXACT_INTEGER + 2) as Float, max_plus_one as Float);
assert_biteq!((MAX_EXACT_INTEGER + 2) as Float, max_plus_two as Float);
// Lossless roundtrips, for integers
assert!(MAX_EXACT_INTEGER - 1 == max_minus_one);
assert!(MAX_EXACT_INTEGER == MAX_EXACT_INTEGER as Float as <Float as TestableFloat>::SInt);
assert!(MAX_EXACT_INTEGER + 1 == max_plus_one);
// The first non-unique conversion, where `max_plus_two` roundtrips to
// one less than the starting value
assert!(MAX_EXACT_INTEGER + 2 != max_plus_two);
// max-1 | max+0 | max+1 | max+2
// After roundtripping, +1 and +2 will equal each other
assert!(max_minus_one != MAX_EXACT_INTEGER);
assert!(MAX_EXACT_INTEGER != max_plus_one);
assert!(max_plus_one == max_plus_two);
}
}
float_test! {
name: min_exact_integer_constant,
attrs: {
f16: #[cfg(any(miri, target_has_reliable_f16))],
f128: #[cfg(any(miri, target_has_reliable_f128))],
},
test<Float> {
// The minimum integer that converts to a unique floating point
// value.
const MIN_EXACT_INTEGER: <Float as TestableFloat>::SInt = Float::MIN_EXACT_INTEGER;
// Same logic as the `max` test, but we work our way leftward
// across the number line from (min_exact + 1) to (min_exact - 2).
let min_plus_one = (MIN_EXACT_INTEGER + 1) as Float as <Float as TestableFloat>::SInt;
let min_minus_one = (MIN_EXACT_INTEGER - 1) as Float as <Float as TestableFloat>::SInt;
let min_minus_two = (MIN_EXACT_INTEGER - 2) as Float as <Float as TestableFloat>::SInt;
// This does an extra round trip back to float for the second operand in
// order to print the results if there is a mismatch
assert_biteq!((MIN_EXACT_INTEGER + 1) as Float, min_plus_one as Float);
assert_biteq!(MIN_EXACT_INTEGER as Float, MIN_EXACT_INTEGER as Float as <Float as TestableFloat>::SInt as Float);
assert_biteq!((MIN_EXACT_INTEGER - 1) as Float, min_minus_one as Float);
// The first non-unique conversion, which roundtrips to one
// greater than the starting value.
assert_biteq!((MIN_EXACT_INTEGER - 1) as Float, (MIN_EXACT_INTEGER - 2) as Float);
assert_biteq!((MIN_EXACT_INTEGER - 2) as Float, min_minus_one as Float);
assert_biteq!((MIN_EXACT_INTEGER - 2) as Float, min_minus_two as Float);
// Lossless roundtrips, for integers
assert!(MIN_EXACT_INTEGER + 1 == min_plus_one);
assert!(MIN_EXACT_INTEGER == MIN_EXACT_INTEGER as Float as <Float as TestableFloat>::SInt);
assert!(MIN_EXACT_INTEGER - 1 == min_minus_one);
// The first non-unique conversion, which roundtrips to one
// greater than the starting value.
assert!(MIN_EXACT_INTEGER - 2 != min_minus_two);
// min-2 | min-1 | min | min+1
// After roundtripping, -2 and -1 will equal each other.
assert!(min_plus_one != MIN_EXACT_INTEGER);
assert!(MIN_EXACT_INTEGER != min_minus_one);
assert!(min_minus_one == min_minus_two);
}
}
// FIXME(f128): Uncomment and adapt these tests once the From<{u64,i64}> impls are added.
// float_test! {
// name: from_u64_i64,

View file

@ -53,6 +53,7 @@
#![feature(f128)]
#![feature(float_algebraic)]
#![feature(float_bits_const)]
#![feature(float_exact_integer_constants)]
#![feature(float_gamma)]
#![feature(float_minimum_maximum)]
#![feature(flt2dec)]