Rust 2018: facing the cast problem


#1

In simple application-level Rust code programmers perform operations like x*y knowing that if their reasoning is wrong regarding the range of those values (or even if they haven’t given much thinking about their ranges), the compiler in debug builds will usually catch the overflow bug. This is quite different from writing down some kind of proof that the expression x*y will never overflow, but it’s a big improvement over equally simple C code that often is a types soup and has wild wrap-around and other problems (but it’s only a moderate improvement on alternative languages like Object Pascal, that was doing something similar since many years: https://www.freepascal.org/docs-html/prog/progsu64.html ). Rust offers several other tricks, like wrapping_mul and so on to tell the compiler a more precise semantics that’s desired (such explicit code is common in Ada language). So overall the integral story in Rust is a big step forward (but I miss ranged integrals in Rust).

But in the same application-level Rust code and even in some library level code you see plenty of “as” casts (including the Rustc compiler, I’ve counted more than 5000 “as” casts in its source outside comments and not including other usages of “as”). Every time you use an “as” cast you’re out the safety net given by overflow tests of debug builds because they truncate silently. This is like trying to sell bullet proof cars without windows. For me this is unreasonable.

So perhaps we can find some solution for Rust 2018… I am not suggesting to remove hard casts from Rust, because it’s a system language, but to let Rust programmers use them only when they explicitly don’t want a safety net (or when they can’t use it). For the short and common use case in application code I’d like truncate-panics in debug builds as default behaviour. I don’t know how this could be done, it sounds like a significant breaking of pre-Rust2018 code… Suggestions are welcome.

In some of my code I don’t use “as” casts for integral values, I use a to!{} macro like this:

#![feature(try_from)]

#[cfg(debug_assertions)]
use std::convert::TryFrom;

#[cfg(not(debug_assertions))]
macro_rules! to { ($e:expr, $t:ident) => (($e) as $t) }

#[cfg(debug_assertions)]
macro_rules! to { ($e:expr, $t:ident) => ($t::try_from($e).unwrap()) }

fn main() {}

It doesn’t work with f64/f32 values and it doesn’t work in const contexts:

const N: u128 = 50;
let a = [0u32; to!{N, usize}];

But in many cases it makes me a bit more relaxed and confident about the reliability of my Rust code.


#2

Before we can talk about deprecating or changing as on numeric types, we need solid & stable alternatives for everything that as currently does, and ideally comprehensive solutions for other related casts as well, so that we can tell people what to use instead of as. This is a wide open design space with lots of bike sheds, hence it’s progressing slowly, but it is progressing. We already have From impls for lossless/widening conversions, and https://github.com/rust-lang/rfcs/pull/2484 is one attempt at covering potentially-lossy ones. Anyone who wants to get rid of as would be well-advised to help move that forward.

Once all the casts people want to do with as have better alternatives, we can start to thing about how to nudge people away from as and towards these alternatives. But in the current state of the standard library it would be premature IMO.

Also, regardless of the above, I think it’s a bit too late to start anything new for the 2018 edition, we’re only about two weeks away (September 13th) from cutting the 1.30 beta that should have all the edition stabilizations.


#3

Without reaching to the merits of the proposal, we should stop here: Rust 2018 goes into beta in a couple weeks; it is too late to include any new idiom change in this edition.


My own view is that as is explicit and fine and does exactly what I expect it to do; there is nothing wrong with using as if you know and have considered the consequences of truncation in this context. The idea that code would be better if we made people replace x as usize with x.try_into().unwrap() when they believe they’ll never overflow a usize seems specious to me; if anything, the fact that the cast type can be inferred makes that code worse.

IMO what would actually be better would be to consider making the totally safe casts, like widening, implicit, so that you only use as when it could lose information, making it more likely that you will pause and think about whether this cast is correct. Having the operator perform both harmless casts and harmful casts makes it so easy to just treat as as boilerplate.

EDIT: I do kind of wish as panicked in debug builds and if you wanted to always truncate you had an API which explicitly opts into guaranteeing that behavior. I don’t know if we can ever do that though.


#4

For this, I think the right answer is .into(), which only lets you do safe numeric conversions. For instance, you can use .into() to convert from u16 to u32, but not from u32 to u16.


#5

Then how is one supposed to do deliberate truncation without generating gobs of pointless code? I’ve been hoping that this thread would result in a recommendation for an as replacement for truncation of simple unsigned integer expressions, retaining only low-order bits. (E.g., instead of (u64_expr) as u32 , using something like (u64_expr).trunc::<u32>() .) I need this type of truncation in crypto code.


#6

Right, sorry.

It’s short, nice, and it’s the unreliable solution. So I think it’s not fine.

That’s the point.

I agree, but RFC2484 tries to introduce other tools in this space.

into() is not a general solution because it doesn’t specify the destination type. Sometimes there is not enough type inference, and in general you want to be explicit (one advantage of “as” casts is that they are explicit).

There is no answer yet, I didn’t propose much solution. trunc::<u32> could be nice.

The need for such operation is well visible. It’s just a matter of how help the correctness of Rust code.


#7

The need for such operation is well visible. It’s just a matter of how help the correctness of Rust code

Until there is a better way to do it though we can’t remove that functionality from as.


#8

I’m firmly in the "as is dangerous and should never be used" camp. There are a few possible solutions:

  • Implement Index for other types than usize. The main source of casts I see comes from switching between usize-only world of stdlib and code trying to use u32 indices.

  • I’d love implicit lossless widening. However, it’s a tough sell to Rust users, because C’s particularly bad lossy implementation of it gave the feature a horrible reputation.

  • into()/try_into() are still unusable for the most important conversions, blocked by implementation of portability lints.


#9

I agree, but I see this as a separated issue. This means I’d like both a better casting story, and probably implicit cast in indexing too. I have many hundreds of foo[to!{i, usize}] in my code.

After using C for years I am against implicit lossless widening. It turns your code into a soup.


#10

I agree with you for numeric casts, but IIRC for pointer casts there is no alternative to using as.

Do we have to solve this at the language level? I feel like this could be implemented independently as a crate.

In the past, I have been trying to use From and Into for lossless casts and the conv crate for lossy numeric casts. However, the latter has serious drawbacks: f64::approx_from(usize) works on 64-bit platforms, but not on 32-bit platforms, where the approximation scheme has to be specified (which is a type parameter of approx_from).


#11

Yes, it’s a language-level problem because “as” is a part of the language. And people use it a lot.


#12

I’ve added a long comment to that thread to enlarge the scope of that discussion.


#13

Using as as the idiomatic syntax for both expect-no-overflow and expect-truncation casts is definitely one of Rust’s mistakes.

I would love to see an expect-no-overflow cast operator that works like the arithmetic operators: overflows panic in debug builds, truncate in release builds. Ideally it would be even more concise than as to encourage people to use it instead of as. (Maybe x@i32?) It might as well allow both narrowing and non-narrowing casts. Might as well work for pointers too.


#14

Wouldn’t the .into() solution mean that u32::from() is always available in cases where you do need an explicit type annotation?


#15

The Kotlin language uses the “as?” cast operator.


#16

You could write x.into():u32 after type ascription (RFC 2522) is stabilized.