Revisiting 'break :blk value'

Here are my counterpoints why I think this is a bad idea:

  • break value is not actually shorter. Having const x = { break value 0; } instead of const x = blk: { break :blk 0; } is almost exact amount of characters. On coumpound statements (e.g. multiple break statements), using labels is actually shorter than using equal amount of break value statments.
  • It introduces a new one-purpose keyword to replace a general concept. The difference between break value 0 and break :blk 0 is mostly aestehtical, but their difference in how the language is specified and explained is added another “small little exception” for no apparent benefit.
  • Using just break block + break loop instead of break value seems underspecified.

I dont think it is “awkward”, because the named block has the same feeling as breaking a named loop.

1 Like

Ideal choice for me:

  • you should be able to break from any block.
  • You can break with value if the block is assigned to a variable.
  • If block assigned to a variable you must break with value.
  • labeled breaks are never needed if there is only one possible interpretation
  • labels must be used, but don’t need to be used, unless there is ambiguity regarding to which block a break is meant for (in the case of a block assignment inside of a loop).

so, something like:

// This should be allowed, since the loop block is not assigned to a value
// label is optional
while(true) {
    const x: u8 = {
          break 2; 
    };
}

// this is also allowed, no ambiguity here
// label is optional
const a = while(true) {
    const x: u8 = 2;
    break 2; 
}

// this is NOT allowed ...
const a = while(true) {
    const x: u8 = {
        break 2;
    };
    // ... even if you uncomment this line
    // break 2; 
}

// above example would be correct if it looked like this
const a = while(true) loop: {
    const x: u8 = block: {
        break :block 2;
    };
    break :loop 1; 
}

Personally, I love Rust’s approach of returning the last used expression (if it doesn’t end with a semifcolon), but I can understand people not liking that

3 Likes

Is there any consideration for introducing a keyword other than “break”?

    const x: u8 = {
          break 2; 
    };

In most programming languages, break means “break the control flow”, but this mnemonic makes little sense in zig where it’s also used to specify the value of the block. The syntactic ambiguities discussed in this thread, to me, relate to how semantically overloaded break is as a term.

test "switch continue" {
    sw: switch (@as(i32, 5)) {
        5 => continue :sw 4,

        // `continue` can occur multiple times within a single switch prong.
        2...4 => |v| {
            if (v > 3) {
                continue :sw 2;
            } else if (v == 3) {

                // `break` can target labeled loops.
                break :sw;
            }

            continue :sw 1;
        },

        1 => return,

        else => unreachable,
    }
}

This example from the zig’s docs demonstrating the labelled switch statement was at first incredibly puzzling because “break” and “continue” don’t really make sense to me, semantically. And now that I’ve gotten used to zig, I’ve gotten used to this, but it’s very idiomatic.

I don’t know about this switch example, but for the uses of break discussed in this thread, what about yield? Is that maybe unusable because zig might have coroutines in the future?

1 Like

Refactoring robustness might be one thing to consider. I feel confident moving around code in status-quo Zig, and labeled blocks is part of that. When changing structure or copy-pasting code, it’s nice to know that Zig code tends to fail to compile rather than silently adopting some new behavior.

3 Likes

break value 0;

Value is a placeholder. The actual proposed syntax is eg break 13

1 Like

This is a bit silly, but I became significantly happier when I realized I don’t have to type awkward blk: and just b: works perfectly fine.

7 Likes

This is a brain dump of what I find annoying/unintuitive about break

const x = blk: while (true) {
    break :blk 0;
};

compiles

const x = blk: {
    break :blk 0;
};

compiles

const x = while (true) {
    break 0;
};

compiles

const x = {
    break 0;
};

doesn’t compile (break is missing label)

const x = if (true) blk: {
    break :blk 0;
} else 1;

compiles

const x = blk: if (true) {
    break :blk 0;
} else 1;

doesn’t compile (labeled if is not a valid construct)

const x = if (true) {
    break 0;
} else 1;

doesn’t compile (break is missing label)

const x: u32 = while (true) {
    break 0;
} else 1;

compiles

