Rust / x86_64, underflow / overflow

No.

I am enamored by Rust's ability to perform overflow checks. A key use case are competitive programming systems like Kattis. On these systems, the following is true:

  • The code is compiled with rustc (a certain version) and with -O.
  • It has to be -O because otherwise the code would very likely time out.
  • I don't get to choose the version of the compiler.
  • I don't get to add compiler flags.
  • I don't get to use cargo or choose cargo.toml.
  • I would love to turn on overflow checking in the source code for some or all of the code, without resorting to checked_ function calls, in a manner similar to C#'s checked blocks

I think this would be a really useful feature.

Are you suggesting that rustc should add a new feature because some programming contest website does not allow you to specify compiler flags? :slight_smile:

1 Like

Programs aren't composed entirely of integer arithmetics.

Integer overflow is not a memory management error.

1 Like

It seems obvious that being able to turn on/off overflow checks for sections of the code (without influencing how a program is compiled) would have benefits that extend beyond programming contests. At least, I would submit that the designers of C# may have had this thought when they added this keyword. For one, it would allow a programmer to express their intent without resorting explicit function calls. Seems potentially very beneficial to me.

... and I don't appear to be the only one. The designers of the checked_expr and checked crates may have felt similarly. So my hunch earlier in this thread was correct, it's possible to do this with macros. It also seems to be only a few dozens of lines of code, easily copied into a submission to a system such as Kattis.

That macro can also add subtle bugs though.

  • The checked arithmetic doesn't apply to nested function calls, only the operations you explicitly write inside it.
  • Internally it uses a closure, so it can move more stuff than it should, potentially resulting in weird compile errors.
  • Any usage of the ? operator inside the checked! macro is not scoped to the function, but to the macro. Take for example:
    fn do_stuff() -> Option<i32> {
    	let i = checked_expr!(1i32 + fallible()?).expect("overflow happened");
    	Some(i)
    }
    
    here if fallible returns None then the expect will panic and None won't be returned from the function.
2 Likes

Yes, but only for code that's lexically inside the macro invocation. "Checkedness" cannot propagate into functions called by the checked code, which makes it less useful than one might think. Also, a macro like that would have to naively transform any operator token found into a checked call, no matter whether it would actually typecheck or not, as macros operate on a purely syntactic level.

2 Likes

You can turn on debug assertions in your release profile to get overflow checks in release mode.

3 Likes

Yes I know. Certainly not as defined by Rust. That is why I phrased it as "To my mind...".

My claim though is that integer overflow is equivalent to an attempt to misuse memory. If you look at it the right way and squint hard enough.

When the result of an integer addition overflows it's because a new to bit has been produced and there is no more space in the integer to put it.

When the result of writing one past the end of an array it fails because a new element has been produced and there is no more space in the array to put it.

See how those two sentences are so similar?

No matter how one looks at it both can have disastrous results if allowed to happen. Rust disallows one but allows the other. This seems all wrong in a language predicated on correctness, or at least inconsistent.

Of course we know why it is so, performance, integer overflow cannot be checked at compile time like the borrow checker checks memory use.

Ideally the hardware would fault on overflow to provide the desired error detection whilst maintaining performance. Same like divide by zero.

BTW, the more I reflect on the design decision to leave integer arithmetic implementation-defined in the language, the more confused I get. With Rust aiming to be a safe language, why would it leave the semantics of such crucial operations as integer arithmetic implementation-defined?

It means that when presented with programs such as:

fn main()
{
    let a : u8 = 255;
    let b : u8 = std::env::args().len() as _;
    let c = a + b;
    println!("{}", c);
}

and you'd be asked what they output when invoked with one argument, you'd be unable to say.

My Rust knowledge is admitted limited, but are there any other examples of implementation-defined behavior in safe Rust, or is this the only one?

ps: is there any precedent for this, i.e., any other language designed in the last 20 years that leaves integer arithmetic implementation defined?

It's a superficial syntactical similarity. There is an enormous difference in how integer arithmetic and memory management are used.

Memory management features a clear, well-defined pattern in the form of the ownership model which makes it easy to statically reason about the correctness of the vast majority of practical programs.

In contrast, integer arithmetic can be correct and incorrect in so many, chaotic ways that it's extremely hard to come up with a practical one-off pattern for statically checking it. The reason integer overflow works the way it works in Rust is expressly not because the developers of Rust are stupid or incompetent or inconsistent. It's just that integer arithmetic is prevasive, therefore statically enforcing correctness at every single use site would be disastrous to ergonomics. Also, integer overflow does not in itself lead to memory
unsafety, so treating it more leniently is justified.

1 Like

Integer arithmetic is not implementation defined. It depends on a compiler flag, sure, but that's not the same as being implementation defined.

I don't really understand where the issue comes from. Having asserts that only run in debug mode is pretty normal.

2 Likes

I was using the term "implementation-defined" as it's for instance used in the C18 standard:

3.4.1
implementation-defined behavior
unspecified behavior where each implementation documents how the choice is made
EXAMPLE An example of implementation-defined behavior is the propagation of the high-order bit when a signed integer is shifted right.

Perhaps Rust uses a different definition of implementation-defined; if not, Rust's behavior is exactly that. One compiler behaves a certain way, a different compiler behaves a different way. Both document their behavior.

I think a difference is that asserts leave clear and visible markers in the code and are usually purposefully inserted, and not used for arithmetic operations in the code mainline.

Beyond that, they're also a frequent source of errors in my experience. (For instance, in a C student assignment, we benchmark with -DNDEBUG and this causes issues with predictable frequency; I haven't checked whether Rust's assert! has some magic that checks for side effects.)

If someone else made a different Rust compiler, I would consider it an incorrect implementation of a Rust compiler if it didn't insert overflow checks in debug mode, or if it did insert overflow checks in release mode. This means that it isn't implementation defined.

As a language that prides itself on making it as easy as possible to write correct but fast code, I think debug-only overflow checks is the best possible choice. We get the overflow checks in our tests, but the code is fast in production.

If you are referring to asserts with side-effects, that's a moot point for overflow checks as they don't have side effects.

To answer the question on Rust's assert!, the way it works is that an assert! always runs, but a debug_assert! only runs in debug mode.

In the C Standard sense of "implementation-defined", everything in Rust is implementation-defined, because there is no formal standard, and even the reference is incomplete. The Rust language is de facto defined, modulo bugs, as "what rustc does". But insofar as Rust can be said to have a specification, behavior on integer overflow is not implementation-defined.

5 Likes

A potential if not justified concern with the current approach is that overflow related bugs escape testing in debug mode but manifest in release mode, perhaps as DoS. Perhaps such vulnerabilities will be the new ReDoS attacks in a few years.

Perhaps, but the reality is that always having overflow checks goes rather far in the slow direction. It's a compromise, and I think it's an excellent one. You can turn on overflow checks in release mode if you have an application where a different trade-off makes more sense, but I do think that the default is good.

1 Like

I would love for the programmer to be in control at all times (and not the person building the code).

Implementing contexts like in C# (that extend to called code) would probably be impossible in Rust with its compilation model, but I disagree with the view that this would make the feature useless.

Maybe we should submit an RFC to introduce an attribute.

There's checked_* for this.

3 Likes

I understand that. For the programmer to be in control of what they intend when they use the ergonomic + operator.

This may be ruffling feathers among the Rust aficionados, but I'll add that this is the way it's done in: Java, C#, Golang, Swift, Python, and even JavaScript to list just the languages where I know it off the top of my head.