Skip to content

What Rust in Action §2.3.3 says about comparing numbers, run

Level: 201 · a companion to a book chapter

One line: Section 2.3.3 of Rust in Action (Tim McNamara, Manning 2021) gets most rules right. But its tolerance example never tests the tolerance, its "cast to the larger type" advice fails once signs differ, its CPU-exception footnote doesn't describe floats, and two details have aged. Fifteen claims, checked against rustc 1.98.0: twelve by the program below, three from the compiler's own output.

The section covers comparison operators, comparing values of different number types, and a sidebar on floating-point hazards. This page goes through it in the book's order. Where a topic has its own lesson here, the note links there instead of repeating it: Comparing two numbers of different types for the first half, What a float actually stores for the second.

The verdicts

# The book says Verdict
1 Values of two different types can't be compared True for numbers, because std implements no mixed number pairs. Not a language rule: String == &str compiles
2 Casting the smaller type to the larger one is safest Incomplete. The larger type must also cover the smaller one's sign: -1i32 as u64 < 1u64 is false
3 300_i32 as i8 returns 44 True
4 b.try_into() returns a Result, and unwrap() crashes if the conversion failed True in general, vacuous here. u16 → i32 has error type Infallible; write i32::from(b)
5 f32 and f64 implement only PartialEq; other number types also Eq True
6 0.1 + 0.2 == 0.3 fails for f64 and passes for f32 True, by luck: 0.1 + 0.6 == 0.7 fails for f32 and passes for f64
7 Compare within f32::EPSILON (shown on 0.1 + 0.1 against 0.2) The example tests nothing: the difference is exactly 0. And EPSILON is the wrong size for large values
8 The square root of a negative number, -42.0.sqrt(), is NaN Only with parentheses. As written it doesn't compile, and the compiler's suggested fix gives -6.48
9 Almost all operations involving NaN return NaN True, and the exceptions are common ones: max, min, powi(0)
10 NaN values are never equal True, inside a Vec too
11 Use is_nan() and is_finite() to crash near the cause True; is_finite() alone rejects both NaN and infinity
12 Illegal or undefined operations trigger a CPU exception Not for floats. The operation sets a status flag and the program carries on
A Listing 2.5 needs use std::convert::TryInto; Only in edition 2018. TryInto is in the prelude from edition 2021
B The float program crashes on line 14 with the report the book shows Aged. The failing assert is on line 16 of the listing, and today's report puts the location first
C The Rust compiler delegates the float comparison to the CPU Half true. In a debug build the compiler has already done the arithmetic; in a release build it also did the comparison

Twelve claims, run

Verified output of comparing_numbers_claims_checked.rs — regenerated by tools/run_examples.py, never hand-typed.

1. THE BOOK: two values of different types cannot be compared
   for the number types, yes: std implements i32 < i32 and nothing else.
   but == and < are traits with a type parameter for the right side,
   and std does implement some cross-type pairs:
     String == &str           : true
     Vec<i32> == [i32; 3]     : true

2. THE BOOK: casting the smaller type to the larger one is safest
     -1i32 as u64 < 1u64      : false   (u64 is larger, and -1 became 18446744073709551615)
     i64::from(-1i32) < 1i64  : true
   larger is not enough: the target must hold every value, sign included.

3. THE BOOK: 300_i32 as i8 returns 44
     300_i32 as i8            : 44
     300 in hex               : 0x12c; as keeps the low byte, 0x2c
     i8::try_from(300)        : Err(TryFromIntError(PosOverflow))

4. THE BOOK: try_into() on the u16 returns a Result, and unwrap() crashes if it failed
     u16 -> i32 try_into type : core::result::Result<i32, core::convert::Infallible>
   the error type is Infallible: this conversion cannot fail. i32::from(b) says so.
   a conversion that can fail, unwrapped:
     panic                    : called `Result::unwrap()` on an `Err` value: TryFromIntError(PosOverflow)

5. THE BOOK: f32 and f64 implement only PartialEq; the other number types also Eq
     1_i32.cmp(&2)            : Less   (Ord, a total order)
     1.0.partial_cmp(&NAN)    : None   (PartialOrd: some pairs have no answer)

