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?
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.
> 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.
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.
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.
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.
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?
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.
> 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.
> 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)`.
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.
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.
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.