Why does Rust not support goto statements?

So I've never really understood the logic behind this decision and I feel like the excuse that you'd usually find is a cop-out more than an actual reason. The usual reason for not having goto is because its harmful/dangerous/whatever. However, considering how advanced compilers are nowadays, I have no reason to doubt that examining control flow to verify that goto is used safely and properly isn't that complicated. (If this were about longjmp/setjmp, I'd understand.) A goto is quite useful -- it functions as an absolute jump to the label given. In some instances it makes writing code easier and makes it easier to understand the program flow and avoids code duplication.

Yes, people can right spaghetti code, but that isn't Rusts problem. There is no way to prevent that. Preventing jumps to anywhere in the program is also undoubtedly trivial given that the compiler can use the type system and verify that a borrow has actually been initialized and won't allow you to jump to a label if that hasn't happened, for example. So I'm pretty sure that gotos can be made much, much safer and better, while still allowing people to use it if they want.

So can someone explain to me why goto (still) isn't supported? I know most languages don't but Rust doesn't have to follow that trend.

So rust does support goto in some limited forms. Namely break and break 'label. Both of these act as a form of goto with an absolute position and are both checked by the compiler.

I would disagree with the statement that it isn't rusts business to prevent spegetti code as that can very easily create memory unsafety.

Yes such things could be checked but that would massively slow down the already slow rustc.

Finally, I would recommend reading Dykstra's paper. It would be great if rust had a safe alternative for all the valid uses of goto. The only one that I think missing is a guaranteed jump table DFA conversion.

5 Likes

People still have to read and unserstand the code. Even if the tangled, arbitrary control flow built on fully general, unstructured jumps were verified to be memory-safe by the compiler, it would still be more likely that the programmer got something logically wrong in it, and the chances of misunderstanding by outside readers would also shoot up.

Goto has been discussed many times here before. Please, search for the relevant topics, and do not beat a dead horse.

19 Likes

Most analysis is done in MIR, which is indeed just a control flow graph that uses effectively a goto, so there is some merit to asking "why no goto".

However, a "tame, domesticated goto" doesn't look like goto anymore. It's become (TCO), or some state machine transform. It's writing code in an extended basic block form, rather than pure goto.

There's benefit to these looser (but still structured) forms of control flow that Rust still has room to grow to support better.

But it's not by "just supporting goto." The primary job of a compiler is to diagnose incorrect code, and communicate why it's wrong and how to fix it. A fully unstructured goto, while possible to prove sound, is nearly impossible to diagnose why it isn't, since there's almost no limitation on what the intent could have been.

19 Likes

That's not a cop-out. It's experience talking.

There is literature explaining that it's (among other things) precisely because goto was passed out in favor of structured programming that led to compiler advancement in the area of control flow. An example of this is Dijkstra's paper on control flow.

The issue is that goto-based control flow is such a recipe for chaos that trying to analyze that and expecting good results is pretty futile, especially when things go wrong.

So now you do have a reason.

At the cost of having a system that is so easy to abuse that lots of other programs will be compromised as lots of other people have such a lack of experience with that "style" of programming that they could have issues even understanding what's going on there.

Not really a good reason IMO. Functions and macros do a better job of that than goto, since those can be parameterized and can be tagged with metadata in the form of attributes.

Actually, as a language that wants to be not just (memory) safe and sound, but also humane, it kind of is.

Bugs are being prevented to a large extent right now by not having goto at all. Its absence is a feature in that way.

It doesn't have to, but it should, for reasons of Rustacean sanity.

The thing to understand is, even if you could use goto effectively and without writing bugs (something that's doubtful, but let's assume that for a moment), lots of other people cannot. Confronting them with this is a surefire way to create more bugs. So it's also about the human interface, not just technical merit.

17 Likes

The reason not to have goto is (as some other people have alluded to) is that it's incompatible with destructors. This, among others, is why Go (a language with safe goto) has defer at function, not block, scope.

In order to lower goto in a sensible way, you need to make sure that:

  • You do not jump past initializers into regions where they are alive.
  • You do not jump over destructors.

The former of these is unsafe, and the later is ostensibly a logic bug, but both are quire bad. Moreover, because destructors get lowered to an LLVM cleanup instruction, there are real caveats around codegen (since cleanup emulates C++ goto, which has this destructor preserving-behavior).

As @CAD97 observes, once you place sufficient restrictions on goto, it turns into call/cc with static continuations. In other words, all of the value you would ever want to get out of goto is emulated perfectly by

'a: { ... break 'a ... }

This syntax does not exist today, but can be emulated using labeled loops; it is also a much-requested syntax. Backwards jumps are just a matter of adding another outer loop.