const x: u32 = while (true) {
    break 0;
} else {
    break 1;
};

doesn’t compile (else is missing block label to return to)

const x: u32 = while (true) {
    break 0;
} else blk: {
    break :blk 1;
};

compiles

const x: u32 = while (true) blk: {
    break :blk 0;
} else blk: {
    break :blk 1;
};

doesn’t compile (value returned from block is ignored)

const x: u32 = blk: while (true) {
    break :blk 0;
} else blk: {
    break :blk 1;
};

doesn’t compile (redefinition of blk)

const x: u32 = blk: while (true) {
    break :blk 0;
} else {
    break :blk 1;
};

compiles

It would feel more intuitive to me if break was something like

break returns a value from the nearest block or the block with the optionally provided label.

with while being

A while loop is used to repeatedly execute an expression until some condition is no longer true or the expression returns a non-void value

That doesn’t work though because

while (true) {
    break;
}

returns a void value, which would loop forever.

In what context does it make sense to use break outside of a block?

sw: switch (thing) {
    0 => break :sw,
    // ...
}

is the same as

sw: switch (thing) {
    0 => {},
    // ...
}

and

while (true) break;
for (0..1) |_| break;

are both seemingly useless.

I just realized that you can break a value from a labeled switch

const x = sw: switch (0) {
    0 => break :sw 1,
    else => unreachable,
};

I feel like there is some satisfying unification of break revolving around blocks that doesn’t fall apart immediately but it’s not coming to me. I would also be a-okay with the removal of unlabeled break.

off-topic: labeled if is maybe useful? Here’s a potential use-case from the zig compiler (resolveReferencesInner in src/Zcu.zig)

while (true) {
    if (type_idx < types.count()) {
        // ...
        continue;
    }
    if (unit_idx < units.count()) {
        // ...
        continue;
    }
    break;
}
i: if (type_idx < types.count()) {
    // ...
    continue :i;
} else if (unit_idx < units.count()) {
    // ...
    continue :i;
}

Maybe this generates better code? Probably not since its just a jmp to the beginning of the loop in both cases. I at least think it’s easier to read the intention. This would also allow for:

const x = blk: if (true) {
    break :blk 0;
} else {
    break :blk 1;
};

instead of

const x = if (true) blk: {
    break :blk 0;
} else blk: {
    break :blk 1;
};

or

const x = blk: {
    if (true) {
        break :blk 0;
    } else {
        break :blk 1;
    }
};
5 Likes

Although I was initially uncomfortable with it, I don’t have any issues with zig’s current block label system now. All the possible improvements I had envisioned eventually seemed less practical than the current state. I find it hard to accept using break for the innermost block without any label at all, because unlabeled blocks can easily have layers added unintentionally, causing the block that break is meant to exit to drift unexpectedly.

However, if I were designing block labels in a new language, I probably wouldn’t reuse :. Even though it’s consistent with assembly and C conventions, its similarity to postfix type annotations means that omitting label would make reading more cumbersome. I would use #label as the block label identifier, and allowing # alone as a block label would also be fine.

3 Likes

That’s actually a nice idea. It would likely also make it easy to upgrade to because zig fmt could likely make that automatically since it’s just a small replacement. And new code could use the single # that handles the simple cases easily.

Side note to all the people naming their blocks blk.I find things become a bit nicer with a less generic name. For example const x = sum: { ..... break :sum };

2 Likes

I usually stick to the idiomatic blk or b to make clear that it is not a meaningful, deliberately chosen name that the reader should pay attention to, but just a technical name required by the language.

Naming things is one of the two hard problems in computer science, and often the best name is no name. Usually a label name does not improve readability here, because the variable you assign the result to already has a good name; an additional label name is just noise.

The main issue with break :label value is that the language needlessly forces me to assign a label name.

2 Likes

When using a labeled block to assign a value, I’ve taken to the habit of just re-using the variable’s name as the block label:

const size: u8 = size: {
    var size: u8 = 0; // I also reuse the name like this
    // Some calculation
    break :size size;
};

It’s a little redundant, but I find it helps me keep track while reading larger blocks.

8 Likes