Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions doc/langref.html.in
Original file line number Diff line number Diff line change
Expand Up @@ -2495,6 +2495,53 @@ or
</p>
{#code|test_exhaustive_switch.zig#}

{#header_close#}

{#header_open|Labeled switch#}
<p>
When a switch statement is labeled, it can be referenced from a
{#syntax#}break{#endsyntax#} or {#syntax#}continue{#endsyntax#}.
{#syntax#}break{#endsyntax#} will return a value from the {#syntax#}
switch{#endsyntax#}.
</p>
<p>
A {#syntax#}continue{#endsyntax#} targeting a switch must have an
operand. When executed, it will jump to the matching prong, as if the
{#syntax#}switch{#endsyntax#} were executed again with the {#syntax#}
continue{#endsyntax#}'s operand replacing the initial switch value.
</p>

{#code|test_switch_continue.zig#}

<p>
Semantically, this is equivalent to the following loop:
</p>
{#code|test_switch_continue_equivalent.zig#}

<p>
This can improve clarity of (for example) state machines, where the syntax {#syntax#}continue :sw .next_state{#endsyntax#} is unambiguous, explicit, and immediately understandable.
</p>
<p>
However, the motivating example is a switch on each element of an array, where using a single switch can improve clarity and performance:
</p>
{#code|test_switch_dispatch_loop.zig#}

<p>
If the operand to {#syntax#}continue{#endsyntax#} is
{#link|comptime#}-known, then it can be lowered to an unconditional branch
to the relevant case. Such a branch is perfectly predicted, and hence
typically very fast to execute.
</p>

<p>
If the operand is runtime-known, each {#syntax#}continue{#endsyntax#} can
embed a conditional branch inline (ideally through a jump table), which
allows a CPU to predict its target independently of any other prong. A
loop-based lowering would force every branch through the same dispatch
point, hindering branch prediction.
</p>


{#header_close#}

{#header_open|Inline Switch Prongs#}
Expand Down
26 changes: 26 additions & 0 deletions doc/langref/test_switch_continue.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const std = @import("std");

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,
}
}

// test
28 changes: 28 additions & 0 deletions doc/langref/test_switch_continue_equivalent.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const std = @import("std");

test "switch continue, equivalent loop" {
var sw: i32 = 5;
while (true) {
switch (sw) {
5 => {
sw = 4;
continue;
},
2...4 => |v| {
if (v > 3) {
sw = 2;
continue;
} else if (v == 3) {
break;
}

sw = 1;
continue;
},
1 => return,
else => unreachable,
}
}
}

// test
38 changes: 38 additions & 0 deletions doc/langref/test_switch_dispatch_loop.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const std = @import("std");
const expectEqual = std.testing.expectEqual;

const Instruction = enum {
add,
mul,
end,
};

fn evaluate(initial_stack: []const i32, code: []const Instruction) !i32 {
var stack = try std.BoundedArray(i32, 8).fromSlice(initial_stack);
var ip: usize = 0;

return vm: switch (code[ip]) {
// Because all code after `continue` is unreachable, this branch does
// not provide a result.
.add => {
try stack.append(stack.pop() + stack.pop());

ip += 1;
continue :vm code[ip];
},
.mul => {
try stack.append(stack.pop() * stack.pop());

ip += 1;
continue :vm code[ip];
},
.end => stack.pop(),
};
}

test "evaluate" {
const result = try evaluate(&.{ 7, 2, -3 }, &.{ .mul, .add, .end });
try expectEqual(1, result);
}

// test
Loading