Given importantly, the primary uses of goto in traditional imperative code are:

  • goto err; we use the funny ? syntax for that.
  • goto cleanup; we use destructors, much like C++; other languages have invented weaker versions of this (Java's try-with, Python's with, Go's defer, among many others).
  • Breaking/continuing out of an inner loop; C++ lacks break/continue-with-label, but every language since Java fixes this bug.

There are arguments about readability, but those are subjective, not compelling, and frankly a bit dismissive. There are significantly stronger arguments against it, and I'm someone who shows up in threads to tell people their proposed new feature is... not super readable.

25 Likes

This syntax does not exist today, but can be emulated using labeled loop s; it is also a much-requested syntax.

It actually does:

#![feature(label_break_value)]

let result = 'block: {
    if foo() { break 'block 1; }
    if bar() { break 'block 2; }
    3
};
8 Likes

The more interesting question to me is "Why should Rust support goto statements?"

As already noted goto statements come with a bunch of problems and challenges. But what good do they bring? I am yet to find an example use case where a goto statement would allow me to solve a problem better than the existing language constructs do.

In languages like C, there are actually cases where goto statements are useful and may even improve code. But in Rust, we have

  • break labels
  • local inner functions
  • Powerful error handling and, more generally, monadic abstractions
  • hygenic macros (including within functions) if things are really bad

Together, these make even the last legitimate goto use cases in C superfluous.

11 Likes

Yes, but it's a nightly feature. I felt it wasn't important whether it was implemented in nightly or not. =)

Wow, talk about an informative topic. I even learned some new Rust syntax (I didn't know you could have labelled blocks -- even if it is only a nightly feature). :slight_smile: I knew about the "goto considered harmful" paper, though I was speaking more about (non-excessive) uses of goto, whereas that paper talks about (excessive) over-use of it.

4 Likes

goto on it's own is:

  1. Basically useless when RAII is involved (the one legitimate use for it is error-cleanup, and also manual jump tables, which needs computed goto anyways)
  2. A huge footgun, especially wrt. destructors and partially-initialized values.

Fun fact, C++ disallows using goto to enter the scope of any local variable, except one initialized by trivial default-init (like int i;, IE. uninitialized value). goto in Rust would run into the exact same problem, except rust doesn't have trivial default-init. So goto could only be used in rust to exit the scope of local variables (which triggers drop-glue, if any). Initness checking arround goto also sounds like a fun nightmare.

5 Likes

This surprises me, I thought we didn’t have a stable way to create state machines which use direct jumps from state to state? Like the stuff described here Peter Liniker by pliniker, and also featured here Tempesta Technologies | Blog | Fast programming languages: C, C++, Rust, and Assembly.

4 Likes

Indeed, you cannot implement irreducible control-flow graphs in Rust. How do you write this C snippet in Rust with labeled breaks? (a, b and c represent three states that loop in that sequence when returning false, and in reverse when true)

while(1) {
    a: if(a()) goto c;
    b: if(b()) goto a;
    c: if(c()) goto b;
}

The best you can do is duplicate one of the states to transform it into a (reducible) CFG with 4 states. E.g.

loop {
    loop {
        if a() || !b() {
            break;
        }
    }
    loop {
        if !c() || b() {
            break;
        }
    }
}

This one does optimize perfectly, but that's relying on LLVM to find and merge identical code blocks.

For reference, here's the previous IRLO discussion where many of these points were discussed: [Pre-RFC] Safe goto with value

So maybe the topic should really be "Why does Rust not support proper tail calls / finite state machines / <insert any flavor of goto that isn't called 'goto'>?"

edit: Meant to reply to matklad. There doesn't seem to be a way to change that in editing.

12 Likes

As the author of that previous thread ([Pre-RFC] Safe goto with value), I suppose I should chime in here, although I think I lost enough hair in the first debate so I'll keep it short.

  • Rust does not have a perfect way to simulate goto. Labeled blocks would almost work, but only if they can be referenced outside their scope (but still in the same function), which the current labeled block/loop feature is future compatible with, but does not directly support at the moment.
  • goto can be done in a safe way, and amounts to a state machine transformation. You can in most cases mimic it using loop { state = match state { ... } }, but this doesn't work in all cases, especially around complex borrowing situations, and it also comes with a runtime cost as this pattern is not yet optimized perfectly (there is a MIR-opt pass PR that was merged but I don't know how effective it is in practice). A direct translation to the MIR goto would solve both problems.
  • Syntax for goto is trickier than you might think, because it needs to be sufficiently signposted that it doesn't mislead readers. The main conclusion of the "safe goto with value" thread, at least from my perspective, was that the best syntax is a compiler-builtin macro that allows writing a bunch of states as if they were functions that call each other as tail calls, possibly using the reserved become keyword, in conjunction with a somewhat heavyweight fsm! wrapper to scope the whole thing. It is important that the syntax be heavyweight in order to discourage overuse (which I don't think is a concern in the modern era but is an important concession to Dijkstra fans), as well as to clearly delimit when gotos are happening and where they can go.

Also, the thread only covered gotos to statically known targets. Computed goto is a whole different ballgame, and I think it would be significantly more complex to support safe computed goto as this requires types like function pointer types but for local labels, which must be tail called, and are also lifetime-scoped to the current call frame and the variable initialization state. While it seems maybe possible, it would be a significant extension to the type system, and I would be inclined to settle for either doing it all in asm! or using regular function pointers.

16 Likes

Related thing is non-local control flow (and effects), see my thread

I wrote a blog before about simulating limited form of goto by macro: 在Rust中实现goto逻辑 - 知乎 (Maybe you'll need Google Translate or something...)

The TL;DR is that you can use these four macro definitions:

macro_rules! region_forward_label {
    (|$lbl_:lifetime| {$($s: stmt)*} $lbl:lifetime <- ) => {
        #[allow(redundant_semicolons, unused_labels, unreachable_code)]
        $lbl : loop {
            $($s)*;
            break;
        }
    };
}

macro_rules! region_backward_label {
    ($lbl:lifetime <- {$($s: stmt)*} |$lbl_:lifetime| ) => {
        #[allow(redundant_semicolons, unused_labels, unreachable_code)]
        $lbl : loop {
            $($s)*;
            break;
        }
    };
}

macro_rules! goto_forward_label {
    ($lbl:lifetime) => {
        break $lbl;
    };
}

macro_rules! goto_backward_label {
    ($lbl:lifetime) => {
        continue $lbl;
    };
}

to achieve this:

region_forward_label! {
    |'goto_label|
    {
        ...;
        goto_forward_label!('goto_label);
        ...;
    }
    'goto_label <-
}

region_backward_label! {
    'goto_label <-
    {
        ...;
        goto_backward_label!('goto_label);
        ...;
    }
    |'goto_label|
}

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.