Most of that paragraph is objectively false:
fn main() {
let i = 1;
let j = 1;
print!("i: {:?}\n", !i);
print!("j: {:?}\n", !j);
// spooky action at a distance
let v = vec![1, 2, 3];
v[i];
}
Only a small percentage of rust programmers could explain what's going on here before compiling and running this program. I fully expect one of the more obnoxious ones to come along any second and defend it by throwing in a lot of unrelated information and not addressing the fact this is a confusing example despite being so simple.I don’t understand the sentiment to want different behaviour for overflow in debug and release mode.
You want to catch overflows because they’re thought of as errors, but not in production. In production you just want silent failure?
Rust has a number of specialised integer operations (overflowing, saturating) that are slightly less ergonomic (they’re not literally called ‘+’ etc.). So we’re only really arguing whether we should be safe or performant by default, when we don’t care to be explicit.
What's confusing about them? They're basically what int and unsigned int were supposed to be. It's just that they realized they had produced a pile of unextensible shit so they had to add new names.
While they were at it, they produced the next version of unextensible shit so now int128 can't be added.
It starts with the premise of "C/C++ have pretty fucked up type systems, and look how much nicer integer type names are in Rust" and also that we should use unsigned integers as the first approximation if we are trying to model ℕ. Was I supposed to disagree with that? It didn't seem like the author was arguing against them, and they seem like pretty trivial, obviously true statements.
Then it starts to talk about how we cannot properly use unsigned in C/C++, because it's fucked up, and it cannot be fixed properly because the proper handling of integers is too expensive. This all sounds sadly familiar and we are nodding and saying: "Well, such is life." But at least we still have Rust, that doesn't suffer from the same inherent UB curse, right? I mean, I'm not deep enough into the details of current compiler implementation, but I've got the impression that the described problems don't apply to Rust. All good then? (edit: obviously, except for the fact that we still allow wrapping in prod builds, because it's expensive not to. But we all knew this already.)
Then it follows up with some pretty contrived (IMO) example of how we cannot mindlessly swap int with uint (obviously? I mean, you basically explicitly check for `i == -1` in this example), framing is as "uint is unintuitve". Was it supposed to be a strawman? It surely is a strawman, but the author doesn't argue against it.
And then a couple more of statements to the support of Stroustrup's "just use signed" (in C/C++), which also don't seem to apply to (saner) Rusts choices.
And then it ends w/o a conclusion. So, what was the conclusion supposed to be, please? I really don't understand what the author tried to convey.
Those where overflow results in wrapping are not integers and any such wrapping is an error that is difficult to detect and detecting it destroys the performance much more than using true integers.
The programming language C did a mistake by having only 2 integer types "signed" and "unsigned" and many modern programming languages have copied the same mistake without thinking. Moreover, the standard committees have made everything worse by defining relatively recently that overflow on "unsigned" integers must be handled by "wrapping", in which case they are no longer "unsigned integers", but they are integer residues modulo 2^N, which is a very different kind of mathematical object, for which the implicit integer promotion rules of C/C++ become broken, so now the standards define incompatible mathematical properties for the unsigned integers.
First of all, a programming language must offer as primitive types all the types that are implemented in hardware.
Instead of 2 types "unsigned" and "signed", the modern CPUs, like the Intel/AMD x86-64 CPUs or the Arm Aarch64 CPUs, implement no less than 8 distinct data types, all 8 being available in different sizes, which should be available as primitive types in any decent programming languages, in order to offer access to the corresponding machine instructions (which otherwise can be used only through inline assembly or opaque compiler intrinsics that are not checked for operand types). This means that the CPU instruction sets have at least a few instructions where the operands are interpreted as having one of those 8 data types.
The 8 integer data types are:
1. (signed) integers where overflow is detected
2. (signed) integers where overflow is handled by saturation (i.e. with infinities)
3. non-negative integers where overflow is detected
4. non-negative integers where overflow is handled by saturation
5. integer residues (most instructions use residues modulo 2^N, but there are also a few instructions for residues modulo 2^(N-1); for residues overflow results in wrapping)
6. bit strings
7. binary polynomials
8. binary polynomial residues (which are elements of Galois fields)
Non-negative integers and integer residues are very different things and the confusion between them cannot result in anything else but bugs.
Conversions between non-negative integers are correct only from a smaller size to a bigger size.
On the contrary, conversions between integer residues are correct only from a bigger size to a smaller size. For example, if you have the residue 65535, you can convert it to 255, because 65535 modulo 256 is 255. But if you have the residue 255 you cannot convert it to a 16-bit number, because there are 256 different 16-bit numbers that have the same 255 residue when converted to 8-bit, and you do not know how to choose one of them.
In any programming language that claims that it is a safe language both the overflow of (signed) integers and the overflow of non-negative integers a.k.a. unsigned integers must generate exceptions. Otherwise any claim of safety is just a lie. The only acceptable alternative to exceptions is to use saturation, which allows the propagation of the errors so that they can still be detected in the final results.
That does not mean that every arithmetic operation must be checked for overflow. There are many cases when a compiler can determine at compile-time that an overflow is impossible, like in most operations with indices or with loop counters, so in such cases the compiler can omit the overflow checks. Otherwise they must be inserted, and especially in release builds, where undetected errors can have much more serious consequences than in debugging builds.