- Start Date: 2014-06-30
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)
Change the semantics of the built-in fixed-size integer types from being defined
as wrapping around on overflow to it being considered a program error (but not
undefined behavior in the C sense), with whether to check for it at compile
and/or runtime being left up to the implementation. Add an attribute to allow
runtime overflow checks to be turned on or off per-scope, similarly to lints.
Add a compiler option to force checking to be on for testing and debugging
purposes. Add a WrappingOps
trait to the standard library with operations
defined as wrapping on overflow for the limited number of cases where this is
the desired semantics, such as hash functions.
The semantics of the basic arithmetic operators and casts on the built-in fixed-size integer types are currently defined to wrap around on overflow. This is is well-defined behavior, so it is already an improvement over C. Yet we should avoid falling into the trap of low expectations.
In the large majority of cases, wrapping around on overflow is not a useful or appropriate semantics: programs will generally not work correctly with the wrapped-around value. Much of the time, programs are merely optimistic that overflow won't happen, but this frequently turns out to be mistaken. When overflow does happen, unexpected behavior and potentially even security bugs can result.
It should be emphasized that wrapping around on overflow is not a memory
safety issue in the safe subset of Rust, which greatly mitigates, but does not
eliminate, the harm that overflow bugs can cause. However, in unsafe
blocks
and when interfacing with existing C libraries, it can be a memory safety
issue; and furthermore, not all security bugs are memory safety bugs. By
indiscriminately using wraparound-on-overflow semantics in every case, whether
or not it is sensible, it becomes difficult to impossible for programmers,
compilers, and analysis tools to reliably determine in which cases overflow is
the expected behavior, and in which cases it should be considered a defect.
In many cases, runtime overflow checks unfortunately present an unacceptable performance burden, especially in a performance-conscious language like Rust. As such, a perfect, one-size-fits-all solution is regrettably not possible. However, it is very much within our power to make better compromises than we currently do.
While the performance cost of checking for overflow is not acceptable in many cases, in many others, it is. Developers should be able to make the tradeoff themselves in a convenient and granular way.
The circumstances where wraparound on overflow is explicitly desired are comparatively rare. The only use cases for wrapping arithmetic that are known to the author at the time of writing are hashes, checksums, and emulation of hardware instructions with wraparound semantics. Therefore, it should be acceptable to only provide named methods for wraparound arithmetic, and not symbolic operators.
-
Clearly distinguish the circumstances where overflow is expected behavior from where it is not.
-
Provide programmers with the tools they need to flexibly make the tradeoff between higher performance and more reliably catching mistakes.
-
Be well-contained and minimally disruptive to the language as it is.
-
Make checked arithmetic fast. Let me emphasize: the actual performance of checked arithmetic is wholly irrelevant to the content of this proposal. It will be relevant to the decisions programmers make when employing the tools this proposal proposes to supply them with, but to the tools themselves it is not.
-
Prepare for future processor architectures and/or compiler advances which may improve the performance of checked arithmetic. If there were mathematical proof that faster checked arithmetic is impossible, the proposal would still be the same.
Many aspects of this proposal and many of the ideas within it were influenced and inspired by a discussion on the rust-dev mailing list. The author is grateful to everyone who provided input, and would like to highlight the following messages in particular as providing motivation for the proposal.
On the limited use cases for wrapping arithmetic:
On the value of distinguishing where overflow is valid from where it is not:
- Gregory Maxwell on June 18
- Gregory Maxwell on June 24
- Robert O'Callahan on June 24
- Jerry Morrison on June 24
The idea of scoped attributes:
On the drawbacks of a type-based approach:
In general:
Further credit is due to the commenters in the GitHub discussion thread.
Currently, the built-in arithmetic operators +
, -
, *
, /
, and %
, as
well as casts using as
on the built-in types i8
..i64
, u8
..u64
, int
,
and uint
are defined as wrapping around on overflow. Change this to define
them, on overflow, as either returning an unspecified result, or not returning
at all (i.e. terminating execution in some fashion, "returning bottom") instead.
The implication is that overflow is considered to be an abnormal circumstance, a program error, and the programmer expects it not to happen, resp. it is her goal to make sure that it will not.
However, the compiler is not allowed to assume that overflow cannot happen, nor to optimize based on this assumption. Where overflow or underflow can be statically detected, the implementation is free, and encouraged, to diagnose it with a warning or an error at compile time, and/or to represent the overflowing value at runtime with some form of bottom (e.g. task failure).
Notes:
-
In theory, the implementation returns an unspecified result. In practice, however, this will most likely be the same as the wraparound result. Implementations should avoid needlessly exacerbating program errors with additional unpredictability or surprising behavior.
-
"Terminating execution in some fashion" will most likely mean failing the task, but the defined semantics of the types do not foreclose on other possibilities.
-
Most importantly: this is not undefined behavior in the C sense. Only the result of the operation is left unspecified, as opposed to the entire program's meaning, as in C. The programmer would not be allowed to rely on a specific, or any, result being returned on overflow, but the compiler would also not be allowed to assume that overflow won't happen and optimize based on this assumption.
To state it plainly: This is for the programmer's benefit, and not the optimizer's.
See also Appendix A.
This depends on RFC 40 being implemented.
Introduce an overflow_checks
attribute which can be used to turn runtime
overflow checks on or off in a given scope. #[overflow_checks(on)]
turns them
on, #[overflow_checks(off)]
turns them off. The attribute can be applied to a
whole crate
, a mod
ule, an fn
, or (as per RFC 40) a given block or
a single expression. When applied to a block, this is analogous to the
checked { }
blocks of C#. As with lint attributes, an overflow_checks
attribute on an inner scope or item will override the effects of any
overflow_checks
attributes on outer scopes or items. Overflow checks can, in
fact, be thought of as a kind of run-time lint. Where overflow checks are in
effect, overflow with the basic arithmetic operations and casts on the built-in
fixed-size integer types will invoke task failure. Where they are not, the
checks are omitted, and the result of the operations is left unspecified (but
will most likely wrap).
Significantly, turning overflow_checks
on or off should only produce an
observable difference in the behavior of the program, beyond the time it takes
to execute, if the program has an overflow bug.
It should also be emphasized that overflow_checks(off)
only disables runtime
overflow checks. Compile-time analysis can and should still be performed where
possible. Perhaps the name could be chosen to make this more obvious, such as
runtime_overflow_checks
, but that starts to get overly verbose.
Illustration of use:
// checks are on for this crate
#![overflow_checks(on)]
// but they are off for this module
#[overflow_checks(off)]
mod some_stuff {
// but they are on for this function
#[overflow_checks(on)]
fn do_thing() {
...
// but they are off for this block
#[overflow_checks(off)] {
...
// but they are on for this expression
let n = #[overflow_checks(on)] (a * b + c);
...
}
...
}
...
}
...
There is an important decision to be made with respect to the default behavior
where neither overflow_checks(on)
nor off
has been explicitly specified. The
author does not presume to know the correct answer, and leaves this open to
debate. The following defaults are possible:
-
The default is
on
. This means that the default is to catch more mistakes. -
The default is
off
. This means that the default is to be faster. (This happens to be the current "default".) -
There is no default, and a decision is forced. If the programmer neglects to explicitly specify a behavior, the compiler will bail out and ask her to specify one.
-
Combination of (1) and (3): The default is
on
, but the compiler emits a warning when falling back to the default behavior. -
Combination of (2) and (3): The default is
off
, but the compiler emits a warning when falling back to the default behavior.
The programmer has the option to turn overflow_checks(off)
due to performance
considerations. However, when testing or debugging the program, for instance
when tracking down a difficult bug, it may be desired to throw performance to
the wind and enable as many checks as possible. For this purpose, provide a
compiler option, e.g. --force-overflow-checks
, which causes overflow checks to
be considered on
even where an attribute has turned them off
. This is
somewhat analogous to the behavior of our current --ndebug
flag and
debug_assert!
macros.
For those use cases where explicit wraparound on overflow is required, such as
hash functions, we must provide operations with such semantics. Accomplish this
by providing the following trait and impls in the prelude
:
pub trait WrappingOps {
fn wrapping_add(self, rhs: Self) -> Self;
fn wrapping_sub(self, rhs: Self) -> Self;
fn wrapping_mul(self, rhs: Self) -> Self;
fn wrapping_div(self, rhs: Self) -> Self;
fn wrapping_rem(self, rhs: Self) -> Self;
}
impl WrappingOps for int
impl WrappingOps for uint
impl WrappingOps for i8
impl WrappingOps for u8
impl WrappingOps for i16
impl WrappingOps for u16
impl WrappingOps for i32
impl WrappingOps for u32
impl WrappingOps for i64
impl WrappingOps for u64
These are implemented to wrap around on overflow unconditionally.
For convenience, also provide a Wrapping<T>
newtype for which the operator
overloads are implemented using the WrappingOps
trait:
pub struct Wrapping<T>(pub T);
impl<T: WrappingOps> Add<Wrapping<T>, Wrapping<T>> for Wrapping<T> {
fn add(&self, other: &Wrapping<T>) -> Wrapping<T> {
self.wrapping_add(*other)
}
}
// Likewise for `Sub`, `Mul`, `Div`, and `Rem`
Note that this is only for potential convenience. The type-based approach has the
drawback that e.g. Vec<int>
and Vec<Wrapping<int>>
are incompatible types.
The recommendation is to not use Vec<Wrapping<int>>
, but to use Vec<int>
and
the wrapping_
* methods directly, instead.
-
Amend the language definition to remove the guarantee that over- and underflow wraps around when using the built-in arithmetic operators and casts on the built-in types. Specify that such over- and underflow counts as a program error, and that it is up to the implementation whether to diagnose it at compile time where possible, and whether to insert checks at runtime, or to return an unspecified result (but without optimizing based on the assumption that overflow cannot happen).
-
Add the
WrappingOps
trait and implementations of it for the built-in types. -
Port existing code which relies on wraparound semantics - primarily hash functions - to use the
wrapping_
* methods.
Any code which does not explicitly depend on wraparound semantics, which is the large majority of existing code, is not affected.
-
Implement RFC 40 for attributes on statements and expressions.
-
Implement the
overflow_checks
attribute and the--force-overflow-checks
compiler flag. -
Potentially emit warnings or errors at compile time when static analysis can show that over- or underflow would happen.
After the backwards incompatible changes have been made, these can be done at any point without breaking the semantics of existing programs. The only effect would be to make it easier to discover bugs.
-
Code where
overflow_checks(off)
is in effect could end up accidentally relying on overflow. Given the relative scarcity of cases where overflow is a favorable circumstance, the risk of this happening seems minor. -
Having to think about whether wraparound arithmetic is appropriate may cause an increased cognitive burden. However, wraparound arithmetic is almost never appropriate. Therefore, programmers should be able to keep using the built-in integer types and to not think about this. Where wraparound semantics are required, it is generally a specialized use case with the implementor well aware of the requirement.
-
The built-in types become "special": the ability to control overflow checks using scoped attributes doesn't extend to user-defined types. If you make a
struct MyNum(int)
andimpl Add for MyNum
using the native+
operation onint
s, whether overflow checks happen forMyNum
is determined by whetheroverflow_checks
ison
oroff
whereimpl Add for MyNum
is declared, not whether they areon
oroff
where the overloaded operators are used.The author considers this to be a serious shortcoming. However, it has the saving grace of being no worse than the status quo, i.e. the change is still a Pareto-improvement. Under the status quo, neither the built-in types nor user-defined types can have overflow checks controlled by scoped attributes. Under this proposal, the situation is improved with built-in types gaining this capability. In light of this, making further improvements, namely extending the capability to user-defined types, can be left to future work.
-
Someone may conduct a benchmark of Rust with overflow checks turned on, post it to the Internet, and mislead the audience into thinking that Rust is a slow language.
Defer any action until later, as advocated by:
Reasons this was not pursued: The proposed changes are relatively well-contained. Doing this after 1.0 would require either breaking existing programs which rely on wraparound semantics, or introducing an entirely new set of integer types and porting all code to use those types, whereas doing it now lets us avoid needlessly proliferating types. Given the paucity of circumstances where wraparound semantics is appropriate, having it be the default is defensible only if better options aren't available.
Where overflow checks are turned off, instead of defining overflow as returning
an unspecified result, define it to wrap around. This would allow us to do
without the WrappingOps
trait and to avoid having unspecified results. See:
Reasons this was not pursued: The official semantics of a type should not change
based on the context. It should be possible to make the choice between turning
checks on
or off
solely based on performance considerations. It should be
possible to distinguish cases where checking was too expensive from where
wraparound was desired. (Wraparound is not usually desired.)
Have the usual arithmetic operators check for overflow, and introduce a new set of operators with wraparound semantics, as done by Swift. Alternately, do the reverse: make the normal operators wrap around, and introduce new ones which check.
Reasons this was not pursued: New, strange operators would pose an entrance barrier to the language. The use cases for wraparound semantics are not common enough to warrant having a separate set of symbolic operators.
Have separate sets of fixed-size integer types which wrap around on overflow and
which are checked for overflow (e.g. u8
, u8c
, i8
, i8c
, ...).
Reasons this was not pursued: Programmers might be confused by having to choose
among so many types. Using different types would introduce compatibility hazards
to APIs. Vec<u8>
and Vec<u8c>
are incompatible. Wrapping arithmetic is not
common enough to warrant a whole separate set of types.
Just use the existing Checked
traits and a Checked<T>
type after the same
fashion as the Wrapping<T>
in this proposal.
Reasons this was not pursued: Wrong defaults. Doesn't enable distinguishing "checking is slow" from "wrapping is desired" from "it was the default".
Reasons this was not pursued: My brain melted. :(
"What should the default be where neither overflow_checks(on)
nor off
has
been explicitly specified?", as discussed in the main text.
Instead of a single WrappingOps
trait, should there perhaps be a separate
trait for each operation, as with the built-in arithmetic traits Add
, Sub
,
and so on? The only purpose of WrappingOps
is to be implemented for the
built-in numeric types, so it's not clear that there would be a benefit to doing
so.
C and C++ define INT_MIN / -1
and INT_MIN % -1
to be undefined behavior, to
make it possible to use a division and remainder instruction to compute them.
Mathematically, however, modulus can never overflow and INT_MIN % -1
has value
0
. Should Rust consider these to be instances of overflow, or should it
guarantee that they return the correct mathematical result, at the cost of a
runtime branch? Division is already slow, so a branch here may be an affordable
cost. If we do this, it would obviate the need for the wrapping_div
and
wrapping_rem
methods, and they could be removed. This isn't intrinsically tied
to the current RFC, and could be discussed separately.
It is not clear whether, or how, overflowing casts between types should be
provided. For casts between signed and unsigned integers of the same size,
the Transmute
trait for safe transmutes would be appropriate in
the future, or the unsafe transmute()
function in the present. For other casts
(between different sized-types, between floats and integers), special operations
might have to be provided if supporting them is desirable.
-
Extend the ability to make use of local
overflow_checks(on|off)
attributes to user-defined types, as discussed under Drawbacks. For newtypes of the built-in types which simply want their arithmetic operations to forward to the built-in ones, this could be accomplished by just adding a newtype deriving feature, which we would eventually like to do anyways. For more involved cases, there may not be an easy solution. -
Look into adopting imprecise exceptions and a similar design to Ada's, and to what is explored in the research on AIR (As Infinitely Ranged) semantics, to improve the performance of checked arithmetic. See also:
-
Make it easier to use integer types of unbounded size, i.e. actual mathematical integers and naturals.
In theory, the below loop would be allowed to print "hello" any number of times from 127 to infinity (inclusive), and may print "hello" a different number of times on different runs of the program, and may print "hedgehogs", but may not print "penguins".
fn main() {
let mut i: i8 = 1;
while i > 0 {
println!("hello");
i = i + 1;
if i != i { println!("penguins"); }
if i + 1 != i + 1 { println!("hedgehogs"); }
}
}
Thus, for example, LLVM's undef
could not be used to represent the value of
100i8 + 100
, because undef
can be two different values in different usage
points.
In practice, implementations should avoid unnecessarily surprising behavior,
and the above loop should almost always print "hello" 127 times, and nothing
else. Implementations would also be encouraged to diagnose at compile time the
fact that this loop, as well as 100i8 + 100
, will always overflow.