Having participated in a long discussion on users.rust-lang.org I've learned how Rust currently handles overflow in (plain) integer arithmetic. The current behavior appears to be the result of a compromise involving two RFCs, RFC 146 which was streamlined into RFC 560 which is currently implemented. I have read the long and insightful discussion in that RFC that justifies the current design, which involves a combination of setting rules (Rust doesn't, by default, support wrapping) and localized enforcement (debug_asserts in debug mode, a special optional -Z option for release mode), all with an eye towards striking a compromise between safety and performance. It also acknowledges a number of drawbacks. I apologize if I missed a more recent RFC on this issue.
Here are my thoughts and opinions.
First, I find the current behavior confusing and easily misunderstood as making wrapping an acceptable default. I should be clear that I understand that's not actually the case: the RFC is clear that overflowing operations (if they don't panic) produce an "undefined" value which presumably makes all derived values undefined as well, and is considered a program error. Still, I was a bit reminded of the signs you sometimes see along US Highways: "Speed limits enforced by aircraft" - which to me at least invoke a chuckle knowing that US police departments haven't had the budget to do so for many years. Perhaps the ominous "undefined value" or "considered program error" may have similar (IMO limited) deterrent value.
Second, the existing divergence of optimized and debug code is already acknowledged in the drawback section of RFC 560. But some programs may never run in a debug environment (or may use test data sets that are not known or too large for a time budget). I'm thinking of online environments such as Leetcode, Kattis, CodeForces which are run outside the cargo environment. Moreover, users of these environments do not even have the option to add compiler switches. This was less of an issue at the time when RFC 560 was written due to the lesser adoption of Rust at that time. In the future, we all expect that Rust will find more and more uses, which may be varied.
Third, I find it really unsatisfying that the programmer lacks control regarding the semantics of plain integer operations in an actual running Rust program, especially in light of what appears to be the lackluster enforcement referred to above, that is, the status quo that it could be either a wrapping or a checked behavior, depending on how their code is compiled or embedded. I believe that few if any other language provides the same degree of uncertainty regarding the actual execution (with perhaps the exception of signed integer overflows in C, which however was not designed recently). It certainly makes it difficult for professors in CS courses to assign questions like: "what does a + b do when you run the program" when exactly this question in reality depends on a number of ifs and whens and buts about the environment.
To the best of my knowledge, this lack of certainty about the actual execution sets Rust apart from Swift, Python, Java, (unsigned ints) in C, Golang, some of which are languages Rust is aiming to take market share from.
Fourth, I would love to be able to write code that at least locally sets a particular execution semantics for plain integers, without relying on how the code will be compiled. Existing approaches include std::num::Wrapping
and the checked_*
methods, neither of which I find very ergonomic.
How to fix this?
I think that, perhaps, Rust should take a stance and make overflow checking the default in all compilation modes, rather than viewing overflow checking as an expendable assertion. This would yield consistent semantics and remove the perceived ambiguity present in the status quo. It would not be a breaking change in terms of language semantics based on RFC 560. (Although the analogy doesn't fully carry over, it's after all the stance Rust takes wrt safe vs unsafe: safe the default, and you opt out with unsafe.)
Then, provide various ways of opting out, such as a crate-level attribute (overflows-checks=false
), a compiler switch (-Z skip-overflow-checks
instead of -Z force-overflow-checks
), etc. to easily mitigate any possible performance impact.
Finally, and perhaps independently of changing the default, address the problem of lack of programmer control, introduce the scoped attribute for checked arithmetic RFC 560 alluded to (but considered future work) that allows a programmer to enforce a particular semantics within a lexical scope. (This would presumably not be a context like C#'s checked context, i.e., it would not extend to functions called from within the scope.) This approach would have the advantage that programmers can choose a desired semantics for their plain integer operations independent of how their code will be compiled or otherwise combined, at least for the portions that they implement). The directive would override crate-level attributes and compiler switches.