6. THE BOOK: 0.1 + 0.2 == 0.3 fails for f64 and passes for f32
     f32  0.1 + 0.2 = 3e99999a   0.3 = 3e99999a   equal: true
     f64  0.1 + 0.2 = 3fd3333333333334   0.3 = 3fd3333333333333   equal: false
   true, and f32 was lucky rather than exact. Try 0.1 + 0.6 instead:
     f32  0.1 + 0.6 == 0.7    : false
     f64  0.1 + 0.6 == 0.7    : true

7. THE BOOK: compare within f32::EPSILON, shown on 0.1 + 0.1 against 0.2
     |0.2 - (0.1 + 0.1)| in f32 : 0e0
     0.1 + 0.1 == 0.2 in f32    : true
   doubling is exact in binary, so the tolerance was never tested.
   Where a tolerance is needed, EPSILON is the wrong size once the numbers grow:
     |2000.3 - (1000.1 + 1000.2)| : 2.2737367544323206e-13
     <= f64::EPSILON              : false   (2.220446049250313e-16)
     <= 1e-12 * 2000.3 (relative) : true

8. THE BOOK: the square root of a negative number, written -42.0.sqrt(), is NaN
     -42.0_f64.sqrt()         : -6.48074069840786
     (-42.0_f64).sqrt()       : NaN
   a method call binds tighter than unary minus: the first is -(42.0.sqrt()).

9. THE BOOK: almost every operation involving NAN returns NAN
     NAN + 1.0                : NaN
     NAN.max(1.0)             : 1
     NAN.min(1.0)             : 1
     NAN.powi(0)              : 1
     1.0_f64.powf(NAN)        : 1
     NAN.hypot(INFINITY)      : inf
     NAN as i32               : 0
     NAN < 1.0                : false
   'almost' is doing work: max, min and three more hand back a real number.

10. THE BOOK: NAN values are never equal
     x == x                   : false
     vec![x] == vec![x]       : false
     x.total_cmp(&x)          : Equal
     assert_eq!(x, x) panics  : true

11. THE BOOK: use is_nan() and is_finite() to crash near the cause
     1.0 / 0.0                : inf
     inf.is_nan()             : false
     inf.is_finite()          : false
     NAN.is_finite()          : false
   is_finite() rejects both; is_nan() alone lets infinity through.

12. THE BOOK: illegal or undefined operations trigger a CPU exception
   not for floats: the lines above divided by zero and took a square root
   of a negative number, and this program is still running.
     1_i32 / 0                : panic, "attempt to divide by zero"
     1_i32.checked_div(0)     : None
   Rust checks an integer divisor for zero in debug and release builds
   alike, so this panic is Rust's own check, not a trap from the CPU.

Reading them in order:

  1. Comparing different types is an impl question. == and < are PartialEq<Rhs> and PartialOrd<Rhs>, generic in the right-hand side, and std implements every number type against itself only. It also implements String against &str, Vec<T> against arrays, and more. So rustc's error for i32 < u16 is E0308 mismatched types, and it suggests b.into(). The lesson has the transcript.
  2. "Larger" isn't the test; "holds every value" is. A u16 in an i32 is safe because every u16 value fits, not because 32 > 16. An i32 in a u64 is larger and loses every negative. From exists exactly where the conversion is safe, so i64::from(a) can't make this mistake. See pick the type that holds both.
  3. 44 is the low byte of 300 (0x12c → 0x2c). Casting with as has the other three silent losses, and i8::try_from(300) reports this one.
  4. The book picked a conversion that can't fail to demonstrate one that can. TryFrom<u16> for i32 comes from std's blanket impl over From, with type Error = Infallible, so .unwrap() there can never panic. The unwrap that can panic is shown with i8::try_from(300). The prose next to the listing also calls this method unsafe() once, which is a typo for unwrap().
  5. Eq and Ord are withheld from floats because of NaN. The consequences (no sort(), no HashMap key) are on What a float actually stores.
  6. The bits match the book exactly: 3e99999a twice for f32, 3fd3333333333334 against …333 for f64. But f32 isn't more exact. Both widths round, and for 0.1 + 0.6 the luck runs the other way. Reading the result as "f32 is safe for this" is the mistake to avoid.
  7. 0.1 + 0.1 is 2 × 0.1, and doubling a binary float is exact. The sum is the float nearest 0.2, so the difference is 0e0 and any tolerance, even zero, passes. When the tolerance does matter, EPSILON is the gap next to 1.0, not a general tolerance: near 2000 the error in 1000.1 + 1000.2 is about 1000 times EPSILON, and only a relative tolerance accepts it.
  8. Unary minus binds looser than a method call, so -42.0_f64.sqrt() is -(42.0_f64.sqrt()). The book's code listing has the parentheses and is right; its prose drops them. Refusal 2 below shows the prose form doesn't compile, and that rustc's suggested fix quietly computes -6.48.
  9. The NaN exceptions are the ones you're most likely to call. f64::max and f64::min return the other argument, so a running maximum over data with a NaN in it just skips the NaN. powi(0), 1.0.powf(NAN) and hypot with an infinity follow IEEE 754's rules for those functions. NAN as i32 is 0, which saturation defines.
  10. vec![x] == vec![x] is false, because Vec's == compares element by element with f64's ==. Python answers True for a list holding the same NaN object, since its containers check identity first. total_cmp is the comparison that calls a NaN equal to itself.
  11. is_finite() is the stronger check. 1.0 / 0.0 is infinity, not NaN, so an is_nan() guard lets it through.
  12. A float operation with no valid result returns NaN or infinity and sets a flag. It doesn't stop the program: this one took a negative square root and divided by zero, then kept printing. IEEE 754 calls these exceptions, but by default they are status flags, not CPU traps. Rust has no stable way to read the flags; C does, through <fenv.h>, and A float exception is a flag ↗ in the C library reads them. The operation that can trap is integer division by zero, and Rust checks for it before dividing: attempt to divide by zero, in debug and release builds alike.

