Rendered at 18:09:44 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
xscott 16 hours ago [-]
> Rust eliminates each of these points: all integer types have an
explicit size, and all types are prefixed with either i or u for
signed and unsigned respectively. There’s no bias towards a
certain size or signedness.
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.
whytevuhuni 11 hours ago [-]
Yeah, this has come up a few times when I was coding Rust. From just reading your code I know for sure that i is a usize (indexing is very common), but don't know what j is (I usually expect Rust to throw some kind of "could not infer integer type" compiler error, and it doesn't do that here). I'm guessing maybe i32. Code at the end of the function changing the meaning of code at the beginning of a function has also surprised me a couple times.
In my particular case I'm using Zed with type hints enabled, so the editor displays that expression as `let i: usize = 1;` automatically (which is really useful in a language with type inference, especially with expression as weird/complex as Rust iterator method chains). You're right that I wouldn't know if I were to open the file in a simple text editor.
mike_hock 21 hours ago [-]
> C has a confusing array of machine-dependent types such as ptrdiff_t and size_t
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.
addaon 21 hours ago [-]
> they produced the next version of unextensible shit so now int128 can't be added
It's hard to think of something more "added" (or, for that matter, more "extensible") than `_BitInt(128)`.
mike_hock 12 hours ago [-]
It has been tacked on as a "totally not an integer" type because the integer system isn't extensible enough to accommodate it.
It could of course be added with an ABI break (bumping intmax_t to int128), but that's not being done because it would be an ABI break. Hence, not extensible in practice.
sshine 16 hours ago [-]
> Given the roughly 30% overhead of overflow checks, I think restricting them to debug builds is a reasonable tradeoff. I can also see an argument for using wrap on overflow even in debug builds for consistency and simplicity.
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.
whytevuhuni 12 hours ago [-]
In production you generally want your code to not be 1.5x-2x slower than C/C++, otherwise people will ditch your whole language with all its other benefits in pursuit of performance.
There's a lot of safe but slow languages, Rust is trying incredibly hard to be a safe language that is still seen to be in that general C/C++ ballpark of performance.
krick 19 hours ago [-]
I don't understand what's the conclusion is. I was supposed to "disagree initially and be convinced by the end", but I ended up losing sight of what I was supposed to agree/disagree, and what should I've been convinced of.
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.
adrian_b 20 hours ago [-]
I found the support of the idea that for integers the overflows should be handled by "wrapping" as exceedingly dumb.
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.
norir 20 hours ago [-]
I agree with your diagnosis but not entirely the solution. I think fixed sized integers in general are a non portable mistake in a language high level language. Instead, a language should provide arbitrary numeric range types and infer the result type of numeric operations. There should be automatic promotion to bignum when the number no longer fits in the machine word for the target platform. This keeps the focus on the data rather than the register sizes, which is incidental complexity. Where performance is critical, the programmer can annotate function types with specific fixed sized types that fit in registers and the compiler can statically enforce necessary bounds checks on possible overflows. I hypothesize that most software is so inefficient due to architectural flaws that it could be rewritten in a safe language with no undefined and/or unsafe numeric operations and still be at least as fast as the existing software even with the additional overhead of bignums and bounds checking.
adrian_b 20 hours ago [-]
I agree that a language should also provide integers of unlimited size (possibly also non-negative integers of unlimited size) like many LISP variants, Python etc.
Integer range types, like in Pascal and Ada, should also be available.
Such types should better be used wherever possible, to reduce the probability of bugs and to make the programs more portable.
Nevertheless, all the fixed-size integer types that are directly implemented in hardware (which currently are the 8 types enumerated above, with 5 sizes from 8 bits to 128 bits, but not all combinations of type and size are provided by the existing CPUs) must also be provided as primitive types by a programming language, to be used when maximum efficiency is necessary, because the difference in performance between using them and using software-defined types can be very large.
Most of that paragraph is objectively false:
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.In my particular case I'm using Zed with type hints enabled, so the editor displays that expression as `let i: usize = 1;` automatically (which is really useful in a language with type inference, especially with expression as weird/complex as Rust iterator method chains). You're right that I wouldn't know if I were to open the file in a simple text editor.
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's hard to think of something more "added" (or, for that matter, more "extensible") than `_BitInt(128)`.
It could of course be added with an ABI break (bumping intmax_t to int128), but that's not being done because it would be an ABI break. Hence, not extensible in practice.
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.
There's a lot of safe but slow languages, Rust is trying incredibly hard to be a safe language that is still seen to be in that general C/C++ ballpark of performance.
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.
Integer range types, like in Pascal and Ada, should also be available.
Such types should better be used wherever possible, to reduce the probability of bugs and to make the programs more portable.
Nevertheless, all the fixed-size integer types that are directly implemented in hardware (which currently are the 8 types enumerated above, with 5 sizes from 8 bits to 128 bits, but not all combinations of type and size are provided by the existing CPUs) must also be provided as primitive types by a programming language, to be used when maximum efficiency is necessary, because the difference in performance between using them and using software-defined types can be very large.