From f4e051e35d8019c9a8d99ccae8f2e9d8f032629a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 17 Jan 2022 15:21:58 -0700 Subject: 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. --- src/Module.zig | 5 +++++ src/Sema.zig | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/Module.zig b/src/Module.zig index a464ac83de..c66509f33a 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -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, }; diff --git a/src/Sema.zig b/src/Sema.zig index cd4d9898cd..9978e5f1a7 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -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); -- cgit v1.2.3