Skip to content

Commit faf7fb6

Browse files
authored
Merge pull request #3173 from orlp/float-next-up-down
Add RFC float-next-up-down.
2 parents b1f51b0 + 8793a4d commit faf7fb6

File tree

1 file changed

+333
-0
lines changed

1 file changed

+333
-0
lines changed

text/3173-float-next-up-down.md

+333
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
- Feature Name: `float_next_up_down`
2+
- Start Date: 2021-09-06
3+
- RFC PR: [rust-lang/rfcs#3173](https://github.com/rust-lang/rfcs/pull/3173)
4+
- Rust Issue: [rust-lang/rust#91399](https://github.com/rust-lang/rust/issues/91399)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
This RFC adds two argumentless methods to `f32`/`f64`, `next_up` and
10+
`next_down`. These functions are specified in the IEEE 754 standard, and provide
11+
the capability to enumerate floating point values in order.
12+
13+
14+
# Motivation
15+
[motivation]: #motivation
16+
17+
Currently it is not possible to answer the question 'which floating point value
18+
comes after `x`' in Rust without intimate knowledge of the IEEE 754 standard.
19+
Answering this question has multiple uses:
20+
21+
- Simply exploratory or educational purposes. Being able to enumerate values is
22+
critical for understanding how floating point numbers work, and how they have
23+
varying precision at different sizes. E.g. one might wonder what sort of
24+
precision `f32` has at numbers around 10,000. With this feature one could
25+
simply print `10_000f32.next_up() - 10_000f32` to find out it is
26+
`0.0009765625`.
27+
28+
- Testing. If you wish to ensure a property holds for all values in a certain
29+
range, you need to be able to enumerate them. One might also want to check if
30+
and how your function fails just outside its supported range.
31+
32+
- Exclusive ranges. If you want to ensure a variable lies within an exclusive
33+
range, these functions can help. E.g. to ensure that `x` lies within [0, 1)
34+
one can write `x.clamp(0.0, 1.0.next_down())`.
35+
36+
37+
# Guide-level explanation
38+
[guide-level-explanation]: #guide-level-explanation
39+
40+
Because floating point numbers have finite precision sometimes you might want to
41+
know which floating point number is *right* below or above a number you already
42+
have. For this you can use the methods `next_down` or `next_up` respectively.
43+
Using them repeatedly allows you to iterate over all the values within a range.
44+
45+
The method `x.next_up()` defined on both `f32` and `f64` returns the smallest
46+
number greater than `x`. Similarly, `x.next_down()` returns the greatest number
47+
less than `x`.
48+
49+
If you wanted to test a function for all `f32` floating point values between 1
50+
and 2, you could for example write:
51+
```rust
52+
let mut x = 1.0;
53+
while x <= 2.0 {
54+
test(x);
55+
x = x.next_up();
56+
}
57+
```
58+
59+
On another occasion might be interested in how much `f32` and `f64` differ in
60+
their precision for numbers around one million. This is easy to figure out:
61+
```rust
62+
dbg!(1_000_000f32.next_up() - 1_000_000.0);
63+
dbg!(1_000_000f64.next_up() - 1_000_000.0);
64+
```
65+
66+
The answer is:
67+
```rust
68+
1_000_000f32.next_up() - 1_000_000.0 = 0.0625
69+
1_000_000f64.next_up() - 1_000_000.0 = 0.00000000011641532182693481
70+
```
71+
72+
If you want to ensure that a value `s` lies within -1 to 1, excluding the
73+
endpoints, this is easy to do:
74+
```rust
75+
s.clamp((-1.0).next_up(), 1.0.next_down())
76+
```
77+
78+
79+
# Reference-level explanation
80+
[reference-level-explanation]: #reference-level-explanation
81+
82+
The functions `nextUp` and `nextDown` are defined precisely (and identically)
83+
in the standards IEEE 754-2008 and IEEE 754-2019. This RFC proposes the methods
84+
`f32::next_up`, `f32::next_down`, `f64::next_up`, and `f64::next_down` with the
85+
behavior exactly as specified in those standards.
86+
87+
To be precise, let `tiny` be the smallest representable positive value and
88+
`max` be the largest representable finite positive value of the floating point
89+
type. Then if `x` is an arbitrary value `x.next_up()` is specified as:
90+
91+
- `x` if `x.is_nan()`,
92+
- `-max` if `x` is negative infinity,
93+
- `-0.0` if `x` is `-tiny`,
94+
- `tiny` if `x` is `0.0` or `-0.0`,
95+
- positive infinity if `x` is `max` or positive infinity, and
96+
- the unambiguous and unique minimal finite value `y` such that `x < y` in
97+
all other cases.
98+
99+
`x.next_down()` is specified as `-(-x).next_up()`.
100+
101+
A reference implementation for `f32` follows, using exclusively integer
102+
arithmetic. The implementation for `f64` is entirely analogous, with the
103+
exception that the constants `0x7fff_ffff` and `0x8000_0001` are replaced by
104+
respectively `0x7fff_ffff_ffff_ffff` and `0x8000_0000_0000_0001`. Using
105+
exclusively integer arithmetic aids stabilization as a `const fn`, reduces
106+
transfers between floating point and integer registers or execution units (which
107+
incur penalties on some processors), and avoids issues with denormal values
108+
potentially flushing to zero during floating point arithmetic operations
109+
on some platforms.
110+
111+
```rust
112+
/// Returns the least number greater than `self`.
113+
///
114+
/// Let `TINY` be the smallest representable positive `f32`. Then,
115+
/// - if `self.is_nan()`, this returns `self`;
116+
/// - if `self` is `NEG_INFINITY`, this returns `-MAX`;
117+
/// - if `self` is `-TINY`, this returns -0.0;
118+
/// - if `self` is -0.0 or +0.0, this returns `TINY`;
119+
/// - if `self` is `MAX` or `INFINITY`, this returns `INFINITY`;
120+
/// - otherwise the unique least value greater than `self` is returned.
121+
///
122+
/// The identity `x.next_up() == -(-x).next_down()` holds for all `x`. When `x`
123+
/// is finite `x == x.next_up().next_down()` also holds.
124+
pub const fn next_up(self) -> Self {
125+
const TINY_BITS: u32 = 0x1; // Smallest positive f32.
126+
const CLEAR_SIGN_MASK: u32 = 0x7fff_ffff;
127+
128+
let bits = self.to_bits();
129+
if self.is_nan() || bits == Self::INFINITY.to_bits() {
130+
return self;
131+
}
132+
133+
let abs = bits & CLEAR_SIGN_MASK;
134+
let next_bits = if abs == 0 {
135+
TINY_BITS
136+
} else if bits == abs {
137+
bits + 1
138+
} else {
139+
bits - 1
140+
};
141+
Self::from_bits(next_bits)
142+
}
143+
144+
/// Returns the greatest number less than `self`.
145+
///
146+
/// Let `TINY` be the smallest representable positive `f32`. Then,
147+
/// - if `self.is_nan()`, this returns `self`;
148+
/// - if `self` is `INFINITY`, this returns `MAX`;
149+
/// - if `self` is `TINY`, this returns 0.0;
150+
/// - if `self` is -0.0 or +0.0, this returns `-TINY`;
151+
/// - if `self` is `-MAX` or `NEG_INFINITY`, this returns `NEG_INFINITY`;
152+
/// - otherwise the unique greatest value less than `self` is returned.
153+
///
154+
/// The identity `x.next_down() == -(-x).next_up()` holds for all `x`. When `x`
155+
/// is finite `x == x.next_down().next_up()` also holds.
156+
pub const fn next_down(self) -> Self {
157+
const NEG_TINY_BITS: u32 = 0x8000_0001; // Smallest (in magnitude) negative f32.
158+
const CLEAR_SIGN_MASK: u32 = 0x7fff_ffff;
159+
160+
let bits = self.to_bits();
161+
if self.is_nan() || bits == Self::NEG_INFINITY.to_bits() {
162+
return self;
163+
}
164+
165+
let abs = bits & CLEAR_SIGN_MASK;
166+
let next_bits = if abs == 0 {
167+
NEG_TINY_BITS
168+
} else if bits == abs {
169+
bits - 1
170+
} else {
171+
bits + 1
172+
};
173+
Self::from_bits(next_bits)
174+
}
175+
```
176+
177+
# Drawbacks
178+
[drawbacks]: #drawbacks
179+
180+
Two more functions get added to `f32` and `f64`, which may be considered
181+
already cluttered by some.
182+
183+
Additionally, there is a minor pitfall regarding signed zero. Repeatedly calling
184+
`next_up` on a negative number will iterate over all values above it, with the
185+
exception of +0.0, only -0.0 will be visited. Similarly starting at positive
186+
number and iterating downwards will only visit +0.0, not -0.0.
187+
188+
However, if we were to define `(-0.0).next_up() == 0.0` we would lose compliance
189+
with the IEEE 754 standard, and lose the property that `x.next_up() > x` for all
190+
finite `x`. It would also lead to the pitfall that `(0.0).next_down()` would not
191+
be the smallest negative number, but -0.0 instead.
192+
193+
Finally, there is a minor risk of confusion regarding precedence with unary
194+
minus. A user might inadvertently write `-1.0.next_up()` instead of
195+
`(-1.0).next_up()`, giving a value on the wrong side of -1. However, this
196+
potential confusion holds for most methods on `f32`/`f64`, and can be avoided
197+
by the cautious by writing `f32::next_up(-1.0)`.
198+
199+
200+
# Rationale and alternatives
201+
[rationale-and-alternatives]: #rationale-and-alternatives
202+
203+
To implement the features described in the motivation the user essentially
204+
*needs* the `next_up`/`next_down` methods, or the alternative mentioned just
205+
below. If these are not available the user must either install a third party
206+
library for what is essentially one elementary function, or implement it
207+
themselves using `to_bits` and `from_bits`. This has several issues or pitfalls:
208+
209+
1. The user might not even be aware that a third party library exists,
210+
searching the standard library in vain. If they find a third party library
211+
they might not be able to judge if it is of sufficient quality and with the
212+
exact semantics they expect.
213+
214+
2. Even if the user is aware of IEEE 754 representation and chooses to
215+
implement it themselves, they might not get the edge cases correct. It is
216+
also a wasted duplicate effort.
217+
218+
3. The user might misunderstand the meaning of `f32::EPSILON`, thinking that
219+
adding this to a number results in the next floating point number.
220+
Alternatively they might misunderstand `f32::MIN_POSITIVE` to be the
221+
smallest positive `f32`, or believe that `x + f32::MIN_POSITIVE` is a
222+
correct implementation of `x.next_up()`.
223+
224+
4. The user might give up entirely and simply choose an arbitrary offset, e.g.
225+
instead of `x.clamp(0, 1.0.next_down())` they end up writing
226+
`x.clamp(0, 1.0 - 1e-9)`.
227+
228+
The main alternative to these two functions is `nextafter(x, y)` (sometimes
229+
called `nexttoward`). This function was specified in IEEE 754-1985 to "return
230+
the next representable neighbor of `x` in the direction toward `y`". If `x == y`
231+
then `x` is supposed to be returned. Besides error signaling and NaNs, that is
232+
the complete specification.
233+
234+
We did not choose this function for three reasons:
235+
236+
- The IEEE specification is lacking, and deprecated. Unfortunately IEEE
237+
754-1985 does not specify how to handle signed zeros at all, and some
238+
implementations (such as the one in the ISO C standard) deviate from the IEEE
239+
754 standard by defining `nextafter(x, y)` as `y` when `x == y`.
240+
Specifications IEEE 754-2008 and IEEE 754-2019 do not mention `nextafter` at
241+
all.
242+
243+
- From an informal study by searching for code using `nextafter` or
244+
`nexttoward` across a variety of languages we found that essentially every
245+
use case in the wild consisted of `nextafter(x, c)` where `c` is a constant
246+
effectively equal to negative or positive infinity. That is, the users would
247+
have been better suited by `x.next_up()` or `x.next_down()`.
248+
249+
Worse still, we also saw a lot of scenarios where `c` was somewhat
250+
arbitrarily chosen to be bigger/smaller than `x`, which might cause bugs when
251+
`x` is carelessly changed without updating `c`.
252+
253+
- The function `next_after` has been deprecated by the libs team in the past
254+
(see [Prior art](#prior-art)).
255+
256+
The advantage of a potential `x.next_toward(y)` method would be that only a
257+
single method would need to be added to `f32`/`f64`, however we argue that this
258+
simply shifts the burden from documentation bloat to code bloat. Other
259+
advantages are that it might considered more readable by some, and that it is
260+
more familiar to those used to `nextafter` in other languages.
261+
262+
Finally, if we were to take inspiration from Julia and Ruby these two functions
263+
could be called `next_float` and `prev_float`, which are arguably more readable,
264+
albeit slightly more ambiguous as to which direction 'next' is.
265+
266+
267+
# Prior art
268+
[prior-art]: #prior-art
269+
270+
First we must mention that Rust used to have the `next_after` function, which
271+
got deprecated in https://github.com/rust-lang/rust/issues/27752. We quote
272+
@alexcrichton:
273+
274+
> We were somewhat ambivalent if I remember correctly on whether to stabilize or
275+
> deprecate these functions. The functionality is likely needed by someone, but
276+
> the names are unfortunately sub-par wrt the rest of the module.
277+
> [...]
278+
> We realize that the FCP for this issue was pretty short, however, so please
279+
> comment with any objections you might have! We're very willing to backport an
280+
> un-deprecate for the few APIs we have this cycle.
281+
282+
One might consider this a formal un-deprecation request, albeit with a different
283+
name and slightly different API.
284+
285+
Within the Rust ecosystem the crate `float_next_after` solely provides the
286+
`x.next_after(y)` method, and has 30,000 all-time downloads at the moment of
287+
writing. The crate `ieee754` provides the `next` and `prev` methods among a few
288+
others and sits at 244,000 all-time downloads.
289+
290+
As for other languages supporting this feature, the list of prior art is
291+
extensive:
292+
293+
- C has `nextafter` and `nexttoward`, essentially identical:
294+
https://en.cppreference.com/w/c/numeric/math/nextafter
295+
296+
- C++ follows in C's footsteps:
297+
https://en.cppreference.com/w/cpp/numeric/math/nextafter
298+
299+
- Python has `nextafter`:
300+
https://docs.python.org/3/library/math.html#math.nextafter
301+
302+
- Java has `nextUp`, `nextDown` and `nextAfter`:
303+
https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#nextUp-double-
304+
305+
- Swift has `nextUp` and `nextDown`:
306+
https://developer.apple.com/documentation/swift/double/1847593-nextup
307+
308+
- Go has `Nextafter`:
309+
https://pkg.go.dev/math#Nextafter
310+
311+
- Julia has `nextfloat` and `prevfloat`:
312+
https://docs.julialang.org/en/v1/base/numbers/#Base.nextfloat
313+
314+
- Ruby has `next_float` and `prev_float`:
315+
https://ruby-doc.org/core-3.0.2/Float.html#next_float-method
316+
317+
318+
# Unresolved questions
319+
[unresolved-questions]: #unresolved-questions
320+
321+
- Which is the better pair of names, `next_up` and `next_down` or `next_float`
322+
and `prev_float`?
323+
324+
# Future possibilities
325+
[future-possibilities]: #future-possibilities
326+
327+
In the future Rust might consider having an iterator for `f32` / `f64` ranges
328+
that uses `next_up` or `next_down` internally.
329+
330+
The method `ulp` might also be considered, being a more precise implementation
331+
of what is approximated as `x.next_up() - x` in this document. Its
332+
implementation would directly compute the correct [ULP](https://en.wikipedia.org/wiki/Unit_in_the_last_place) by inspecting the exponent
333+
field of the IEEE 754 number.

0 commit comments

Comments
 (0)