Commit f4e051e35d

Andrew Kelley <andrew@ziglang.org>
2022-01-17 23:21:58
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.
1 parent 79628d4
Changed files (4)
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,
     };
 
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);
test/behavior/switch.zig
@@ -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;
+}
test/behavior/switch_stage1.zig
@@ -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;
 }