commit e0108dec54deaf044e869bb76aa4a40813913277 (tree)
parent aa2b178029fc466f847c0cb663c3b32c7809a357
Author: Justus Klausecker <justus@klausecker.de>
Date: Wed, 17 Dec 2025 16:43:28 +0100
AstGen: allow labels to provide separate `break` and `continue` targets
Enhances `GenZir` to allow labels to provide separate `break` and `continue`
target blocks and adds some more information on continue targets to
communicate whether the target is a switch block or cannot be targeted by
`continue` at all.
The main motivation is enabling this:
```
const result: u32 = operand catch |err| label: switch (err) {
else => continue :label error.MyError,
error.MyError => break :label 1,
};
```
to be lowered to something like this:
```
%1 = block({
%2 = is_non_err(%operand)
%3 = condbr(%2, {
%4 = err_union_payload_unsafe(%operand)
%5 = break(%1, result) // targets enclosing `block`
}, {
%6 = err_union_code(%operand)
%7 = switch_block(%6,
else => {
%8 = switch_continue(%7, "error.MyError") // targets `switch_block`
},
"error.MyError" => {
%9 = break(%1, @one) // targets enclosing `block`
},
)
%10 = break(%1, @void_value)
})
})
```
which makes the non-error case and all breaks from switch prongs, but not
continues from switch prongs, peers.
This is required to avoid the problems described in gh#11957 for labeled
switches without having to introduce a fairly complex special case to the
`switch_block_err_union` logic. Since this construct is very rare in practice,
introducing this additional complexity just to save a few ZIR bytes is
likely not worth it, so the simplified lowering described above will be
used instead.
As a nice bonus, AstGen can now also detect a `continue` trying to target
a labeled block and emit an appropriate error message.
Diffstat:
2 files changed, 236 insertions(+), 189 deletions(-)
diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig
@@ -2162,92 +2162,101 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
// Look for the label in the scope.
var scope = parent_scope;
- while (true) {
- switch (scope.tag) {
- .gen_zir => {
- const block_gz = scope.cast(GenZir).?;
+ find_scope: switch (scope.tag) {
+ .gen_zir => {
+ const gen_zir = scope.cast(GenZir).?;
- if (block_gz.cur_defer_node.unwrap()) |cur_defer_node| {
- // We are breaking out of a `defer` block.
- return astgen.failNodeNotes(node, "cannot break out of defer expression", .{}, &.{
- try astgen.errNoteNode(
- cur_defer_node,
- "defer expression here",
- .{},
- ),
- });
- }
+ if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| {
+ // We are breaking out of a `defer` block.
+ return astgen.failNodeNotes(node, "cannot break out of defer expression", .{}, &.{
+ try astgen.errNoteNode(
+ cur_defer_node,
+ "defer expression here",
+ .{},
+ ),
+ });
+ }
- const block_inst = blk: {
- if (opt_break_label.unwrap()) |break_label| {
- if (block_gz.label) |*label| {
- if (try astgen.tokenIdentEql(label.token, break_label)) {
- label.used = true;
- break :blk label.block_inst;
- }
- }
- } else if (block_gz.break_block.unwrap()) |i| {
- break :blk i;
+ if (opt_break_label.unwrap()) |break_label| labeled: {
+ if (gen_zir.label) |*label| {
+ if (try astgen.tokenIdentEql(label.token, break_label)) {
+ label.used = true;
+ break :labeled;
}
- // If not the target, start over with the parent
- scope = block_gz.parent;
- continue;
- };
- // If we made it here, this block is the target of the break expr
-
- const break_tag: Zir.Inst.Tag = if (block_gz.is_inline)
- .break_inline
- else
- .@"break";
-
- const rhs = opt_rhs.unwrap() orelse {
- _ = try rvalue(parent_gz, block_gz.break_result_info, .void_value, node);
-
- try genDefers(parent_gz, scope, parent_scope, .normal_only);
-
- // As our last action before the break, "pop" the error trace if needed
- if (!block_gz.is_comptime)
- _ = try parent_gz.addRestoreErrRetIndex(.{ .block = block_inst }, .always, node);
+ }
+ // gz without or with different label, continue to parent scopes.
+ scope = gen_zir.parent;
+ continue :find_scope scope.tag;
+ } else if (!gen_zir.allow_unlabeled_control_flow) {
+ // This `break` is unlabeled and the gz we've found doesn't allow
+ // unlabeled control flow. Continue to parent scopes.
+ scope = gen_zir.parent;
+ continue :find_scope scope.tag;
+ }
- _ = try parent_gz.addBreak(break_tag, block_inst, .void_value);
- return Zir.Inst.Ref.unreachable_value;
- };
+ const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline)
+ .break_inline
+ else
+ .@"break";
- const operand = try reachableExpr(parent_gz, parent_scope, block_gz.break_result_info, rhs, node);
+ if (opt_rhs.unwrap()) |rhs| {
+ // We have a `break` operand.
+ const operand = try reachableExpr(parent_gz, parent_scope, gen_zir.break_result_info, rhs, node);
try genDefers(parent_gz, scope, parent_scope, .normal_only);
// As our last action before the break, "pop" the error trace if needed
- if (!block_gz.is_comptime)
- try restoreErrRetIndex(parent_gz, .{ .block = block_inst }, block_gz.break_result_info, rhs, operand);
-
- switch (block_gz.break_result_info.rl) {
+ if (!gen_zir.is_comptime) {
+ try restoreErrRetIndex(parent_gz, .{ .block = gen_zir.break_target }, gen_zir.break_result_info, rhs, operand);
+ }
+ switch (gen_zir.break_result_info.rl) {
.ptr => {
// In this case we don't have any mechanism to intercept it;
// we assume the result location is written, and we break with void.
- _ = try parent_gz.addBreak(break_tag, block_inst, .void_value);
+ _ = try parent_gz.addBreak(break_tag, gen_zir.break_target, .void_value);
},
.discard => {
- _ = try parent_gz.addBreak(break_tag, block_inst, .void_value);
+ _ = try parent_gz.addBreak(break_tag, gen_zir.break_target, .void_value);
},
else => {
- _ = try parent_gz.addBreakWithSrcNode(break_tag, block_inst, operand, rhs);
+ _ = try parent_gz.addBreakWithSrcNode(break_tag, gen_zir.break_target, operand, rhs);
},
}
- return Zir.Inst.Ref.unreachable_value;
- },
- .local_val => scope = scope.cast(Scope.LocalVal).?.parent,
- .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent,
- .namespace => break,
- .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent,
- .top => unreachable,
- }
- }
- if (opt_break_label.unwrap()) |break_label| {
- const label_name = try astgen.identifierTokenString(break_label);
- return astgen.failTok(break_label, "label not found: '{s}'", .{label_name});
- } else {
- return astgen.failNode(node, "break expression outside loop", .{});
+ return .unreachable_value;
+ } else {
+ _ = try rvalue(parent_gz, gen_zir.break_result_info, .void_value, node);
+
+ try genDefers(parent_gz, scope, parent_scope, .normal_only);
+
+ // As our last action before the break, "pop" the error trace if needed
+ if (!gen_zir.is_comptime)
+ _ = try parent_gz.addRestoreErrRetIndex(.{ .block = gen_zir.break_target }, .always, node);
+
+ _ = try parent_gz.addBreak(break_tag, gen_zir.break_target, .void_value);
+ return .unreachable_value;
+ }
+ },
+ .local_val => {
+ scope = scope.cast(Scope.LocalVal).?.parent;
+ continue :find_scope scope.tag;
+ },
+ .local_ptr => {
+ scope = scope.cast(Scope.LocalPtr).?.parent;
+ continue :find_scope scope.tag;
+ },
+ .defer_normal, .defer_error => {
+ scope = scope.cast(Scope.Defer).?.parent;
+ continue :find_scope scope.tag;
+ },
+ .namespace => {
+ if (opt_break_label.unwrap()) |break_label| {
+ const label_name = try astgen.identifierTokenString(break_label);
+ return astgen.failTok(break_label, "label not found: '{s}'", .{label_name});
+ } else {
+ return astgen.failNode(node, "break expression outside loop", .{});
+ }
+ },
+ .top => unreachable,
}
}
@@ -2262,100 +2271,116 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
// Look for the label in the scope.
var scope = parent_scope;
- while (true) {
- switch (scope.tag) {
- .gen_zir => {
- const gen_zir = scope.cast(GenZir).?;
+ find_scope: switch (scope.tag) {
+ .gen_zir => {
+ const gen_zir = scope.cast(GenZir).?;
- if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| {
- return astgen.failNodeNotes(node, "cannot continue out of defer expression", .{}, &.{
- try astgen.errNoteNode(
- cur_defer_node,
- "defer expression here",
- .{},
- ),
- });
- }
- const continue_block = gen_zir.continue_block.unwrap() orelse {
- scope = gen_zir.parent;
- continue;
- };
- if (opt_break_label.unwrap()) |break_label| blk: {
- if (gen_zir.label) |*label| {
- if (try astgen.tokenIdentEql(label.token, break_label)) {
- const maybe_switch_tag = astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)];
- if (opt_rhs != .none) switch (maybe_switch_tag) {
- .switch_block, .switch_block_ref => {},
- else => return astgen.failNode(node, "cannot continue loop with operand", .{}),
- } else switch (maybe_switch_tag) {
- .switch_block, .switch_block_ref => return astgen.failNode(node, "cannot continue switch without operand", .{}),
- else => {},
- }
-
- label.used = true;
- label.used_for_continue = true;
- break :blk;
+ if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| {
+ return astgen.failNodeNotes(node, "cannot continue out of defer expression", .{}, &.{
+ try astgen.errNoteNode(
+ cur_defer_node,
+ "defer expression here",
+ .{},
+ ),
+ });
+ }
+
+ if (opt_break_label.unwrap()) |break_label| labeled: {
+ if (gen_zir.label) |*label| {
+ if (try astgen.tokenIdentEql(label.token, break_label)) {
+ switch (gen_zir.continue_target) {
+ .none => {
+ return astgen.failNode(node, "continue cannot target labeled block", .{});
+ },
+ .@"break" => if (opt_rhs != .none) {
+ return astgen.failNode(node, "cannot continue loop with operand", .{});
+ },
+ .switch_continue => if (opt_rhs == .none) {
+ return astgen.failNode(node, "cannot continue switch without operand", .{});
+ },
}
- }
- // found continue but either it has a different label, or no label
- scope = gen_zir.parent;
- continue;
- } else if (gen_zir.label) |label| {
- // This `continue` is unlabeled. If the gz we've found corresponds to a labeled
- // `switch`, ignore it and continue to parent scopes.
- switch (astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)]) {
- .switch_block, .switch_block_ref => {
- scope = gen_zir.parent;
- continue;
- },
- else => {},
+ label.used = true;
+ label.used_for_continue = true;
+ break :labeled;
}
}
+ // gz without or with different label, continue to parent scopes.
+ scope = gen_zir.parent;
+ continue :find_scope scope.tag;
+ } else if (gen_zir.allow_unlabeled_control_flow) {
+ // This `continue` is unlabeled. If the gz we've found doesn't
+ // provide a `continue` target or corresponds to a labeled
+ // `switch`, ignore it and continue to parent scopes.
+ switch (gen_zir.continue_target) {
+ .none, .switch_continue => {
+ scope = gen_zir.parent;
+ continue :find_scope scope.tag;
+ },
+ .@"break" => {},
+ }
+ } else {
+ // We don't have a break label and the gz we found doesn't allow
+ // unlabeled control flow, so we continue to its parent scopes.
+ scope = gen_zir.parent;
+ continue :find_scope scope.tag;
+ }
- if (opt_rhs.unwrap()) |rhs| {
- // We need to figure out the result info to use.
- // The type should match
- const operand = try reachableExpr(parent_gz, parent_scope, gen_zir.continue_result_info, rhs, node);
-
+ switch (gen_zir.continue_target) {
+ .none => unreachable, // should have failed or continued to parent scopes by now
+ .@"break" => |block| {
try genDefers(parent_gz, scope, parent_scope, .normal_only);
- // As our last action before the continue, "pop" the error trace if needed
- if (!gen_zir.is_comptime)
- _ = try parent_gz.addRestoreErrRetIndex(.{ .block = continue_block }, .always, node);
-
- _ = try parent_gz.addBreakWithSrcNode(.switch_continue, continue_block, operand, rhs);
- return Zir.Inst.Ref.unreachable_value;
- }
-
- try genDefers(parent_gz, scope, parent_scope, .normal_only);
+ const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline)
+ .break_inline
+ else
+ .@"break";
+ if (break_tag == .break_inline) {
+ _ = try parent_gz.addUnNode(.check_comptime_control_flow, block.toRef(), node);
+ }
- const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline)
- .break_inline
- else
- .@"break";
- if (break_tag == .break_inline) {
- _ = try parent_gz.addUnNode(.check_comptime_control_flow, continue_block.toRef(), node);
- }
+ // As our last action before the continue, "pop" the error trace if needed
+ if (!gen_zir.is_comptime) {
+ _ = try parent_gz.addRestoreErrRetIndex(.{ .block = block }, .always, node);
+ }
+ _ = try parent_gz.addBreak(break_tag, block, .void_value);
+ return .unreachable_value;
+ },
+ .switch_continue => |switch_block| {
+ const rhs = opt_rhs.unwrap().?; // checked above
+ const operand = try reachableExpr(parent_gz, parent_scope, gen_zir.continue_result_info, rhs, node);
- // As our last action before the continue, "pop" the error trace if needed
- if (!gen_zir.is_comptime)
- _ = try parent_gz.addRestoreErrRetIndex(.{ .block = continue_block }, .always, node);
+ try genDefers(parent_gz, scope, parent_scope, .normal_only);
- _ = try parent_gz.addBreak(break_tag, continue_block, .void_value);
- return Zir.Inst.Ref.unreachable_value;
- },
- .local_val => scope = scope.cast(Scope.LocalVal).?.parent,
- .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent,
- .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent,
- .namespace => break,
- .top => unreachable,
- }
- }
- if (opt_break_label.unwrap()) |break_label| {
- const label_name = try astgen.identifierTokenString(break_label);
- return astgen.failTok(break_label, "label not found: '{s}'", .{label_name});
- } else {
- return astgen.failNode(node, "continue expression outside loop", .{});
+ // As our last action before the continue, "pop" the error trace if needed
+ if (!gen_zir.is_comptime) {
+ _ = try parent_gz.addRestoreErrRetIndex(.{ .block = switch_block }, .always, node);
+ }
+ _ = try parent_gz.addBreakWithSrcNode(.switch_continue, switch_block, operand, rhs);
+ return .unreachable_value;
+ },
+ }
+ },
+ .local_val => {
+ scope = scope.cast(Scope.LocalVal).?.parent;
+ continue :find_scope scope.tag;
+ },
+ .local_ptr => {
+ scope = scope.cast(Scope.LocalPtr).?.parent;
+ continue :find_scope scope.tag;
+ },
+ .defer_normal, .defer_error => {
+ scope = scope.cast(Scope.Defer).?.parent;
+ continue :find_scope scope.tag;
+ },
+ .namespace => {
+ if (opt_break_label.unwrap()) |break_label| {
+ const label_name = try astgen.identifierTokenString(break_label);
+ return astgen.failTok(break_label, "label not found: '{s}'", .{label_name});
+ } else {
+ return astgen.failNode(node, "continue expression outside loop", .{});
+ }
+ },
+ .top => unreachable,
}
}
@@ -2509,10 +2534,9 @@ fn labeledBlockExpr(
try gz.instructions.append(astgen.gpa, block_inst);
var block_scope = gz.makeSubBlock(parent_scope);
block_scope.is_inline = force_comptime;
- block_scope.label = GenZir.Label{
- .token = label_token,
- .block_inst = block_inst,
- };
+ block_scope.label = .{ .token = label_token };
+ block_scope.break_target = block_inst;
+ block_scope.continue_target = .none;
block_scope.setBreakResultInfo(block_ri);
if (force_comptime) block_scope.is_comptime = true;
defer block_scope.unstack();
@@ -6574,7 +6598,6 @@ fn whileExpr(
var loop_scope = parent_gz.makeSubBlock(scope);
loop_scope.is_inline = is_inline;
- loop_scope.setBreakResultInfo(block_ri);
defer loop_scope.unstack();
var cond_scope = parent_gz.makeSubBlock(&loop_scope.base);
@@ -6707,14 +6730,13 @@ fn whileExpr(
_ = try loop_scope.addNode(repeat_tag, node);
try loop_scope.setBlockBody(loop_block);
- loop_scope.break_block = loop_block.toOptional();
- loop_scope.continue_block = continue_block.toOptional();
if (while_full.label_token) |label_token| {
- loop_scope.label = .{
- .token = label_token,
- .block_inst = loop_block,
- };
+ loop_scope.label = .{ .token = label_token };
}
+ loop_scope.allow_unlabeled_control_flow = true;
+ loop_scope.break_target = loop_block;
+ loop_scope.continue_target = .{ .@"break" = continue_block };
+ loop_scope.setBreakResultInfo(block_ri);
// done adding instructions to loop_scope, can now stack then_scope
then_scope.instructions_top = then_scope.instructions.items.len;
@@ -6787,10 +6809,12 @@ fn whileExpr(
break :s &else_scope.base;
}
};
- // Remove the continue block and break block so that `continue` and `break`
- // control flow apply to outer loops; not this one.
- loop_scope.continue_block = .none;
- loop_scope.break_block = .none;
+ // Remove label and forbid unlabeled control flow to this scope so that
+ // `continue` and `break` control flow apply to outer loops; not this one.
+ loop_scope.label = null;
+ loop_scope.allow_unlabeled_control_flow = false;
+ loop_scope.continue_target = undefined;
+ loop_scope.break_target = undefined;
const else_result = try fullBodyExpr(&else_scope, sub_scope, loop_scope.break_result_info, else_node, .allow_branch_hint);
if (is_statement) {
_ = try addEnsureResult(&else_scope, else_result, else_node);
@@ -6979,14 +7003,12 @@ fn forExpr(
const cond_block = try loop_scope.makeBlockInst(block_tag, node);
try cond_scope.setBlockBody(cond_block);
- loop_scope.break_block = loop_block.toOptional();
- loop_scope.continue_block = cond_block.toOptional();
if (for_full.label_token) |label_token| {
- loop_scope.label = .{
- .token = label_token,
- .block_inst = loop_block,
- };
+ loop_scope.label = .{ .token = label_token };
}
+ loop_scope.allow_unlabeled_control_flow = true;
+ loop_scope.break_target = loop_block;
+ loop_scope.continue_target = .{ .@"break" = cond_block };
const then_node = for_full.ast.then_expr;
var then_scope = parent_gz.makeSubBlock(&cond_scope.base);
@@ -7077,10 +7099,12 @@ fn forExpr(
if (for_full.ast.else_expr.unwrap()) |else_node| {
const sub_scope = &else_scope.base;
- // Remove the continue block and break block so that `continue` and `break`
- // control flow apply to outer loops; not this one.
- loop_scope.continue_block = .none;
- loop_scope.break_block = .none;
+ // Remove label and forbid unlabeled control flow to this scope so that
+ // `continue` and `break` control flow apply to outer loops; not this one.
+ loop_scope.label = null;
+ loop_scope.allow_unlabeled_control_flow = false;
+ loop_scope.continue_target = undefined;
+ loop_scope.break_target = undefined;
const else_result = try fullBodyExpr(&else_scope, sub_scope, loop_scope.break_result_info, else_node, .allow_branch_hint);
if (is_statement) {
_ = try addEnsureResult(&else_scope, else_result, else_node);
@@ -7818,7 +7842,9 @@ fn switchExpr(
const switch_block = try parent_gz.makeBlockInst(switch_tag, node);
if (switch_full.label_token) |label_token| {
- block_scope.continue_block = switch_block.toOptional();
+ block_scope.label = .{ .token = label_token };
+ block_scope.break_target = switch_block;
+ block_scope.continue_target = .{ .switch_continue = switch_block };
block_scope.continue_result_info = .{
.rl = if (any_payload_is_ref)
.{ .ref_coerced_ty = raw_operand_ty_ref }
@@ -7826,12 +7852,7 @@ fn switchExpr(
.{ .coerced_ty = raw_operand_ty_ref },
};
- block_scope.label = .{
- .token = label_token,
- .block_inst = switch_block,
- };
- // `break` can target this via `label.block_inst`
- // `break_result_info` already set by `setBreakResultInfo`
+ // `break_result_info` already set by `setBreakResultInfo` above.
}
// We re-use this same scope for all cases, including the special prong, if any.
@@ -11916,8 +11937,8 @@ const GenZir = struct {
/// whenever we know Sema will analyze the current block with `is_comptime`,
/// for instance when we're within a `struct_decl` or a `block_comptime`.
is_comptime: bool,
- /// Whether we're in an expression within a `@TypeOf` operand. In this case, closure of runtime
- /// variables is permitted where it is usually not.
+ /// Whether we're in an expression within a `@TypeOf` operand. In this case,
+ /// closure of runtime variables is permitted where it is usually not.
is_typeof: bool = false,
/// This is set to true for a `GenZir` of a `block_inline`, indicating that
/// exits from this block should use `break_inline` rather than `break`.
@@ -11938,10 +11959,27 @@ const GenZir = struct {
/// if use is strictly nested. This saves prior size of list for unstacking.
instructions_top: usize,
label: ?Label = null,
- break_block: Zir.Inst.OptionalIndex = .none,
- continue_block: Zir.Inst.OptionalIndex = .none,
+ /// If `true`, unlabeled `break` and `continue` exprs can target this `GenZir`.
+ allow_unlabeled_control_flow: bool = false,
+ /// If `label` is `null` and `unlabeled_control_flow_target` is `false`,
+ /// this is unused and may be `undefined`.
+ /// Otherwise, this is the target for a `break` instruction when a `break`
+ /// targets this `GenZir`.
+ break_target: Zir.Inst.Index = undefined,
+ /// If `label` is `null` and `unlabeled_control_flow_target` is `false`,
+ /// this is unused and may be `undefined`.
+ continue_target: union(enum) {
+ /// A `continue` cannot target this `GenZir`; emit an error.
+ none,
+ /// Emit a `break` instruction targeting this block.
+ @"break": Zir.Inst.Index,
+ /// Emit a `switch_continue` instruction targeting this `switch_block`.
+ switch_continue: Zir.Inst.Index,
+ } = undefined,
/// Only valid when setBreakResultInfo is called.
break_result_info: AstGen.ResultInfo = undefined,
+ /// If `continue_target` is *not* `switch_continue`, this is unused and may
+ /// be `undefined`.
continue_result_info: AstGen.ResultInfo = undefined,
suspend_node: Ast.Node.OptionalIndex = .none,
@@ -12008,7 +12046,6 @@ const GenZir = struct {
const Label = struct {
token: Ast.TokenIndex,
- block_inst: Zir.Inst.Index,
used: bool = false,
used_for_continue: bool = false,
};
diff --git a/test/cases/compile_errors/labeled_block_continue.zig b/test/cases/compile_errors/labeled_block_continue.zig
@@ -0,0 +1,10 @@
+export fn foo() void {
+ const result: u32 = b: {
+ continue :b 123;
+ };
+ _ = result;
+}
+
+// error
+//
+// :3:9: error: continue cannot target labeled block