Reducing clippy warnings when working with f32

I had been working through Casey Muratori's Handmade Hero course and writing it in Rust: GitHub - jehugaleahsa/handmade-hero-rs: Casey Muratori's handmade hero - but in Rust! Because I like hard-mode. · GitHub. Honestly, I haven't touched it since last Fall, but I always think about how I could improve what I had.

I had Clippy warnings turned all the way up, to help me learn best practices. One thing I learned is using from/try_from anywhere I could to avoid as casts. Handmade Hero is about making video games, so f32/f64 operations are extremely common.

One question I have for the community is if there is a good way to avoid my many, many #[allow(clippy::possible_truncation)], #[allow(clippy::cast_sign_loss)], etc. when working with floating point numbers. It's mostly going from f32 to u8, or f32 to usize, etc. and less about going from integers to floats.

Is there a popular library out there that wraps floating points with types that are checked to make from/try_from safe? Those types would provide guarantees like non-negative, allow guaranteeing integers after trunc or round is called, fitting within a u8 value range, etc.?

I assume such a library would have a little overhead guaranteeing each thing, but that's okay. I could also see such a library providing "saturating"/clamping conversions, too, which would avoid some of the cost.

Does such a library exist? Am I just missing a concept that stdlib already provides?

In my game development work, even though I am quite interested in ensuring my arithmetic is correct, I currently allow the lints cast_possible_truncation, cast_precision_loss, and cast_sign_loss. Two of those I have noted as “with a set of replacement functions, I could enable these”, but it hasn’t been one of the sub-projects I’ve pursued yet.

Something to understand about clippy::pedantic is that it’s expected that you might have to go more out of your way to satisfy it, and that you might disable some lints because they don’t provide value to your application.

As to having better numerical types, I’m not aware of a library that provides general solutions to your problem, but I would recommend writing your own functions that solve the problems you encounter. The problem with as casting isn’t that the things it does are wrong, it's that the things it does are not obvious. You can make them obvious by writing more explicit functions. For example, here’s one of my functions:

#[inline(always)]
pub const fn u32size(value: u32) -> usize {
    #[cfg(target_pointer_width = "16")]
    compile_error!("platforms with less than 32-bit `usize` are not supported");

    value as usize
}

With this function, it is documented that no truncation can ever happen.

Of course, sometimes you want some kind of lossy conversion. You can write a function that does what you need:

pub fn round_saturate_to_u8(value: f32) -> u8 {
    #![expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    value.round() as u8
}

The important thing is not that you never do a lossy conversion, but that you know when and why you’re doing it.

yes, several libraries have checked numeric conversions. the two I know of are num-traits and cast

personally I recommend cast. it is lean and focused, the code is concise and very pleasant to read. and, it returns Result instead of Option, so you know the reason when the conversion fails.

use num-traits if you already has it as a dependency for other use cases, and you don't need to examine the reason of failed conversions.

TBH? I'd say just don't do that.

There's a bunch of stuff in clippy that I wouldn't say is best practice, especially the stuff that got moved to pedantic because even clippy doesn't really think it's necessarily better.

Better to run it occasionally at higher levels to see what it says, but otherwise leave it set conservatively. It good to learn that map exists, but you don't need to change all your if-lets to map.

Clippy seems to like map, I recently had an example where it suggested map ( I think I had written in a slightly convoluted way), but in the end I used ?, which I find easier to read (and clippy didn't object).

Small thing first: the lint is clippy::cast_possible_truncation, not clippy::possible_truncation — the cast_ prefix is easy to lose and the shorter name is silently unknown.

The bigger thing is that I think the question is one level off from where the real decision is. Since Rust 1.45, float to integer as casts are saturating and fully defined — there is no UB left to protect against. Run on 1.96:

300.0f32  as u8    = 255
-5.0f32   as u8    = 0
f32::NAN  as u8    = 0
f32::INF  as u8    = 255
-1.0f32   as usize = 0
1e20f32   as i32   = 2147483647
255.9f32  as u8    = 255

So the cast never does anything surprising to memory. What it does is silently pick a value for you when the input was out of range. Whether that is correct depends entirely on what the number means, and that is the split I'd use rather than a blanket allow or a wrapper type:

When the value is a quantity with a meaningful ceiling — a colour channel, a normalised weight, an alpha — saturation is precisely the behaviour you want. (x * 255.0) as u8 clamping to 255 is right. Here cast_possible_truncation is pure noise and #[allow] is the correct answer, not a workaround.

When the value is an index or a count — f32 as usize for a tile lookup, a buffer offset — saturation is the worst possible outcome, because you get a plausible in-range index instead of a panic and the bug surfaces three frames later as corrupted geometry. Here the lint is telling you something true and a wrapper type won't help, because the wrapper still has to decide what to do at the boundary and it will guess wrong.

That split is why I'd push back gently on the wrapper-library idea: a type that guarantees "non-negative" or "fits in u8" has to be constructed from something, and the construction site is exactly where the interesting decision lives. Moving it into a type moves the decision out of view.

What has actually worked for me is two named free functions at each boundary rather than one general abstraction — one that saturates and says so in its name, one that returns Result — and then #[allow] at module level on the rendering modules where saturating is the spec. It reads better than as and it makes the grep for "places that can silently clamp an index" finite.

For the cases where you do want the checked path, cast is the right recommendation over num-traits if you aren't already pulling the latter in.