How to break a switch body?

If looks only the h way works in the following code.
Am I missing something?

const std = @import("std");

fn f() void {
	var n:usize = 5;
	switch (n) blk: {
		else => {
			break: blk;
		},
	}
}

fn g() void {
	var n:usize = 5;
	blk: switch (n) {
		else => {
			break: blk;
		},
	}
}

fn h() void {
	var n:usize = 5;
	blk: { switch (n) {
		else => {
			break: blk;
		},
	}}
}

pub fn main() void {
	f();
	g();
	h();
}
1 Like

Check out this thread if you haven’t already: Multiline IF blocks as expressions?

From the Zig documentation:

All branches of a switch expression must be able to be coerced to a common type.

We’d have a real conundrum on our hands given that you can return values from a switch. For instance, if we assign from a switch statement but could also arbitrarily break them, you could have inconsistent return values (one would be void, the other might be int).

To be clear, when you say ā€œbreak from a switchā€, are you referring to the classic case of fallthrough (which Zig does not have)?

2 Likes

Since there is no case fallthrough in zig, breaking from the inner block also has the same effect:

switch (n) {
	else => blk: {
		if(someCondition) break :blk;
		...
	},
}

In my opinion this is more readable because the label is closer to the break.

6 Likes

This is the preferred way of doing it.

1 Like

Since there is no case fallthrough in zig, breaking from the inner block also has the same effect: …

I’m aware of this. But there are tons of branches. Labeling every one seems verbose.

From the Zig documentation:

All branches of a switch expression must be able to be coerced to a common type.

We’d have a real conundrum on our hands given that you can return values from a switch. For instance, if we assign from a switch statement but could also arbitrarily break them, you could have inconsistent return values (one would be void, the other might be int).

I’m not sure I totally understand these. Is there any bad of the pattern used in function h?

To be clear, when you say ā€œbreak from a switchā€, are you referring to the classic case of fallthrough (which Zig does not have)?

I mean making execution jump out of the whole switch block.

No problem, let’s have a look at some pseudo code to understand what is meant by that statement in the documentation. I’m using pseudo code because the example would not compile, so this is a counter factual.

Let’s say that I want to have a variable assigned to the result of a switch statement:

var result = switch (value) {
    /// Branch 1: 
    ///     return an integer

    /// Branch 2:
    ///     if predicate, break the switch
    ///     else return an integer
}

If we go through branch 1, we’ll return an integer - so the type of ā€œresultā€ would be the same integer type.

However, in branch 2, if the predicate is true, we’ll just break. So that leaves the question - if we break, then what is the type of ā€œresultā€? It’s not an integer, because we aren’t returning an integer, we’re just breaking out of the statement (it would probably be ā€œvoidā€ because we aren’t returning anything). That’s the problem - one code path returns integers, the other code path can return void.

So returning to the documentation:

All branches of a switch expression must be able to be coerced to a common type.

We can’t coerce the types to be each other (void → int or int → void).

1 Like

Thanks for the more detailed explanations. So the switch and if-else blocks are always treated as expressions. That is reasonable.

1 Like

related:

4 Likes

Now, we can break out switch blocks by using the same label for the whole switch block. Should the same be also supported for if and loop blocks?

fn foo(n: u8) void {
    const v = blk: switch (n) { // okay
        0...7 => {
            // ... 
            break :blk n+n;
        },
        else => {
            // ...
            break :blk n+n+n;
        },
    };
    @import("std").debug.print("{}\n", .{ v } );
}

fn bar(n: u8) void {
    const v = blk: if (n < 8) { // error: expected ';' after statement
        // ... 
        break :blk n+n;
    } else {
        // ...
        break :blk n+n+n;
    };
    @import("std").debug.print("{}\n", .{ v } );
}

This is why I always write the type after the result for readability. You can see the result type before examining the branches.

var result: i32 = switch (value) {
    // branches
}

In fact I do it in the simplest cases too. If possible I avoid infered.

var x: u64 = some_global_array[42];
const y: []const u8 = some_arg;
1 Like

I use this ā€˜trick’ in my Z80 emulator to skip a common ā€˜epilogue’ for specific switch prongs, in the original C code this was simply implemented with gotos instead of he usual ā€˜case-break’:

  next: {
      switch (self.step) {
          // BEGIN DECODE
          0x0 => { }, // ...this prong runs the `self.fetch()` after the switch
          0x1 => { self.step = 0x200; break :next; }, // ...this prong skips the `self.fetch()`
          // ...
      };
      bus = self.fetch(bus);
  }

…it’s also possible to jump into different epilogues, as long as they are nested.

PS: the C version basically looked like this (one of few clean and justified use cases for goto heh)

    switch (step) {
        case 0: break;
        case 1: step = 0x200; goto next;
        // ...
    }
    bus = fetch(bus);
next:
    // ...

…the generated code is the same for both language versions (assuming LLVM backend).

1 Like