Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RFC float-next-up-down. #3173

Merged
merged 5 commits into from
Nov 30, 2021
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
333 changes: 333 additions & 0 deletions text/0000-float-next-up-down.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
- Feature Name: `float_next_up_down`
- Start Date: 2021-09-06
- RFC PR: [rust-lang/rfcs#3173](https://github.com/rust-lang/rfcs/pull/3173)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

This RFC adds two argumentless methods to `f32`/`f64`, `next_up` and
`next_down`. These functions are specified in the IEEE 754 standard, and provide
the capability to enumerate floating point values in order.


# Motivation
[motivation]: #motivation

Currently it is not possible to answer the question 'which floating point value
comes after `x`' in Rust without intimate knowledge of the IEEE 754 standard.
Answering this question has multiple uses:

- Simply exploratory or educational purposes. Being able to enumerate values is
critical for understanding how floating point numbers work, and how they have
varying precision at different sizes. E.g. one might wonder what sort of
precision `f32` has at numbers around 10,000. With this feature one could
simply print `10_000f32.next_up() - 10_000f32` to find out it is
`0.0009765625`.

- Testing. If you wish to ensure a property holds for all values in a certain
range, you need to be able to enumerate them. One might also want to check if
and how your function fails just outside its supported range.

- Exclusive ranges. If you want to ensure a variable lies within an exclusive
range, these functions can help. E.g. to ensure that `x` lies within [0, 1)
one can write `x.clamp(0.0, 1.0.next_down())`.


# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

Because floating point numbers have finite precision sometimes you might want to
know which floating point number is *right* below or above a number you already
have. For this you can use the methods `next_down` or `next_up` respectively.
Using them repeatedly allows you to iterate over all the values within a range.

The method `x.next_up()` defined on both `f32` and `f64` returns the smallest
number greater than `x`. Similarly, `x.next_down()` returns the greatest number
less than `x`.

If you wanted to test a function for all `f32` floating point values between 1
and 2, you could for example write:
```rust
let mut x = 1.0;
while x <= 2.0 {
test(x);
x = x.next_up();
}
```

On another occasion might be interested in how much `f32` and `f64` differ in
their precision for numbers around one million. This is easy to figure out:
```rust
dbg!(1_000_000f32.next_up() - 1_000_000.0);
dbg!(1_000_000f64.next_up() - 1_000_000.0);
```

The answer is:
```rust
1_000_000f32.next_up() - 1_000_000.0 = 0.0625
1_000_000f64.next_up() - 1_000_000.0 = 0.00000000011641532182693481
```

If you want to ensure that a value `s` lies within -1 to 1, excluding the
endpoints, this is easy to do:
```rust
s.clamp((-1.0).next_up(), 1.0.next_down())
```


# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The functions `nextUp` and `nextDown` are defined precisely (and identically)
in the standards IEEE 754-2008 and IEEE 754-2019. This RFC proposes the methods
`f32::next_up`, `f32::next_down`, `f64::next_up`, and `f64::next_down` with the
behavior exactly as specified in those standards.

To be precise, let `tiny` be the smallest representable positive value and
`max` be the largest representable finite positive value of the floating point
type. Then if `x` is an arbitrary value `x.next_up()` is specified as:

- `x` if `x.is_nan()`,
- `-max` if `x` is negative infinity,
- `-0.0` if `x` is `-tiny`,
- `tiny` if `x` is `0.0` or `-0.0`,
- positive infinity if `x` is `max` or positive infinity, and
- the unambiguous and unique minimal finite value `y` such that `x < y` in
all other cases.

`x.next_down()` is specified as `-(-x).next_up()`.

A reference implementation for `f32` follows, using exclusively integer
arithmetic. The implementation for `f64` is entirely analogous, with the
exception that the constants `0x7fff_ffff` and `0x8000_0001` are replaced by
respectively `0x7fff_ffff_ffff_ffff` and `0x8000_0000_0000_0001`. Using
exclusively integer arithmetic aids stabilization as a `const fn`, reduces
transfers between floating point and integer registers or execution units (which
incur penalties on some processors), and avoids issues with denormal values
potentially flushing to zero during floating point arithmetic operations
on some platforms.

```rust
/// Returns the least number greater than `self`.
///
/// Let `TINY` be the smallest representable positive `f32`. Then,
/// - if `self.is_nan()`, this returns `self`;
/// - if `self` is `NEG_INFINITY`, this returns `-MAX`;
/// - if `self` is `-TINY`, this returns -0.0;
/// - if `self` is -0.0 or +0.0, this returns `TINY`;
/// - if `self` is `MAX` or `INFINITY`, this returns `INFINITY`;
/// - otherwise the unique least value greater than `self` is returned.
///
/// The identity `x.next_up() == -(-x).next_down()` holds for all `x`. When `x`
/// is finite `x == x.next_up().next_down()` also holds.
pub const fn next_up(self) -> Self {
const TINY_BITS: u32 = 0x1; // Smallest positive f32.
const CLEAR_SIGN_MASK: u32 = 0x7fff_ffff;

let bits = self.to_bits();
if self.is_nan() || bits == Self::INFINITY.to_bits() {
return self;
}

let abs = bits & CLEAR_SIGN_MASK;
let next_bits = if abs == 0 {
TINY_BITS
} else if bits == abs {
bits + 1
} else {
bits - 1
};
Self::from_bits(next_bits)
}

/// Returns the greatest number less than `self`.
///
/// Let `TINY` be the smallest representable positive `f32`. Then,
/// - if `self.is_nan()`, this returns `self`;
/// - if `self` is `INFINITY`, this returns `MAX`;
/// - if `self` is `TINY`, this returns 0.0;
/// - if `self` is -0.0 or +0.0, this returns `-TINY`;
/// - if `self` is `-MAX` or `NEG_INFINITY`, this returns `NEG_INFINITY`;
/// - otherwise the unique greatest value less than `self` is returned.
///
/// The identity `x.next_down() == -(-x).next_up()` holds for all `x`. When `x`
/// is finite `x == x.next_down().next_up()` also holds.
pub const fn next_down(self) -> Self {
const NEG_TINY_BITS: u32 = 0x8000_0001; // Smallest (in magnitude) negative f32.
const CLEAR_SIGN_MASK: u32 = 0x7fff_ffff;

let bits = self.to_bits();
if self.is_nan() || bits == Self::NEG_INFINITY.to_bits() {
return self;
}

let abs = bits & CLEAR_SIGN_MASK;
let next_bits = if abs == 0 {
NEG_TINY_BITS
} else if bits == abs {
bits - 1
} else {
bits + 1
};
Self::from_bits(next_bits)
}
```

# Drawbacks
[drawbacks]: #drawbacks

Two more functions get added to `f32` and `f64`, which may be considered
already cluttered by some.

Additionally, there is a minor pitfall regarding signed zero. Repeatedly calling
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behaviour follows from the definition of the function, doesn't it? The definition says:

Returns the largest/smallest number more/less than self.

Since 0.0 and -0.0 compare equal, 0.0.next_down() == -0.0 would violate the contract.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if we didn't want this we would need to change the definition of the function, and depart from IEEE 754 compliance. But I still felt I needed to mention this potential pitfall.

`next_up` on a negative number will iterate over all values above it, with the
exception of +0.0, only -0.0 will be visited. Similarly starting at positive
number and iterating downwards will only visit +0.0, not -0.0.

However, if we were to define `(-0.0).next_up() == 0.0` we would lose compliance
with the IEEE 754 standard, and lose the property that `x.next_up() > x` for all
finite `x`. It would also lead to the pitfall that `(0.0).next_down()` would not
be the smallest negative number, but -0.0 instead.

Finally, there is a minor risk of confusion regarding precedence with unary
minus. A user might inadvertently write `-1.0.next_up()` instead of
`(-1.0).next_up()`, giving a value on the wrong side of -1. However, this
potential confusion holds for most methods on `f32`/`f64`, and can be avoided
by the cautious by writing `f32::next_up(-1.0)`.


# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

To implement the features described in the motivation the user essentially
*needs* the `next_up`/`next_down` methods, or the alternative mentioned just
below. If these are not available the user must either install a third party
library for what is essentially one elementary function, or implement it
themselves using `to_bits` and `from_bits`. This has several issues or pitfalls:

1. The user might not even be aware that a third party library exists,
searching the standard library in vain. If they find a third party library
they might not be able to judge if it is of sufficient quality and with the
exact semantics they expect.

2. Even if the user is aware of IEEE 754 representation and chooses to
implement it themselves, they might not get the edge cases correct. It is
also a wasted duplicate effort.

3. The user might misunderstand the meaning of `f32::EPSILON`, thinking that
adding this to a number results in the next floating point number.
Alternatively they might misunderstand `f32::MIN_POSITIVE` to be the
smallest positive `f32`, or believe that `x + f32::MIN_POSITIVE` is a
correct implementation of `x.next_up()`.

4. The user might give up entirely and simply choose an arbitrary offset, e.g.
instead of `x.clamp(0, 1.0.next_down())` they end up writing
`x.clamp(0, 1.0 - 1e-9)`.

The main alternative to these two functions is `nextafter(x, y)` (sometimes
called `nexttoward`). This function was specified in IEEE 754-1985 to "return
the next representable neighbor of `x` in the direction toward `y`". If `x == y`
then `x` is supposed to be returned. Besides error signaling and NaNs, that is
the complete specification.

We did not choose this function for three reasons:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

next_up and next_down are also easier to implement more efficiently as they don't need to dispatch algorithm based on 2 inputs.


- The IEEE specification is lacking, and deprecated. Unfortunately IEEE
754-1985 does not specify how to handle signed zeros at all, and some
implementations (such as the one in the ISO C standard) deviate from the IEEE
754 standard by defining `nextafter(x, y)` as `y` when `x == y`.
Specifications IEEE 754-2008 and IEEE 754-2019 do not mention `nextafter` at
all.

- From an informal study by searching for code using `nextafter` or
`nexttoward` across a variety of languages we found that essentially every
use case in the wild consisted of `nextafter(x, c)` where `c` is a constant
effectively equal to negative or positive infinity. That is, the users would
have been better suited by `x.next_up()` or `x.next_down()`.

Worse still, we also saw a lot of scenarios where `c` was somewhat
arbitrarily chosen to be bigger/smaller than `x`, which might cause bugs when
`x` is carelessly changed without updating `c`.

- The function `next_after` has been deprecated by the libs team in the past
(see [Prior art](#prior-art)).

The advantage of a potential `x.next_toward(y)` method would be that only a
single method would need to be added to `f32`/`f64`, however we argue that this
simply shifts the burden from documentation bloat to code bloat. Other
advantages are that it might considered more readable by some, and that it is
more familiar to those used to `nextafter` in other languages.

Finally, if we were to take inspiration from Julia and Ruby these two functions
could be called `next_float` and `prev_float`, which are arguably more readable,
albeit slightly more ambiguous as to which direction 'next' is.


# Prior art
[prior-art]: #prior-art

First we must mention that Rust used to have the `next_after` function, which
got deprecated in https://github.com/rust-lang/rust/issues/27752. We quote
@alexcrichton:

> We were somewhat ambivalent if I remember correctly on whether to stabilize or
> deprecate these functions. The functionality is likely needed by someone, but
> the names are unfortunately sub-par wrt the rest of the module.
> [...]
> We realize that the FCP for this issue was pretty short, however, so please
> comment with any objections you might have! We're very willing to backport an
> un-deprecate for the few APIs we have this cycle.

One might consider this a formal un-deprecation request, albeit with a different
name and slightly different API.

Within the Rust ecosystem the crate `float_next_after` solely provides the
`x.next_after(y)` method, and has 30,000 all-time downloads at the moment of
writing. The crate `ieee754` provides the `next` and `prev` methods among a few
others and sits at 244,000 all-time downloads.

As for other languages supporting this feature, the list of prior art is
extensive:

- C has `nextafter` and `nexttoward`, essentially identical:
https://en.cppreference.com/w/c/numeric/math/nextafter

- C++ follows in C's footsteps:
https://en.cppreference.com/w/cpp/numeric/math/nextafter

- Python has `nextafter`:
https://docs.python.org/3/library/math.html#math.nextafter

- Java has `nextUp`, `nextDown` and `nextAfter`:
https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#nextUp-double-

- Swift has `nextUp` and `nextDown`:
https://developer.apple.com/documentation/swift/double/1847593-nextup

- Go has `Nextafter`:
https://pkg.go.dev/math#Nextafter

- Julia has `nextfloat` and `prevfloat`:
https://docs.julialang.org/en/v1/base/numbers/#Base.nextfloat

- Ruby has `next_float` and `prev_float`:
https://ruby-doc.org/core-3.0.2/Float.html#next_float-method


# Unresolved questions
[unresolved-questions]: #unresolved-questions

- Which is the better pair of names, `next_up` and `next_down` or `next_float`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_float is already implied by the type in Rust.

and `prev_float`?

# Future possibilities
[future-possibilities]: #future-possibilities

In the future Rust might consider having an iterator for `f32` / `f64` ranges
that uses `next_up` or `next_down` internally.

The method `ulp` might also be considered, being a more precise implementation
of what is approximated as `x.next_up() - x` in this document. Its
implementation would directly compute the correct [ULP](https://en.wikipedia.org/wiki/Unit_in_the_last_place) by inspecting the exponent
field of the IEEE 754 number.