What happens when casting a big float to a int?

Viewed 1404

I was wondering what would happen when I cast a very large float value to an integer. This is an example I wrote:

fn main() {
    let x = 82747650246702476024762_f32;//-1_i16;
    let y = x as u8;
    let z = x as i32;
    println!("{} {} {}", x, y, z);
}

and the output is:

$ ./casts                                     
82747650000000000000000 0 -2147483648

Obviously the float wouldn't fit in any of the integers, but since Rust so strongly advertises that it is safe, I would have expected an error of some kind. These operations use the llvm fptosi and fptoui instructions, which produce a so called poison value if the value doesn't fit within the type it has been casted to. This may produce undefined behavior, which is very bad, especially when writing Rust code.

How can I be sure my float to int casts don't result in undefined behavior in Rust? And why would Rust even allow this (as it is known for creating safe code)?

2 Answers

In Rust 1.44 and earlier, if you use as to cast a floating-point number to an integer type and the floating-point number does not fit¹ in the target type, the result is an undefined value², and most things that you can do with it cause undefined behavior.

This serious issue (#10184) was fixed in Rust 1.45. Since that release, float-to-integer casts saturate instead (that is, values that are too large or small are converted to T::MAX or T::MIN, respectively; NaN is converted to 0).

In older versions of Rust, you can enable the new, safe behavior with the -Z saturating-float-casts flag. Note that saturating casts may be slightly slower since they have to check the type bounds first. If you really need to avoid the check, the standard library provides to_int_unchecked. Since the behavior is undefined when the number is out of range, you must use unsafe.

(There used to be a similar issue for certain integer-to-float casts, but it was resolved by making such casts always saturating. This change was not considered a performance regression and there is no way to opt in to the old behavior.)

Related questions


¹ "Fit" here means either NaN, or a number of such large magnitude that it cannot be approximated by the smaller type. 8.7654321_f64 will still be truncated to 8 by an as u8 cast, even though the value cannot be represented exactly by the destination type -- loss of precision does not cause undefined behavior, only being out of range does.

² A "poison" value in LLVM, as you correctly note in the question, but Rust itself does not distinguish between undef and poison values.

Part of your problem is "as" does lossy casts, i.e. you can cast a u8 to a u16 no problem but a u16 can't always fit in a u8, so the u16 is truncated. You can read about the behavior of "as" here.

Rust is designed to be memory safe, which means you can't access memory you shouldn't , or have data races, (unless you use unsafe) but you can still have memory leaks and other undesirable behavior.

What you described is unexpected behavior, but it is still well defined, these are very different things. Two different rust compilers would compile this to code that has the same result, if it was undefined behavior then the compiler implementer could have this compile to whatever they wanted, or not compile at all.

Edit: As pointed out in the comments and other answers, casting a float to an int using as currently causes undefined behavior, this is due to a known bug in the compiler.

Related