In terms of implementing the actual continue 'label
, cost is effectively none: MIR already uses goto 'label
for control flow IIRC. (For printing; in memory it's a control flow graph.)
Making the continue 'label
operation unsafe
is "enough" to make it not unsound to include. But what more can we do? e.g. Cranelift's EBB model is fully correct for their SSA values.
(All code here should be treated as in an implicit unsafe
context.)
Using a label from a function that does not define it is insant UB, as it trashes the stack by jumping the call stack without actually modifying the stack:
fn outer_ub() {
let l = make_label();
continue *l;
}
fn main() {
let l = &'label;
ub(l);
'label: { }
return l;
}
fn inner_ub(l: Label) {
continue *l;
}
The UB in outer_ub
can be categorically prevented just by making the type of l
&Label
and tying it to a function-local lifetime. The UB in inner_ub
is not possible to be prevented with just current Rust's checks, and would require some extra magic to prevent label references from being passed to other functions. If we have that extra magic, then it wouldn't make sense to apply a lifetime to label references as that same magic can prevent it escaping with better error messages.
Perhaps just making the type unnameable would be enough, so long as it can't be captured into an existential.
Jumping to a label that uses a place that is not yet initialized is UB.
continue 'label;
let o = 5;
'label: { dbg!(&o); }
In a best case scenario, this accesses an uninitialized place and that's it. In a worst case scenario, this jumps over the stack pointer offset for the o
place on the stack and gets the stack pointer out of sync.
This could be prevented by disallowing taking a reference to a label that has declarations between the point of reference and the label, but doing so is overly restrictive from the actual requirement of not having any declarations (or even temporaries, potentially?) between the labelled continue
and the label it ends up jumping to.
It is UB to jump out of a scope with stack slots unless we teach the compiler to (run drop glue and) do stack deallocation before the labelled continue
(which is fully possible, and moves it away from "just" a computed goto
, but is worth mentioning here).
{
let v = vec![0, 1, 2, 3];
continue 'label;
}
'label { }
If we don't fix the stack to deallocate the v
place, then it desyncs the stack.
TL;DR: I think that the complexities of when a labelled continue would be valid means that basically none other than what is equivalent to a match
is actually sound, and we should just encourage that pattern instead and potentially make it easier (and make sure it optimizes well)
let j = compute_which_branch();
match j {
Jump::One => { .. }
Jump::Two => { .. }
Jump::Three => { .. }
Jump::Four => { .. }
Jump::Five => { .. }
}
If that match
can be implemented as a jump table rather than a series of jump-if-equal, it should be possible for the compiler to emit it, and probably should be possible for the developer to hint one way or another.