No, this is wrong. It's a common misconception though. You would only want that in a hypothetical world where all computers are exactly the same.
Undefined and implementation defined behavior is what allows us to have performance at all. Here are some simple examples.
Suppose we want to make division by zero and null pointer dereference defined. Now every time you write a/b or *x, the compiler will be forced to emit an extra branching check before this operation.
Something much more common---addition. What about signed overflow? Do you want the compiler to emit an overflow check in advance? Similar reasoning for shift instructions.
UB in the language specification allows compilers to optimize based on the assumption that the programs you write won't have undefined behavior. If compilers are not able to do this, it becomes impossible to implement most optimizations we rely on. It's a very core feature of modern language specifications, not an oversight you can fix by thinking about it for 10 minutes.
Some of these checks could be removed by languages with better compilers and likely more restrictions. That is the better approach. As a user, I don't want to run code that is potentially unsafe and/or insecure.
You would not want to force these by default, nobody wants it. You can not statically determine them unnecessary in for the vast majority of code, even stuff as simple as `print(read(a) + read(b))`.
I know that languages like Java have a NullPointerException which they can throw and handle for situations like this, but they're also built on a highly specified virtual machine architecture that is consistent across hardware platforms. This also does not guarantee that your program is safe from crashing when this exception gets thrown, as you have to handle it somewhere. For something as general as this it will probably be in the Main function, so you might as well let it go unhandled as there's not that much you can do at that point.
For a language like C++ it is simpler, easier, and I would argue more correct, to just let the hardware handle the situation, which in this case would trigger a memory error of trying to access invalid memory. As the real issue is probably somewhere else in the code which isn't being handled correctly and the bad data is flowing through to the place where it accesses the null pointer and the program crashes.
To add to that in a lot of cases the program isn't crashing while trying to access address 0, it's crashing trying to access address 200, or 1000, or something like that, and putting in simplistic checks isn't going to catch those. You could argue that the check should guard against accessing the lowest 1k of memory, but then when do you stop, at 64k? Then you have an issue with programs that must fit within 1k of memory.
Leaving it unspecified is the better choice.
Given that has proven to be a completely false assumption, I don't think there's a justification for compilers continuing to make it. Whatever performance gains they are making are simply not worth the unreliability they are courting.
This part is correct. The problem is in how to deal with this. If you want the compiler to correctly deal with code having undefined behavior, often the only possibility is to assume that all code has undefined behavior. That means, almost every operation gets a runtime branch. That is completely incompatible with how modern hardware works.
The rest is wrong, but again, this is a common misconception. Language designers and compiler writers are not idiots, contrary to popular belief. UB as a concept exists for a reason. It's not for marginal performance boosts, it is to enable any compiler based transformation, and a notion of portability.
A good example is WebAssembly*—address 0x00000000 is a perfectly fine and well-defined address in linear memory. In practice though, most code you’ll come across targeting WebAssembly treats it as if dereferencing it is undefined behavior.
* Of course WebAssembly is a compiler target rather than a language, but it serves as a good example of the point you’re making.
If a pointer can be null, it must be an optional pointer, and you must in fact check before you dereference it. This is what you want. Is it ok to write a program which segfaults at random because you didn't check for a pointer which can be null? Of course not. If you don't null-check the return value of e.g. malloc, your program is invalid.
But the benefit is in the other direction. Careful C checks for null before using a pointer, and keeping track of whether null has been checked is a manual process. This results in redundant null checks if you can't statically prove (by staring at the code and thinking very hard) that it isn't null. So in practice you're likely to have a combination of not checking and getting burned, and checking a pointer which was already checked. To do otherwise you have to understand the complete call graph, this is infeasible.
Zig doesn't do any of this. If it's a pointer, you can safely dereference it. If it's an optional pointer, you must check, and then: it's a pointer. Safe to pass down the call stack and freely use. If you want C behavior you can always YOLO and just say `yoloptr.?.*`.
Overflow addition and divide by zero are safety checked undefined behavior, a critical concept in the specification. They will panic with a stack trace in debug and ReleaseSafe mode, and blow demons out of your nose in ReleaseFast and ReleaseSmall modes. There's also +% for guaranteed wraparound twos-complement overflow, and +| for saturating addition. Also `@addWithOverflow` if your jam is checking the overflow bit. Unwrapping an optional without checking it is also safety-checked UB: if you were wrong about the assumption that the payload carries a value, you'll get a panic and stack trace on the line where you did `yolo.?`.
Shift operations require that the right hand side of the shift be a type log2(Type.bitwidth) of the left hand side. Zig allows integers of any width, so for a: u64, calling a << b requires that b be a u6 or smaller. Which is fine: if you know values will be within 0..63, you declare them u6, and if you want to shift on a byte, you truncate it: you were going to mask it anyway, right? Zig simply refuses to let you forget this. Addition of two u6 is just as fast as addition of the underlying bytes because of, you got it, safety-checked undefined behavior. In release mode it will just do what the chip does.
There's a common theme here: some things require undefined behavior for performance. Zig does what it can to crash your program if that behavior is exhibited while you're developing it. Other things require that you take some well-defined actions or you'll get UB: Zig tracks those in the type system.
You'll note that undefined behavior is very much a part of the Zig specification, for the same reasons as in C. But that's not a great excuse to make staying within the boundaries of defined behavior as pointlessly difficult as it is in C.
The debug modes you mention are also available in various forms in C and C++ compilers. For example ASan and UBSan in clang will do exactly what you have described. The question is, then whether these belong in the language specification or left to individual tools.
Language specification is unavoidable when using said language.
no such thing ever existed.
For a bunch of languages outside the C-centric world, specifications don't exist.
The intuitive distinction is that the second one is for compiler/library developers, and the former is for users.
A specification can not leave any room for ambiguity or anything up to interpretation. If it does (and this happens), it is treated as a bug to be fixed.
If you can make it work in a way that has acceptable performance characteristics, every systems language will adopt your technique overnight.
Huh, doesn't that sound familiar?
This is not the case. It's two's compliment overflow.
Also, since we're being pedantic here: it's not actually about "debug mode" or "release mode", it is tied to a flag, and compilers must have that flag on in debug mode. This gives the ability to move release mode to also produce the flag in the future, if it's decided that the overhead is worth it. We'll see if it ever is.
> Huh, doesn't that sound familiar?
Nope, it is completely different from undefined behavior, which gives the compiler license to do anything it wants. These are well defined semantics, the polar opposite of UB.
So if we're going to be pedantic, it's safe Rust which has defined semantics for basically everything. A considerable accomplishment, to be sure.
Okay, here is an example showing that rust follows LLVM behavior when the optimizer is turned on. LLVM addition produces poison when signed wrap happens. I'm a little bit puzzled about the vehement responses in the comments wow. I have worked on several compilers (including a few patches to Rust), and this is all common knowledge.
define noundef i32 @square(i32 noundef %x, i32 noundef %y) unnamed_addr #0 !dbg !7 {
%_0 = add i32 %y, %x, !dbg !12
ret i32 %_0, !dbg !13
}
Let's compare like to like, here's one with equivalent C++ code: https://godbolt.org/z/Y4MnGeof4The C++ output:
define dso_local noundef i32 @square(int, int)(i32 noundef %0, i32 noundef %1) local_unnamed_addr #0 !dbg !99 {
tail call void @llvm.dbg.value(metadata i32 %0, metadata !104, metadata !DIExpression()), !dbg !106
tail call void @llvm.dbg.value(metadata i32 %1, metadata !105, metadata !DIExpression()), !dbg !106
%3 = add nsw i32 %1, %0, !dbg !107
ret i32 %3, !dbg !108
}
> LLVM addition produces poison when signed wrap happens.https://llvm.org/docs/LangRef.html#add-instruction
> nuw and nsw stand for “No Unsigned Wrap” and “No Signed Wrap”, respectively. If the nuw and/or nsw keywords are present, the result value of the add is a poison value if unsigned and/or signed overflow, respectively, occurs.
Note that Rust produces `add`. The C++ produces `add nsw`. No poison in Rust, poison in C++.
Here is an example of these differences producing different results, due to the differences in behavior: https://godbolt.org/z/Gaonnc985
Rust:
define noundef zeroext i1 @test() unnamed_addr #0 !dbg !14 {
ret i1 true, !dbg !15
}
C++: define dso_local noundef zeroext i1 @test()() local_unnamed_addr #0 !dbg !123 {
tail call void @llvm.dbg.value(metadata i32 undef, metadata !128, metadata !DIExpression()), !dbg !129
ret i1 false, !dbg !130
}
This is because in Rust, the wrapping behavior means that this will always be true, but in C++, because it is UB, the compiler assumes it will always be false.> I'm a little bit puzzled about the vehement responses in the comments wow.
You are claiming that Rust has semantics that it was very, very deliberately designed to not have.
The more interesting part is that the mode can be individually modified on a per-block basis with the @setRuntimeSafety builtin, so it's practical to identify the performance-critical parts of the program and turn off safety checks only for them. Or the opposite: identify tricky code which is doing something complex, and turn on runtime safety there, regardless of the build status.
That's why this sort of thing should be part of the specification. @setRuntimeSafety would be meaningless without the concept of safety-checked undefined behavior.
I would say that making optionals and fat pointers (slices) a part of the type system is possibly more important, but it all combines to give a fighting chance of getting user-controlled resource management correct.
Given the topic of the Fine Article, it's worth briefly noting that `defer` and `errdefer` are keywords in Zig. Both the test allocator, and the GeneralPurposeAllocator in safe mode, will panic if you leak memory by forgetting to use these, or rather, forget to free allocations generally. My impression is that the only major category of memory bugs these tools won't catch in development is double-free, and that's being worked on.
—— I'm still entirely unconvinced.
The thing is, wrap-around is not only well-defined, it's common, and EXPECTED.
Example:
static inline u32 __hash_32_generic(u32 val)
{
return val * GOLDEN_RATIO_32;
}
and dammit, I absolutely DO NOT THINK we should annotate this as some
kind of "special multiply".
—-Full thread: https://lore.kernel.org/lkml/CAHk-=wi5YPwWA8f5RAf_Hi8iL0NhGJ...
Signed integer overflow, on the other hand, is undefined. The compiler is allowd to assume it never happens and can re-arrange or eliminate code as it sees fit under that assumption.
How many lines will this code print?
for (int i = INT_MAX-1; i < 0; ++i) printf("I'm in danger!\n");No, it's really not. Do this experiment: for the next ten thousand lines of code you right, every time you do an integer arithmetic operation, ask yourself if the code would be correct if it wrapped around. I would be shocked if the answer was "yes" in as much as 1% of the time.
(The most recent arithmetic expression I wrote was summing up statistics counters. Wraparound is most definitely not correct in that scenario! Actually, I suspect saturation behavior would be more often correct than wraparound behavior.)
This is a case where I think Linus is 100% wrong. Integer overflow is frequently a problem, and demanding the compiler only check for it in cases where it's wrong amounts to demanding the compiler read the programmer's mind (which goes about as well as you'd expect). Taint tracking is also not a viable solution, as anyone who has implemented taint tracking for overflow checks is well aware.
For the kernel, which deals with a lot of device drivers, ring buffers, and hashes, wraparound is often what you want. The same is likely to be true for things like microcontroller firmware and such.
In data analysis or monte carlo simulations, it's very rarely what you want, indeed.
For example, I opened up https://elixir.bootlin.com/linux/latest/source/drivers/firew... as a random source file in the Linux kernel, and I didn't see a single line where wraparound would be correct behavior.
There are definitely cases where wraparound behavior is correct. There are also cases hard errors on overflow isn't desirable (say, statistics counters), but it's still hard to call wraparound the correct behavior (e.g., saturation would probably work better for statistics than wraparound). There are also cases where you could probably prove that overflow can't happen. But if you made the default behavior a squawk that wraparound occurred, and instead made developers annotate all the cases where that was desirable to silence the squawk, even in the entire Linux kernel, I'd suspect you'd end up with fewer than 1000 places.
This is sort of the point of the exercise--wraparound behavior is often what you want when you think about overflow, but you actually spend so much of your time not thinking about it that you miss how frequently wraparound behavior isn't what you wanted.
If wraparound is ok for that particular multiplication, tell the compiler that. As a sibling comment says, this is seldom the case, but it does happen, in particular, expecting byte addition or multiplication to wrap around can be useful.
The actual expectation of the vast majority of arithmetic in a computer program is that the result will be correct in the ordinary schoolyard sense. While developing that program, it should absolutely panic if that isn't the case. "Well defined" doesn't mean correct.
I don't understand your objection to spelling that `val *% GOLDEN_RATIO_32` is. When someone sees that (especially you, later, coming back to your own code) it clearly indicates that wrapping is expected, or at least allowed. That's good.
For example: Rust will silently wrap signed integers in release mode even when it’s considered a bug and crashes in debug mode.
You have missed my point.
Every other example you mention is done by rust in release mode and the performance impact is minimal, so I would say it's a good counterexample to your claims that defining these things would hamstring performance (signed integer overflow especially is an obvious no-brainer for defining. Note that doesn't necessarily mean overflow checks! Even just defining the result precisely would remove a lot of footguns).
This is wrong, because you would define them to have the behavior that the architecture in question does, so no changes would be needed. For integer division this would mean entering an implementation-defined exceptional state that does not by default continue execution (on Linux, SIGFPE with the optional ability to handle that signal). For dereferencing a pointer, it should have the same semantics as a load/store to any other address--if something is there it works normally, if the memory is unmapped e.g. for typical Linux x86 programs you get SIGSEGV (just as you would for accessing any other unmapped address).
Suppose now, there are two architectures with slightly differing behavior.
Can the compiler still optimize signed x + 1 > x to true?
And even then there are tools to help define much of that - if you want well defined wrapped signed integers, great. If you want to trap on overflow, there's an option for that. Lots of compiler warnings and other static analysis tools (that would just be default-rejected by the compiler today if it didn't have historical baggage, but they exist and can be enabled to do that rejection).
Yes, there's many issues with the ecosystem (and tooling - those options above should be default IMHO), but massively overstating them won't actually help anyone make better software.
And other languages often have similar amounts of "undefined behavior" - but just don't document it as such, relying on a single implementation being "Defined Correct", and hope they're not actually being relied on if anything changes. Just like C, only undocumentated.
This is what I mean by it becoming "meme" - things like "Undefined Behavior" or "Memory Safety" have become a discussion-ending "Objective Badness", hiding the real intent - being "Languages I Do No Like" (or, most often, are a poor fit for the actual job I'm trying to do. Which is fine, but not rejecting that those jobs actually exist).
But they mean real things that we can improve in terms of software quality, and safety - but that's rarely the intended result when those terms are now brought up. And many things we can do right now with existing systems to improve things, to not throw away huge amounts of already well-tested code. To do a staged improvement, and not let "perfect" be the enemy of better.
> You don't want C, you want a language that actually gives defined semantics to all combinations of language constructs.
So, Zig?If a function has an error type (indicated by a ! in the return type), you have a few options. You can use `result = try foo();`, which will propagate the error out of the function (which now must have ! in its signature). Or you can use `result = foo() catch default;` or `result = foo() catch unreachable;`. The former substitutes a default value, the latter is undefined behavior if there's an error (panic, in debug and ReleaseSafe modes).
Or, just `result = foo();` gives `result` an error-union type, of the intended result or the error. To do anything useful with that you have to unwrap it with an if statement.
It's a different, simpler mechanism, with much less impact on performance, and (my opinion) more likely to end up with correct code. If you want to propagate errors the way exceptions do, every function call needs a `try` and every return value needs a ! in the return type. Sometimes that's what you need, but normally error propagation is shallow, and ends at the first call which can plausibly do anything about the error.
It also has tagged unions as a general mechanism for returning one of several enumerated values, while requiring the caller to exhaustively switch on all the possibilities to use the value. And it has comptime generics ^_^. But it doesn't use them to implement optionals or errors.
Correctness can be established well enough - even if guaranteed automatically - in a language with UB.