Sema: fix comptime break semantics

Previously, breaking from an outer block at comptime would result in
incorrect control flow. Now there is a mechanism, `error.ComptimeBreak`,
similar to `error.ComptimeReturn`, to send comptime control flow further
up the stack, to its matching block.

This commit also introduces a new log scope. To use it, pass
`--debug-log sema_zir` and you will see 1 line per ZIR instruction
semantically analyzed. This is useful when you want to understand what
comptime control flow is doing while debugging the compiler.

One more `switch` test case is passing.
This commit is contained in:
Andrew Kelley 2022-01-17 15:21:58 -07:00
parent 79628d48a4
commit f4e051e35d
4 changed files with 59 additions and 24 deletions

View File

@ -2486,6 +2486,9 @@ pub const CompileError = error{
/// In a comptime scope, a return instruction was encountered. This error is only seen when
/// doing a comptime function call.
ComptimeReturn,
/// In a comptime scope, a break instruction was encountered. This error is only seen when
/// evaluating a comptime block.
ComptimeBreak,
};
pub fn deinit(mod: *Module) void {
@ -4446,6 +4449,7 @@ pub fn analyzeFnBody(mod: *Module, decl: *Decl, func: *Fn, arena: Allocator) Sem
error.NeededSourceLocation => unreachable,
error.GenericPoison => unreachable,
error.ComptimeReturn => unreachable,
error.ComptimeBreak => unreachable,
else => |e| return e,
};
if (opt_opv) |opv| {
@ -4478,6 +4482,7 @@ pub fn analyzeFnBody(mod: *Module, decl: *Decl, func: *Fn, arena: Allocator) Sem
error.NeededSourceLocation => @panic("zig compiler bug: NeededSourceLocation"),
error.GenericPoison => @panic("zig compiler bug: GenericPoison"),
error.ComptimeReturn => @panic("zig compiler bug: ComptimeReturn"),
error.ComptimeBreak => @panic("zig compiler bug: ComptimeBreak"),
else => |e| return e,
};

View File

@ -39,6 +39,9 @@ func: ?*Module.Fn,
fn_ret_ty: Type,
branch_quota: u32 = 1000,
branch_count: u32 = 0,
/// Populated when returning `error.ComptimeBreak`. Used to communicate the
/// break instruction up the stack to find the corresponding Block.
comptime_break_inst: Zir.Inst.Index = undefined,
/// This field is updated when a new source location becomes active, so that
/// instructions which do not have explicitly mapped source locations still have
/// access to the source location set by the previous instruction which did
@ -486,8 +489,31 @@ pub fn deinit(sema: *Sema) void {
/// has no peers.
fn resolveBody(sema: *Sema, block: *Block, body: []const Zir.Inst.Index) CompileError!Air.Inst.Ref {
const break_inst = try sema.analyzeBody(block, body);
const operand_ref = sema.code.instructions.items(.data)[break_inst].@"break".operand;
return sema.resolveInst(operand_ref);
const break_data = sema.code.instructions.items(.data)[break_inst].@"break";
// For comptime control flow, we need to detect when `analyzeBody` reports
// that we need to break from an outer block. In such case we
// use Zig's error mechanism to send control flow up the stack until
// we find the corresponding block to this break.
if (block.is_comptime) {
if (block.label) |label| {
if (label.zir_block != break_data.block_inst) {
sema.comptime_break_inst = break_inst;
return error.ComptimeBreak;
}
}
}
return sema.resolveInst(break_data.operand);
}
pub fn analyzeBody(
sema: *Sema,
block: *Block,
body: []const Zir.Inst.Index,
) CompileError!Zir.Inst.Index {
return sema.analyzeBodyInner(block, body) catch |err| switch (err) {
error.ComptimeBreak => sema.comptime_break_inst,
else => |e| return e,
};
}
/// ZIR instructions which are always `noreturn` return this. This matches the
@ -505,7 +531,7 @@ const always_noreturn: CompileError!Zir.Inst.Index = @as(Zir.Inst.Index, undefin
/// instruction. In this case, the `Zir.Inst.Index` part of the return value will be
/// the break instruction. This communicates both which block the break applies to, as
/// well as the operand. No block scope needs to be created for this strategy.
pub fn analyzeBody(
fn analyzeBodyInner(
sema: *Sema,
block: *Block,
body: []const Zir.Inst.Index,
@ -541,6 +567,9 @@ pub fn analyzeBody(
const result = while (true) {
crash_info.setBodyIndex(i);
const inst = body[i];
std.log.scoped(.sema_zir).debug("sema ZIR {s} %{d}", .{
block.src_decl.src_namespace.file_scope.sub_file_path, inst,
});
const air_inst: Air.Inst.Ref = switch (tags[inst]) {
// zig fmt: off
.alloc => try sema.zirAlloc(block, inst),
@ -4319,6 +4348,7 @@ fn analyzeCall(
const result = result: {
_ = sema.analyzeBody(&child_block, fn_info.body) catch |err| switch (err) {
error.ComptimeReturn => break :result inlining.comptime_result,
error.ComptimeBreak => unreachable, // Can't break through a fn call.
else => |e| return e,
};
break :result try sema.analyzeBlockBody(block, call_src, &child_block, merges);

View File

@ -326,3 +326,24 @@ test "anon enum literal used in switch on union enum" {
},
}
}
test "switch all prongs unreachable" {
try testAllProngsUnreachable();
comptime try testAllProngsUnreachable();
}
fn testAllProngsUnreachable() !void {
try expect(switchWithUnreachable(1) == 2);
try expect(switchWithUnreachable(2) == 10);
}
fn switchWithUnreachable(x: i32) i32 {
while (true) {
switch (x) {
1 => return 2,
2 => break,
else => continue,
}
}
return 10;
}

View File

@ -3,27 +3,6 @@ const expect = std.testing.expect;
const expectError = std.testing.expectError;
const expectEqual = std.testing.expectEqual;
test "switch all prongs unreachable" {
try testAllProngsUnreachable();
comptime try testAllProngsUnreachable();
}
fn testAllProngsUnreachable() !void {
try expect(switchWithUnreachable(1) == 2);
try expect(switchWithUnreachable(2) == 10);
}
fn switchWithUnreachable(x: i32) i32 {
while (true) {
switch (x) {
1 => return 2,
2 => break,
else => continue,
}
}
return 10;
}
fn return_a_number() anyerror!i32 {
return 1;
}