I suppose it could be enlightening to understand why it wasn't a good decision for C or C++ at the time either.
> some architectures might use a trap/invalid representation
Traps on what? Access of an invalid representation? What if such representations doesn't exist?
Yeah - certain bit-patterns are just "invalid" rather than representing any given value. It's much nicer to debug, because you get an immediate failure (at the point where your code tries to access the uninitialized variable) rather than having a corrupt value propagate through your program.
> What if such representations doesn't exist?
Then you can't implement that strategy (other than by emulating it with considerable overhead, e.g. by having an extra marker byte for each variable in your program and checking it on every access). Hence why the C standard doesn't require you to do this.
As originally intended, C left the behaviour undefined so that users on platforms that did have trap representations would be able to take advantage of them. (It's very hard to rigorously specify what accessing a trap representation should do without impeding the compiler's ability to reorder memory accesses). Unfortunately it's ended up being used to do the opposite by modern compilers - not only do they not trap on access to uninitialized values, they abuse the undefined behaviour rules to propagate unexpected behaviour even further from the code that caused it.
We use types to restrain complexity. It was a mistake in C# to allow every object to be null. A better type system would allow devs to make a contract to easily disallow this and they try to fix this. Now here we have a blog post that seems to be fine with a function parameter of type u8 not actually being of 0-255. That's a huge change I always understood the type. Do I now have to do implement a null-check equivalent?
Undefined behavior for unsafe code is fine. But there has to be a transition were we go back to classical behavior. And in the blog posts example, this should be somewhere in main. Certainly not the seemingly safe always_returns_true.
So if the programmer doesn't in fact know what they're doing then they can cause bad things to happen outside the `unsafe` block, as this post shows.
It's not a valid value of that type - it's not a value you'll ever see if you're using the language in accordance with the spec (and, in the case of Rust, not a value you can ever see in safe Rust). It's an uninitialised value.
> We use types to restrain complexity. It was a mistake in C# to allow every object to be null. A better type system would allow devs to make a contract to easily disallow this and they try to fix this. Now here we have a blog post that seems to be fine with a function parameter of type u8 not actually being of 0-255. That's a huge change I always understood the type. Do I now have to do implement a null-check equivalent?
The point is for the language to do the null-check equivalent for you. A trap representation is null done better. Silently defaulting to a valid value is even worse than silently defaulting to null, because the value propagates even further from the point where it's wrong - imagine e.g. a Map implementation that, rather than returning null for a key that isn't present, returned an arbitrary value.
(Of course in the case of a Map, returning Maybe is better. But there's no way to do an equivalent thing for uninitialized variables, unless we made every single field of every single struct be Optional, and that's actually just equivalent to reintroducing null - the advantage of using Optional is the ability to have values that aren't Optional, at least in safe code).
> Undefined behavior for unsafe code is fine. But there has to be a transition were we go back to classical behavior.
Unfortunately no, that's not and has never been how undefined behaviour works. Undefined behaviour anywhere in your program invalidates the whole program and can lead to arbitrary behaviour anywhere else in your program (this has always been true with or without trap representations).
Pragmatically, what you want in the blog post's example is to get an error that tells you that the bug is that x was uninitialized, as soon and as close as possible to the point where x is actually used uninitialized. Ideally that would be on the "let x = ..." line (and if you didn't use "unsafe", that line would already be an error), but given that you've made the mistake, you're better off having an error as soon as you touch x (which happens in always_returns_true). Then you can see what the problem is and what's caused it. If always_returns_true runs "successfully", returning false, then you don't actually find out there's a bug until later (potentially much later) in your program, and have to do a lot of detective work to find out what went wrong.
I even have a post about this. :D https://www.ralfj.de/blog/2016/01/09/the-scope-of-unsafe.htm...
> In the example of the post, the compiler would look into `always_returns_true`
You can't look at each function in isolation. One important optimization all modern compilers use is inlining: short functions (like this `always_returns_true`) or functions that are only used once (like this `always_returns_true`) have their body inserted directly into the caller function, which allows further optimizations like constant propagation.
You definitely _can_ look at each function in isolation (in this example it's even sufficient to get the "best" possible version of that function"), but I do know that you'd usually do an inline pass, and further optimization passes afterwards. I don't see how that changes anything, though. If the function was inlined you'd get the same expression, and still you'd be unable to tell anything about `x`, except that it would have a definite value that you cannot observe. Again, you could argue that no matter the value, the expression would be `true`, so you could replace it.
But `always_returns_true` is not an example of how this particular undefined behavior can be useful as an optimization, merely an example of how it can be dangerous. For some examples of how it can be useful:
- Document describing the origins of `undef` in LLVM: http://nondot.org/sabre/LLVMNotes/UndefinedValue.txt
Basically, it helps to be able to replace "cond ? some_value : undef" with "some_value", especially when the code has been transformed to SSA form.
- Some architectures literally have a 257th possible value, like Itanium [1] [2]. On Itanium, every register can be set to "Not a Thing", i.e. uninitialized, and the CPU will trap if you try to store such a value to memory. Ironically, NaT was created in order to make the CPU's behavior more defined in a certain case, or at least more predictable... argh, I'm too tired to explain it properly; look at section 2.3.1 of [2] for a somewhat confusing explanation.
[1] https://devblogs.microsoft.com/oldnewthing/20040119-00/?p=41...
[2] https://www.cse.unsw.edu.au/~cs9244/06/seminars/07-gaol.pdf
> LLVM propagates the `undef` outward through the expression until the whole condition is `undef`, and then it arbitrarily picks that the condition should be false.
I think this illustrates what I find counter-intuitive about this whole mess; any function of `undef` shouldn't itself be `undef`. `undef < undef` is false in my head. `undef < 150` is just unknown, not undefined, since we don't know what `undef` is.
> Basically, it helps to be able to replace "cond ? some_value : undef" with "some_value"
This feels really contrived; in what setting would this actually be useful?
The text file I linked to explains it in more detail, so I'll defer to that.
> I think this illustrates what I find counter-intuitive about this whole mess; any function of `undef` shouldn't itself be `undef`. `undef < undef` is false in my head. `undef < 150` is just unknown, not undefined, since we don't know what `undef` is.
Except that for `undef < undef`, what if they're two different undefs? You would have to track each potentially uninitialized value separately. And then the optimizer would want to strategically choose values for the different undefs – e.g. "we want to merge 'cond ? some_value : undef123' with 'some_value', so let's set undef123 equal to some_value... except that could negatively impact this other section of code that uses it". It's certainly possible, but it would make the optimizer's job somewhat harder.
See also: