Skip to content

Commit

Permalink
Auto merge of #78618 - workingjubilee:ieee754-fmt, r=m-ou-se
Browse files Browse the repository at this point in the history
Add IEEE 754 compliant fmt/parse of -0, infinity, NaN

This pull request improves the Rust float formatting/parsing libraries to comply with IEEE 754's formatting expectations around certain special values, namely signed zero, the infinities, and NaN. It also adds IEEE 754 compliance tests that, while less stringent in certain places than many of the existing flt2dec/dec2flt capability tests, are intended to serve as the beginning of a roadmap to future compliance with the standard. Some relevant documentation is also adjusted with clarifying remarks.

This PR follows from discussion in rust-lang/rfcs#1074, and closes #24623.

The most controversial change here is likely to be that -0 is now printed as -0. Allow me to explain: While there appears to be community support for an opt-in toggle of printing floats as if they exist in the naively expected domain of numbers, i.e. not the extended reals (where floats live), IEEE 754-2019 is clear that a float converted to a string should be capable of being transformed into the original floating point bit-pattern when it satisfies certain conditions (namely, when it is an actual numeric value i.e. not a NaN and the original and destination float width are the same). -0 is given special attention here as a value that should have its sign preserved. In addition, the vast majority of other programming languages not only output `-0` but output `-0.0` here.

While IEEE 754 offers a broad leeway in how to handle producing what it calls a "decimal character sequence", it is clear that the operations a language provides should be capable of round tripping, and it is confusing to advertise the f32 and f64 types as binary32 and binary64 yet have the most basic way of producing a string and then reading it back into a floating point number be non-conformant with the standard. Further, existing documentation suggested that e.g. -0 would be printed with -0 regardless of the presence of the `+` fmt character, but it prints "+0" instead if given such (which was what led to the opening of #24623).

There are other parsing and formatting issues for floating point numbers which prevent Rust from complying with the standard, as well as other well-documented challenges on the arithmetic level, but I hope that this can be the beginning of motion towards solving those challenges.
  • Loading branch information
bors committed Mar 27, 2021
2 parents feaac19 + e8dfbac commit aef1140
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 211 deletions.
5 changes: 2 additions & 3 deletions library/alloc/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,8 @@
//!
//! * `+` - This is intended for numeric types and indicates that the sign
//! should always be printed. Positive signs are never printed by
//! default, and the negative sign is only printed by default for the
//! `Signed` trait. This flag indicates that the correct sign (`+` or `-`)
//! should always be printed.
//! default, and the negative sign is only printed by default for signed values.
//! This flag indicates that the correct sign (`+` or `-`) should always be printed.
//! * `-` - Currently not used
//! * `#` - This flag indicates that the "alternate" form of printing should
//! be used. The alternate forms are:
Expand Down
3 changes: 1 addition & 2 deletions library/alloc/tests/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ fn test_format_macro_interface() {
t!(format!("{:+10.3e}", -1.2345e6f64), " -1.234e6");

// Float edge cases
t!(format!("{}", -0.0), "0");
t!(format!("{:?}", -0.0), "-0.0");
t!(format!("{}", -0.0), "-0");
t!(format!("{:?}", 0.0), "0.0");

// sign aware zero padding
Expand Down
19 changes: 6 additions & 13 deletions library/core/src/fmt/float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,14 @@ where
}

// Common code of floating point Debug and Display.
fn float_to_decimal_common<T>(
fmt: &mut Formatter<'_>,
num: &T,
negative_zero: bool,
min_precision: usize,
) -> Result
fn float_to_decimal_common<T>(fmt: &mut Formatter<'_>, num: &T, min_precision: usize) -> Result
where
T: flt2dec::DecodableFloat,
{
let force_sign = fmt.sign_plus();
let sign = match (force_sign, negative_zero) {
(false, false) => flt2dec::Sign::Minus,
(false, true) => flt2dec::Sign::MinusRaw,
(true, false) => flt2dec::Sign::MinusPlus,
(true, true) => flt2dec::Sign::MinusPlusRaw,
let sign = match force_sign {
false => flt2dec::Sign::Minus,
true => flt2dec::Sign::MinusPlus,
};

if let Some(precision) = fmt.precision {
Expand Down Expand Up @@ -156,14 +149,14 @@ macro_rules! floating {
#[stable(feature = "rust1", since = "1.0.0")]
impl Debug for $ty {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
float_to_decimal_common(fmt, self, true, 1)
float_to_decimal_common(fmt, self, 1)
}
}

#[stable(feature = "rust1", since = "1.0.0")]
impl Display for $ty {
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
float_to_decimal_common(fmt, self, false, 0)
float_to_decimal_common(fmt, self, 0)
}
}

Expand Down
12 changes: 7 additions & 5 deletions library/core/src/num/dec2flt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,13 +239,15 @@ fn dec2flt<T: RawFloat>(s: &str) -> Result<T, ParseFloatError> {
ParseResult::Valid(decimal) => convert(decimal)?,
ParseResult::ShortcutToInf => T::INFINITY,
ParseResult::ShortcutToZero => T::ZERO,
ParseResult::Invalid => match s {
"inf" => T::INFINITY,
"NaN" => T::NAN,
_ => {
ParseResult::Invalid => {
if s.eq_ignore_ascii_case("nan") {
T::NAN
} else if s.eq_ignore_ascii_case("inf") || s.eq_ignore_ascii_case("infinity") {
T::INFINITY
} else {
return Err(pfe_invalid());
}
},
}
};

match sign {
Expand Down
32 changes: 6 additions & 26 deletions library/core/src/num/flt2dec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,45 +399,25 @@ fn digits_to_exp_str<'a>(
/// Sign formatting options.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Sign {
/// Prints `-` only for the negative non-zero values.
Minus, // -inf -1 0 0 1 inf nan
/// Prints `-` only for any negative values (including the negative zero).
MinusRaw, // -inf -1 -0 0 1 inf nan
/// Prints `-` for the negative non-zero values, or `+` otherwise.
MinusPlus, // -inf -1 +0 +0 +1 +inf nan
/// Prints `-` for any negative values (including the negative zero), or `+` otherwise.
MinusPlusRaw, // -inf -1 -0 +0 +1 +inf nan
/// Prints `-` for any negative value.
Minus, // -inf -1 -0 0 1 inf nan
/// Prints `-` for any negative value, or `+` otherwise.
MinusPlus, // -inf -1 -0 +0 +1 +inf nan
}

/// Returns the static byte string corresponding to the sign to be formatted.
/// It can be either `""`, `"+"` or `"-"`.
fn determine_sign(sign: Sign, decoded: &FullDecoded, negative: bool) -> &'static str {
match (*decoded, sign) {
(FullDecoded::Nan, _) => "",
(FullDecoded::Zero, Sign::Minus) => "",
(FullDecoded::Zero, Sign::MinusRaw) => {
(_, Sign::Minus) => {
if negative {
"-"
} else {
""
}
}
(FullDecoded::Zero, Sign::MinusPlus) => "+",
(FullDecoded::Zero, Sign::MinusPlusRaw) => {
if negative {
"-"
} else {
"+"
}
}
(_, Sign::Minus | Sign::MinusRaw) => {
if negative {
"-"
} else {
""
}
}
(_, Sign::MinusPlus | Sign::MinusPlusRaw) => {
(_, Sign::MinusPlus) => {
if negative {
"-"
} else {
Expand Down
Loading

0 comments on commit aef1140

Please sign in to comment.