What the compiler says

Refusal 1: use std::convert::TryInto;. The book was written for edition 2018, where TryInto had to be imported. Under 2018, rustc 1.98.0 refuses the call without the import and says why:

Abridged: real rustc 1.98.0 output for try_into_2018.rs, --edition 2018
error[E0599]: no method named `try_into` found for type `u16` in the current scope
   --> try_into_2018.rs:3:26
    |
  3 |     let widened: i32 = b.try_into().unwrap();
    |                          ^^^^^^^^
    |
    = help: items from traits can only be used if the trait is in scope
    = note: 'std::convert::TryInto' is included in the prelude starting in Edition 2021

Under --edition 2021 or 2024, the same file compiles without the use and prints 100. The import is harmless in newer editions, just unnecessary. The book's general point stands: a trait method needs its trait in scope. Inherent methods such as f64::sqrt or i32::abs never do.

Refusal 2: -42.0.sqrt(). An unsuffixed float literal has no type yet, so there is no sqrt to call:

Real rustc 1.98.0 output for negative_root.rs
error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`
 --> negative_root.rs:2:19
  |
2 |     let x = -42.0.sqrt();
  |                   ^^^^
  |
help: you must specify a concrete type for this numeric value, like `f32`
  |
2 |     let x = -42.0_f32.sqrt();
  |                  ++++

Apply the help and it compiles, printing -6.4807405: the square root of positive 42, negated. The fix you want is (-42.0_f32).sqrt(). Claim 8 in the output above runs both forms.

Real output 3: the report. The book says the float program crashes on line 14 and shows a one-line report naming ch2-add-floats.rs.rs:14:5. In the listing as the book prints it, the f64 assert is on line 16, and a file with that layout panics there. rustc 1.98.0 also formats the report differently: location first, message on its own line, and a thread id:

Real output: a two-assert float file with the f64 assert on line 16, rustc 1.98.0, x86-64 macOS
thread 'main' (28871827) panicked at ch2-add-floats.rs:16:5:
assertion failed: xyz.0 + xyz.1 == xyz.2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The thread id changes on every run.

Does the CPU do the comparison?

The sidebar says the compiler delegates float comparisons to the CPU. Take a tolerance check on constants:

fn main() {
    let sum: f32 = 0.1 + 0.1;
    assert!((0.2 - sum).abs() <= f32::EPSILON);
}

In a debug build the CPU does compare, but the arithmetic before it is already gone. The compiler added and subtracted the constants itself, and loads the result, 0.0, with xorps:

Real output: rustc 1.98.0 --emit=asm -C opt-level=0, x86-64 macOS (main only, .cfi directives dropped)
__RNvCscE6FTKshoeA_9tolerance4main:
	pushq	%rbp
	movq	%rsp, %rbp
	xorps	%xmm0, %xmm0
	callq	__RNvMNtCsl7QZrza34zr_4core3f32f3absCscE6FTKshoeA_9tolerance
	movaps	%xmm0, %xmm1
	movss	LCPI4_0(%rip), %xmm0
	ucomiss	%xmm1, %xmm0
	jae	LBB4_2
	leaq	l_anon.e2037ec45069cb490f1439758f9c4e9d.1(%rip), %rdi
	movl	$51, %esi
	leaq	l_anon.e2037ec45069cb490f1439758f9c4e9d.3(%rip), %rdx
	callq	__RNvNtCsl7QZrza34zr_4core9panicking5panic
LBB4_2:
	popq	%rbp
	retq

LCPI4_0 is .long 0x34000000, which is f32::EPSILON, and ucomiss is the CPU's comparison. In a release build nothing is left for the CPU:

Real output: rustc 1.98.0 --emit=asm -C opt-level=3, x86-64 macOS (main only, .cfi directives dropped)
__RNvCscE6FTKshoeA_9tolerance4main:
	pushq	%rbp
	movq	%rsp, %rbp
	popq	%rbp
	retq

The compiler did IEEE 754 arithmetic itself, in software, and got the same bits the CPU would have. That's why the example programs on this page wrap their operands in std::hint::black_box ↗: to make the CPU do the work being claimed about. Integer division by zero is the opposite case. Even at -O, the division is guarded by Rust's own check:

Abridged: rustc 1.98.0 -O --emit=asm, x86-64 macOS — fn div(a: i32, b: i32) -> i32 { a / b }, the lines before idivl, with ; comments added
	testl	%esi, %esi
	je	LBB4_3            ; b == 0      -> panic_const_div_by_zero
	movl	%esi, %eax
	notl	%eax
	leal	-2147483648(%rdi), %ecx
	orl	%eax, %ecx
	je	LBB4_2            ; MIN / -1    -> panic_const_div_overflow
	movl	%edi, %eax
	cltd
	idivl	%esi

The two jumps come before idivl, the instruction that would trap. The comments are added; the instructions are rustc's.

Where this leaves you

Docs: PartialOrd ↗ · f64::EPSILON ↗ · f64::max ↗ · f64::total_cmp ↗ · Infallible ↗ · Rust 2021 prelude additions ↗

Po polsku

Podrozdział 2.3.3 książki Rust in Action dobrze opisuje zasady porównywania liczb, ale kilka szczegółów wymaga poprawki. Sprawdzone na rustc 1.98.0:

  • Liczb różnych typów nie da się porównać, bo biblioteka standardowa nie ma takich implementacji cech (traits) PartialOrd<Rhs>. To nie jest reguła języka: String == &str się kompiluje.
  • Rada „rzutuj mniejszy typ na większy” nie wystarcza: -1i32 as u64 < 1u64 daje false. Docelowy typ musi mieścić wszystkie wartości, łącznie ze znakiem, więc pisz i64::from.
  • W listingu 2.5 try_into().unwrap() dla u16 → i32 nigdy nie spanikuje, bo typ błędu to Infallible. A use std::convert::TryInto; jest potrzebne tylko w edycji 2018.
  • Przykład z f32::EPSILON niczego nie sprawdza: w f32 suma 0.1 + 0.1 jest dokładnie równa 0.2, a różnica wynosi 0. Stała EPSILON to odstęp obok 1.0, nie uniwersalna tolerancja.
  • -42.0.sqrt() się nie kompiluje, a podpowiedź kompilatora, -42.0_f32.sqrt(), daje -6.48, bo minus wiąże słabiej niż wywołanie metody. NaN daje dopiero (-42.0_f32).sqrt().
  • NAN.max(1.0) zwraca 1, więc „prawie wszystkie operacje z NaN dają NaN” to prawda z ważnymi wyjątkami.
  • Wbrew przypisowi działanie zmiennoprzecinkowe bez poprawnego wyniku nie wywołuje wyjątku procesora, tylko ustawia flagę stanu i program działa dalej. Pułapkę wywołuje dzielenie całkowite przez zero, a Rust sprawdza dzielnik wcześniej.

Szukaj po polsku: porównywanie liczb zmiennoprzecinkowych · epsilon maszynowy · rust f32::EPSILON · rust NaN max min · IEEE 754 exception flags · rust 2021 prelude TryInto