Overflowing literals


#1

Rust still has this pitfall:

fn main() {
    println!("{}", 100_000_000_000);
}

Compiles with:

warning: literal out of range for i32
 --> ...\bug.rs:2:20
  |
2 |     println!("{}", 100_000_000_000);
  |                    ^^^^^^^^^^^^^^^
  |
  = note: #[warn(overflowing_literals)] on by default

Output: 1215752192

The only acceptable alternatives I see for this code are:

  1. Give a hard compilation error because the literal overflows an i32;
  2. Put the value inside a type it can fit into, like i64 or u64 (and eventually i128 or u128). If there is no fitting type (past u128::MAX), give a compilation error.

#2

How would making it a hard error improve the situation? You are already getting a warning. We can make it deny-by-default to strengthen the importance of the lint. I don’t see the advantage of making this breaking change.

Automatically inferring a bigger type depending on a value seems like too much magic imo.

We could add a compiler suggestion to add a suffix with a fitting type.


#3

How would making it a hard error improve the situation? You are already getting a warning.

Isn’t it a bug in the code? If you have a wrong syntax in a program you don’t give a “syntax warning”. If a programmer wants a wrapping-around literal there should be a method/syntax for this uncommon special case. But the general case should be aligned with the rest of the language that handles overflows as bugs.

Also, I see the fact that “as” could lose some information on default as a small design mistake that eventually we’ll have to work around encouraging the use of other kinds of conversions (like try_from) and discouraging “as” as much as possible (it should be regarded almost like unsafe{}, to be used only when you know what you’re doing and not as a first tool for common use by Rust newbies too).

I don’t see the advantage of making this breaking change.

I see it as a compiler bug fix, that are allowed to break broken code, to improve the overall situation.

Automatically inferring a bigger type depending on a value seems like too much magic imo.

It’s a tiny bit of magic, but I see it not too much different from inferring a number to be f64 because it ends with “.0”. And I agree this is a breaking change that could be unacceptable…

We could add a compiler suggestion to add a suffix with a fitting type.

A compiler error plus compiler suggestion sounds OK :slight_smile:


#4

IIRC, this was done because constant expressions weren’t properly type-checked in the past?

If this is technically possible now, IMO this should go through the usual deprecation process (future compatibility lints, etc), there shouldn’t be much breakage in practice if I’m not missing anything.


#5

I still don’t see why we should error here. There’s no difference imo in println!("{}", 100_000_000_000); and println!("{}", 100_000 * 1_000_000);, but only the first one should error, while the second one will keep being a warning?

This has been deeply discussed in https://github.com/rust-lang/rfcs/pull/1229 already and the decision was that it should be a warning.


#6

What if both become errors? (because they are).

Is the blocker code like this?

fn elts_per_page<T>() -> usize {
    if mem::size_of::<T>() == 0 {
        usize::MAX
    } else {
        4096 / mem::size_of::<T>()
    }
}

Such special cases should not break the general rule that overflows are bugs. In D language such cases are solved replacing the “if” with a “static if” that doesn’t compile the else branch when T is a zero sized type. In Rust you could use an explicit method that doesn’t generate compile-time errors:

fn elts_per_page<T>() -> usize {
    if mem::size_of::<T>() == 0 {
        usize::MAX
    } else {
        4096.unsafe_div(mem::size_of::<T>())
    }
}

#7

There is no blocker. This was a decision in the RFC. I argued the error point, it was decided against it.

Soon such hacks won’t be necessary anymore. With https://github.com/rust-lang/rust/pull/45002 we can evaluate conditions, too.


#8

Yes, the first one can be an error while the second stays being a warning.
If we can’t eliminate a mistake in general, it doesn’t mean we shouldn’t eliminate it in one well defined common case.
Regarding “no difference”, 100_000_000_000 is a passive constant, and 100_000 * 1_000_000 is a run-time operation that just happens to be performed at compile time as an optimization (unless it’s evaluated in constexpr context), so there is a difference.
(If 100_000 * 1_000_000 is evaluated in constexpr context it should be a hard error too, that’s what C++ does as well, see http://coliru.stacked-crooked.com/a/859b3f28ab223dab)

(I haven’t yet read https://github.com/rust-lang/rfcs/pull/1229 though.)


#9

With all due respect - not all overflows are bugs. Some algorithms, esp. of the bigint area depend heavily on overflows and wrapping arithmetic.

I needed to shove buckets of wrapping_* into my implementation of ED25519, which made the code ugly and unmaintainable. Attempt to use std::num::Wrapping didn’t help either, as wrapped integers lack bitwise operators - and bigints require both wrapping arithmetic and bitwise operators on the same data.


#10

I surely agree. When I write crypto-related code I often need wrapping operations. But the default for a good and modern language is to catch overflows on default. Then the language should offer ways to perform wrapping (or other behaviour, like saturation, if desired). If the current Rust+Std design (with wrapping_, saturation and Wrapping, etc) is not good enough then some better ways to handle the situation should be added.

wrapped integers lack bitwise operators

Is adding bitwise operators to wrapped integers going to solve your biggest problems? If the answer is positive, then writing a Rust enhancement request is a good idea.


#11

Is adding bitwise operators to wrapped integers going to solve your biggest problems? If the answer is positive, then writing a Rust enhancement request is a good idea.

Well, I actually dumped Rust for C++ for this project. Aside from crypto, it uses low-level Linux interface and I gave up rewriting every struct I need from <linux/*.h> or <sys/*.h>. And no, no automatic tool managed to crawl through it successfully.

I still have much love for Rust and, I’ll look into enhancing wrapping wrapper with bitwise ops and perhaps propose a pull request.