Zig doesn't provide any rationale for why it picked UB rather than wrapping. By default Rust's release builds give the integer overflows wrapping, so (1u8 + 255u8 == 0u8) rather than panic, so as to avoid paying for the checks.
This is probably not what you wanted, your code has a bug (if it was what you wanted, you should use the Wrapping type wrapper which says what you meant, not just insist this code must be compiled with specific settings) but you didn't have to pay for checks and your program continues to have defined behaviour, like any normal bug.
It is very rare that you need the unchecked behaviour for performance. Rare enough that although Wrapping and Saturating wrappers exist in Rust, even the basic operations for unchecked arithmetic are still nightly only. Most often what people meant is a checked arithmetic operation in which they need to write code to handle the case where there would be overflow, not an unchecked operation, Rust even has caution notes to guide newbies who might write a manual check - pushing them towards the pit of success - hey, instead of your manual check and then unsafe arithmetic, why not use this nice checked function which, in fact, compiles to the same machine code.
> By default Rust's release builds give the integer overflows wrapping, so (1u8 + 255u8 == 0u8) rather than panic, so as to avoid paying for the checks.
I consider that to have been a mistake, and hopefully one we can change. Note that this is about defaults, you can build your own project as release with overflow panics. I'd wish the language had a mechanism to select the math overflow behavior in a more granular way that can be propagated to called functions (in effect, I want integer effects) instead of relying exclusively in the type system:
fn bar(a: i32, b: i32) -> i32 where i32 is Saturating {
a + b
}
fn foo(a: i32, b: i32) -> i32 where i32 is Wrapping {
// the `a + b` wraps on overflow, but the call to
// bar overrides the effect of the current function
// and will saturate instead.
a + b + bar(a, b)
}
With this crates can provide control to their callers on math overflow behavior without having to provide type parameters in every API with a bounds for something like https://docs.rs/num-traits/0.2.19/num_traits/.
When you say it's a mistake (in your opinion) do you mean that you'd have picked panic in release builds by default? Or do you think Rust 1.0 without full blown effects was the mistake and so you'd actually want effects here and no smaller change is worthwhile ?
Personally I'm not as bothered about this as I was initially, whereas I'm at least as annoyed today by some 'as' casts as I was when I learned Rust -- if I could have your integer effects or abolish narrowing 'as' then I'd abolish narrowing 'as' in a heartbeat. Let people explicitly say what they meant, if I have a u16 and I try to put that in a u8, it will not fit, make me write the fallible conversion and say what happens when it fails. This strikes me as especially hazardous for inferred casts. foo as _ could do more or less anything, it is easily possible that it does something I hadn't considered and will regret, make me write what I meant and we'll avoid that.
> Zig doesn't provide any rationale for why it picked UB rather than wrapping
There's no need to provide a rationale because it's obvious, from a performance POV:
1) (a) UB on overflow > (b)wrapping on overflow
2) (b)wrapping on overflow > (c)trap on overflow.
So when you create a language you have to pick a default behaviour, Zig allow both (a) xor (c) with ReleaseFast and ReleaseSafe..
(1) is because this allows the compiler to do "better" optimisations, which unfortunately can create lots of pain for you if your code has a bug.
(2) is because these f.. CPU designers don't provide an 'add_trap_on_overflow' instruction so at the very least the overflow check instruction degrades the instruction cache utilisation.
Alas no, you've written a greater than sign but you'll find in reality it's often only the same. But you've significantly weakened the language, so you just made the language worse and you need to identify what you got for this price.
On the one hand, since you didn't promise wrapping in some cases you'll astonish your programmers when you don't provide it but that's what they expected, on the other since can't always get better performance you'll sometimes disappoint them by not going any faster despite not promising wrapping.
This might all be worth it if in the usual case you were much faster, but, in practice that's not what we see.
One can reasonably argue that the only reason why people expect wraparound is because it was the default in C, not because it actually makes sense. If the code actually depends on wraparound to produce the correct result, making that explicit in the operators, as Zig does, is surely a better choice, not the least because it gives people reading the code a clear indication that they should be paying attention to that. OTOH most code out there in the wild treats it more as a "never gonna happen" situation and doesn't deal with it at all, which isn't really made any worse with full-fledged UB.
Integer wrapping on overflow is not just a C thing, it happens at the hardware level as part of ALU instructions. It's actually kind of difficult to come up with a different behaviour that makes sense. Saturating arithmetic requires additional transistors.
It happens on hardware level for a single opcode, sure, but a 1:1 mapping between such an opcode and arithmetic operators in a high-level PL isn't a given, especially in presence of advanced optimizations.
In any case, PLs don't have to blindly follow what the hardware does as the default. Many early PLs did checked arithmetic by default. Conversely, many instruction sets from that era have specific opcodes to facilitate overflow checking.
The reason why we got it in C specifically is because of its "high-level PDP assembly" origins.
It’s worth pointing out that Zig also just straight up gives you wrapping and saturating adds with ‘+%’ and ‘+|’ operations and same for other arithmetic operations.
This is probably not what you wanted, your code has a bug (if it was what you wanted, you should use the Wrapping type wrapper which says what you meant, not just insist this code must be compiled with specific settings) but you didn't have to pay for checks and your program continues to have defined behaviour, like any normal bug.
It is very rare that you need the unchecked behaviour for performance. Rare enough that although Wrapping and Saturating wrappers exist in Rust, even the basic operations for unchecked arithmetic are still nightly only. Most often what people meant is a checked arithmetic operation in which they need to write code to handle the case where there would be overflow, not an unchecked operation, Rust even has caution notes to guide newbies who might write a manual check - pushing them towards the pit of success - hey, instead of your manual check and then unsafe arithmetic, why not use this nice checked function which, in fact, compiles to the same machine code.