Lossy Numeric Casting in Rust
Tags: rust, clippy, linter
Published: 2018-08-07 13:19:28

---
title: Lossy Numeric Casting in Rust
published: true
tags: rust, clippy, linter
canonical_url: https://benjamincongdon.me/blog/2018/08/06/Clippys-cast_lossless-Lint-Warning/
cover_image: https://benjamincongdon.me/img/2018-08-06-Clippy%27s-cast_lossless-Lint-Warning/yosemite2.jpg
---

Recently, I was trying out [clippy](https://github.com/rust-lang-nursery/rust-clippy) — a new linting and static analysis tool for Rust, when I ran into a lint warning that wasn’t immediately clear to me: `warning: casting u8 to u16 may become silently lossy if types change`.

For context, this is the snippet of code I ran through `clippy`:

```rust
fn main() {
    let a: u8 = 0x12;
    let b: u8 = 0x34;
    assert_eq!(silly_max(a, b), b as u16);
}

fn silly_max(a: u8, b: u8) -> u16 {
    let x = a as u16;
    let y = b as u16;

    if x > y {
        x
    } else {
        y
    }
}
```

Essentially, we take two 8-bit unsigned integers, cast them to 16-bit unsigned integers, and compare the casted results, returning the greater of the two.

Running `clippy` on this code, this is the lint error I received:

```
warning: casting u8 to u16 may become silently lossy if types change
 --> src/main.rs:8:13
  |
8 | let x = a as u16;
  | ^^^^^^^^ help: try: `u16::from(a)`
  |
  = note: #[warn(cast_lossless)] on by default
  = help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#cast_lossless

```

Now, the issue with this code isn’t immediately obvious. And that’s because this code is _”correct”_ in that, at present, it does what it should do.

The potential error, as `clippy` suggests, is what _could_ happen if I later change this code. Suppose in the future, the design of the program changes and I actually want to find the max of two `u32`s. Look what would happen:

```rust
fn main() {
    let a: u32 = 0x00000002;
    let b: u32 = 0x10000001;
    assert_eq!(silly_max(a, b), b as u16);
}

fn silly_max(a: u32, b: u32) -> u16 {
    let x = a as u16;
    let y = b as u16;

    if x > y {
        x
    } else {
        y
    }
}
```

When we try to run this code, we get:

```
Running `target/debug/silly_max`
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `1`', src/main.rs:4:5

```

Clearly something went wrong. This is a classic truncation issue. When we cast `u32->u16`, we lose the top 16 most significant bits of our integers. `0x10000001` gets truncated to `0x0001` and `0x00000002` becomes `0x0002`. Under truncation, our comparison operation doesn’t always work how we’d expect.

Luckily in this case, the truncation was rather clear. However, our code still compiled. We only noticed _at runtime_ that we’d silently introduced a bug. (Just as `clippy` warned)

Let’s go back to the first version and implement the fix `clippy` suggested, using `u16::from` instead of casting via `as`.

```rust
fn silly_max(a: u8, b: u8) -> u16 {
    let x = u16::from(a);
    let y = u16::from(b);

    if x > y {
        x
    } else {
        y
    }
}
```

The above code compiles and works as expected. Now, lets make the same code change (switching the types of `a` and `b` to `u32`).

```rust
fn silly_max(a: u32, b: u32) -> u16 {
    let x = u16::from(a);
    let y = u16::from(b);

    if x > y {
        x
    } else {
        y
    }
}
```

Now, we get a friendly compiler error:

```
 --> src/main.rs:8:13
  |
8 | let x = u16::from(a);
  | ^^^^^^^^^ the trait `std::convert::From<u32>` is not implemented for `u16`
  |
  = help: the following implementations were found:
            <u16 as std::convert::From<bool>>
            <u16 as std::convert::From<u8>>

```

This is what we want! Instead of silently introducing the possibility of truncation into our program, the compiler lets us know that we’re trying to do an unsafe translation.

But what if we _want_ this possible truncation behavior? Well, you can still use `as` and you’ll get truncated casts. Interestingly, `clippy` doesn’t complain if you do `u32 -> u16` via `as` — it assumes you know what you’re doing.

So, what’s the moral of the story?

> Prefer using the `std::convert::From` trait for doing type conversions. The `as` keyword performs “safe” casts (casting won’t cause a panic), but you can still run into classes of errors that can be avoided by using designated converter methods.

Here are some resources if you’re interested in learning more:

- [Cast Operation Semantics - The Rust Reference](https://doc.rust-lang.org/reference/expressions/operator-expr.html#semantics)
- [cast\_lossless Explanation - Clippy](https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#cast_lossless)