Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: disallow "fall through" to a labeled statement #32565

Closed
griesemer opened this issue Jun 12, 2019 · 27 comments
Closed

Proposal: disallow "fall through" to a labeled statement #32565

griesemer opened this issue Jun 12, 2019 · 27 comments
Labels
FrozenDueToAge Proposal v2 A language change or incompatible library change
Milestone

Comments

@griesemer
Copy link
Contributor

griesemer commented Jun 12, 2019

[Edited to make an exception for labeled loops where the labels are only used to break out of (or continue) the loops with a break or continue statement.]

We propose that the language be changed such that the statement immediately before a labeled statement (in the same block) must be a terminating statement or an explicit fallthrough statement if (and only if) the label is the target of a goto statement. (Note that a goto statement jumping to the label is a terminating statement and thus also a valid choice.)

Specifically, loops that are labeled only so that they can be exited or continued with "labeled" break or continue statements (with no goto statement jumping to the same label anywhere) do not need a "fallthrough" to enter the loop.

This is not a backward-compatible change.

Rationale

Currently the language permits code to "fall through" into a label. For instance, the following code is permitted:

   // arbitrary statement which is not a terminating statement

exit:
  // handle special condition
  return ...
}

Here, an exit label is used to handle a special condition, for instance an error. Code such as this is not uncommon in functions that deal with a great number of conditions that all need to be satisfied for some reason, and which bail if any of the conditions is not satisfied. Examples of such code can be found in the compiler's type checker and other places. In many of these situations it is arguably an error if the code before the label "falls through" into the code following the label. Typically the code before the label will be a return statement.

In situations such as these, using a goto and a label in this semi-structured way is a simple, and very effective approach, but often looked down upon due the negative reputation goto's have. Sometimes a goto is exactly the right solution, and any rewrite or factoring only leads to more complexity.

Disallowing unintended fall throughs into labeled sections of a block will prevent a class of stupid errors. If fall through is intended, it's trivially achieved with an explicit fallthrough statement (or a goto to the label) which has the nice side-effect of documenting the intent clearly.

The language has a related requirement already for switch statements (note that in original C, a switch statement is essentially a computed goto).

Implementation

Besides updating the spec and related documentation, making this change would require a relatively simple change to the compiler's label and goto checker code in the front-end. The analogous change would be needed in go/types and gccgo. And of course, existing code that currently falls through would have to be adjusted (this could be done with gofmt).

@gopherbot gopherbot added this to the Proposal milestone Jun 12, 2019
@griesemer griesemer added the v2 A language change or incompatible library change label Jun 12, 2019
@Merovius
Copy link
Contributor

Can't this be done as a vet-check instead, for much of the benefit without the breakage?

Personally, I pretty much only use labeled statements to label outer loops to break out of (or continue). IMO, it is pretty awkward having to fallthrough to that - it's just a label to refer to the loop.

IMHO the upsides don't outweigh the downsides on this one…

@MichaelTJones
Copy link
Contributor

MichaelTJones commented Jun 12, 2019

This is a surprising proposal in that it has the tail wagging the dog.

The meaning of labels in Go1 is as a tag, a handle for "there" in goto label and a label for "what to break from" in break label. This proposal suggests that because some developers write code that (a) enters a labeled section by sequential execution, (b) where they do not want be in the labeled section, and, (c) they also don't want to write return, goto, break, continue, or panic at the end of their preceding code, that the language should change to make their refusal to say what happens before the label a compile error.

I strongly prefer the Go1 definition of this as a programmer error. Let's look at cases:

a := 1
if b {
   goto label
}
a *= 8
// I, programmer, refuse to do anything special here
label:
a *=2
return a

When the developer says, "but I wanted 2 or 8, not 2 or 16!", this proposal changes the language. I'd rather change the programmer. Not so much to resist change as to defend reason in the fundamental sequential nature of imperative programming, not just in Go, but in nearly all programming (other than SNOBOL with T and F both defined), and also in recipes, knitting and crochet, preflight checklists, and most every structured human endeavor.

If they want 2 or 8, they need not to refuse to put code where that comment is.

@UFOXD
Copy link

UFOXD commented Jun 12, 2019

remove fall through

@alanfo
Copy link

alanfo commented Jun 12, 2019

@griesemer

