Commit 59447e5305

mlugg <mlugg@mlugg.co.uk>
2024-02-24 05:29:42
compiler: decide dbg_var scoping based on AIR blocks
This commit eliminates the `dbg_block_{begin,end}` instructions from both ZIR and AIR. Instead, lexical scoping of `dbg_var_{ptr,val}` instructions is decided based on the AIR block they exist within. This is a much more robust system, and also results in a huge drop in ZIR bytes - around 7% for Sema.zig. This required some enhancements to Sema to prevent elision of blocks when they are required for debug variable scoping. This can be observed by looking at the AIR for the following simple test program with and without `-fstrip`: ```zig export fn f() void { { var a: u32 = 0; _ = &a; } { var a: u32 = 0; _ = &a; } } ``` When `-fstrip` is passed, no AIR blocks are generated. When `-fno-strip` is passed, the ZIR blocks are lowered to true AIR blocks to give correct lexical scoping to the debug vars. The changes here incidentally reolve #19060. A corresponding behavior test has been added. Resolves: #19060
1 parent 031f231
src/arch/aarch64/CodeGen.zig
@@ -813,10 +813,6 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .dbg_inline_end,
             => try self.airDbgInline(inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => try self.airDbgBlock(inst),
-
             .call              => try self.airCall(inst, .auto),
             .call_always_tail  => try self.airCall(inst, .always_tail),
             .call_never_tail   => try self.airCall(inst, .never_tail),
@@ -4634,11 +4630,6 @@ fn airDbgInline(self: *Self, inst: Air.Inst.Index) !void {
     return self.finishAir(inst, .dead, .{ .none, .none, .none });
 }
 
-fn airDbgBlock(self: *Self, inst: Air.Inst.Index) !void {
-    // TODO emit debug info lexical block
-    return self.finishAir(inst, .dead, .{ .none, .none, .none });
-}
-
 fn airDbgVar(self: *Self, inst: Air.Inst.Index) !void {
     const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op;
     const operand = pl_op.operand;
@@ -5066,6 +5057,7 @@ fn airBlock(self: *Self, inst: Air.Inst.Index) !void {
     const ty_pl = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_pl;
     const extra = self.air.extraData(Air.Block, ty_pl.payload);
     const body: []const Air.Inst.Index = @ptrCast(self.air.extra[extra.end..][0..extra.data.body_len]);
+    // TODO emit debug info lexical block
     try self.genBody(body);
 
     // relocations for `br` instructions
src/arch/arm/CodeGen.zig
@@ -799,10 +799,6 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .dbg_inline_end,
             => try self.airDbgInline(inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => try self.airDbgBlock(inst),
-
             .call              => try self.airCall(inst, .auto),
             .call_always_tail  => try self.airCall(inst, .always_tail),
             .call_never_tail   => try self.airCall(inst, .never_tail),
@@ -4587,11 +4583,6 @@ fn airDbgInline(self: *Self, inst: Air.Inst.Index) !void {
     return self.finishAir(inst, .dead, .{ .none, .none, .none });
 }
 
-fn airDbgBlock(self: *Self, inst: Air.Inst.Index) !void {
-    // TODO emit debug info lexical block
-    return self.finishAir(inst, .dead, .{ .none, .none, .none });
-}
-
 fn airDbgVar(self: *Self, inst: Air.Inst.Index) !void {
     const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op;
     const operand = pl_op.operand;
@@ -4997,6 +4988,7 @@ fn airBlock(self: *Self, inst: Air.Inst.Index) !void {
     const ty_pl = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_pl;
     const extra = self.air.extraData(Air.Block, ty_pl.payload);
     const body: []const Air.Inst.Index = @ptrCast(self.air.extra[extra.end..][0..extra.data.body_len]);
+    // TODO emit debug info lexical block
     try self.genBody(body);
 
     // relocations for `br` instructions
src/arch/riscv64/CodeGen.zig
@@ -632,10 +632,6 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .dbg_inline_end,
             => try self.airDbgInline(inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => try self.airDbgBlock(inst),
-
             .call              => try self.airCall(inst, .auto),
             .call_always_tail  => try self.airCall(inst, .always_tail),
             .call_never_tail   => try self.airCall(inst, .never_tail),
@@ -1894,11 +1890,6 @@ fn airDbgInline(self: *Self, inst: Air.Inst.Index) !void {
     return self.finishAir(inst, .dead, .{ .none, .none, .none });
 }
 
-fn airDbgBlock(self: *Self, inst: Air.Inst.Index) !void {
-    // TODO emit debug info lexical block
-    return self.finishAir(inst, .dead, .{ .none, .none, .none });
-}
-
 fn airDbgVar(self: *Self, inst: Air.Inst.Index) !void {
     const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op;
     const name = self.air.nullTerminatedString(pl_op.payload);
@@ -2084,6 +2075,7 @@ fn airBlock(self: *Self, inst: Air.Inst.Index) !void {
     const ty_pl = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_pl;
     const extra = self.air.extraData(Air.Block, ty_pl.payload);
     const body: []const Air.Inst.Index = @ptrCast(self.air.extra[extra.end..][0..extra.data.body_len]);
+    // TODO emit debug info lexical block
     try self.genBody(body);
 
     for (self.blocks.getPtr(inst).?.relocs.items) |reloc| try self.performReloc(reloc);
src/arch/sparc64/CodeGen.zig
@@ -645,10 +645,6 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .dbg_inline_end,
             => try self.airDbgInline(inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => try self.airDbgBlock(inst),
-
             .call              => try self.airCall(inst, .auto),
             .call_always_tail  => try self.airCall(inst, .always_tail),
             .call_never_tail   => try self.airCall(inst, .never_tail),
@@ -1146,6 +1142,7 @@ fn airBlock(self: *Self, inst: Air.Inst.Index) !void {
     const ty_pl = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_pl;
     const extra = self.air.extraData(Air.Block, ty_pl.payload);
     const body: []const Air.Inst.Index = @ptrCast(self.air.extra[extra.end..][0..extra.data.body_len]);
+    // TODO emit debug info lexical block
     try self.genBody(body);
 
     // relocations for `bpcc` instructions
@@ -1655,11 +1652,6 @@ fn airCtz(self: *Self, inst: Air.Inst.Index) !void {
     return self.finishAir(inst, result, .{ ty_op.operand, .none, .none });
 }
 
-fn airDbgBlock(self: *Self, inst: Air.Inst.Index) !void {
-    // TODO emit debug info lexical block
-    return self.finishAir(inst, .dead, .{ .none, .none, .none });
-}
-
 fn airDbgInline(self: *Self, inst: Air.Inst.Index) !void {
     const ty_fn = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_fn;
     const mod = self.bin_file.comp.module.?;
src/arch/wasm/CodeGen.zig
@@ -1913,8 +1913,6 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         // TODO
         .dbg_inline_begin,
         .dbg_inline_end,
-        .dbg_block_begin,
-        .dbg_block_end,
         => func.finishAir(inst, .none, &.{}),
 
         .dbg_var_ptr => func.airDbgVar(inst, true),
src/arch/x86_64/CodeGen.zig
@@ -2106,10 +2106,6 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .dbg_inline_end,
             => try self.airDbgInline(inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => try self.airDbgBlock(inst),
-
             .call              => try self.airCall(inst, .auto),
             .call_always_tail  => try self.airCall(inst, .always_tail),
             .call_never_tail   => try self.airCall(inst, .never_tail),
@@ -12976,12 +12972,6 @@ fn airDbgInline(self: *Self, inst: Air.Inst.Index) !void {
     self.finishAirBookkeeping();
 }
 
-fn airDbgBlock(self: *Self, inst: Air.Inst.Index) !void {
-    _ = inst;
-    // TODO emit debug info lexical block
-    self.finishAirBookkeeping();
-}
-
 fn airDbgVar(self: *Self, inst: Air.Inst.Index) !void {
     const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op;
     const operand = pl_op.operand;
@@ -13428,6 +13418,7 @@ fn airBlock(self: *Self, inst: Air.Inst.Index) !void {
     const ty_pl = self.air.instructions.items(.data)[@intFromEnum(inst)].ty_pl;
     const extra = self.air.extraData(Air.Block, ty_pl.payload);
     const body: []const Air.Inst.Index = @ptrCast(self.air.extra[extra.end..][0..extra.data.body_len]);
+    // TODO emit debug info lexical block
     try self.genBody(body);
 
     var block_data = self.blocks.fetchRemove(inst).?;
src/codegen/c.zig
@@ -3268,10 +3268,6 @@ fn genBodyInner(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail,
             .dbg_inline_end,
             => try airDbgInline(f, inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => .none,
-
             .call              => try airCall(f, inst, .auto),
             .call_always_tail  => .none,
             .call_never_tail   => try airCall(f, inst, .never_tail),
src/codegen/llvm.zig
@@ -4768,8 +4768,6 @@ pub const FuncGen = struct {
         scope: Builder.Metadata,
     }) = .{},
 
-    scope_stack: std.ArrayListUnmanaged(Builder.Metadata) = .{},
-
     base_line: u32,
     prev_dbg_line: c_uint,
     prev_dbg_column: c_uint,
@@ -4813,7 +4811,6 @@ pub const FuncGen = struct {
 
     fn deinit(self: *FuncGen) void {
         self.wip.deinit();
-        self.scope_stack.deinit(self.gpa);
         self.inlined.deinit(self.gpa);
         self.func_inst_table.deinit(self.gpa);
         self.blocks.deinit(self.gpa);
@@ -5112,8 +5109,6 @@ pub const FuncGen = struct {
                 .dbg_stmt => try self.airDbgStmt(inst),
                 .dbg_inline_begin => try self.airDbgInlineBegin(inst),
                 .dbg_inline_end => try self.airDbgInlineEnd(inst),
-                .dbg_block_begin => try self.airDbgBlockBegin(),
-                .dbg_block_end => try self.airDbgBlockEnd(),
                 .dbg_var_ptr => try self.airDbgVarPtr(inst),
                 .dbg_var_val => try self.airDbgVarVal(inst),
 
@@ -5131,6 +5126,19 @@ pub const FuncGen = struct {
         }
     }
 
+    fn genBodyDebugScope(self: *FuncGen, body: []const Air.Inst.Index) Error!void {
+        if (self.wip.strip) return self.genBody(body);
+        const old_scope = self.scope;
+        self.scope = try self.dg.object.builder.debugLexicalBlock(
+            old_scope,
+            self.file,
+            self.prev_dbg_line,
+            self.prev_dbg_column,
+        );
+        try self.genBody(body);
+        self.scope = old_scope;
+    }
+
     pub const CallAttr = enum {
         Auto,
         NeverTail,
@@ -5820,7 +5828,7 @@ pub const FuncGen = struct {
         const inst_ty = self.typeOfIndex(inst);
 
         if (inst_ty.isNoReturn(mod)) {
-            try self.genBody(body);
+            try self.genBodyDebugScope(body);
             return .none;
         }
 
@@ -5836,7 +5844,7 @@ pub const FuncGen = struct {
         });
         defer assert(self.blocks.remove(inst));
 
-        try self.genBody(body);
+        try self.genBodyDebugScope(body);
 
         self.wip.cursor = .{ .block = parent_bb };
 
@@ -6683,26 +6691,6 @@ pub const FuncGen = struct {
         return .none;
     }
 
-    fn airDbgBlockBegin(self: *FuncGen) Allocator.Error!Builder.Value {
-        const o = self.dg.object;
-
-        try self.scope_stack.append(self.gpa, self.scope);
-
-        const old = self.scope;
-        self.scope = try o.builder.debugLexicalBlock(
-            old,
-            self.file,
-            self.prev_dbg_line,
-            self.prev_dbg_column,
-        );
-        return .none;
-    }
-
-    fn airDbgBlockEnd(self: *FuncGen) !Builder.Value {
-        self.scope = self.scope_stack.pop();
-        return .none;
-    }
-
     fn airDbgVarPtr(self: *FuncGen, inst: Air.Inst.Index) !Builder.Value {
         const o = self.dg.object;
         const mod = o.module;
@@ -7488,7 +7476,7 @@ pub const FuncGen = struct {
         for (body_tail[1..]) |body_inst| {
             switch (air_tags[@intFromEnum(body_inst)]) {
                 .ret => return true,
-                .dbg_stmt, .dbg_block_end => continue,
+                .dbg_stmt => continue,
                 else => return false,
             }
         }
src/codegen/spirv.zig
@@ -2322,8 +2322,6 @@ const DeclGen = struct {
             .dbg_inline_begin          => return self.airDbgInlineBegin(inst),
             .dbg_inline_end            => return self.airDbgInlineEnd(inst),
             .dbg_var_ptr, .dbg_var_val => return self.airDbgVar(inst),
-            .dbg_block_begin  => return,
-            .dbg_block_end    => return,
 
             .unwrap_errunion_err => try self.airErrUnionErr(inst),
             .unwrap_errunion_payload => try self.airErrUnionPayload(inst),
src/Liveness/Verify.zig
@@ -48,8 +48,6 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
             .dbg_stmt,
             .dbg_inline_begin,
             .dbg_inline_end,
-            .dbg_block_begin,
-            .dbg_block_end,
             .fence,
             .ret_addr,
             .frame_addr,
src/Air.zig
@@ -443,10 +443,6 @@ pub const Inst = struct {
         /// Result type is always void.
         /// Uses the `dbg_stmt` field.
         dbg_stmt,
-        /// Marks the beginning of a semantic scope for debug info variables.
-        dbg_block_begin,
-        /// Marks the end of a semantic scope for debug info variables.
-        dbg_block_end,
         /// Marks the start of an inline call.
         /// Uses the `ty_fn` field.
         dbg_inline_begin,
@@ -1454,8 +1450,6 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
         .dbg_stmt,
         .dbg_inline_begin,
         .dbg_inline_end,
-        .dbg_block_begin,
-        .dbg_block_end,
         .dbg_var_ptr,
         .dbg_var_val,
         .store,
@@ -1612,8 +1606,6 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
         .@"try",
         .try_ptr,
         .dbg_stmt,
-        .dbg_block_begin,
-        .dbg_block_end,
         .dbg_inline_begin,
         .dbg_inline_end,
         .dbg_var_ptr,
src/AstGen.zig
@@ -2445,8 +2445,6 @@ fn blockExprStmts(gz: *GenZir, parent_scope: *Scope, statements: []const Ast.Nod
 
     if (statements.len == 0) return;
 
-    try gz.addDbgBlockBegin();
-
     var block_arena = std.heap.ArenaAllocator.init(gz.astgen.gpa);
     defer block_arena.deinit();
     const block_arena_allocator = block_arena.allocator();
@@ -2518,8 +2516,6 @@ fn blockExprStmts(gz: *GenZir, parent_scope: *Scope, statements: []const Ast.Nod
         }
     }
 
-    try gz.addDbgBlockEnd();
-
     try genDefers(gz, parent_scope, scope, .normal_only);
     try checkUsed(gz, parent_scope, scope);
 }
@@ -2804,8 +2800,6 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
             .dbg_stmt,
             .dbg_var_ptr,
             .dbg_var_val,
-            .dbg_block_begin,
-            .dbg_block_end,
             .ensure_result_used,
             .ensure_result_non_error,
             .ensure_err_union_payload_void,
@@ -3026,7 +3020,6 @@ fn deferStmt(
     var opt_remapped_err_code: Zir.Inst.OptionalIndex = .none;
     const have_err_code = scope_tag == .defer_error and payload_token != 0;
     const sub_scope = if (!have_err_code) &defer_gen.base else blk: {
-        try gz.addDbgBlockBegin();
         const ident_name = try gz.astgen.identAsString(payload_token);
         const remapped_err_code: Zir.Inst.Index = @enumFromInt(gz.astgen.instructions.len);
         opt_remapped_err_code = remapped_err_code.toOptional();
@@ -3052,7 +3045,6 @@ fn deferStmt(
     };
     _ = try unusedResultExpr(&defer_gen, sub_scope, expr_node);
     try checkUsed(gz, scope, sub_scope);
-    if (have_err_code) try gz.addDbgBlockEnd();
     _ = try defer_gen.addBreak(.break_inline, @enumFromInt(0), .void_value);
 
     // We must handle ref_table for remapped_err_code manually.
@@ -6245,7 +6237,6 @@ fn ifExpr(
 
     var payload_val_scope: Scope.LocalVal = undefined;
 
-    try then_scope.addDbgBlockBegin();
     const then_node = if_full.ast.then_expr;
     const then_sub_scope = s: {
         if (if_full.error_token != null) {
@@ -6305,7 +6296,6 @@ fn ifExpr(
     const then_result = try expr(&then_scope, then_sub_scope, block_scope.break_result_info, then_node);
     try checkUsed(parent_gz, &then_scope.base, then_sub_scope);
     if (!then_scope.endsWithNoReturn()) {
-        try then_scope.addDbgBlockEnd();
         _ = try then_scope.addBreakWithSrcNode(.@"break", block, then_result, then_node);
     }
 
@@ -6319,7 +6309,6 @@ fn ifExpr(
 
     const else_node = if_full.ast.else_expr;
     if (else_node != 0) {
-        try else_scope.addDbgBlockBegin();
         const sub_scope = s: {
             if (if_full.error_token) |error_token| {
                 const tag: Zir.Inst.Tag = if (payload_is_ref)
@@ -6347,7 +6336,6 @@ fn ifExpr(
             }
         };
         const else_result = try expr(&else_scope, sub_scope, block_scope.break_result_info, else_node);
-        try else_scope.addDbgBlockEnd();
         if (!else_scope.endsWithNoReturn()) {
             // As our last action before the break, "pop" the error trace if needed
             if (do_err_trace)
@@ -6579,7 +6567,6 @@ fn whileExpr(
     // done adding instructions to loop_scope, can now stack then_scope
     then_scope.instructions_top = then_scope.instructions.items.len;
 
-    try then_scope.addDbgBlockBegin();
     const then_node = while_full.ast.then_expr;
     if (opt_payload_inst.unwrap()) |payload_inst| {
         try then_scope.instructions.append(astgen.gpa, payload_inst);
@@ -6593,7 +6580,6 @@ fn whileExpr(
     if (while_full.ast.cont_expr != 0) {
         _ = try unusedResultExpr(&then_scope, then_sub_scope, while_full.ast.cont_expr);
     }
-    try then_scope.addDbgBlockEnd();
 
     continue_scope.instructions_top = continue_scope.instructions.items.len;
     _ = try unusedResultExpr(&continue_scope, &continue_scope.base, then_node);
@@ -6610,7 +6596,6 @@ fn whileExpr(
 
     const else_node = while_full.ast.else_expr;
     if (else_node != 0) {
-        try else_scope.addDbgBlockBegin();
         const sub_scope = s: {
             if (while_full.error_token) |error_token| {
                 const tag: Zir.Inst.Tag = if (payload_is_ref)
@@ -6647,7 +6632,6 @@ fn whileExpr(
         }
 
         try checkUsed(parent_gz, &else_scope.base, sub_scope);
-        try else_scope.addDbgBlockEnd();
         if (!else_scope.endsWithNoReturn()) {
             _ = try else_scope.addBreakWithSrcNode(break_tag, loop_block, else_result, else_node);
         }
@@ -6849,8 +6833,6 @@ fn forExpr(
     var then_scope = parent_gz.makeSubBlock(&cond_scope.base);
     defer then_scope.unstack();
 
-    try then_scope.addDbgBlockBegin();
-
     const capture_scopes = try gpa.alloc(Scope.LocalVal, for_full.ast.inputs.len);
     defer gpa.free(capture_scopes);
 
@@ -6916,7 +6898,6 @@ fn forExpr(
     _ = try addEnsureResult(&then_scope, then_result, then_node);
 
     try checkUsed(parent_gz, &then_scope.base, then_sub_scope);
-    try then_scope.addDbgBlockEnd();
 
     const break_tag: Zir.Inst.Tag = if (is_inline) .break_inline else .@"break";
 
@@ -7149,8 +7130,6 @@ fn switchExprErrUnion(
         case_scope.instructions_top = parent_gz.instructions.items.len;
         defer case_scope.unstack();
 
-        try case_scope.addDbgBlockBegin();
-
         const unwrap_payload_tag: Zir.Inst.Tag = if (payload_is_ref)
             .err_union_payload_unsafe_ptr
         else
@@ -7173,7 +7152,6 @@ fn switchExprErrUnion(
                         catch_or_if_node,
                     ),
                 };
-                try case_scope.addDbgBlockEnd();
                 _ = try case_scope.addBreakWithSrcNode(
                     .@"break",
                     switch_block,
@@ -7184,7 +7162,6 @@ fn switchExprErrUnion(
             .@"if" => {
                 var payload_val_scope: Scope.LocalVal = undefined;
 
-                try case_scope.addDbgBlockBegin();
                 const then_node = if_full.ast.then_expr;
                 const then_sub_scope = s: {
                     assert(if_full.error_token != null);
@@ -7228,7 +7205,6 @@ fn switchExprErrUnion(
                 );
                 try checkUsed(parent_gz, &case_scope.base, then_sub_scope);
                 if (!case_scope.endsWithNoReturn()) {
-                    try case_scope.addDbgBlockEnd();
                     _ = try case_scope.addBreakWithSrcNode(
                         .@"break",
                         switch_block,
@@ -7407,7 +7383,6 @@ fn switchExprErrUnion(
             if (do_err_trace and nodeMayAppendToErrorTrace(tree, operand_node))
                 _ = try case_scope.addSaveErrRetIndex(.always);
 
-            try case_scope.addDbgBlockBegin();
             if (dbg_var_name != .empty) {
                 try case_scope.addDbgVar(.dbg_var_val, dbg_var_name, dbg_var_inst);
             }
@@ -7421,7 +7396,6 @@ fn switchExprErrUnion(
                 try case_scope.addDbgVar(.dbg_var_val, err_name, err_inst.toRef());
                 any_uses_err_capture = true;
             }
-            try case_scope.addDbgBlockEnd();
 
             if (!parent_gz.refIsNoReturn(case_result)) {
                 if (do_err_trace)
@@ -7868,7 +7842,6 @@ fn switchExpr(
             case_scope.instructions_top = parent_gz.instructions.items.len;
             defer case_scope.unstack();
 
-            try case_scope.addDbgBlockBegin();
             if (dbg_var_name != .empty) {
                 try case_scope.addDbgVar(.dbg_var_val, dbg_var_name, dbg_var_inst);
             }
@@ -7878,7 +7851,6 @@ fn switchExpr(
             const target_expr_node = case.ast.target_expr;
             const case_result = try expr(&case_scope, sub_scope, block_scope.break_result_info, target_expr_node);
             try checkUsed(parent_gz, &case_scope.base, sub_scope);
-            try case_scope.addDbgBlockEnd();
             if (!parent_gz.refIsNoReturn(case_result)) {
                 _ = try case_scope.addBreakWithSrcNode(.@"break", switch_block, case_result, target_expr_node);
             }
@@ -13165,29 +13137,6 @@ const GenZir = struct {
             },
         } });
     }
-
-    fn addDbgBlockBegin(gz: *GenZir) !void {
-        if (gz.is_comptime) return;
-
-        _ = try gz.add(.{ .tag = .dbg_block_begin, .data = undefined });
-    }
-
-    fn addDbgBlockEnd(gz: *GenZir) !void {
-        if (gz.is_comptime) return;
-        const gpa = gz.astgen.gpa;
-
-        const tags = gz.astgen.instructions.items(.tag);
-        const last_inst = gz.instructions.items[gz.instructions.items.len - 1];
-        // remove dbg_block_begin immediately followed by dbg_block_end
-        if (tags[@intFromEnum(last_inst)] == .dbg_block_begin) {
-            _ = gz.instructions.pop();
-            return;
-        }
-
-        const new_index: Zir.Inst.Index = @enumFromInt(gz.astgen.instructions.len);
-        try gz.astgen.instructions.append(gpa, .{ .tag = .dbg_block_end, .data = undefined });
-        try gz.instructions.append(gpa, new_index);
-    }
 };
 
 /// This can only be for short-lived references; the memory becomes invalidated
src/Liveness.zig
@@ -329,8 +329,6 @@ pub fn categorizeOperand(
         .dbg_stmt,
         .dbg_inline_begin,
         .dbg_inline_end,
-        .dbg_block_begin,
-        .dbg_block_end,
         .unreach,
         .ret_addr,
         .frame_addr,
@@ -967,8 +965,6 @@ fn analyzeInst(
         .dbg_stmt,
         .dbg_inline_begin,
         .dbg_inline_end,
-        .dbg_block_begin,
-        .dbg_block_end,
         .fence,
         .ret_addr,
         .frame_addr,
src/print_air.zig
@@ -319,8 +319,6 @@ const Writer = struct {
             .cmp_vector, .cmp_vector_optimized => try w.writeCmpVector(s, inst),
             .vector_store_elem => try w.writeVectorStoreElem(s, inst),
 
-            .dbg_block_begin, .dbg_block_end => {},
-
             .work_item_id,
             .work_group_size,
             .work_group_id,
src/print_zir.zig
@@ -510,10 +510,6 @@ const Writer = struct {
 
             .dbg_stmt => try self.writeDbgStmt(stream, inst),
 
-            .dbg_block_begin,
-            .dbg_block_end,
-            => try stream.writeAll(")"),
-
             .closure_get => try self.writeInstNode(stream, inst),
 
             .@"defer" => try self.writeDefer(stream, inst),
src/Sema.zig
@@ -364,6 +364,12 @@ pub const Block = struct {
 
     c_import_buf: ?*std.ArrayList(u8) = null,
 
+    /// If not `null`, this boolean is set when a `dbg_var_ptr` or `dbg_var_val`
+    /// instruction is emitted. It signals that the innermost lexically
+    /// enclosing `block`/`block_inline` should be translated into a real AIR
+    /// `block` in order for codegen to match lexical scoping for debug vars.
+    need_debug_scope: ?*bool = null,
+
     const ComptimeReason = union(enum) {
         c_import: struct {
             block: *Block,
@@ -482,6 +488,7 @@ pub const Block = struct {
             .float_mode = parent.float_mode,
             .c_import_buf = parent.c_import_buf,
             .error_return_trace_index = parent.error_return_trace_index,
+            .need_debug_scope = parent.need_debug_scope,
         };
     }
 
@@ -986,8 +993,6 @@ fn analyzeBodyInner(
     crash_info.push();
     defer crash_info.pop();
 
-    var dbg_block_begins: u32 = 0;
-
     // We use a while (true) loop here to avoid a redundant way of breaking out of
     // the loop. The only way to break out of the loop is with a `noreturn`
     // instruction.
@@ -1332,18 +1337,6 @@ fn analyzeBodyInner(
                 i += 1;
                 continue;
             },
-            .dbg_block_begin => {
-                dbg_block_begins += 1;
-                try zirDbgBlockBegin(block);
-                i += 1;
-                continue;
-            },
-            .dbg_block_end => {
-                dbg_block_begins -= 1;
-                try zirDbgBlockEnd(block);
-                i += 1;
-                continue;
-            },
             .ensure_err_union_payload_void => {
                 try sema.zirEnsureErrUnionPayloadVoid(block, inst);
                 i += 1;
@@ -1641,10 +1634,12 @@ fn analyzeBodyInner(
                 const inline_body = sema.code.bodySlice(extra.end, extra.data.body_len);
                 const gpa = sema.gpa;
 
-                const opt_break_data = b: {
+                const opt_break_data, const need_debug_scope = b: {
                     // Create a temporary child block so that this inline block is properly
                     // labeled for any .restore_err_ret_index instructions
                     var child_block = block.makeSubBlock();
+                    var need_debug_scope = false;
+                    child_block.need_debug_scope = &need_debug_scope;
 
                     // If this block contains a function prototype, we need to reset the
                     // current list of parameters and restore it later.
@@ -1665,7 +1660,11 @@ fn analyzeBodyInner(
                     child_block.instructions = block.instructions;
                     defer block.instructions = child_block.instructions;
 
-                    break :b try sema.analyzeBodyBreak(&child_block, inline_body);
+                    const result = try sema.analyzeBodyBreak(&child_block, inline_body);
+                    if (need_debug_scope) {
+                        _ = try sema.ensurePostHoc(block, inst);
+                    }
+                    break :b .{ result, need_debug_scope };
                 };
 
                 // A runtime conditional branch that needs a post-hoc block to be
@@ -1686,28 +1685,22 @@ fn analyzeBodyInner(
                         // since it crosses a runtime branch.
                         // It may pass through our currently being analyzed block_inline or it
                         // may point directly to it. In the latter case, this modifies the
-                        // block that we are about to look up in the post_hoc_blocks map below.
+                        // block that we looked up in the post_hoc_blocks map above.
                         try sema.addRuntimeBreak(block, break_data);
-                    } else {
-                        // Here the comptime control flow ends with noreturn; however
-                        // we have runtime control flow continuing after this block.
-                        // This branch is therefore handled by the `i += 1; continue;`
-                        // logic below.
                     }
 
                     try labeled_block.block.instructions.appendSlice(gpa, block.instructions.items[block_index..]);
                     block.instructions.items.len = block_index;
 
-                    const block_result = try sema.analyzeBlockBody(block, inst_data.src(), &labeled_block.block, &labeled_block.label.merges);
+                    const block_result = try sema.analyzeBlockBody(block, inst_data.src(), &labeled_block.block, &labeled_block.label.merges, need_debug_scope);
                     {
                         // Destroy the ad-hoc block entry so that it does not interfere with
                         // the next iteration of comptime control flow, if any.
                         labeled_block.destroy(gpa);
                         assert(sema.post_hoc_blocks.remove(new_block_inst));
                     }
-                    map.putAssumeCapacity(inst, block_result);
-                    i += 1;
-                    continue;
+
+                    break :blk block_result;
                 }
 
                 const break_data = opt_break_data orelse break always_noreturn;
@@ -1860,19 +1853,6 @@ fn analyzeBodyInner(
         i += 1;
     };
 
-    // balance out dbg_block_begins in case of early noreturn
-    if (!block.is_comptime and !block.ownerModule().strip) {
-        const noreturn_inst = block.instructions.popOrNull();
-        while (dbg_block_begins > 0) {
-            dbg_block_begins -= 1;
-            _ = try block.addInst(.{
-                .tag = .dbg_block_end,
-                .data = undefined,
-            });
-        }
-        if (noreturn_inst) |some| try block.instructions.append(sema.gpa, some);
-    }
-
     // We may have overwritten the capture scope due to a `repeat` instruction where
     // the body had a capture; restore it now.
     block.wip_capture_scope = parent_capture_scope;
@@ -5757,7 +5737,7 @@ fn zirLoop(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileError
         );
         sema.air_extra.appendSliceAssumeCapacity(@ptrCast(loop_block.instructions.items));
     }
-    return sema.analyzeBlockBody(parent_block, src, &child_block, merges);
+    return sema.analyzeBlockBody(parent_block, src, &child_block, merges, false);
 }
 
 fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
@@ -5945,13 +5925,31 @@ fn resolveBlockBody(
     if (child_block.is_comptime) {
         return sema.resolveBody(child_block, body, body_inst);
     } else {
+        var need_debug_scope = false;
+        child_block.need_debug_scope = &need_debug_scope;
         if (sema.analyzeBodyInner(child_block, body)) |_| {
-            return sema.analyzeBlockBody(parent_block, src, child_block, merges);
+            return sema.analyzeBlockBody(parent_block, src, child_block, merges, need_debug_scope);
         } else |err| switch (err) {
             error.ComptimeBreak => {
                 // Comptime control flow is happening, however child_block may still contain
                 // runtime instructions which need to be copied to the parent block.
-                try parent_block.instructions.appendSlice(sema.gpa, child_block.instructions.items);
+                if (need_debug_scope and child_block.instructions.items.len > 0) {
+                    // We need a runtime block for scoping reasons.
+                    _ = try child_block.addBr(merges.block_inst, .void_value);
+                    try parent_block.instructions.append(sema.gpa, merges.block_inst);
+                    try sema.air_extra.ensureUnusedCapacity(sema.gpa, @typeInfo(Air.Block).Struct.fields.len +
+                        child_block.instructions.items.len);
+                    sema.air_instructions.items(.data)[@intFromEnum(merges.block_inst)] = .{ .ty_pl = .{
+                        .ty = .void_type,
+                        .payload = sema.addExtraAssumeCapacity(Air.Block{
+                            .body_len = @intCast(child_block.instructions.items.len),
+                        }),
+                    } };
+                    sema.air_extra.appendSliceAssumeCapacity(@ptrCast(child_block.instructions.items));
+                } else {
+                    // We can copy instructions directly to the parent block.
+                    try parent_block.instructions.appendSlice(sema.gpa, child_block.instructions.items);
+                }
 
                 const break_inst = sema.comptime_break_inst;
                 const break_data = sema.code.instructions.items(.data)[@intFromEnum(break_inst)].@"break";
@@ -5973,6 +5971,7 @@ fn analyzeBlockBody(
     src: LazySrcLoc,
     child_block: *Block,
     merges: *Block.Merges,
+    need_debug_scope: bool,
 ) CompileError!Air.Inst.Ref {
     const tracy = trace(@src());
     defer tracy.end();
@@ -5987,25 +5986,57 @@ fn analyzeBlockBody(
     if (merges.results.items.len == 0) {
         // No need for a block instruction. We can put the new instructions
         // directly into the parent block.
+        if (need_debug_scope) {
+            // The code following this block is unreachable, as the block has no
+            // merges, so we don't necessarily need to emit this as an AIR block.
+            // However, we need a block *somewhere* to make the scoping correct,
+            // so forward this request to the parent block.
+            if (parent_block.need_debug_scope) |ptr| ptr.* = true;
+        }
         try parent_block.instructions.appendSlice(gpa, child_block.instructions.items);
         return child_block.instructions.items[child_block.instructions.items.len - 1].toRef();
     }
     if (merges.results.items.len == 1) {
-        const last_inst_index = child_block.instructions.items.len - 1;
-        const last_inst = child_block.instructions.items[last_inst_index];
-        if (sema.getBreakBlock(last_inst)) |br_block| {
-            if (br_block == merges.block_inst) {
-                // No need for a block instruction. We can put the new instructions directly
-                // into the parent block. Here we omit the break instruction.
-                const without_break = child_block.instructions.items[0..last_inst_index];
-                try parent_block.instructions.appendSlice(gpa, without_break);
-                return merges.results.items[0];
-            }
+        // If the `break` is trailing, we may be able to elide the AIR block here
+        // by appending the new instructions directly to the parent block.
+        if (!need_debug_scope) {
+            const last_inst_index = child_block.instructions.items.len - 1;
+            const last_inst = child_block.instructions.items[last_inst_index];
+            if (sema.getBreakBlock(last_inst)) |br_block| {
+                if (br_block == merges.block_inst) {
+                    // Great, the last instruction is the break! Put the instructions
+                    // directly into the parent block.
+                    try parent_block.instructions.appendSlice(gpa, child_block.instructions.items[0..last_inst_index]);
+                    return merges.results.items[0];
+                }
+            }
+        }
+        // Okay, we need a runtime block. If the value is comptime-known, the
+        // block should just return void, and we return the merge result
+        // directly. Otherwise, we can defer to the logic below.
+        if (try sema.resolveValue(merges.results.items[0])) |result_val| {
+            // Create a block containing all instruction from the body.
+            try parent_block.instructions.append(gpa, merges.block_inst);
+            try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.Block).Struct.fields.len +
+                child_block.instructions.items.len);
+            sema.air_instructions.items(.data)[@intFromEnum(merges.block_inst)] = .{ .ty_pl = .{
+                .ty = .void_type,
+                .payload = sema.addExtraAssumeCapacity(Air.Block{
+                    .body_len = @intCast(child_block.instructions.items.len),
+                }),
+            } };
+            sema.air_extra.appendSliceAssumeCapacity(@ptrCast(child_block.instructions.items));
+            // Rewrite the break to just give value {}; the value is
+            // comptime-known and will be returned directly.
+            sema.air_instructions.items(.data)[@intFromEnum(merges.br_list.items[0])].br.operand = .void_value;
+            return Air.internedToRef(result_val.toIntern());
         }
     }
     // It is impossible to have the number of results be > 1 in a comptime scope.
     assert(!child_block.is_comptime); // Should already got a compile error in the condbr condition.
 
+    // Note that we'll always create an AIR block here, so `need_debug_scope` is irrelevant.
+
     // Need to set the type and emit the Block instruction. This allows machine code generation
     // to emit a jump instruction to after the block when it encounters the break.
     try parent_block.instructions.append(gpa, merges.block_inst);
@@ -6383,24 +6414,6 @@ fn zirDbgStmt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!voi
     });
 }
 
-fn zirDbgBlockBegin(block: *Block) CompileError!void {
-    if (block.is_comptime or block.ownerModule().strip) return;
-
-    _ = try block.addInst(.{
-        .tag = .dbg_block_begin,
-        .data = undefined,
-    });
-}
-
-fn zirDbgBlockEnd(block: *Block) CompileError!void {
-    if (block.is_comptime or block.ownerModule().strip) return;
-
-    _ = try block.addInst(.{
-        .tag = .dbg_block_end,
-        .data = undefined,
-    });
-}
-
 fn zirDbgVar(
     sema: *Sema,
     block: *Block,
@@ -6432,6 +6445,15 @@ fn addDbgVar(
     if (try sema.typeRequiresComptime(val_ty)) return;
     if (!(try sema.typeHasRuntimeBits(val_ty))) return;
 
+    // To ensure the lexical scoping is known to backends, this alloc must be
+    // within a real runtime block. We set a flag which communicates information
+    // to the closest lexically enclosing block:
+    // * If it is a `block_inline`, communicates to logic in `analyzeBodyInner`
+    //   to create a post-hoc block.
+    // * Otherwise, communicates to logic in `resolveBlockBody` to create a
+    //   real `block` instruction.
+    if (block.need_debug_scope) |ptr| ptr.* = true;
+
     try sema.queueFullTypeResolution(operand_ty);
 
     // Add the name to the AIR.
@@ -7585,7 +7607,7 @@ fn analyzeCall(
                     error.ComptimeReturn => break :result inlining.comptime_result,
                     else => |e| return e,
                 };
-                break :result try sema.analyzeBlockBody(block, call_src, &child_block, merges);
+                break :result try sema.analyzeBlockBody(block, call_src, &child_block, merges, false);
             };
 
             if (!is_comptime_call and !block.is_typeof and
@@ -11528,7 +11550,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
     sema.air_extra.appendSliceAssumeCapacity(@ptrCast(true_instructions));
     sema.air_extra.appendSliceAssumeCapacity(@ptrCast(sub_block.instructions.items));
 
-    return sema.analyzeBlockBody(block, main_src, &child_block, merges);
+    return sema.analyzeBlockBody(block, main_src, &child_block, merges, false);
 }
 
 fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_ref: bool) CompileError!Air.Inst.Ref {
@@ -12150,7 +12172,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         false,
     );
 
-    return sema.analyzeBlockBody(block, src, &child_block, merges);
+    return sema.analyzeBlockBody(block, src, &child_block, merges, false);
 }
 
 const SpecialProng = struct {
@@ -13163,8 +13185,6 @@ fn validateErrSetSwitch(
                 const tags = sema.code.instructions.items(.tag);
                 const datas = sema.code.instructions.items(.data);
                 for (else_case.body) |else_inst| switch (tags[@intFromEnum(else_inst)]) {
-                    .dbg_block_begin,
-                    .dbg_block_end,
                     .dbg_stmt,
                     .dbg_var_val,
                     .ret_type,
@@ -13416,8 +13436,6 @@ fn maybeErrorUnwrap(
             .@"unreachable" => if (!block.wantSafety()) return false,
             .err_union_code => if (!allow_err_code_inst) return false,
             .save_err_ret_index,
-            .dbg_block_begin,
-            .dbg_block_end,
             .dbg_stmt,
             .str,
             .as_node,
@@ -13430,10 +13448,7 @@ fn maybeErrorUnwrap(
 
     for (body) |inst| {
         const air_inst = switch (tags[@intFromEnum(inst)]) {
-            .dbg_block_begin,
-            .dbg_block_end,
-            .err_union_code,
-            => continue,
+            .err_union_code => continue,
             .dbg_stmt => {
                 try sema.zirDbgStmt(block, inst);
                 continue;
@@ -13499,8 +13514,6 @@ fn maybeErrorUnwrapComptime(sema: *Sema, block: *Block, body: []const Zir.Inst.I
     const tags = sema.code.instructions.items(.tag);
     const inst = for (body) |inst| {
         switch (tags[@intFromEnum(inst)]) {
-            .dbg_block_begin,
-            .dbg_block_end,
             .dbg_stmt,
             .save_err_ret_index,
             => {},
@@ -19092,55 +19105,64 @@ fn zirTryPtr(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileErr
     return try_inst;
 }
 
+fn ensurePostHoc(sema: *Sema, block: *Block, dest_block: Zir.Inst.Index) !*LabeledBlock {
+    const gop = sema.inst_map.getOrPutAssumeCapacity(dest_block);
+    if (gop.found_existing) existing: {
+        // This may be a *result* from an earlier iteration of an inline loop.
+        // In this case, there will not be a post-hoc block entry, and we can
+        // continue with the logic below.
+        const new_block_inst = gop.value_ptr.*.toIndex() orelse break :existing;
+        return sema.post_hoc_blocks.get(new_block_inst) orelse break :existing;
+    }
+
+    try sema.post_hoc_blocks.ensureUnusedCapacity(sema.gpa, 1);
+
+    const new_block_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len);
+    gop.value_ptr.* = new_block_inst.toRef();
+    try sema.air_instructions.append(sema.gpa, .{
+        .tag = .block,
+        .data = undefined,
+    });
+    const labeled_block = try sema.gpa.create(LabeledBlock);
+    labeled_block.* = .{
+        .label = .{
+            .zir_block = dest_block,
+            .merges = .{
+                .src_locs = .{},
+                .results = .{},
+                .br_list = .{},
+                .block_inst = new_block_inst,
+            },
+        },
+        .block = .{
+            .parent = block,
+            .sema = sema,
+            .src_decl = block.src_decl,
+            .namespace = block.namespace,
+            .wip_capture_scope = block.wip_capture_scope,
+            .instructions = .{},
+            .label = &labeled_block.label,
+            .inlining = block.inlining,
+            .is_comptime = block.is_comptime,
+        },
+    };
+    sema.post_hoc_blocks.putAssumeCapacityNoClobber(new_block_inst, labeled_block);
+    return labeled_block;
+}
+
 // A `break` statement is inside a runtime condition, but trying to
 // break from an inline loop. In such case we must convert it to
 // a runtime break.
 fn addRuntimeBreak(sema: *Sema, child_block: *Block, break_data: BreakData) !void {
-    const gop = sema.inst_map.getOrPutAssumeCapacity(break_data.block_inst);
-    const labeled_block = if (!gop.found_existing) blk: {
-        try sema.post_hoc_blocks.ensureUnusedCapacity(sema.gpa, 1);
-
-        const new_block_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len);
-        gop.value_ptr.* = new_block_inst.toRef();
-        try sema.air_instructions.append(sema.gpa, .{
-            .tag = .block,
-            .data = undefined,
-        });
-        const labeled_block = try sema.gpa.create(LabeledBlock);
-        labeled_block.* = .{
-            .label = .{
-                .zir_block = break_data.block_inst,
-                .merges = .{
-                    .src_locs = .{},
-                    .results = .{},
-                    .br_list = .{},
-                    .block_inst = new_block_inst,
-                },
-            },
-            .block = .{
-                .parent = child_block,
-                .sema = sema,
-                .src_decl = child_block.src_decl,
-                .namespace = child_block.namespace,
-                .wip_capture_scope = child_block.wip_capture_scope,
-                .instructions = .{},
-                .label = &labeled_block.label,
-                .inlining = child_block.inlining,
-                .is_comptime = child_block.is_comptime,
-            },
-        };
-        sema.post_hoc_blocks.putAssumeCapacityNoClobber(new_block_inst, labeled_block);
-        break :blk labeled_block;
-    } else blk: {
-        const new_block_inst = gop.value_ptr.*.toIndex().?;
-        const labeled_block = sema.post_hoc_blocks.get(new_block_inst).?;
-        break :blk labeled_block;
-    };
+    const labeled_block = try sema.ensurePostHoc(child_block, break_data.block_inst);
 
     const operand = try sema.resolveInst(break_data.operand);
     const br_ref = try child_block.addBr(labeled_block.label.merges.block_inst, operand);
+
     try labeled_block.label.merges.results.append(sema.gpa, operand);
     try labeled_block.label.merges.br_list.append(sema.gpa, br_ref.toIndex().?);
+    try labeled_block.label.merges.src_locs.append(sema.gpa, null);
+
     labeled_block.block.runtime_index.increment();
     if (labeled_block.block.runtime_cond == null and labeled_block.block.runtime_loop == null) {
         labeled_block.block.runtime_cond = child_block.runtime_cond orelse child_block.runtime_loop;
@@ -19481,8 +19503,10 @@ fn analyzeRet(
             return error.ComptimeReturn;
         }
         // We are inlining a function call; rewrite the `ret` as a `break`.
+        const br_inst = try block.addBr(inlining.merges.block_inst, operand);
         try inlining.merges.results.append(sema.gpa, operand);
-        _ = try block.addBr(inlining.merges.block_inst, operand);
+        try inlining.merges.br_list.append(sema.gpa, br_inst.toIndex().?);
+        try inlining.merges.src_locs.append(sema.gpa, operand_src);
         return always_noreturn;
     } else if (block.is_comptime) {
         return sema.fail(block, src, "function called at runtime cannot return value at comptime", .{});
src/Zir.zig
@@ -392,10 +392,6 @@ pub const Inst = struct {
         /// Same as `dbg_var_ptr` but the local is always a const and the operand
         /// is the local's value.
         dbg_var_val,
-        /// Marks the beginning of a semantic scope for debug info variables.
-        dbg_block_begin,
-        /// Marks the end of a semantic scope for debug info variables.
-        dbg_block_end,
         /// Uses a name to identify a Decl and takes a pointer to it.
         /// Uses the `str_tok` union field.
         decl_ref,
@@ -1107,8 +1103,6 @@ pub const Inst = struct {
                 .dbg_stmt,
                 .dbg_var_ptr,
                 .dbg_var_val,
-                .dbg_block_begin,
-                .dbg_block_end,
                 .decl_ref,
                 .decl_val,
                 .load,
@@ -1335,8 +1329,6 @@ pub const Inst = struct {
                 .dbg_stmt,
                 .dbg_var_ptr,
                 .dbg_var_val,
-                .dbg_block_begin,
-                .dbg_block_end,
                 .ensure_result_used,
                 .ensure_result_non_error,
                 .ensure_err_union_payload_void,
@@ -1663,8 +1655,6 @@ pub const Inst = struct {
                 .dbg_stmt = .dbg_stmt,
                 .dbg_var_ptr = .str_op,
                 .dbg_var_val = .str_op,
-                .dbg_block_begin = .tok,
-                .dbg_block_end = .tok,
                 .decl_ref = .str_tok,
                 .decl_val = .str_tok,
                 .load = .un_node,
test/behavior/eval.zig
@@ -1714,3 +1714,21 @@ test "const with specified type initialized with typed array is comptime-known"
     comptime assert(x[1] == 2);
     comptime assert(x[2] == 3);
 }
+
+test "block with comptime-known result but possible runtime exit is comptime-known" {
+    var t: bool = true;
+    _ = &t;
+
+    const a: comptime_int = a: {
+        if (!t) return error.TestFailed;
+        break :a 123;
+    };
+
+    const b: comptime_int = b: {
+        if (t) break :b 456;
+        return error.TestFailed;
+    };
+
+    comptime assert(a == 123);
+    comptime assert(b == 456);
+}