compiler: implement labeled switch/continue

This commit is contained in:
mlugg 2024-04-28 21:44:57 +01:00
parent 5fb4a7df38
commit 5e12ca9fe3
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
22 changed files with 1602 additions and 382 deletions

View file

@ -1184,14 +1184,7 @@ pub fn lastToken(tree: Ast, node: Node.Index) TokenIndex {
n = extra.sentinel; n = extra.sentinel;
}, },
.@"continue" => { .@"continue", .@"break" => {
if (datas[n].lhs != 0) {
return datas[n].lhs + end_offset;
} else {
return main_tokens[n] + end_offset;
}
},
.@"break" => {
if (datas[n].rhs != 0) { if (datas[n].rhs != 0) {
n = datas[n].rhs; n = datas[n].rhs;
} else if (datas[n].lhs != 0) { } else if (datas[n].lhs != 0) {
@ -1895,6 +1888,15 @@ pub fn taggedUnionEnumTag(tree: Ast, node: Node.Index) full.ContainerDecl {
}); });
} }
pub fn switchFull(tree: Ast, node: Node.Index) full.Switch {
const data = &tree.nodes.items(.data)[node];
return tree.fullSwitchComponents(.{
.switch_token = tree.nodes.items(.main_token)[node],
.condition = data.lhs,
.sub_range = data.rhs,
});
}
pub fn switchCaseOne(tree: Ast, node: Node.Index) full.SwitchCase { pub fn switchCaseOne(tree: Ast, node: Node.Index) full.SwitchCase {
const data = &tree.nodes.items(.data)[node]; const data = &tree.nodes.items(.data)[node];
const values: *[1]Node.Index = &data.lhs; const values: *[1]Node.Index = &data.lhs;
@ -2206,6 +2208,21 @@ fn fullContainerDeclComponents(tree: Ast, info: full.ContainerDecl.Components) f
return result; return result;
} }
fn fullSwitchComponents(tree: Ast, info: full.Switch.Components) full.Switch {
const token_tags = tree.tokens.items(.tag);
const tok_i = info.switch_token -| 1;
var result: full.Switch = .{
.ast = info,
.label_token = null,
};
if (token_tags[tok_i] == .colon and
token_tags[tok_i -| 1] == .identifier)
{
result.label_token = tok_i - 1;
}
return result;
}
fn fullSwitchCaseComponents(tree: Ast, info: full.SwitchCase.Components, node: Node.Index) full.SwitchCase { fn fullSwitchCaseComponents(tree: Ast, info: full.SwitchCase.Components, node: Node.Index) full.SwitchCase {
const token_tags = tree.tokens.items(.tag); const token_tags = tree.tokens.items(.tag);
const node_tags = tree.nodes.items(.tag); const node_tags = tree.nodes.items(.tag);
@ -2477,6 +2494,13 @@ pub fn fullContainerDecl(tree: Ast, buffer: *[2]Ast.Node.Index, node: Node.Index
}; };
} }
pub fn fullSwitch(tree: Ast, node: Node.Index) ?full.Switch {
return switch (tree.nodes.items(.tag)[node]) {
.@"switch", .switch_comma => tree.switchFull(node),
else => null,
};
}
pub fn fullSwitchCase(tree: Ast, node: Node.Index) ?full.SwitchCase { pub fn fullSwitchCase(tree: Ast, node: Node.Index) ?full.SwitchCase {
return switch (tree.nodes.items(.tag)[node]) { return switch (tree.nodes.items(.tag)[node]) {
.switch_case_one, .switch_case_inline_one => tree.switchCaseOne(node), .switch_case_one, .switch_case_inline_one => tree.switchCaseOne(node),
@ -2829,6 +2853,17 @@ pub const full = struct {
}; };
}; };
pub const Switch = struct {
ast: Components,
label_token: ?TokenIndex,
pub const Components = struct {
switch_token: TokenIndex,
condition: Node.Index,
sub_range: Node.Index,
};
};
pub const SwitchCase = struct { pub const SwitchCase = struct {
inline_token: ?TokenIndex, inline_token: ?TokenIndex,
/// Points to the first token after the `|`. Will either be an identifier or /// Points to the first token after the `|`. Will either be an identifier or
@ -3287,7 +3322,8 @@ pub const Node = struct {
@"suspend", @"suspend",
/// `resume lhs`. rhs is unused. /// `resume lhs`. rhs is unused.
@"resume", @"resume",
/// `continue`. lhs is token index of label if any. rhs is unused. /// `continue :lhs rhs`
/// both lhs and rhs may be omitted.
@"continue", @"continue",
/// `break :lhs rhs` /// `break :lhs rhs`
/// both lhs and rhs may be omitted. /// both lhs and rhs may be omitted.

View file

@ -1144,7 +1144,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
.error_set_decl => return errorSetDecl(gz, ri, node), .error_set_decl => return errorSetDecl(gz, ri, node),
.array_access => return arrayAccess(gz, scope, ri, node), .array_access => return arrayAccess(gz, scope, ri, node),
.@"comptime" => return comptimeExprAst(gz, scope, ri, node), .@"comptime" => return comptimeExprAst(gz, scope, ri, node),
.@"switch", .switch_comma => return switchExpr(gz, scope, ri.br(), node), .@"switch", .switch_comma => return switchExpr(gz, scope, ri.br(), node, tree.fullSwitch(node).?),
.@"nosuspend" => return nosuspendExpr(gz, scope, ri, node), .@"nosuspend" => return nosuspendExpr(gz, scope, ri, node),
.@"suspend" => return suspendExpr(gz, scope, node), .@"suspend" => return suspendExpr(gz, scope, node),
@ -2160,6 +2160,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
if (break_label != 0) { if (break_label != 0) {
if (block_gz.label) |*label| { if (block_gz.label) |*label| {
if (try astgen.tokenIdentEql(label.token, break_label)) { if (try astgen.tokenIdentEql(label.token, break_label)) {
const maybe_switch_tag = astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)];
switch (maybe_switch_tag) {
.switch_block, .switch_block_ref => return astgen.failNode(node, "cannot break from switch", .{}),
else => {},
}
label.used = true; label.used = true;
break :blk label.block_inst; break :blk label.block_inst;
} }
@ -2234,6 +2239,11 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
const tree = astgen.tree; const tree = astgen.tree;
const node_datas = tree.nodes.items(.data); const node_datas = tree.nodes.items(.data);
const break_label = node_datas[node].lhs; const break_label = node_datas[node].lhs;
const rhs = node_datas[node].rhs;
if (break_label == 0 and rhs != 0) {
return astgen.failNode(node, "cannot continue with operand without label", .{});
}
// Look for the label in the scope. // Look for the label in the scope.
var scope = parent_scope; var scope = parent_scope;
@ -2258,6 +2268,15 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
if (break_label != 0) blk: { if (break_label != 0) blk: {
if (gen_zir.label) |*label| { if (gen_zir.label) |*label| {
if (try astgen.tokenIdentEql(label.token, break_label)) { if (try astgen.tokenIdentEql(label.token, break_label)) {
const maybe_switch_tag = astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)];
if (rhs != 0) 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 = true;
break :blk; break :blk;
} }
@ -2265,8 +2284,35 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
// found continue but either it has a different label, or no label // found continue but either it has a different label, or no label
scope = gen_zir.parent; scope = gen_zir.parent;
continue; 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 => {},
}
} }
if (rhs != 0) {
// 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);
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) const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline)
.break_inline .break_inline
else else
@ -2284,12 +2330,7 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
}, },
.local_val => scope = scope.cast(Scope.LocalVal).?.parent, .local_val => scope = scope.cast(Scope.LocalVal).?.parent,
.local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent, .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent,
.defer_normal => { .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent,
const defer_scope = scope.cast(Scope.Defer).?;
scope = defer_scope.parent;
try parent_gz.addDefer(defer_scope.index, defer_scope.len);
},
.defer_error => scope = scope.cast(Scope.Defer).?.parent,
.namespace => break, .namespace => break,
.top => unreachable, .top => unreachable,
} }
@ -2881,6 +2922,7 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
.panic, .panic,
.trap, .trap,
.check_comptime_control_flow, .check_comptime_control_flow,
.switch_continue,
=> { => {
noreturn_src_node = statement; noreturn_src_node = statement;
break :b true; break :b true;
@ -7546,7 +7588,8 @@ fn switchExpr(
parent_gz: *GenZir, parent_gz: *GenZir,
scope: *Scope, scope: *Scope,
ri: ResultInfo, ri: ResultInfo,
switch_node: Ast.Node.Index, node: Ast.Node.Index,
switch_full: Ast.full.Switch,
) InnerError!Zir.Inst.Ref { ) InnerError!Zir.Inst.Ref {
const astgen = parent_gz.astgen; const astgen = parent_gz.astgen;
const gpa = astgen.gpa; const gpa = astgen.gpa;
@ -7555,14 +7598,14 @@ fn switchExpr(
const node_tags = tree.nodes.items(.tag); const node_tags = tree.nodes.items(.tag);
const main_tokens = tree.nodes.items(.main_token); const main_tokens = tree.nodes.items(.main_token);
const token_tags = tree.tokens.items(.tag); const token_tags = tree.tokens.items(.tag);
const operand_node = node_datas[switch_node].lhs; const operand_node = node_datas[node].lhs;
const extra = tree.extraData(node_datas[switch_node].rhs, Ast.Node.SubRange); const extra = tree.extraData(node_datas[node].rhs, Ast.Node.SubRange);
const case_nodes = tree.extra_data[extra.start..extra.end]; const case_nodes = tree.extra_data[extra.start..extra.end];
const need_rl = astgen.nodes_need_rl.contains(switch_node); const need_rl = astgen.nodes_need_rl.contains(node);
const block_ri: ResultInfo = if (need_rl) ri else .{ const block_ri: ResultInfo = if (need_rl) ri else .{
.rl = switch (ri.rl) { .rl = switch (ri.rl) {
.ptr => .{ .ty = (try ri.rl.resultType(parent_gz, switch_node)).? }, .ptr => .{ .ty = (try ri.rl.resultType(parent_gz, node)).? },
.inferred_ptr => .none, .inferred_ptr => .none,
else => ri.rl, else => ri.rl,
}, },
@ -7573,11 +7616,16 @@ fn switchExpr(
const LocTag = @typeInfo(ResultInfo.Loc).@"union".tag_type.?; const LocTag = @typeInfo(ResultInfo.Loc).@"union".tag_type.?;
const need_result_rvalue = @as(LocTag, block_ri.rl) != @as(LocTag, ri.rl); const need_result_rvalue = @as(LocTag, block_ri.rl) != @as(LocTag, ri.rl);
if (switch_full.label_token) |label_token| {
try astgen.checkLabelRedefinition(scope, label_token);
}
// We perform two passes over the AST. This first pass is to collect information // We perform two passes over the AST. This first pass is to collect information
// for the following variables, make note of the special prong AST node index, // for the following variables, make note of the special prong AST node index,
// and bail out with a compile error if there are multiple special prongs present. // and bail out with a compile error if there are multiple special prongs present.
var any_payload_is_ref = false; var any_payload_is_ref = false;
var any_has_tag_capture = false; var any_has_tag_capture = false;
var any_non_inline_capture = false;
var scalar_cases_len: u32 = 0; var scalar_cases_len: u32 = 0;
var multi_cases_len: u32 = 0; var multi_cases_len: u32 = 0;
var inline_cases_len: u32 = 0; var inline_cases_len: u32 = 0;
@ -7595,6 +7643,15 @@ fn switchExpr(
if (token_tags[ident + 1] == .comma) { if (token_tags[ident + 1] == .comma) {
any_has_tag_capture = true; any_has_tag_capture = true;
} }
// If the first capture is ignored, then there is no runtime-known
// capture, as the tag capture must be for an inline prong.
// This check isn't perfect, because for things like enums, the
// first prong *is* comptime-known for inline prongs! But such
// knowledge requires semantic analysis.
if (!mem.eql(u8, tree.tokenSlice(ident), "_")) {
any_non_inline_capture = true;
}
} }
// Check for else/`_` prong. // Check for else/`_` prong.
if (case.ast.values.len == 0) { if (case.ast.values.len == 0) {
@ -7614,7 +7671,7 @@ fn switchExpr(
); );
} else if (underscore_src) |some_underscore| { } else if (underscore_src) |some_underscore| {
return astgen.failNodeNotes( return astgen.failNodeNotes(
switch_node, node,
"else and '_' prong in switch expression", "else and '_' prong in switch expression",
.{}, .{},
&[_]u32{ &[_]u32{
@ -7655,7 +7712,7 @@ fn switchExpr(
); );
} else if (else_src) |some_else| { } else if (else_src) |some_else| {
return astgen.failNodeNotes( return astgen.failNodeNotes(
switch_node, node,
"else and '_' prong in switch expression", "else and '_' prong in switch expression",
.{}, .{},
&[_]u32{ &[_]u32{
@ -7704,6 +7761,12 @@ fn switchExpr(
const raw_operand = try expr(parent_gz, scope, operand_ri, operand_node); const raw_operand = try expr(parent_gz, scope, operand_ri, operand_node);
const item_ri: ResultInfo = .{ .rl = .none }; const item_ri: ResultInfo = .{ .rl = .none };
// If this switch is labeled, it will have `continue`s targeting it, and thus we need the operand type
// to provide a result type.
const raw_operand_ty_ref = if (switch_full.label_token != null) t: {
break :t try parent_gz.addUnNode(.typeof, raw_operand, operand_node);
} else undefined;
// This contains the data that goes into the `extra` array for the SwitchBlock/SwitchBlockMulti, // This contains the data that goes into the `extra` array for the SwitchBlock/SwitchBlockMulti,
// except the first cases_nodes.len slots are a table that indexes payloads later in the array, with // except the first cases_nodes.len slots are a table that indexes payloads later in the array, with
// the special case index coming first, then scalar_case_len indexes, then multi_cases_len indexes // the special case index coming first, then scalar_case_len indexes, then multi_cases_len indexes
@ -7725,7 +7788,22 @@ fn switchExpr(
try emitDbgStmtForceCurrentIndex(parent_gz, operand_lc); try emitDbgStmtForceCurrentIndex(parent_gz, operand_lc);
// This gets added to the parent block later, after the item expressions. // This gets added to the parent block later, after the item expressions.
const switch_tag: Zir.Inst.Tag = if (any_payload_is_ref) .switch_block_ref else .switch_block; const switch_tag: Zir.Inst.Tag = if (any_payload_is_ref) .switch_block_ref else .switch_block;
const switch_block = try parent_gz.makeBlockInst(switch_tag, switch_node); 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.continue_result_info = .{
.rl = if (any_payload_is_ref)
.{ .ref_coerced_ty = raw_operand_ty_ref }
else
.{ .coerced_ty = raw_operand_ty_ref },
};
block_scope.label = .{
.token = label_token,
.block_inst = switch_block,
};
}
// We re-use this same scope for all cases, including the special prong, if any. // We re-use this same scope for all cases, including the special prong, if any.
var case_scope = parent_gz.makeSubBlock(&block_scope.base); var case_scope = parent_gz.makeSubBlock(&block_scope.base);
@ -7946,6 +8024,8 @@ fn switchExpr(
.has_else = special_prong == .@"else", .has_else = special_prong == .@"else",
.has_under = special_prong == .under, .has_under = special_prong == .under,
.any_has_tag_capture = any_has_tag_capture, .any_has_tag_capture = any_has_tag_capture,
.any_non_inline_capture = any_non_inline_capture,
.has_continue = switch_full.label_token != null,
.scalar_cases_len = @intCast(scalar_cases_len), .scalar_cases_len = @intCast(scalar_cases_len),
}, },
}); });
@ -7982,7 +8062,7 @@ fn switchExpr(
} }
if (need_result_rvalue) { if (need_result_rvalue) {
return rvalue(parent_gz, ri, switch_block.toRef(), switch_node); return rvalue(parent_gz, ri, switch_block.toRef(), node);
} else { } else {
return switch_block.toRef(); return switch_block.toRef();
} }
@ -11824,6 +11904,7 @@ const GenZir = struct {
continue_block: Zir.Inst.OptionalIndex = .none, continue_block: Zir.Inst.OptionalIndex = .none,
/// Only valid when setBreakResultInfo is called. /// Only valid when setBreakResultInfo is called.
break_result_info: AstGen.ResultInfo = undefined, break_result_info: AstGen.ResultInfo = undefined,
continue_result_info: AstGen.ResultInfo = undefined,
suspend_node: Ast.Node.Index = 0, suspend_node: Ast.Node.Index = 0,
nosuspend_node: Ast.Node.Index = 0, nosuspend_node: Ast.Node.Index = 0,

View file

@ -924,7 +924,6 @@ fn expectContainerField(p: *Parse) !Node.Index {
/// / KEYWORD_errdefer Payload? BlockExprStatement /// / KEYWORD_errdefer Payload? BlockExprStatement
/// / IfStatement /// / IfStatement
/// / LabeledStatement /// / LabeledStatement
/// / SwitchExpr
/// / VarDeclExprStatement /// / VarDeclExprStatement
fn expectStatement(p: *Parse, allow_defer_var: bool) Error!Node.Index { fn expectStatement(p: *Parse, allow_defer_var: bool) Error!Node.Index {
if (p.eatToken(.keyword_comptime)) |comptime_token| { if (p.eatToken(.keyword_comptime)) |comptime_token| {
@ -995,7 +994,6 @@ fn expectStatement(p: *Parse, allow_defer_var: bool) Error!Node.Index {
.rhs = try p.expectBlockExprStatement(), .rhs = try p.expectBlockExprStatement(),
}, },
}), }),
.keyword_switch => return p.expectSwitchExpr(),
.keyword_if => return p.expectIfStatement(), .keyword_if => return p.expectIfStatement(),
.keyword_enum, .keyword_struct, .keyword_union => { .keyword_enum, .keyword_struct, .keyword_union => {
const identifier = p.tok_i + 1; const identifier = p.tok_i + 1;
@ -1238,7 +1236,7 @@ fn expectIfStatement(p: *Parse) !Node.Index {
}); });
} }
/// LabeledStatement <- BlockLabel? (Block / LoopStatement) /// LabeledStatement <- BlockLabel? (Block / LoopStatement / SwitchExpr)
fn parseLabeledStatement(p: *Parse) !Node.Index { fn parseLabeledStatement(p: *Parse) !Node.Index {
const label_token = p.parseBlockLabel(); const label_token = p.parseBlockLabel();
const block = try p.parseBlock(); const block = try p.parseBlock();
@ -1247,6 +1245,9 @@ fn parseLabeledStatement(p: *Parse) !Node.Index {
const loop_stmt = try p.parseLoopStatement(); const loop_stmt = try p.parseLoopStatement();
if (loop_stmt != 0) return loop_stmt; if (loop_stmt != 0) return loop_stmt;
const switch_expr = try p.parseSwitchExpr();
if (switch_expr != 0) return switch_expr;
if (label_token != 0) { if (label_token != 0) {
const after_colon = p.tok_i; const after_colon = p.tok_i;
const node = try p.parseTypeExpr(); const node = try p.parseTypeExpr();
@ -2072,7 +2073,7 @@ fn expectTypeExpr(p: *Parse) Error!Node.Index {
/// / KEYWORD_break BreakLabel? Expr? /// / KEYWORD_break BreakLabel? Expr?
/// / KEYWORD_comptime Expr /// / KEYWORD_comptime Expr
/// / KEYWORD_nosuspend Expr /// / KEYWORD_nosuspend Expr
/// / KEYWORD_continue BreakLabel? /// / KEYWORD_continue BreakLabel? Expr?
/// / KEYWORD_resume Expr /// / KEYWORD_resume Expr
/// / KEYWORD_return Expr? /// / KEYWORD_return Expr?
/// / BlockLabel? LoopExpr /// / BlockLabel? LoopExpr
@ -2098,7 +2099,7 @@ fn parsePrimaryExpr(p: *Parse) !Node.Index {
.main_token = p.nextToken(), .main_token = p.nextToken(),
.data = .{ .data = .{
.lhs = try p.parseBreakLabel(), .lhs = try p.parseBreakLabel(),
.rhs = undefined, .rhs = try p.parseExpr(),
}, },
}); });
}, },
@ -2627,7 +2628,6 @@ fn parseSuffixExpr(p: *Parse) !Node.Index {
/// / KEYWORD_anyframe /// / KEYWORD_anyframe
/// / KEYWORD_unreachable /// / KEYWORD_unreachable
/// / STRINGLITERAL /// / STRINGLITERAL
/// / SwitchExpr
/// ///
/// ContainerDecl <- (KEYWORD_extern / KEYWORD_packed)? ContainerDeclAuto /// ContainerDecl <- (KEYWORD_extern / KEYWORD_packed)? ContainerDeclAuto
/// ///
@ -2647,6 +2647,7 @@ fn parseSuffixExpr(p: *Parse) !Node.Index {
/// LabeledTypeExpr /// LabeledTypeExpr
/// <- BlockLabel Block /// <- BlockLabel Block
/// / BlockLabel? LoopTypeExpr /// / BlockLabel? LoopTypeExpr
/// / BlockLabel? SwitchExpr
/// ///
/// LoopTypeExpr <- KEYWORD_inline? (ForTypeExpr / WhileTypeExpr) /// LoopTypeExpr <- KEYWORD_inline? (ForTypeExpr / WhileTypeExpr)
fn parsePrimaryTypeExpr(p: *Parse) !Node.Index { fn parsePrimaryTypeExpr(p: *Parse) !Node.Index {
@ -2753,6 +2754,10 @@ fn parsePrimaryTypeExpr(p: *Parse) !Node.Index {
p.tok_i += 2; p.tok_i += 2;
return p.parseWhileTypeExpr(); return p.parseWhileTypeExpr();
}, },
.keyword_switch => {
p.tok_i += 2;
return p.expectSwitchExpr();
},
.l_brace => { .l_brace => {
p.tok_i += 2; p.tok_i += 2;
return p.parseBlock(); return p.parseBlock();
@ -3029,8 +3034,17 @@ fn parseWhileTypeExpr(p: *Parse) !Node.Index {
} }
/// SwitchExpr <- KEYWORD_switch LPAREN Expr RPAREN LBRACE SwitchProngList RBRACE /// SwitchExpr <- KEYWORD_switch LPAREN Expr RPAREN LBRACE SwitchProngList RBRACE
fn parseSwitchExpr(p: *Parse) !Node.Index {
const switch_token = p.eatToken(.keyword_switch) orelse return null_node;
return p.expectSwitchSuffix(switch_token);
}
fn expectSwitchExpr(p: *Parse) !Node.Index { fn expectSwitchExpr(p: *Parse) !Node.Index {
const switch_token = p.assertToken(.keyword_switch); const switch_token = p.assertToken(.keyword_switch);
return p.expectSwitchSuffix(switch_token);
}
fn expectSwitchSuffix(p: *Parse, switch_token: TokenIndex) !Node.Index {
_ = try p.expectToken(.l_paren); _ = try p.expectToken(.l_paren);
const expr_node = try p.expectExpr(); const expr_node = try p.expectExpr();
_ = try p.expectToken(.r_paren); _ = try p.expectToken(.r_paren);

View file

@ -314,6 +314,9 @@ pub const Inst = struct {
/// break instruction in a block, and the target block is the parent. /// break instruction in a block, and the target block is the parent.
/// Uses the `break` union field. /// Uses the `break` union field.
break_inline, break_inline,
/// Branch from within a switch case to the case specified by the operand.
/// Uses the `break` union field. `block_inst` refers to a `switch_block` or `switch_block_ref`.
switch_continue,
/// Checks that comptime control flow does not happen inside a runtime block. /// Checks that comptime control flow does not happen inside a runtime block.
/// Uses the `un_node` union field. /// Uses the `un_node` union field.
check_comptime_control_flow, check_comptime_control_flow,
@ -1273,6 +1276,7 @@ pub const Inst = struct {
.panic, .panic,
.trap, .trap,
.check_comptime_control_flow, .check_comptime_control_flow,
.switch_continue,
=> true, => true,
}; };
} }
@ -1512,6 +1516,7 @@ pub const Inst = struct {
.break_inline, .break_inline,
.condbr, .condbr,
.condbr_inline, .condbr_inline,
.switch_continue,
.compile_error, .compile_error,
.ret_node, .ret_node,
.ret_load, .ret_load,
@ -1597,6 +1602,7 @@ pub const Inst = struct {
.bool_br_or = .pl_node, .bool_br_or = .pl_node,
.@"break" = .@"break", .@"break" = .@"break",
.break_inline = .@"break", .break_inline = .@"break",
.switch_continue = .@"break",
.check_comptime_control_flow = .un_node, .check_comptime_control_flow = .un_node,
.for_len = .pl_node, .for_len = .pl_node,
.call = .pl_node, .call = .pl_node,
@ -2288,6 +2294,7 @@ pub const Inst = struct {
}, },
@"break": struct { @"break": struct {
operand: Ref, operand: Ref,
/// Index of a `Break` payload.
payload_index: u32, payload_index: u32,
}, },
dbg_stmt: LineColumn, dbg_stmt: LineColumn,
@ -2945,9 +2952,13 @@ pub const Inst = struct {
has_under: bool, has_under: bool,
/// If true, at least one prong has an inline tag capture. /// If true, at least one prong has an inline tag capture.
any_has_tag_capture: bool, any_has_tag_capture: bool,
/// If true, at least one prong has a capture which may not
/// be comptime-known via `inline`.
any_non_inline_capture: bool,
has_continue: bool,
scalar_cases_len: ScalarCasesLen, scalar_cases_len: ScalarCasesLen,
pub const ScalarCasesLen = u28; pub const ScalarCasesLen = u26;
pub fn specialProng(bits: Bits) SpecialProng { pub fn specialProng(bits: Bits) SpecialProng {
const has_else: u2 = @intFromBool(bits.has_else); const has_else: u2 = @intFromBool(bits.has_else);
@ -3750,6 +3761,7 @@ fn findDeclsInner(
.bool_br_or, .bool_br_or,
.@"break", .@"break",
.break_inline, .break_inline,
.switch_continue,
.check_comptime_control_flow, .check_comptime_control_flow,
.builtin_call, .builtin_call,
.cmp_lt, .cmp_lt,

View file

@ -429,6 +429,14 @@ pub const Inst = struct {
/// Result type is always noreturn; no instructions in a block follow this one. /// Result type is always noreturn; no instructions in a block follow this one.
/// Uses the `pl_op` field. Operand is the condition. Payload is `SwitchBr`. /// Uses the `pl_op` field. Operand is the condition. Payload is `SwitchBr`.
switch_br, switch_br,
/// Switch branch which can dispatch back to itself with a different operand.
/// Result type is always noreturn; no instructions in a block follow this one.
/// Uses the `pl_op` field. Operand is the condition. Payload is `SwitchBr`.
loop_switch_br,
/// Dispatches back to a branch of a parent `loop_switch_br`.
/// Result type is always noreturn; no instructions in a block follow this one.
/// Uses the `br` field. `block_inst` is a `loop_switch_br` instruction.
switch_dispatch,
/// Given an operand which is an error union, splits control flow. In /// Given an operand which is an error union, splits control flow. In
/// case of error, control flow goes into the block that is part of this /// case of error, control flow goes into the block that is part of this
/// instruction, which is guaranteed to end with a return instruction /// instruction, which is guaranteed to end with a return instruction
@ -1454,6 +1462,8 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
.br, .br,
.cond_br, .cond_br,
.switch_br, .switch_br,
.loop_switch_br,
.switch_dispatch,
.ret, .ret,
.ret_safe, .ret_safe,
.ret_load, .ret_load,
@ -1618,6 +1628,8 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
.call_never_inline, .call_never_inline,
.cond_br, .cond_br,
.switch_br, .switch_br,
.loop_switch_br,
.switch_dispatch,
.@"try", .@"try",
.try_cold, .try_cold,
.try_ptr, .try_ptr,
@ -1903,7 +1915,10 @@ pub const UnwrappedSwitch = struct {
pub fn unwrapSwitch(air: *const Air, switch_inst: Inst.Index) UnwrappedSwitch { pub fn unwrapSwitch(air: *const Air, switch_inst: Inst.Index) UnwrappedSwitch {
const inst = air.instructions.get(@intFromEnum(switch_inst)); const inst = air.instructions.get(@intFromEnum(switch_inst));
assert(inst.tag == .switch_br); switch (inst.tag) {
.switch_br, .loop_switch_br => {},
else => unreachable, // assertion failure
}
const pl_op = inst.data.pl_op; const pl_op = inst.data.pl_op;
const extra = air.extraData(SwitchBr, pl_op.payload); const extra = air.extraData(SwitchBr, pl_op.payload);
const hint_bag_count = std.math.divCeil(usize, extra.data.cases_len + 1, 10) catch unreachable; const hint_bag_count = std.math.divCeil(usize, extra.data.cases_len + 1, 10) catch unreachable;

View file

@ -222,7 +222,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
if (!checkRef(data.un_op, zcu)) return false; if (!checkRef(data.un_op, zcu)) return false;
}, },
.br => { .br, .switch_dispatch => {
if (!checkRef(data.br.operand, zcu)) return false; if (!checkRef(data.br.operand, zcu)) return false;
}, },
@ -380,7 +380,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
)) return false; )) return false;
}, },
.switch_br => { .switch_br, .loop_switch_br => {
const switch_br = air.unwrapSwitch(inst); const switch_br = air.unwrapSwitch(inst);
if (!checkRef(switch_br.operand, zcu)) return false; if (!checkRef(switch_br.operand, zcu)) return false;
var it = switch_br.iterateCases(); var it = switch_br.iterateCases();

View file

@ -31,6 +31,7 @@ tomb_bits: []usize,
/// * `try`, `try_ptr` - points to a `CondBr` in `extra` at this index. The error path (the block /// * `try`, `try_ptr` - points to a `CondBr` in `extra` at this index. The error path (the block
/// in the instruction) is considered the "else" path, and the rest of the block the "then". /// in the instruction) is considered the "else" path, and the rest of the block the "then".
/// * `switch_br` - points to a `SwitchBr` in `extra` at this index. /// * `switch_br` - points to a `SwitchBr` in `extra` at this index.
/// * `loop_switch_br` - points to a `SwitchBr` in `extra` at this index.
/// * `block` - points to a `Block` in `extra` at this index. /// * `block` - points to a `Block` in `extra` at this index.
/// * `asm`, `call`, `aggregate_init` - the value is a set of bits which are the extra tomb /// * `asm`, `call`, `aggregate_init` - the value is a set of bits which are the extra tomb
/// bits of operands. /// bits of operands.
@ -68,8 +69,8 @@ pub const Block = struct {
/// Liveness analysis runs in several passes. Each pass iterates backwards over instructions in /// Liveness analysis runs in several passes. Each pass iterates backwards over instructions in
/// bodies, and recurses into bodies. /// bodies, and recurses into bodies.
const LivenessPass = enum { const LivenessPass = enum {
/// In this pass, we perform some basic analysis of loops to gain information the main pass /// In this pass, we perform some basic analysis of loops to gain information the main pass needs.
/// needs. In particular, for every `loop`, we track the following information: /// In particular, for every `loop` and `loop_switch_br`, we track the following information:
/// * Every outer block which the loop body contains a `br` to. /// * Every outer block which the loop body contains a `br` to.
/// * Every outer loop which the loop body contains a `repeat` to. /// * Every outer loop which the loop body contains a `repeat` to.
/// * Every operand referenced within the loop body but created outside the loop. /// * Every operand referenced within the loop body but created outside the loop.
@ -91,7 +92,8 @@ fn LivenessPassData(comptime pass: LivenessPass) type {
.loop_analysis => struct { .loop_analysis => struct {
/// The set of blocks which are exited with a `br` instruction at some point within this /// The set of blocks which are exited with a `br` instruction at some point within this
/// body and which we are currently within. Also includes `loop`s which are the target /// body and which we are currently within. Also includes `loop`s which are the target
/// of a `repeat` instruction. /// of a `repeat` instruction, and `loop_switch_br`s which are the target of a
/// `switch_dispatch` instruction.
breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{}, breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{},
/// The set of operands for which we have seen at least one usage but not their birth. /// The set of operands for which we have seen at least one usage but not their birth.
@ -330,6 +332,7 @@ pub fn categorizeOperand(
.trap, .trap,
.breakpoint, .breakpoint,
.repeat, .repeat,
.switch_dispatch,
.dbg_stmt, .dbg_stmt,
.unreach, .unreach,
.ret_addr, .ret_addr,
@ -662,21 +665,17 @@ pub fn categorizeOperand(
return .complex; return .complex;
}, },
.@"try", .try_cold => {
return .complex; .@"try",
}, .try_cold,
.try_ptr, .try_ptr_cold => { .try_ptr,
return .complex; .try_ptr_cold,
}, .loop,
.loop => { .cond_br,
return .complex; .switch_br,
}, .loop_switch_br,
.cond_br => { => return .complex,
return .complex;
},
.switch_br => {
return .complex;
},
.wasm_memory_grow => { .wasm_memory_grow => {
const pl_op = air_datas[@intFromEnum(inst)].pl_op; const pl_op = air_datas[@intFromEnum(inst)].pl_op;
if (pl_op.operand == operand_ref) return matchOperandSmallIndex(l, inst, 0, .none); if (pl_op.operand == operand_ref) return matchOperandSmallIndex(l, inst, 0, .none);
@ -1206,6 +1205,7 @@ fn analyzeInst(
.br => return analyzeInstBr(a, pass, data, inst), .br => return analyzeInstBr(a, pass, data, inst),
.repeat => return analyzeInstRepeat(a, pass, data, inst), .repeat => return analyzeInstRepeat(a, pass, data, inst),
.switch_dispatch => return analyzeInstSwitchDispatch(a, pass, data, inst),
.assembly => { .assembly => {
const extra = a.air.extraData(Air.Asm, inst_datas[@intFromEnum(inst)].ty_pl.payload); const extra = a.air.extraData(Air.Asm, inst_datas[@intFromEnum(inst)].ty_pl.payload);
@ -1262,7 +1262,8 @@ fn analyzeInst(
.@"try", .try_cold => return analyzeInstCondBr(a, pass, data, inst, .@"try"), .@"try", .try_cold => return analyzeInstCondBr(a, pass, data, inst, .@"try"),
.try_ptr, .try_ptr_cold => return analyzeInstCondBr(a, pass, data, inst, .try_ptr), .try_ptr, .try_ptr_cold => return analyzeInstCondBr(a, pass, data, inst, .try_ptr),
.cond_br => return analyzeInstCondBr(a, pass, data, inst, .cond_br), .cond_br => return analyzeInstCondBr(a, pass, data, inst, .cond_br),
.switch_br => return analyzeInstSwitchBr(a, pass, data, inst), .switch_br => return analyzeInstSwitchBr(a, pass, data, inst, false),
.loop_switch_br => return analyzeInstSwitchBr(a, pass, data, inst, true),
.wasm_memory_grow => { .wasm_memory_grow => {
const pl_op = inst_datas[@intFromEnum(inst)].pl_op; const pl_op = inst_datas[@intFromEnum(inst)].pl_op;
@ -1412,6 +1413,35 @@ fn analyzeInstRepeat(
return analyzeOperands(a, pass, data, inst, .{ .none, .none, .none }); return analyzeOperands(a, pass, data, inst, .{ .none, .none, .none });
} }
fn analyzeInstSwitchDispatch(
a: *Analysis,
comptime pass: LivenessPass,
data: *LivenessPassData(pass),
inst: Air.Inst.Index,
) !void {
// This happens to be identical to `analyzeInstBr`, but is separated anyway for clarity.
const inst_datas = a.air.instructions.items(.data);
const br = inst_datas[@intFromEnum(inst)].br;
const gpa = a.gpa;
switch (pass) {
.loop_analysis => {
try data.breaks.put(gpa, br.block_inst, {});
},
.main_analysis => {
const block_scope = data.block_scopes.get(br.block_inst).?; // we should always be repeating an enclosing loop
const new_live_set = try block_scope.live_set.clone(gpa);
data.live_set.deinit(gpa);
data.live_set = new_live_set;
},
}
return analyzeOperands(a, pass, data, inst, .{ br.operand, .none, .none });
}
fn analyzeInstBlock( fn analyzeInstBlock(
a: *Analysis, a: *Analysis,
comptime pass: LivenessPass, comptime pass: LivenessPass,
@ -1482,6 +1512,102 @@ fn analyzeInstBlock(
} }
} }
fn writeLoopInfo(
a: *Analysis,
data: *LivenessPassData(.loop_analysis),
inst: Air.Inst.Index,
old_breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void),
old_live: std.AutoHashMapUnmanaged(Air.Inst.Index, void),
) !void {
const gpa = a.gpa;
// `loop`s are guaranteed to have at least one matching `repeat`.
// Similarly, `loop_switch_br`s have a matching `switch_dispatch`.
// However, we no longer care about repeats of this loop for resolving
// which operands must live within it.
assert(data.breaks.remove(inst));
const extra_index: u32 = @intCast(a.extra.items.len);
const num_breaks = data.breaks.count();
try a.extra.ensureUnusedCapacity(gpa, 1 + num_breaks);
a.extra.appendAssumeCapacity(num_breaks);
var it = data.breaks.keyIterator();
while (it.next()) |key| {
const block_inst = key.*;
a.extra.appendAssumeCapacity(@intFromEnum(block_inst));
}
log.debug("[{}] %{}: includes breaks to {}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.breaks) });
// Now we put the live operands from the loop body in too
const num_live = data.live_set.count();
try a.extra.ensureUnusedCapacity(gpa, 1 + num_live);
a.extra.appendAssumeCapacity(num_live);
it = data.live_set.keyIterator();
while (it.next()) |key| {
const alive = key.*;
a.extra.appendAssumeCapacity(@intFromEnum(alive));
}
log.debug("[{}] %{}: maintain liveness of {}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.live_set) });
try a.special.put(gpa, inst, extra_index);
// Add back operands which were previously alive
it = old_live.keyIterator();
while (it.next()) |key| {
const alive = key.*;
try data.live_set.put(gpa, alive, {});
}
// And the same for breaks
it = old_breaks.keyIterator();
while (it.next()) |key| {
const block_inst = key.*;
try data.breaks.put(gpa, block_inst, {});
}
}
/// When analyzing a loop in the main pass, sets up `data.live_set` to be the set
/// of operands known to be alive when the loop repeats.
fn resolveLoopLiveSet(
a: *Analysis,
data: *LivenessPassData(.main_analysis),
inst: Air.Inst.Index,
) !void {
const gpa = a.gpa;
const extra_idx = a.special.fetchRemove(inst).?.value;
const num_breaks = data.old_extra.items[extra_idx];
const breaks: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + 1 ..][0..num_breaks]);
const num_loop_live = data.old_extra.items[extra_idx + num_breaks + 1];
const loop_live: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + num_breaks + 2 ..][0..num_loop_live]);
// This is necessarily not in the same control flow branch, because loops are noreturn
data.live_set.clearRetainingCapacity();
try data.live_set.ensureUnusedCapacity(gpa, @intCast(loop_live.len));
for (loop_live) |alive| data.live_set.putAssumeCapacity(alive, {});
log.debug("[{}] %{}: block live set is {}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
for (breaks) |block_inst| {
// We might break to this block, so include every operand that the block needs alive
const block_scope = data.block_scopes.get(block_inst).?;
var it = block_scope.live_set.keyIterator();
while (it.next()) |key| {
const alive = key.*;
try data.live_set.put(gpa, alive, {});
}
}
log.debug("[{}] %{}: loop live set is {}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
}
fn analyzeInstLoop( fn analyzeInstLoop(
a: *Analysis, a: *Analysis,
comptime pass: LivenessPass, comptime pass: LivenessPass,
@ -1505,86 +1631,14 @@ fn analyzeInstLoop(
try analyzeBody(a, pass, data, body); try analyzeBody(a, pass, data, body);
// `loop`s are guaranteed to have at least one matching `repeat`. try writeLoopInfo(a, data, inst, old_breaks, old_live);
// However, we no longer care about repeats of this loop itself.
assert(data.breaks.remove(inst));
const extra_index: u32 = @intCast(a.extra.items.len);
const num_breaks = data.breaks.count();
try a.extra.ensureUnusedCapacity(gpa, 1 + num_breaks);
a.extra.appendAssumeCapacity(num_breaks);
var it = data.breaks.keyIterator();
while (it.next()) |key| {
const block_inst = key.*;
a.extra.appendAssumeCapacity(@intFromEnum(block_inst));
}
log.debug("[{}] %{}: includes breaks to {}", .{ pass, inst, fmtInstSet(&data.breaks) });
// Now we put the live operands from the loop body in too
const num_live = data.live_set.count();
try a.extra.ensureUnusedCapacity(gpa, 1 + num_live);
a.extra.appendAssumeCapacity(num_live);
it = data.live_set.keyIterator();
while (it.next()) |key| {
const alive = key.*;
a.extra.appendAssumeCapacity(@intFromEnum(alive));
}
log.debug("[{}] %{}: maintain liveness of {}", .{ pass, inst, fmtInstSet(&data.live_set) });
try a.special.put(gpa, inst, extra_index);
// Add back operands which were previously alive
it = old_live.keyIterator();
while (it.next()) |key| {
const alive = key.*;
try data.live_set.put(gpa, alive, {});
}
// And the same for breaks
it = old_breaks.keyIterator();
while (it.next()) |key| {
const block_inst = key.*;
try data.breaks.put(gpa, block_inst, {});
}
}, },
.main_analysis => { .main_analysis => {
const extra_idx = a.special.fetchRemove(inst).?.value; // remove because this data does not exist after analysis try resolveLoopLiveSet(a, data, inst);
const num_breaks = data.old_extra.items[extra_idx];
const breaks: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + 1 ..][0..num_breaks]);
const num_loop_live = data.old_extra.items[extra_idx + num_breaks + 1];
const loop_live: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + num_breaks + 2 ..][0..num_loop_live]);
// This is necessarily not in the same control flow branch, because loops are noreturn
data.live_set.clearRetainingCapacity();
try data.live_set.ensureUnusedCapacity(gpa, @intCast(loop_live.len));
for (loop_live) |alive| {
data.live_set.putAssumeCapacity(alive, {});
}
log.debug("[{}] %{}: block live set is {}", .{ pass, inst, fmtInstSet(&data.live_set) });
for (breaks) |block_inst| {
// We might break to this block, so include every operand that the block needs alive
const block_scope = data.block_scopes.get(block_inst).?;
var it = block_scope.live_set.keyIterator();
while (it.next()) |key| {
const alive = key.*;
try data.live_set.put(gpa, alive, {});
}
}
// Now, `data.live_set` is the operands which must be alive when the loop repeats. // Now, `data.live_set` is the operands which must be alive when the loop repeats.
// Move them into a block scope for corresponding `repeat` instructions to notice. // Move them into a block scope for corresponding `repeat` instructions to notice.
log.debug("[{}] %{}: loop live set is {}", .{ pass, inst, fmtInstSet(&data.live_set) });
try data.block_scopes.putNoClobber(gpa, inst, .{ try data.block_scopes.putNoClobber(gpa, inst, .{
.live_set = data.live_set.move(), .live_set = data.live_set.move(),
}); });
@ -1720,6 +1774,7 @@ fn analyzeInstSwitchBr(
comptime pass: LivenessPass, comptime pass: LivenessPass,
data: *LivenessPassData(pass), data: *LivenessPassData(pass),
inst: Air.Inst.Index, inst: Air.Inst.Index,
is_dispatch_loop: bool,
) !void { ) !void {
const inst_datas = a.air.instructions.items(.data); const inst_datas = a.air.instructions.items(.data);
const pl_op = inst_datas[@intFromEnum(inst)].pl_op; const pl_op = inst_datas[@intFromEnum(inst)].pl_op;
@ -1730,6 +1785,17 @@ fn analyzeInstSwitchBr(
switch (pass) { switch (pass) {
.loop_analysis => { .loop_analysis => {
var old_breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{};
defer old_breaks.deinit(gpa);
var old_live: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{};
defer old_live.deinit(gpa);
if (is_dispatch_loop) {
old_breaks = data.breaks.move();
old_live = data.live_set.move();
}
var it = switch_br.iterateCases(); var it = switch_br.iterateCases();
while (it.next()) |case| { while (it.next()) |case| {
try analyzeBody(a, pass, data, case.body); try analyzeBody(a, pass, data, case.body);
@ -1738,9 +1804,24 @@ fn analyzeInstSwitchBr(
const else_body = it.elseBody(); const else_body = it.elseBody();
try analyzeBody(a, pass, data, else_body); try analyzeBody(a, pass, data, else_body);
} }
if (is_dispatch_loop) {
try writeLoopInfo(a, data, inst, old_breaks, old_live);
}
}, },
.main_analysis => { .main_analysis => {
if (is_dispatch_loop) {
try resolveLoopLiveSet(a, data, inst);
try data.block_scopes.putNoClobber(gpa, inst, .{
.live_set = data.live_set.move(),
});
}
defer if (is_dispatch_loop) {
log.debug("[{}] %{}: popped loop block scop", .{ pass, inst });
var scope = data.block_scopes.fetchRemove(inst).?.value;
scope.live_set.deinit(gpa);
};
// This is, all in all, just a messier version of the `cond_br` logic. If you're trying // This is, all in all, just a messier version of the `cond_br` logic. If you're trying
// to understand it, I encourage looking at `analyzeInstCondBr` first. // to understand it, I encourage looking at `analyzeInstCondBr` first.

View file

@ -447,6 +447,16 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
try self.verifyMatchingLiveness(repeat.loop_inst, expected_live); try self.verifyMatchingLiveness(repeat.loop_inst, expected_live);
}, },
.switch_dispatch => {
const br = data[@intFromEnum(inst)].br;
try self.verifyOperand(inst, br.operand, self.liveness.operandDies(inst, 0));
const expected_live = self.loops.get(br.block_inst) orelse
return invalid("%{}: loop %{} not in scope", .{ @intFromEnum(inst), @intFromEnum(br.block_inst) });
try self.verifyMatchingLiveness(br.block_inst, expected_live);
},
.block, .dbg_inline_block => |tag| { .block, .dbg_inline_block => |tag| {
const ty_pl = data[@intFromEnum(inst)].ty_pl; const ty_pl = data[@intFromEnum(inst)].ty_pl;
const block_ty = ty_pl.ty.toType(); const block_ty = ty_pl.ty.toType();
@ -494,11 +504,11 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
// The same stuff should be alive after the loop as before it. // The same stuff should be alive after the loop as before it.
const gop = try self.loops.getOrPut(self.gpa, inst); const gop = try self.loops.getOrPut(self.gpa, inst);
if (gop.found_existing) return invalid("%{}: loop already exists", .{@intFromEnum(inst)});
defer { defer {
var live = self.loops.fetchRemove(inst).?; var live = self.loops.fetchRemove(inst).?;
live.value.deinit(self.gpa); live.value.deinit(self.gpa);
} }
if (gop.found_existing) return invalid("%{}: loop already exists", .{@intFromEnum(inst)});
gop.value_ptr.* = try self.live.clone(self.gpa); gop.value_ptr.* = try self.live.clone(self.gpa);
try self.verifyBody(loop_body); try self.verifyBody(loop_body);
@ -528,7 +538,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
try self.verifyInst(inst); try self.verifyInst(inst);
}, },
.switch_br => { .switch_br, .loop_switch_br => {
const switch_br = self.air.unwrapSwitch(inst); const switch_br = self.air.unwrapSwitch(inst);
const switch_br_liveness = try self.liveness.getSwitchBr( const switch_br_liveness = try self.liveness.getSwitchBr(
self.gpa, self.gpa,
@ -539,13 +549,22 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
try self.verifyOperand(inst, switch_br.operand, self.liveness.operandDies(inst, 0)); try self.verifyOperand(inst, switch_br.operand, self.liveness.operandDies(inst, 0));
var live = self.live.move(); // Excluding the operand (which we just handled), the same stuff should be alive
defer live.deinit(self.gpa); // after the loop as before it.
{
const gop = try self.loops.getOrPut(self.gpa, inst);
if (gop.found_existing) return invalid("%{}: loop already exists", .{@intFromEnum(inst)});
gop.value_ptr.* = self.live.move();
}
defer {
var live = self.loops.fetchRemove(inst).?;
live.value.deinit(self.gpa);
}
var it = switch_br.iterateCases(); var it = switch_br.iterateCases();
while (it.next()) |case| { while (it.next()) |case| {
self.live.deinit(self.gpa); self.live.deinit(self.gpa);
self.live = try live.clone(self.gpa); self.live = try self.loops.get(inst).?.clone(self.gpa);
for (switch_br_liveness.deaths[case.idx]) |death| try self.verifyDeath(inst, death); for (switch_br_liveness.deaths[case.idx]) |death| try self.verifyDeath(inst, death);
try self.verifyBody(case.body); try self.verifyBody(case.body);
@ -554,7 +573,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
const else_body = it.elseBody(); const else_body = it.elseBody();
if (else_body.len > 0) { if (else_body.len > 0) {
self.live.deinit(self.gpa); self.live.deinit(self.gpa);
self.live = try live.clone(self.gpa); self.live = try self.loops.get(inst).?.clone(self.gpa);
for (switch_br_liveness.deaths[switch_br.cases_len]) |death| try self.verifyDeath(inst, death); for (switch_br_liveness.deaths[switch_br.cases_len]) |death| try self.verifyDeath(inst, death);
try self.verifyBody(else_body); try self.verifyBody(else_body);
} }

File diff suppressed because it is too large Load diff

View file

@ -292,6 +292,7 @@ pub fn getUnsignedIntInner(
.none => 0, .none => 0,
else => |payload| Value.fromInterned(payload).getUnsignedIntInner(strat, zcu, tid), else => |payload| Value.fromInterned(payload).getUnsignedIntInner(strat, zcu, tid),
}, },
.enum_tag => |enum_tag| return Value.fromInterned(enum_tag.int).getUnsignedIntInner(strat, zcu, tid),
else => null, else => null,
}, },
}; };

View file

@ -735,6 +735,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.block => try self.airBlock(inst), .block => try self.airBlock(inst),
.br => try self.airBr(inst), .br => try self.airBr(inst),
.repeat => return self.fail("TODO implement `repeat`", .{}), .repeat => return self.fail("TODO implement `repeat`", .{}),
.switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
.trap => try self.airTrap(), .trap => try self.airTrap(),
.breakpoint => try self.airBreakpoint(), .breakpoint => try self.airBreakpoint(),
.ret_addr => try self.airRetAddr(inst), .ret_addr => try self.airRetAddr(inst),
@ -825,6 +826,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.field_parent_ptr => try self.airFieldParentPtr(inst), .field_parent_ptr => try self.airFieldParentPtr(inst),
.switch_br => try self.airSwitch(inst), .switch_br => try self.airSwitch(inst),
.loop_switch_br => return self.fail("TODO implement `loop_switch_br`", .{}),
.slice_ptr => try self.airSlicePtr(inst), .slice_ptr => try self.airSlicePtr(inst),
.slice_len => try self.airSliceLen(inst), .slice_len => try self.airSliceLen(inst),

View file

@ -722,6 +722,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.block => try self.airBlock(inst), .block => try self.airBlock(inst),
.br => try self.airBr(inst), .br => try self.airBr(inst),
.repeat => return self.fail("TODO implement `repeat`", .{}), .repeat => return self.fail("TODO implement `repeat`", .{}),
.switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
.trap => try self.airTrap(), .trap => try self.airTrap(),
.breakpoint => try self.airBreakpoint(), .breakpoint => try self.airBreakpoint(),
.ret_addr => try self.airRetAddr(inst), .ret_addr => try self.airRetAddr(inst),
@ -812,6 +813,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.field_parent_ptr => try self.airFieldParentPtr(inst), .field_parent_ptr => try self.airFieldParentPtr(inst),
.switch_br => try self.airSwitch(inst), .switch_br => try self.airSwitch(inst),
.loop_switch_br => return self.fail("TODO implement `loop_switch_br`", .{}),
.slice_ptr => try self.airSlicePtr(inst), .slice_ptr => try self.airSlicePtr(inst),
.slice_len => try self.airSliceLen(inst), .slice_len => try self.airSliceLen(inst),

View file

@ -1580,6 +1580,7 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
.block => try func.airBlock(inst), .block => try func.airBlock(inst),
.br => try func.airBr(inst), .br => try func.airBr(inst),
.repeat => return func.fail("TODO implement `repeat`", .{}), .repeat => return func.fail("TODO implement `repeat`", .{}),
.switch_dispatch => return func.fail("TODO implement `switch_dispatch`", .{}),
.trap => try func.airTrap(), .trap => try func.airTrap(),
.breakpoint => try func.airBreakpoint(), .breakpoint => try func.airBreakpoint(),
.ret_addr => try func.airRetAddr(inst), .ret_addr => try func.airRetAddr(inst),
@ -1669,6 +1670,7 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
.field_parent_ptr => try func.airFieldParentPtr(inst), .field_parent_ptr => try func.airFieldParentPtr(inst),
.switch_br => try func.airSwitchBr(inst), .switch_br => try func.airSwitchBr(inst),
.loop_switch_br => return func.fail("TODO implement `loop_switch_br`", .{}),
.ptr_slice_len_ptr => try func.airPtrSliceLenPtr(inst), .ptr_slice_len_ptr => try func.airPtrSliceLenPtr(inst),
.ptr_slice_ptr_ptr => try func.airPtrSlicePtrPtr(inst), .ptr_slice_ptr_ptr => try func.airPtrSlicePtrPtr(inst),

View file

@ -577,6 +577,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.block => try self.airBlock(inst), .block => try self.airBlock(inst),
.br => try self.airBr(inst), .br => try self.airBr(inst),
.repeat => return self.fail("TODO implement `repeat`", .{}), .repeat => return self.fail("TODO implement `repeat`", .{}),
.switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
.trap => try self.airTrap(), .trap => try self.airTrap(),
.breakpoint => try self.airBreakpoint(), .breakpoint => try self.airBreakpoint(),
.ret_addr => @panic("TODO try self.airRetAddr(inst)"), .ret_addr => @panic("TODO try self.airRetAddr(inst)"),
@ -667,6 +668,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.field_parent_ptr => @panic("TODO try self.airFieldParentPtr(inst)"), .field_parent_ptr => @panic("TODO try self.airFieldParentPtr(inst)"),
.switch_br => try self.airSwitch(inst), .switch_br => try self.airSwitch(inst),
.loop_switch_br => return self.fail("TODO implement `loop_switch_br`", .{}),
.slice_ptr => try self.airSlicePtr(inst), .slice_ptr => try self.airSlicePtr(inst),
.slice_len => try self.airSliceLen(inst), .slice_len => try self.airSliceLen(inst),

View file

@ -1904,6 +1904,7 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
.breakpoint => func.airBreakpoint(inst), .breakpoint => func.airBreakpoint(inst),
.br => func.airBr(inst), .br => func.airBr(inst),
.repeat => return func.fail("TODO implement `repeat`", .{}), .repeat => return func.fail("TODO implement `repeat`", .{}),
.switch_dispatch => return func.fail("TODO implement `switch_dispatch`", .{}),
.int_from_bool => func.airIntFromBool(inst), .int_from_bool => func.airIntFromBool(inst),
.cond_br => func.airCondBr(inst), .cond_br => func.airCondBr(inst),
.intcast => func.airIntcast(inst), .intcast => func.airIntcast(inst),
@ -1985,6 +1986,7 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
.field_parent_ptr => func.airFieldParentPtr(inst), .field_parent_ptr => func.airFieldParentPtr(inst),
.switch_br => func.airSwitchBr(inst), .switch_br => func.airSwitchBr(inst),
.loop_switch_br => return func.fail("TODO implement `loop_switch_br`", .{}),
.trunc => func.airTrunc(inst), .trunc => func.airTrunc(inst),
.unreach => func.airUnreachable(inst), .unreach => func.airUnreachable(inst),

View file

@ -2248,6 +2248,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.block => try self.airBlock(inst), .block => try self.airBlock(inst),
.br => try self.airBr(inst), .br => try self.airBr(inst),
.repeat => return self.fail("TODO implement `repeat`", .{}), .repeat => return self.fail("TODO implement `repeat`", .{}),
.switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
.trap => try self.airTrap(), .trap => try self.airTrap(),
.breakpoint => try self.airBreakpoint(), .breakpoint => try self.airBreakpoint(),
.ret_addr => try self.airRetAddr(inst), .ret_addr => try self.airRetAddr(inst),
@ -2336,6 +2337,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
.field_parent_ptr => try self.airFieldParentPtr(inst), .field_parent_ptr => try self.airFieldParentPtr(inst),
.switch_br => try self.airSwitchBr(inst), .switch_br => try self.airSwitchBr(inst),
.loop_switch_br => return self.fail("TODO implement `loop_switch_br`", .{}),
.slice_ptr => try self.airSlicePtr(inst), .slice_ptr => try self.airSlicePtr(inst),
.slice_len => try self.airSliceLen(inst), .slice_len => try self.airSliceLen(inst),

View file

@ -321,6 +321,9 @@ pub const Function = struct {
/// by type alignment. /// by type alignment.
/// The value is whether the alloc needs to be emitted in the header. /// The value is whether the alloc needs to be emitted in the header.
allocs: std.AutoArrayHashMapUnmanaged(LocalIndex, bool) = .{}, allocs: std.AutoArrayHashMapUnmanaged(LocalIndex, bool) = .{},
/// Maps from `loop_switch_br` instructions to the allocated local used
/// for the switch cond. Dispatches should set this local to the new cond.
loop_switch_conds: std.AutoHashMapUnmanaged(Air.Inst.Index, LocalIndex) = .{},
fn resolveInst(f: *Function, ref: Air.Inst.Ref) !CValue { fn resolveInst(f: *Function, ref: Air.Inst.Ref) !CValue {
const gop = try f.value_map.getOrPut(ref); const gop = try f.value_map.getOrPut(ref);
@ -531,6 +534,7 @@ pub const Function = struct {
f.blocks.deinit(gpa); f.blocks.deinit(gpa);
f.value_map.deinit(); f.value_map.deinit();
f.lazy_fns.deinit(gpa); f.lazy_fns.deinit(gpa);
f.loop_switch_conds.deinit(gpa);
} }
fn typeOf(f: *Function, inst: Air.Inst.Ref) Type { fn typeOf(f: *Function, inst: Air.Inst.Ref) Type {
@ -3376,16 +3380,18 @@ fn genBodyInner(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail,
=> unreachable, => unreachable,
// Instructions that are known to always be `noreturn` based on their tag. // Instructions that are known to always be `noreturn` based on their tag.
.br => return airBr(f, inst), .br => return airBr(f, inst),
.repeat => return airRepeat(f, inst), .repeat => return airRepeat(f, inst),
.cond_br => return airCondBr(f, inst), .switch_dispatch => return airSwitchDispatch(f, inst),
.switch_br => return airSwitchBr(f, inst), .cond_br => return airCondBr(f, inst),
.loop => return airLoop(f, inst), .switch_br => return airSwitchBr(f, inst, false),
.ret => return airRet(f, inst, false), .loop_switch_br => return airSwitchBr(f, inst, true),
.ret_safe => return airRet(f, inst, false), // TODO .loop => return airLoop(f, inst),
.ret_load => return airRet(f, inst, true), .ret => return airRet(f, inst, false),
.trap => return airTrap(f, f.object.writer()), .ret_safe => return airRet(f, inst, false), // TODO
.unreach => return airUnreach(f), .ret_load => return airRet(f, inst, true),
.trap => return airTrap(f, f.object.writer()),
.unreach => return airUnreach(f),
// Instructions which may be `noreturn`. // Instructions which may be `noreturn`.
.block => res: { .block => res: {
@ -4786,6 +4792,46 @@ fn airRepeat(f: *Function, inst: Air.Inst.Index) !void {
try writer.print("goto zig_loop_{d};\n", .{@intFromEnum(repeat.loop_inst)}); try writer.print("goto zig_loop_{d};\n", .{@intFromEnum(repeat.loop_inst)});
} }
fn airSwitchDispatch(f: *Function, inst: Air.Inst.Index) !void {
const pt = f.object.dg.pt;
const zcu = pt.zcu;
const br = f.air.instructions.items(.data)[@intFromEnum(inst)].br;
const writer = f.object.writer();
if (try f.air.value(br.operand, pt)) |cond_val| {
// Comptime-known dispatch. Iterate the cases to find the correct
// one, and branch directly to the corresponding case.
const switch_br = f.air.unwrapSwitch(br.block_inst);
var it = switch_br.iterateCases();
const target_case_idx: u32 = target: while (it.next()) |case| {
for (case.items) |item| {
const val = Value.fromInterned(item.toInterned().?);
if (cond_val.compareHetero(.eq, val, zcu)) break :target case.idx;
}
for (case.ranges) |range| {
const low = Value.fromInterned(range[0].toInterned().?);
const high = Value.fromInterned(range[1].toInterned().?);
if (cond_val.compareHetero(.gte, low, zcu) and
cond_val.compareHetero(.lte, high, zcu))
{
break :target case.idx;
}
}
} else switch_br.cases_len;
try writer.print("goto zig_switch_{d}_dispatch_{d};\n", .{ @intFromEnum(br.block_inst), target_case_idx });
return;
}
// Runtime-known dispatch. Set the switch condition, and branch back.
const cond = try f.resolveInst(br.operand);
const cond_local = f.loop_switch_conds.get(br.block_inst).?;
try f.writeCValue(writer, .{ .local = cond_local }, .Other);
try writer.writeAll(" = ");
try f.writeCValue(writer, cond, .Initializer);
try writer.writeAll(";\n");
try writer.print("goto zig_switch_{d}_loop;", .{@intFromEnum(br.block_inst)});
}
fn airBitcast(f: *Function, inst: Air.Inst.Index) !CValue { fn airBitcast(f: *Function, inst: Air.Inst.Index) !CValue {
const ty_op = f.air.instructions.items(.data)[@intFromEnum(inst)].ty_op; const ty_op = f.air.instructions.items(.data)[@intFromEnum(inst)].ty_op;
const inst_ty = f.typeOfIndex(inst); const inst_ty = f.typeOfIndex(inst);
@ -5004,15 +5050,34 @@ fn airCondBr(f: *Function, inst: Air.Inst.Index) !void {
try genBodyInner(f, else_body); try genBodyInner(f, else_body);
} }
fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void { fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void {
const pt = f.object.dg.pt; const pt = f.object.dg.pt;
const zcu = pt.zcu; const zcu = pt.zcu;
const gpa = f.object.dg.gpa;
const switch_br = f.air.unwrapSwitch(inst); const switch_br = f.air.unwrapSwitch(inst);
const condition = try f.resolveInst(switch_br.operand); const init_condition = try f.resolveInst(switch_br.operand);
try reap(f, inst, &.{switch_br.operand}); try reap(f, inst, &.{switch_br.operand});
const condition_ty = f.typeOf(switch_br.operand); const condition_ty = f.typeOf(switch_br.operand);
const writer = f.object.writer(); const writer = f.object.writer();
// For dispatches, we will create a local alloc to contain the condition value.
// This may not result in optimal codegen for switch loops, but it minimizes the
// amount of C code we generate, which is probably more desirable here (and is simpler).
const condition = if (is_dispatch_loop) cond: {
const new_local = try f.allocLocal(inst, condition_ty);
try f.writeCValue(writer, new_local, .Other);
try writer.writeAll(" = ");
try f.writeCValue(writer, init_condition, .Initializer);
try writer.writeAll(";\n");
try writer.print("zig_switch_{d}_loop:", .{@intFromEnum(inst)});
try f.loop_switch_conds.put(gpa, inst, new_local.new_local);
break :cond new_local;
} else init_condition;
defer if (is_dispatch_loop) {
assert(f.loop_switch_conds.remove(inst));
};
try writer.writeAll("switch ("); try writer.writeAll("switch (");
const lowered_condition_ty = if (condition_ty.toIntern() == .bool_type) const lowered_condition_ty = if (condition_ty.toIntern() == .bool_type)
@ -5030,7 +5095,6 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
try writer.writeAll(") {"); try writer.writeAll(") {");
f.object.indent_writer.pushIndent(); f.object.indent_writer.pushIndent();
const gpa = f.object.dg.gpa;
const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1); const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
defer gpa.free(liveness.deaths); defer gpa.free(liveness.deaths);
@ -5045,9 +5109,15 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
try f.object.indent_writer.insertNewline(); try f.object.indent_writer.insertNewline();
try writer.writeAll("case "); try writer.writeAll("case ");
const item_value = try f.air.value(item, pt); const item_value = try f.air.value(item, pt);
if (item_value.?.getUnsignedInt(zcu)) |item_int| try writer.print("{}\n", .{ // If `item_value` is a pointer with a known integer address, print the address
try f.fmtIntLiteral(try pt.intValue(lowered_condition_ty, item_int)), // with no cast to avoid a warning.
}) else { write_val: {
if (condition_ty.isPtrAtRuntime(zcu)) {
if (item_value.?.getUnsignedInt(zcu)) |item_int| {
try writer.print("{}", .{try f.fmtIntLiteral(try pt.intValue(lowered_condition_ty, item_int))});
break :write_val;
}
}
if (condition_ty.isPtrAtRuntime(zcu)) { if (condition_ty.isPtrAtRuntime(zcu)) {
try writer.writeByte('('); try writer.writeByte('(');
try f.renderType(writer, Type.usize); try f.renderType(writer, Type.usize);
@ -5057,9 +5127,14 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
} }
try writer.writeByte(':'); try writer.writeByte(':');
} }
try writer.writeByte(' '); try writer.writeAll(" {\n");
f.object.indent_writer.pushIndent();
try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, false); if (is_dispatch_loop) {
try writer.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
}
try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
f.object.indent_writer.popIndent();
try writer.writeByte('}');
// The case body must be noreturn so we don't need to insert a break. // The case body must be noreturn so we don't need to insert a break.
} }
@ -5095,11 +5170,19 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
try f.object.dg.renderValue(writer, (try f.air.value(range[1], pt)).?, .Other); try f.object.dg.renderValue(writer, (try f.air.value(range[1], pt)).?, .Other);
try writer.writeByte(')'); try writer.writeByte(')');
} }
try writer.writeAll(") "); try writer.writeAll(") {\n");
try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, false); f.object.indent_writer.pushIndent();
if (is_dispatch_loop) {
try writer.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
}
try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
f.object.indent_writer.popIndent();
try writer.writeByte('}');
} }
} }
if (is_dispatch_loop) {
try writer.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
}
if (else_body.len > 0) { if (else_body.len > 0) {
// Note that this must be the last case, so we do not need to use `genBodyResolveState` since // Note that this must be the last case, so we do not need to use `genBodyResolveState` since
// the parent block will do it (because the case body is noreturn). // the parent block will do it (because the case body is noreturn).

View file

@ -1721,6 +1721,7 @@ pub const Object = struct {
.func_inst_table = .{}, .func_inst_table = .{},
.blocks = .{}, .blocks = .{},
.loops = .{}, .loops = .{},
.switch_dispatch_info = .{},
.sync_scope = if (owner_mod.single_threaded) .singlethread else .system, .sync_scope = if (owner_mod.single_threaded) .singlethread else .system,
.file = file, .file = file,
.scope = subprogram, .scope = subprogram,
@ -4845,6 +4846,10 @@ pub const FuncGen = struct {
/// Maps `loop` instructions to the bb to branch to to repeat the loop. /// Maps `loop` instructions to the bb to branch to to repeat the loop.
loops: std.AutoHashMapUnmanaged(Air.Inst.Index, Builder.Function.Block.Index), loops: std.AutoHashMapUnmanaged(Air.Inst.Index, Builder.Function.Block.Index),
/// Maps `loop_switch_br` instructions to the information required to lower
/// dispatches (`switch_dispatch` instructions).
switch_dispatch_info: std.AutoHashMapUnmanaged(Air.Inst.Index, SwitchDispatchInfo),
sync_scope: Builder.SyncScope, sync_scope: Builder.SyncScope,
const Fuzz = struct { const Fuzz = struct {
@ -4857,6 +4862,33 @@ pub const FuncGen = struct {
} }
}; };
const SwitchDispatchInfo = struct {
/// These are the blocks corresponding to each switch case.
/// The final element corresponds to the `else` case.
/// Slices allocated into `gpa`.
case_blocks: []Builder.Function.Block.Index,
/// This is `.none` if `jmp_table` is set, since we won't use a `switch` instruction to dispatch.
switch_weights: Builder.Function.Instruction.BrCond.Weights,
/// If not `null`, we have manually constructed a jump table to reach the desired block.
/// `table` can be used if the value is between `min` and `max` inclusive.
/// We perform this lowering manually to avoid some questionable behavior from LLVM.
/// See `airSwitchBr` for details.
jmp_table: ?JmpTable,
const JmpTable = struct {
min: Builder.Constant,
max: Builder.Constant,
in_bounds_hint: enum { none, unpredictable, likely, unlikely },
/// Pointer to the jump table itself, to be used with `indirectbr`.
/// The index into the jump table is the dispatch condition minus `min`.
/// The table values are `blockaddress` constants corresponding to blocks in `case_blocks`.
table: Builder.Constant,
/// `true` if `table` conatins a reference to the `else` block.
/// In this case, the `indirectbr` must include the `else` block in its target list.
table_includes_else: bool,
};
};
const BreakList = union { const BreakList = union {
list: std.MultiArrayList(struct { list: std.MultiArrayList(struct {
bb: Builder.Function.Block.Index, bb: Builder.Function.Block.Index,
@ -4872,6 +4904,11 @@ pub const FuncGen = struct {
self.func_inst_table.deinit(gpa); self.func_inst_table.deinit(gpa);
self.blocks.deinit(gpa); self.blocks.deinit(gpa);
self.loops.deinit(gpa); self.loops.deinit(gpa);
var it = self.switch_dispatch_info.valueIterator();
while (it.next()) |info| {
self.gpa.free(info.case_blocks);
}
self.switch_dispatch_info.deinit(gpa);
} }
fn todo(self: *FuncGen, comptime format: []const u8, args: anytype) Error { fn todo(self: *FuncGen, comptime format: []const u8, args: anytype) Error {
@ -5182,16 +5219,18 @@ pub const FuncGen = struct {
.work_group_id => try self.airWorkGroupId(inst), .work_group_id => try self.airWorkGroupId(inst),
// Instructions that are known to always be `noreturn` based on their tag. // Instructions that are known to always be `noreturn` based on their tag.
.br => return self.airBr(inst), .br => return self.airBr(inst),
.repeat => return self.airRepeat(inst), .repeat => return self.airRepeat(inst),
.cond_br => return self.airCondBr(inst), .switch_dispatch => return self.airSwitchDispatch(inst),
.switch_br => return self.airSwitchBr(inst), .cond_br => return self.airCondBr(inst),
.loop => return self.airLoop(inst), .switch_br => return self.airSwitchBr(inst, false),
.ret => return self.airRet(inst, false), .loop_switch_br => return self.airSwitchBr(inst, true),
.ret_safe => return self.airRet(inst, true), .loop => return self.airLoop(inst),
.ret_load => return self.airRetLoad(inst), .ret => return self.airRet(inst, false),
.trap => return self.airTrap(inst), .ret_safe => return self.airRet(inst, true),
.unreach => return self.airUnreach(inst), .ret_load => return self.airRetLoad(inst),
.trap => return self.airTrap(inst),
.unreach => return self.airUnreach(inst),
// Instructions which may be `noreturn`. // Instructions which may be `noreturn`.
.block => res: { .block => res: {
@ -6093,6 +6132,202 @@ pub const FuncGen = struct {
_ = try self.wip.br(loop_bb); _ = try self.wip.br(loop_bb);
} }
fn lowerSwitchDispatch(
self: *FuncGen,
switch_inst: Air.Inst.Index,
cond_ref: Air.Inst.Ref,
dispatch_info: SwitchDispatchInfo,
) !void {
const o = self.ng.object;
const pt = o.pt;
const zcu = pt.zcu;
const cond_ty = self.typeOf(cond_ref);
const switch_br = self.air.unwrapSwitch(switch_inst);
if (try self.air.value(cond_ref, pt)) |cond_val| {
// Comptime-known dispatch. Iterate the cases to find the correct
// one, and branch to the corresponding element of `case_blocks`.
var it = switch_br.iterateCases();
const target_case_idx = target: while (it.next()) |case| {
for (case.items) |item| {
const val = Value.fromInterned(item.toInterned().?);
if (cond_val.compareHetero(.eq, val, zcu)) break :target case.idx;
}
for (case.ranges) |range| {
const low = Value.fromInterned(range[0].toInterned().?);
const high = Value.fromInterned(range[1].toInterned().?);
if (cond_val.compareHetero(.gte, low, zcu) and
cond_val.compareHetero(.lte, high, zcu))
{
break :target case.idx;
}
}
} else dispatch_info.case_blocks.len - 1;
const target_block = dispatch_info.case_blocks[target_case_idx];
target_block.ptr(&self.wip).incoming += 1;
_ = try self.wip.br(target_block);
return;
}
// Runtime-known dispatch.
const cond = try self.resolveInst(cond_ref);
if (dispatch_info.jmp_table) |jmp_table| {
// We should use the constructed jump table.
// First, check the bounds to branch to the `else` case if needed.
const inbounds = try self.wip.bin(
.@"and",
try self.cmp(.normal, .gte, cond_ty, cond, jmp_table.min.toValue()),
try self.cmp(.normal, .lte, cond_ty, cond, jmp_table.max.toValue()),
"",
);
const jmp_table_block = try self.wip.block(1, "Then");
const else_block = dispatch_info.case_blocks[dispatch_info.case_blocks.len - 1];
else_block.ptr(&self.wip).incoming += 1;
_ = try self.wip.brCond(inbounds, jmp_table_block, else_block, switch (jmp_table.in_bounds_hint) {
.none => .none,
.unpredictable => .unpredictable,
.likely => .then_likely,
.unlikely => .else_likely,
});
self.wip.cursor = .{ .block = jmp_table_block };
// Figure out the list of blocks we might branch to.
// This includes all case blocks, but it might not include the `else` block if
// the table is dense.
const target_blocks_len = dispatch_info.case_blocks.len - @intFromBool(!jmp_table.table_includes_else);
const target_blocks = dispatch_info.case_blocks[0..target_blocks_len];
// Make sure to cast the index to a usize so it's not treated as negative!
const table_index = try self.wip.cast(
.zext,
try self.wip.bin(.@"sub nuw", cond, jmp_table.min.toValue(), ""),
try o.lowerType(Type.usize),
"",
);
const target_ptr_ptr = try self.wip.gep(
.inbounds,
.ptr,
jmp_table.table.toValue(),
&.{table_index},
"",
);
const target_ptr = try self.wip.load(.normal, .ptr, target_ptr_ptr, .default, "");
// Do the branch!
_ = try self.wip.indirectbr(target_ptr, target_blocks);
// Mark all target blocks as having one more incoming branch.
for (target_blocks) |case_block| {
case_block.ptr(&self.wip).incoming += 1;
}
return;
}
// We must lower to an actual LLVM `switch` instruction.
// The switch prongs will correspond to our scalar cases. Ranges will
// be handled by conditional branches in the `else` prong.
const llvm_usize = try o.lowerType(Type.usize);
const cond_int = if (cond.typeOfWip(&self.wip).isPointer(&o.builder))
try self.wip.cast(.ptrtoint, cond, llvm_usize, "")
else
cond;
const llvm_cases_len, const last_range_case = info: {
var llvm_cases_len: u32 = 0;
var last_range_case: ?u32 = null;
var it = switch_br.iterateCases();
while (it.next()) |case| {
if (case.ranges.len > 0) last_range_case = case.idx;
llvm_cases_len += @intCast(case.items.len);
}
break :info .{ llvm_cases_len, last_range_case };
};
// The `else` of the LLVM `switch` is the actual `else` prong only
// if there are no ranges. Otherwise, the `else` will have a
// conditional chain before the "true" `else` prong.
const llvm_else_block = if (last_range_case == null)
dispatch_info.case_blocks[dispatch_info.case_blocks.len - 1]
else
try self.wip.block(0, "RangeTest");
llvm_else_block.ptr(&self.wip).incoming += 1;
var wip_switch = try self.wip.@"switch"(cond_int, llvm_else_block, llvm_cases_len, dispatch_info.switch_weights);
defer wip_switch.finish(&self.wip);
// Construct the actual cases. Set the cursor to the `else` block so
// we can construct ranges at the same time as scalar cases.
self.wip.cursor = .{ .block = llvm_else_block };
var it = switch_br.iterateCases();
while (it.next()) |case| {
const case_block = dispatch_info.case_blocks[case.idx];
for (case.items) |item| {
const llvm_item = (try self.resolveInst(item)).toConst().?;
const llvm_int_item = if (llvm_item.typeOf(&o.builder).isPointer(&o.builder))
try o.builder.castConst(.ptrtoint, llvm_item, llvm_usize)
else
llvm_item;
try wip_switch.addCase(llvm_int_item, case_block, &self.wip);
}
case_block.ptr(&self.wip).incoming += @intCast(case.items.len);
if (case.ranges.len == 0) continue;
// Add a conditional for the ranges, directing to the relevant bb.
// We don't need to consider `cold` branch hints since that information is stored
// in the target bb body, but we do care about likely/unlikely/unpredictable.
const hint = switch_br.getHint(case.idx);
var range_cond: ?Builder.Value = null;
for (case.ranges) |range| {
const llvm_min = try self.resolveInst(range[0]);
const llvm_max = try self.resolveInst(range[1]);
const cond_part = try self.wip.bin(
.@"and",
try self.cmp(.normal, .gte, cond_ty, cond, llvm_min),
try self.cmp(.normal, .lte, cond_ty, cond, llvm_max),
"",
);
if (range_cond) |prev| {
range_cond = try self.wip.bin(.@"or", prev, cond_part, "");
} else range_cond = cond_part;
}
// If the check fails, we either branch to the "true" `else` case,
// or to the next range condition.
const range_else_block = if (case.idx == last_range_case.?)
dispatch_info.case_blocks[dispatch_info.case_blocks.len - 1]
else
try self.wip.block(0, "RangeTest");
_ = try self.wip.brCond(range_cond.?, case_block, range_else_block, switch (hint) {
.none, .cold => .none,
.unpredictable => .unpredictable,
.likely => .then_likely,
.unlikely => .else_likely,
});
case_block.ptr(&self.wip).incoming += 1;
range_else_block.ptr(&self.wip).incoming += 1;
// Construct the next range conditional (if any) in the false branch.
self.wip.cursor = .{ .block = range_else_block };
}
}
fn airSwitchDispatch(self: *FuncGen, inst: Air.Inst.Index) !void {
const br = self.air.instructions.items(.data)[@intFromEnum(inst)].br;
const dispatch_info = self.switch_dispatch_info.get(br.block_inst).?;
return self.lowerSwitchDispatch(br.block_inst, br.operand, dispatch_info);
}
fn airCondBr(self: *FuncGen, inst: Air.Inst.Index) !void { fn airCondBr(self: *FuncGen, inst: Air.Inst.Index) !void {
const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op; const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op;
const cond = try self.resolveInst(pl_op.operand); const cond = try self.resolveInst(pl_op.operand);
@ -6257,36 +6492,123 @@ pub const FuncGen = struct {
return fg.wip.extractValue(err_union, &.{offset}, ""); return fg.wip.extractValue(err_union, &.{offset}, "");
} }
fn airSwitchBr(self: *FuncGen, inst: Air.Inst.Index) !void { fn airSwitchBr(self: *FuncGen, inst: Air.Inst.Index, is_dispatch_loop: bool) !void {
const o = self.ng.object; const o = self.ng.object;
const zcu = o.pt.zcu;
const switch_br = self.air.unwrapSwitch(inst); const switch_br = self.air.unwrapSwitch(inst);
const cond = try self.resolveInst(switch_br.operand); // For `loop_switch_br`, we need these BBs prepared ahead of time to generate dispatches.
// For `switch_br`, they allow us to sometimes generate better IR by sharing a BB between
// This is not necessarily the actual `else` prong; it first contains conditionals // scalar and range cases in the same prong.
// for any range cases. It's just the `else` of the LLVM switch. // +1 for `else` case. This is not the same as the LLVM `else` prong, as that may first contain
const llvm_else_block = try self.wip.block(1, "Default"); // conditionals to handle ranges.
const case_blocks = try self.gpa.alloc(Builder.Function.Block.Index, switch_br.cases_len + 1);
const case_blocks = try self.gpa.alloc(Builder.Function.Block.Index, switch_br.cases_len);
defer self.gpa.free(case_blocks); defer self.gpa.free(case_blocks);
// We set incoming as 0 for now, and increment it as we construct the switch. // We set incoming as 0 for now, and increment it as we construct dispatches.
for (case_blocks) |*b| b.* = try self.wip.block(0, "Case"); for (case_blocks[0 .. case_blocks.len - 1]) |*b| b.* = try self.wip.block(0, "Case");
case_blocks[case_blocks.len - 1] = try self.wip.block(0, "Default");
const llvm_usize = try o.lowerType(Type.usize); // There's a special case here to manually generate a jump table in some cases.
const cond_int = if (cond.typeOfWip(&self.wip).isPointer(&o.builder)) //
try self.wip.cast(.ptrtoint, cond, llvm_usize, "") // Labeled switch in Zig is intended to follow the "direct threading" pattern. We would ideally use a jump
else // table, and each `continue` has its own indirect `jmp`, to allow the branch predictor to more accurately
cond; // use data patterns to predict future dispatches. The problem, however, is that LLVM emits fascinatingly
// bad asm for this. Not only does it not share the jump table -- which we really need it to do to prevent
// destroying the cache -- but it also actually generates slightly different jump tables for each case,
// and *a separate conditional branch beforehand* to handle dispatching back to the case we're currently
// within(!!).
//
// This asm is really, really, not what we want. As such, we will construct the jump table manually where
// appropriate (the values are dense and relatively few), and use it when lowering dispatches.
const llvm_cases_len = llvm_cases_len: { const jmp_table: ?SwitchDispatchInfo.JmpTable = jmp_table: {
var len: u32 = 0; if (!is_dispatch_loop) break :jmp_table null;
// On a 64-bit target, 1024 pointers in our jump table is about 8K of pointers. This seems just
// about acceptable - it won't fill L1d cache on most CPUs.
const max_table_len = 1024;
const cond_ty = self.typeOf(switch_br.operand);
switch (cond_ty.zigTypeTag(zcu)) {
.bool, .pointer => break :jmp_table null,
.@"enum", .int, .error_set => {},
else => unreachable,
}
if (cond_ty.intInfo(zcu).signedness == .signed) break :jmp_table null;
// Don't worry about the size of the type -- it's irrelevant, because the prong values could be fairly dense.
// If they are, then we will construct a jump table.
const min, const max = self.switchCaseItemRange(switch_br);
const min_int = min.getUnsignedInt(zcu) orelse break :jmp_table null;
const max_int = max.getUnsignedInt(zcu) orelse break :jmp_table null;
const table_len = max_int - min_int + 1;
if (table_len > max_table_len) break :jmp_table null;
const table_elems = try self.gpa.alloc(Builder.Constant, @intCast(table_len));
defer self.gpa.free(table_elems);
// Set them all to the `else` branch, then iterate over the AIR switch
// and replace all values which correspond to other prongs.
@memset(table_elems, try o.builder.blockAddrConst(
self.wip.function,
case_blocks[case_blocks.len - 1],
));
var item_count: u32 = 0;
var it = switch_br.iterateCases(); var it = switch_br.iterateCases();
while (it.next()) |case| len += @intCast(case.items.len); while (it.next()) |case| {
break :llvm_cases_len len; const case_block = case_blocks[case.idx];
const case_block_addr = try o.builder.blockAddrConst(
self.wip.function,
case_block,
);
for (case.items) |item| {
const val = Value.fromInterned(item.toInterned().?);
const table_idx = val.toUnsignedInt(zcu) - min_int;
table_elems[@intCast(table_idx)] = case_block_addr;
item_count += 1;
}
for (case.ranges) |range| {
const low = Value.fromInterned(range[0].toInterned().?);
const high = Value.fromInterned(range[1].toInterned().?);
const low_idx = low.toUnsignedInt(zcu) - min_int;
const high_idx = high.toUnsignedInt(zcu) - min_int;
@memset(table_elems[@intCast(low_idx)..@intCast(high_idx + 1)], case_block_addr);
item_count += @intCast(high_idx + 1 - low_idx);
}
}
const table_llvm_ty = try o.builder.arrayType(table_elems.len, .ptr);
const table_val = try o.builder.arrayConst(table_llvm_ty, table_elems);
const table_variable = try o.builder.addVariable(
try o.builder.strtabStringFmt("__jmptab_{d}", .{@intFromEnum(inst)}),
table_llvm_ty,
.default,
);
try table_variable.setInitializer(table_val, &o.builder);
table_variable.setLinkage(.internal, &o.builder);
table_variable.setUnnamedAddr(.unnamed_addr, &o.builder);
const table_includes_else = item_count != table_len;
break :jmp_table .{
.min = try o.lowerValue(min.toIntern()),
.max = try o.lowerValue(max.toIntern()),
.in_bounds_hint = if (table_includes_else) .none else switch (switch_br.getElseHint()) {
.none, .cold => .none,
.unpredictable => .unpredictable,
.likely => .likely,
.unlikely => .unlikely,
},
.table = table_variable.toConst(&o.builder),
.table_includes_else = table_includes_else,
};
}; };
const weights: Builder.Function.Instruction.BrCond.Weights = weights: { const weights: Builder.Function.Instruction.BrCond.Weights = weights: {
if (jmp_table != null) break :weights .none; // not used
// First pass. If any weights are `.unpredictable`, unpredictable. // First pass. If any weights are `.unpredictable`, unpredictable.
// If all are `.none` or `.cold`, none. // If all are `.none` or `.cold`, none.
var any_likely = false; var any_likely = false;
@ -6304,6 +6626,13 @@ pub const FuncGen = struct {
} }
if (!any_likely) break :weights .none; if (!any_likely) break :weights .none;
const llvm_cases_len = llvm_cases_len: {
var len: u32 = 0;
var it = switch_br.iterateCases();
while (it.next()) |case| len += @intCast(case.items.len);
break :llvm_cases_len len;
};
var weights = try self.gpa.alloc(Builder.Metadata, llvm_cases_len + 1); var weights = try self.gpa.alloc(Builder.Metadata, llvm_cases_len + 1);
defer self.gpa.free(weights); defer self.gpa.free(weights);
@ -6336,75 +6665,66 @@ pub const FuncGen = struct {
break :weights @enumFromInt(@intFromEnum(tuple)); break :weights @enumFromInt(@intFromEnum(tuple));
}; };
var wip_switch = try self.wip.@"switch"(cond_int, llvm_else_block, llvm_cases_len, weights); const dispatch_info: SwitchDispatchInfo = .{
defer wip_switch.finish(&self.wip); .case_blocks = case_blocks,
.switch_weights = weights,
.jmp_table = jmp_table,
};
if (is_dispatch_loop) {
try self.switch_dispatch_info.putNoClobber(self.gpa, inst, dispatch_info);
}
defer if (is_dispatch_loop) {
assert(self.switch_dispatch_info.remove(inst));
};
// Generate the initial dispatch.
// If this is a simple `switch_br`, this is the only dispatch.
try self.lowerSwitchDispatch(inst, switch_br.operand, dispatch_info);
// Iterate the cases and generate their bodies.
var it = switch_br.iterateCases(); var it = switch_br.iterateCases();
var any_ranges = false;
while (it.next()) |case| { while (it.next()) |case| {
if (case.ranges.len > 0) any_ranges = true;
const case_block = case_blocks[case.idx]; const case_block = case_blocks[case.idx];
case_block.ptr(&self.wip).incoming += @intCast(case.items.len);
// Handle scalar items, and generate the block.
// We'll generate conditionals for the ranges later on.
for (case.items) |item| {
const llvm_item = (try self.resolveInst(item)).toConst().?;
const llvm_int_item = if (llvm_item.typeOf(&o.builder).isPointer(&o.builder))
try o.builder.castConst(.ptrtoint, llvm_item, llvm_usize)
else
llvm_item;
try wip_switch.addCase(llvm_int_item, case_block, &self.wip);
}
self.wip.cursor = .{ .block = case_block }; self.wip.cursor = .{ .block = case_block };
if (switch_br.getHint(case.idx) == .cold) _ = try self.wip.callIntrinsicAssumeCold(); if (switch_br.getHint(case.idx) == .cold) _ = try self.wip.callIntrinsicAssumeCold();
try self.genBodyDebugScope(null, case.body, .poi); try self.genBodyDebugScope(null, case.body, .none);
} }
self.wip.cursor = .{ .block = case_blocks[case_blocks.len - 1] };
const else_body = it.elseBody(); const else_body = it.elseBody();
self.wip.cursor = .{ .block = llvm_else_block };
if (any_ranges) {
const cond_ty = self.typeOf(switch_br.operand);
// Add conditionals for the ranges, directing to the relevant bb.
// We don't need to consider `cold` branch hints since that information is stored
// in the target bb body, but we do care about likely/unlikely/unpredictable.
it = switch_br.iterateCases();
while (it.next()) |case| {
if (case.ranges.len == 0) continue;
const case_block = case_blocks[case.idx];
const hint = switch_br.getHint(case.idx);
case_block.ptr(&self.wip).incoming += 1;
const next_else_block = try self.wip.block(1, "Default");
var range_cond: ?Builder.Value = null;
for (case.ranges) |range| {
const llvm_min = try self.resolveInst(range[0]);
const llvm_max = try self.resolveInst(range[1]);
const cond_part = try self.wip.bin(
.@"and",
try self.cmp(.normal, .gte, cond_ty, cond, llvm_min),
try self.cmp(.normal, .lte, cond_ty, cond, llvm_max),
"",
);
if (range_cond) |prev| {
range_cond = try self.wip.bin(.@"or", prev, cond_part, "");
} else range_cond = cond_part;
}
_ = try self.wip.brCond(range_cond.?, case_block, next_else_block, switch (hint) {
.none, .cold => .none,
.unpredictable => .unpredictable,
.likely => .then_likely,
.unlikely => .else_likely,
});
self.wip.cursor = .{ .block = next_else_block };
}
}
if (switch_br.getElseHint() == .cold) _ = try self.wip.callIntrinsicAssumeCold(); if (switch_br.getElseHint() == .cold) _ = try self.wip.callIntrinsicAssumeCold();
if (else_body.len != 0) { if (else_body.len > 0) {
try self.genBodyDebugScope(null, else_body, .poi); try self.genBodyDebugScope(null, it.elseBody(), .none);
} else { } else {
_ = try self.wip.@"unreachable"(); _ = try self.wip.@"unreachable"();
} }
}
// No need to reset the insert cursor since this instruction is noreturn. fn switchCaseItemRange(self: *FuncGen, switch_br: Air.UnwrappedSwitch) [2]Value {
const zcu = self.ng.object.pt.zcu;
var it = switch_br.iterateCases();
var min: ?Value = null;
var max: ?Value = null;
while (it.next()) |case| {
for (case.items) |item| {
const val = Value.fromInterned(item.toInterned().?);
const low = if (min) |m| val.compareHetero(.lt, m, zcu) else true;
const high = if (max) |m| val.compareHetero(.gt, m, zcu) else true;
if (low) min = val;
if (high) max = val;
}
for (case.ranges) |range| {
const vals: [2]Value = .{
Value.fromInterned(range[0].toInterned().?),
Value.fromInterned(range[1].toInterned().?),
};
const low = if (min) |m| vals[0].compareHetero(.lt, m, zcu) else true;
const high = if (max) |m| vals[1].compareHetero(.gt, m, zcu) else true;
if (low) min = vals[0];
if (high) max = vals[1];
}
}
return .{ min.?, max.? };
} }
fn airLoop(self: *FuncGen, inst: Air.Inst.Index) !void { fn airLoop(self: *FuncGen, inst: Air.Inst.Index) !void {

View file

@ -296,11 +296,12 @@ const Writer = struct {
.aggregate_init => try w.writeAggregateInit(s, inst), .aggregate_init => try w.writeAggregateInit(s, inst),
.union_init => try w.writeUnionInit(s, inst), .union_init => try w.writeUnionInit(s, inst),
.br => try w.writeBr(s, inst), .br => try w.writeBr(s, inst),
.switch_dispatch => try w.writeBr(s, inst),
.repeat => try w.writeRepeat(s, inst), .repeat => try w.writeRepeat(s, inst),
.cond_br => try w.writeCondBr(s, inst), .cond_br => try w.writeCondBr(s, inst),
.@"try", .try_cold => try w.writeTry(s, inst), .@"try", .try_cold => try w.writeTry(s, inst),
.try_ptr, .try_ptr_cold => try w.writeTryPtr(s, inst), .try_ptr, .try_ptr_cold => try w.writeTryPtr(s, inst),
.switch_br => try w.writeSwitchBr(s, inst), .loop_switch_br, .switch_br => try w.writeSwitchBr(s, inst),
.cmpxchg_weak, .cmpxchg_strong => try w.writeCmpxchg(s, inst), .cmpxchg_weak, .cmpxchg_strong => try w.writeCmpxchg(s, inst),
.fence => try w.writeFence(s, inst), .fence => try w.writeFence(s, inst),
.atomic_load => try w.writeAtomicLoad(s, inst), .atomic_load => try w.writeAtomicLoad(s, inst),

View file

@ -302,6 +302,7 @@ const Writer = struct {
.@"break", .@"break",
.break_inline, .break_inline,
.switch_continue,
=> try self.writeBreak(stream, inst), => try self.writeBreak(stream, inst),
.slice_start => try self.writeSliceStart(stream, inst), .slice_start => try self.writeSliceStart(stream, inst),

View file

@ -88,6 +88,7 @@ test {
_ = @import("behavior/struct_contains_null_ptr_itself.zig"); _ = @import("behavior/struct_contains_null_ptr_itself.zig");
_ = @import("behavior/struct_contains_slice_of_itself.zig"); _ = @import("behavior/struct_contains_slice_of_itself.zig");
_ = @import("behavior/switch.zig"); _ = @import("behavior/switch.zig");
_ = @import("behavior/switch_loop.zig");
_ = @import("behavior/switch_prong_err_enum.zig"); _ = @import("behavior/switch_prong_err_enum.zig");
_ = @import("behavior/switch_prong_implicit_cast.zig"); _ = @import("behavior/switch_prong_implicit_cast.zig");
_ = @import("behavior/switch_on_captured_error.zig"); _ = @import("behavior/switch_on_captured_error.zig");

View file

@ -0,0 +1,205 @@
const builtin = @import("builtin");
const std = @import("std");
const expect = std.testing.expect;
test "simple switch loop" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
const S = struct {
fn doTheTest() !void {
var start: u32 = undefined;
start = 32;
const result: u32 = s: switch (start) {
0 => 0,
1 => 1,
2 => 2,
3 => 3,
else => |x| continue :s x / 2,
};
try expect(result == 2);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "switch loop with ranges" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
const S = struct {
fn doTheTest() !void {
var start: u32 = undefined;
start = 32;
const result = s: switch (start) {
0...3 => |x| x,
else => |x| continue :s x / 2,
};
try expect(result == 2);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "switch loop on enum" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
const S = struct {
const E = enum { a, b, c };
fn doTheTest() !void {
var start: E = undefined;
start = .a;
const result: u32 = s: switch (start) {
.a => continue :s .b,
.b => continue :s .c,
.c => 123,
};
try expect(result == 123);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "switch loop on tagged union" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
const S = struct {
const U = union(enum) {
a: u32,
b: f32,
c: f32,
};
fn doTheTest() !void {
var start: U = undefined;
start = .{ .a = 80 };
const result = s: switch (start) {
.a => |x| switch (x) {
0...49 => continue :s .{ .b = @floatFromInt(x) },
50 => continue :s .{ .c = @floatFromInt(x) },
else => continue :s .{ .a = x / 2 },
},
.b => |x| x,
.c => return error.TestFailed,
};
try expect(result == 40.0);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "switch loop dispatching instructions" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
const S = struct {
const Inst = union(enum) {
set: u32,
add: u32,
sub: u32,
end,
};
fn doTheTest() !void {
var insts: [5]Inst = undefined;
@memcpy(&insts, &[5]Inst{
.{ .set = 123 },
.{ .add = 100 },
.{ .sub = 50 },
.{ .sub = 10 },
.end,
});
var i: u32 = 0;
var cur: u32 = undefined;
eval: switch (insts[0]) {
.set => |x| {
cur = x;
i += 1;
continue :eval insts[i];
},
.add => |x| {
cur += x;
i += 1;
continue :eval insts[i];
},
.sub => |x| {
cur -= x;
i += 1;
continue :eval insts[i];
},
.end => {},
}
try expect(cur == 163);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}
test "switch loop with pointer capture" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
const S = struct {
const U = union(enum) {
a: u32,
b: u32,
c: u32,
};
fn doTheTest() !void {
var a: U = .{ .a = 100 };
var b: U = .{ .b = 200 };
var c: U = .{ .c = 300 };
inc: switch (a) {
.a => |*x| {
x.* += 1;
continue :inc b;
},
.b => |*x| {
x.* += 10;
continue :inc c;
},
.c => |*x| {
x.* += 50;
},
}
try expect(a.a == 101);
try expect(b.b == 210);
try expect(c.c == 350);
}
};
try S.doTheTest();
try comptime S.doTheTest();
}