The problem with exposing undefined variables to safe code is that undef in LLVM isn't consistent. If an integer can change value without anyone actually assigning to it, then it's pretty easy to see how that could result in bounds checks being bypassed and turning into full UB.
This could be fixed by freezing all variables, but that seems like it would have overhead at least as bad as assuming that all loops have side-effects. A lot of the core team seem to prefer "the mere act of creating uninitialized variables is UB" semantics anyway, even if LLVM is less strict. In any case, fixing it so that exposing undefined variables to safe code is okay seems like a cure that's as bad as the disease.
Most of the compile time regressions I think should come from LLVM needing to do halting analysis on code which normally would be just directly eliminated and other additional overhead from having to analyze/handle code which had already been eliminated.
I don't have the LLVM experience to do the analysis without spending more time on it then I can currently afford. But I will try to see if I can find some notable/interesting cases wrt. runtime regressions.
I thought coming right after @notriddle's concrete example made it pretty clear what I meant by this, but oh well, let's try again.
My point was basically that @notriddle's example generalizes. For any do_x() operation that users expect to actually do X, but the compiler might simply ignore, a user could plausibly write code such as:
let x: Foo = MaybeUninit::uninit();
do_x();
if x_was_done() { /* initialize x */ }
x.assume_init()
and reasonably expect this to be sound, UB-free code, when it in fact isn't.
It's not a technical point about the precise meaning of UB or the tradeoffs of various LLVM passes, it's a squisher point about making a piece of the language this misleading is going to make people write incorrect, UB-triggering code that we could've prevented. If we really can't guarantee that do_x() will do X, then we need to pick a less misleading name like maybe_do_x(), and if that's not useful to anyone it shouldn't be in the language at all.
If we really can't guarantee that loop {} does an infinite loop, then we should've called it maybe_loop {} or not put it in the language. But of course, that's been stable forever, so it's really not an option in this case. Or to put it another way: in the extremely unlikely event that there are no other choices, accepting the regressions is less bad than making loop {} sporadically not loop.
But hopefully this will all be completely moot once we understand the regressions better.
I agree with you, but my example wasn't actually ergonomics-based. The Rust compiler uses control flow graph analysis to determine if a variable is initialized, and if the loop doesn't loop, then it can produce uninitialized variables with entirely safe code.
I know what you mean but do_x (or any part in it) can only be eliminated by LLVM if it has no forward progress.
Which means if do_x is eliminated it couldn't have done anything which would have initialized x or else it would not have been removed.
Well that is expect hanging the thread. To prevent x_was_done to have been reached.
So yes without question it makes unsafe code harder to write. But I don't think you would normally run into it without e.g. explicitly using loop {} for debug and then getting a compiler warning that you messed up the unsafe code.
In the end it's the responsibility of the programmer to make sure unsafe functions are used safely and this includes parts of the language not yet specified at all. So the solutions would still be possible.
Edit: Let's just pretend I didn't say that . This would make it basically impossible to write unsafe code safely.
static BOOL: AtomicBool = AtomicBool::new(false);
// guarantees that BOOL is true when the function completes!
fn do_x<E: ExitPredicate>() {
loop {
if E::exit_predicate() {
BOOL.store(true, Ordering::SeqCst);
return;
}
}
}
This function might not complete for some implementations of E. However, if the loop is removed in these versions, the function completes without setting BOOL to true, which should not be possible.
This can only cause UB if unsafe code is involved, but even without unsafe, it can cause errors and vulnerabilities.
Normally this loop can not be removed as (potentially) interacting with a atomic counts as making forward progress! So the loop would count as making forward progress and everything would be fine.
BUT if the compiler can at compile time prove that E::exit_predicate() always resolves to false and then used dead code elimination to eliminate it and afterwards runs the forward progress analysis then it actually could remove the loop.
Worse it's really hard to determine if this can happen, e.g. in the simplest case we have [impl...] fn exit_predicate() -> bool { false } and it's clear that it might very well happen. But in a more complex case it might involve (multiple levels of) const propagation and the code might very well look like it could produce true even through the compiler proves it can't.
(Through I don't know if llvm executes the analysis of forward progress in a specific order, e.g. if forward progress is only analyzed before doing const propagation and dead code eliminations it would practically with the current llvm impl not be a problem but then we are discussing a more theoretical problem).
So this now (re-)convinced me that forward progress UB is absolute madness, at least in the way it's in the C++ standard.
Sadly I'm also convinced that we will most likely need some form of forward progress rules.
Which makes this a pretty tricky problem to solve.
We also still might want to consider some of the thinks as a ad-hock temporary and most important forward compatible solution to reduce the amount of practical impact this has (i.e. doesn't fix the bug but makes it harder to hit it). At least if we can't come up with a better solution in the next few month.
(Like injecting magic side-effect marker only into loop statements instead of all places and/or providing a hang_forever() function for embedded as a guaranteed forward compatible and "proper" way to (busy) hang forever, note such a function might make sense as some platforms might provide better ways to hang forever then loop {}, but this is pretty off-topic for now).
In my opinion, this is an outdated rule. The same problems in rust occur with similar frequency in C and C++ code. If it were removed from the C and C++ language, I assume llvm would also remove it, lest they break strictly conforming code. I can probably propose it, but WG21 moves rather slowly, and WG14 moves even slower.
I certainly use loop{}, but in my case I can justify adding an asm! which I just indicate as volatile (I use loop in 65816 code, which has a Wait for Interrupts intstruction, which kills the system clock). In other code (like my x86_64 OS kernel), doing this is harder (I suppose I could hlt), and it gets even harder in userspace that needs to achieve the same result (an init process cannot use hlt, because that is a privileged instruction and thus requires CPL=0).
As far as I know, spin_loop() has no observable behavior which cannot be elided, so loop{spin_loop()}; could still be removed by llvm, and adding a side-effect would cost system resources in that case.
I have to point out, though, that the rule has already been partially removed from C, or, to be more precise, it was never unconditional in C in the first place. The idiomatic equivalent of loop {} in C, for (;;) {}, is not allowed to be removed. (See N1570 ยง6.8.5p6.) There has been an open bug against LLVM/Clang for this for years now, and they have shown no interest in fixing it. Maybe if C++ changed to match, or better, dropped the rule entirely, that would make them change their minds, but I'm not optimistic.
I started a discussion reguarding this on std-proposals. They seem eagar to point my freestanding code towards the standard std::condition_variable, or the posix sigwait(2)...
I don't believe they are looking to change it. Even if I were to go arround the list, from this, I'm going to guess neither EWG nor CWG is going to be an ally in this quest. Quite annoying that llvm is violating the C standard, and making it more difficult for other languages that don't have the same loop termination guartantees.
I guess I stick to rust for anywhere I need it.