Just wondering whether the timing of the present proposal has anything to do with the try proposal (#32437) where @josharian suggested that labelled error handlers (based on something you'd originally mooted yourself) might be preferable to using defer?

I can't say I'd be in favor of it otherwise as only a few days ago I remember writing some code where I needed to jump to a spot intermediate between two nested for statements.

Are you saying that fallthrough would become available in any such scenario and not just in switch statements as at present?

@deanveloper
Copy link

I would love to see the actual uses of labels in everyday repositories. Standard library may not be a good test case for this because it requires lots of micro-optimization so it may have higher prevalence of things like goto. I have a feeling that labels are mainly used in order to label and break out of outer loops, which require fallthrough behavior. I may be wrong though, as I am only speaking from personal experience, so it would be good to see some data.

@earthboundkid
Copy link
Contributor

To prevent accidental fallthrough, isn't it sufficient to add panic("unreachable") before the label in code using goto in this way? Obviously, testing could miss it and like the old switch break pair, it's easy to omit, but goto code is so unusual nowadays, I don't think there would be that much burden to add it as a code review step or whatever. Plus you only need one panic, versus many breaks in a switch.

@MichaelTJones
Copy link
Contributor

MichaelTJones commented Jun 12, 2019 via email

@rogpeppe
Copy link
Contributor

I like the intent of this proposal, but I agree with @Merovius that it's not appropriate for labelled loops, where it's entirely appropriate enter the loop but use a label to break out.

Maybe the rule could be that the rule only applies to labels that are the target of a goto (as opposed to a break or continue statement).

@Merovius
Copy link
Contributor

@rogpeppe I'd be on board with that

@griesemer
Copy link
Contributor Author

@Merovius , @rogpeppe Indeed, you make an excellent point. I missed that last night. I've updated the proposal accordingly: The requirement for an explicit fallthrough to a labeled statement now only applies to labeled statements which are the target of a goto statement.

@griesemer
Copy link
Contributor Author

@MichaelTJones The example you are giving is exactly the kind of code where a fallthrough would be appropriate. If there's any good reason for not changing that code into the structured and much more easily readable

a := 1
if !b {
   a *= 8
}
a *=2
return a

the programmer should be forced to say so, explicitly. This may just be an unfortunate example, so I'd be curious if you have more convincing ones.

(Please also note that I amended the proposal to accommodate loops better.)

@griesemer
Copy link
Contributor Author

@alanfo Yes, a fallthrough would not just become available but would be required in cases outlined in the proposal (but see the amendment for loops). There is no denying that this idea came up while thinking about the consequences of using a try jumping to a label rather than doing a return.

@networkimprov
Copy link

Are there documented cases where the current label rules caused bugs? If so, could you list references?

@DeedleFake
Copy link

DeedleFake commented Jun 12, 2019

While I certainly agree that this should only apply to labeled statements that are the targets of gotos and not ones that are being used to identify loops, proper error messages for this are very important. For example:

// ...

// This got added later.
if somethingOrOther {
  goto loop
}

// ...

loop:
for {
  // ...
  if yetAnotherThing {
    break loop
  }
  // ...
}

When that goto gets inserted, suddenly the loop label becomes an error. The error message should have something like

./example.go:30:2: reachable labeled statement requires explicit fallthrough because of goto at ./example.go:25:2

Or something.

On a related note, something similar might be nice for the shadowed during return error.

@deanveloper
Copy link

deanveloper commented Jun 12, 2019

I think a much more easily noticeable rule would be to disallow fallthrough to labelled terminating statements, rather than disallow fallthrough for labelled statements that are the targets of gotos.

Maybe that's not the correct rule, I'm not sure yet. But I feel like the rule should be based on what the label is labelling rather than whether or not the label is the target of a goto.

@griesemer
Copy link
Contributor Author

@DeedleFake This proposal was not meant to encourage unjustified uses of gotos. They should be extremely sparingly used, if at all. But there are rare cases where a goto is exactly the right thing. If that is the case in your specific example, it is ok to have to write that fallthrough into the loop.

@ohir
Copy link

ohir commented Jun 13, 2019

Excuse me, but IMO such a detail do not deserve a huge breaking¹ change. Warnings about possible slip should be left to go vet.

¹. It is breaking mental model of the language. It not only adds a half of a new meaning to the fallthrough keyword, but also makes its interpretation contextual to the obscure details level »if (and only if) the label is the target of«.

What should I tell to the junior programmer who once a million lines of code will see such egregiously outplaced fallthrough?

@Merovius
Copy link
Contributor

@ohir FTR, it is already important whether or not a label is used. And it's not an "only if" - AIUI you can use fallthrough even if the label is not the target of a goto (and instead the target of a break/continue).

@ohir
Copy link

ohir commented Jun 14, 2019

@Merovius I cited "only if" from the proposal. I am aware that it was added later in response to @rogpeppe good concerns. My opinion is that people who know where to put a goto need not a nanny compiler but people who do not feel good in goto's presence will either stuck at 'misplaced' fallthrhrough or will have to remember a murky rule about rare usage of the keyword that for long had only been a part of the switch construct.

@Merovius
Copy link
Contributor

Another case I just stumbled over that IMO is interesting in light of this proposal: go/scanner.go. IMO requiring a fallthrough as the first line of the function is overkill. Arguably, this code could be fixed some other way (maybe the function should just recurse), but it's an example to keep in mind.

@earthboundkid
Copy link
Contributor

Good case for a become statement.

@griesemer
Copy link
Contributor Author

@Merovius Yes, indeed, thanks for pointing this out. The scanner could just use a for {} loop and a continue inside (recursion makes the scanner sensitive to stack overflow if fed malicious code - and it's trivially avoided). The only reason for the goto was that I didn't want to have the entire body indented because of the loop.

Maybe this proposal is not such a good idea after all.

@josharian
Copy link
Contributor

You could restrict the proposal to labels that are the target of a forward goto. (Suitably phrased, this also could take care of labels attached to switch and for statements, since those are always backwards-looking.)

@josharian
Copy link
Contributor

This restriction wouldn’t necessarily require adding a new meaning for fallthrough. Since there is by definition a label present, you could use a goto and a panic:

  goto L
  // ...
  goto L
  panic(“required terminating statement”)
L:
  // ...

Given that, it seems worth exploring this initially as a vet check. (With all the restrictions we’ve discovered to date.)

cc @mvdan

@MichaelTJones
Copy link
Contributor

MichaelTJones commented Jun 16, 2019 via email

@griesemer
Copy link
Contributor Author

@josharian Since a goto is a terminating statement, you don't even need the panic.

@griesemer
Copy link
Contributor Author

I'm going to retract this proposal. The proposal as stated is not backward-compatible and it doesn't seem warranted to introduce a non-backward compatible language change for what is admittedly a marginal safety improvement. Furthermore, this proposal doesn't address an urgent need.

Note that a vet check wouldn't be possible if it required that one use fallthrough as proposed because fallthrough is not permitted outside switch statements. One could require people to write

    goto L

L: ...

i.e., use a goto statement to the label immediately following instead of a fallthrough if this were desirable.

Closing.

@golang golang locked and limited conversation to collaborators Jul 16, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests