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

make break apply to all blocks, not just loops #2990

Closed
andrewrk opened this issue Aug 1, 2019 · 13 comments
Closed

make break apply to all blocks, not just loops #2990

andrewrk opened this issue Aug 1, 2019 · 13 comments
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@andrewrk
Copy link
Member

andrewrk commented Aug 1, 2019

Currently, to break from a block with a value, one must label the block:

var x = block_label: {
    break :block_label value;
};

The reason for this is that, in other languages, such as C, break applies to the innermost loop or switch statement, and not blocks.

This is a conservative decision meant to minimize surprise and confusion at the control flow keyword doing something slightly, but crucially, different.

However, this is an instance of legacy design decisions of C holding Zig back from what may be the best design. Perhaps it's a place where Zig should boldly break expectations (:drum: :drum: :boom: ) and do it how it should have been done since the beginning.

If this proposal is accepted, then break will always apply to the innermost loop or block, and the example can be rewritten like this:

var x = {
    break value;
};
@andrewrk andrewrk added breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. labels Aug 1, 2019
@andrewrk andrewrk added this to the 0.6.0 milestone Aug 1, 2019
@andrewrk
Copy link
Member Author

andrewrk commented Aug 1, 2019

One argument in favor of this is that break already works differently than C inside a switch.

@mikdusan
Copy link
Member

mikdusan commented Aug 1, 2019

I like... but a few clarifying questions...

fn foo(i: u8) u32 {
    if (i == 0) {
        break; // Q1: jump to end of if-block?
    }

    if (i == 0) break; // Q2: no if-block, but outer block expects u32: compile error void expecting u32?

    if (i == 0) break 0; // Q3: no if-block, but outer block expects u32: return 0?

    while (true) {
        if (i == 0) break; // Q4: jump to end of while-block?
    }

    while (it.next()) |value| if (value == 5) break; // Q5: no if-block, jump to next statement after while?
}

@andrewrk
Copy link
Member Author

andrewrk commented Aug 1, 2019

Here are the answers, according to this proposal:

Q1: jump to end of if-block?

Yes

Q2: no if-block, but outer block expects u32: compile error void expecting u32?

This would be "error: must use return to return from function"

Q3: no if-block, but outer block expects u32: return 0?

"error: must use return to return from function"

Q4: jump to end of while-block?

This is a good question, because what's expected here is for break to apply to the while loop, but to do that, strict consistency must be broken to make the block attached to the while loop special. And I think that's what this issue proposes. Yes, the break would break out of the while loop.

Here's a related example:

suspend {
    break; // instead of suspending, the async function continues executing
}
// equivalent to:
label: suspend {
    break :label; // instead of suspending, the async function continues executing
}
suspend label: {
    break :label; // skips x +=1 and completes the suspend 
    x += 1;
}

Likewise:

while (true) {
    if (i == 0) break; // break out of the while loop
}
// equivalent to
label: while (true) {
    if (i == 0) break :label; // break out of the while loop
}
while (true) label: {
    if (i == 0) break :label; // skips over x +=1 and continues the loop
    x += 1;
}
// equivalent to
while (true) {
    if (i == 0) continue; // skips over x +=1 and continues the loop
    x += 1;
}

That is a downside of this proposal. This example is a bit inconsistent.

Q5: no if-block, jump to next statement after while?

Yes, that break would apply to the while loop.

@andrewrk
Copy link
Member Author

andrewrk commented Aug 1, 2019

One big downside of this is that a lot of code that looks like this will break in horrible ways:

while (foo) {
    if (bar) {
        break;
    }
}

@hryx
Copy link
Sponsor Contributor

hryx commented Aug 2, 2019

Rad! One more example to provide/ask clarification:

if (condition) {
    break;
} else {
    // jumps to here?
}
// or here?

I assume the latter, as the former would wreak havoc on this, or at least be super confusing:

if (optional) |x| {
    break;
} else {
    // but here we should assume that `optional` was null!
}

Maybe the answer is obvious but I thought I'd bring it up because in this case, break escapes not only the curly-enclosed block, but its enclosing if-else construct.

@Rocknest
Copy link
Contributor

Rocknest commented Aug 2, 2019

(in short: i am against this proposal)

This is so confusing to me, maybe i am just to used to C's rules that all major languages have adopted. I think that Zig needs unnamed blocks (to get rid of blk: {}).

But its probably better to left loop control flow keywords untouched and invent something new, like a new keyword to control blocks, i think yield could be used, if its not needed in upcoming coroutines/generators.

I am thinking of a block that is labelled with # (zig does not have operators that use this character i believe).

var x = #{ //unnamed labelled block
    yield value;
};

It could also replace named blocks that exist now

var x = #label { //not a hashtag
    yield #label value;
};

This could be possible (it yields the void..) but i think that blocks and loops should be separate - break, continue for loops and yield (or something else) for blocks.

while (true) #{
    if (i == 0) yield; // skips over x +=1 and continues the loop
    x += 1;
}
// equivalent to
while (true) {
    if (i == 0) continue; // skips over x +=1 and continues the loop
    x += 1;
}

@andrewrk
Copy link
Member Author

andrewrk commented Aug 2, 2019

@Rocknest your proposal is #732 which is closed

@Rocknest
Copy link
Contributor

Rocknest commented Aug 2, 2019

@andrewrk soo you closed it because people should understand zig control flow without being an expert in zig syntax, but your new proposal goes right against this.

@andrewrk
Copy link
Member Author

andrewrk commented Aug 2, 2019

That's right. And the argument I made in #732 (comment) still applies here. There are good reasons for and against this proposal.

@daurnimator
Copy link
Collaborator

One potential solution to this might be closures and doing an IIFE.

var x = (fn() sometype {
    return value;
})();

This would depend on closures (#229)

@rohlem
Copy link
Contributor

rohlem commented Aug 2, 2019

(EDIT: I guess this stray thought developed into an alternative proposal, which takes this use case one step further.)
(!please re-read! EDIT2: Overhauled, trying to strip out all flavour text & restructure)

I personally don't quite see a use case for labelling the block inside a loop (while(cond()) blk: {break :blk}), since that amounts to a continue (a keyword for exactly this use case).
If this use ends up being discouraged (-> compiler error), I think it's reasonable to let a simple break inside "control flow blocks" (loops, branches, control flow behaviour) apply to the control flow structure associated.

Here's an (imo quite conservative) idea on how to de-clutter scenarios involving loops: Introduce implicit block labels named after their control flow structure tokens (maybe prefixed/suffixed, here with non-optimal :, to disambiguate) Example:

var x = while(flag){
  // break :while 1; // break from the while, as explicitly specified
  // break 2; // would break from the `while` the current block is associated with, or be an error if we want the programmer to be explicit
  if(a){
    break :while 3; // break from the while loop from within the if's block, as explicitly specified
  }
  if(b){
    break :if 4; // break from the if; unused value triggers compiler error
    //as-of-original-proposal equivalent: break 4;
  }
}

This extends quite nicely to nesting these constructs (to a reasonable degree):

while(running()){
  for(getElements()) |element| {
    switch(element.kind){
      .abort => break :while, // it is clear we refer to the while loop
      .pause => break :for, // it is clear we refer to the for loop
      else => break :switch, // superfluous here, but doesn't hurt to be explicit imo
    }
    //common handling per else-element...
  }
  // pause or something once elements have been processed
}

With these implicit labels in place, if we required labels to break from "control flow blocks", I think most (all?) ambiguities can be caught by unused value errors:

while(true){
  var z = if(a) 1 else 2; // correct, obvious solution
  var y = if(a) {break :if 1;} else {break :else 2;} // correct, though maybe "else" branches should reuse the label of the "base" structure? (in this case :if)
  var x = if(a) {break 1;} else {break 2;}; // error: no label breaking from "control flow block"
  var w = if(a) {break :while 1;} else {break :while 2;}; // potentially correct? (although the assignment is technically unreachable code)
  var v = while(flag){
    var u = {break 4;}; // since break is the only way for a value to reach the assignment, it's probably not meant to apply to the while loop.
    {break 4;} // unnecessary nested block -> unused value -> compilation error
  }
}

... sorry this got so long. I also see benefits in trying to merge the concepts of break and return altogether, which brings us (maybe too) close to inline (nested/lambda/closure) functions.

@andrewrk
Copy link
Member Author

andrewrk commented Aug 2, 2019

@hryx the proposal is as you suggested, the latter of your example.

@CurtisFenner
Copy link

CurtisFenner commented Aug 2, 2019

I think this proposal, if it including the modification to the current behavior of valueless break;, would make unlabeled valueless break so easy to misunderstand/misused/misread/miswrite, that if this proposal is accepted, it should be removed from the language. However, I am worried that this proposal is not worth what would be lost.


Since normally break; would be the last statement in a block, it no longer can be used as it was before. It can only be used as mikdusan pointed out, in a control flow statement without {}:

if (cond) break;

However, I think it is extremely confusing and error prone that in this proposal, the above would be different from

if (cond) { break; }

This would definitely be extremely confusing to people new to Zig. I think making changes around break; would be extremely errorprone (from programmers forgetting about the introduction/removal of otherwise harmless {}), even among people very familiar with Zig (the goto fail bug was written by a professional C programmer, afterall).

The only ways I see to solve this are to either

  • make break; illegal, since it no longer expresses any programmer intent clearly
    • I suspect that if the compiler does not do this, a 3rd party linter eventually would
  • make the behavior of break; and break value; different (where break; for example breaks to the nearest containing loop while break value; breaks to the containing {} block)
    • I think weighing everything (see following paragraphs), this may be the better option, but it does make break quite inconsistent, which presents similar risks to the above due to now need to understand two behaviors of break! Because I think refactoring between them would be uncommon (maybe someone can give an example where this is reasonable), I think this would most likely have a bigger impact on programmers that are new to Zig learning that these features are different, rather than affecting programs written by those familiar with the language.
  • introduce these concepts with separate syntax; possibly by introducing break :while/break :for as suggested above by rohlem

However, I think removing unlabeled break; from the language is a bad result. I don't have data, but I would guess that the vast, vast majority of valueless break;s do not include a label (since nested loops are probably not all that common to begin with, and wanting to break multiple levels, but not fully return, is even more uncommon). Requiring the ceremony of a label makes the loops&breaks harder to read (because the idiom is much more complicated and introduces multiple elements, the labels, and thus more need for careful review when reading coding) and also punishes the common, usually easy-to-understand case.

The introduction of break :while; / break :for;, meaning break out of the immediately containing while/for might be a good solution to this. (Perhaps it would be illegal inside multiple nested loops, which would then require a labeled break).

Since I view break; as a not so small loss, I'm particularly skeptical that the proposed feature would be used enough in comparison to currently used valueless break; to justify such a trade. This is especially important considering that this will make Zig more foreign to current C/Rust/Go/etc developers. This will increase frustration with the language and consequently make friction against adoption, which will undermine Zig's more-pragmatic-than-C approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking Implementing this issue could cause existing code to no longer compile or have different behavior. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

No branches or pull requests

7 participants