Commit 6026a5f217

mlugg <mlugg@mlugg.co.uk>
2024-12-29 17:24:07
compiler: ensure result of `block_comptime` is comptime-known
To avoid this PR regressing error messages, most of the work here has gone towards improving error notes for why code was comptime-evaluated. ZIR `block_comptime` now stores a "comptime reason", the enum for which is also used by Sema. There are two types in Sema: * `ComptimeReason` represents the reason we started evaluating something at comptime. * `BlockComptimeReason` represents the reason a given block is evaluated at comptime; it's either a `ComptimeReason` with an attached source location, or it's because we're in a function which was called at comptime (and that function's `Block` should be consulted for the "parent" reason). Every `Block` stores a `?BlockComptimeReason`. The old `is_comptime` field is replaced with a trivial `isComptime()` method which returns whether that reason is non-`null`. Lastly, the handling for `block_comptime` has been simplified. It was previously going through an unnecessary runtime-handling path; now, it is a trivial sub block exited through a `break_inline` instruction. Resolves: #22296
1 parent 6d67658
lib/std/zig/AstGen.zig
@@ -97,6 +97,7 @@ fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void {
             Zir.Inst.Ref,
             Zir.Inst.Index,
             Zir.Inst.Declaration.Name,
+            std.zig.SimpleComptimeReason,
             Zir.NullTerminatedString,
             => @intFromEnum(@field(extra, field.name)),
 
@@ -379,7 +380,7 @@ const coerced_type_ri: ResultInfo = .{ .rl = .{ .coerced_ty = .type_type } };
 const coerced_bool_ri: ResultInfo = .{ .rl = .{ .coerced_ty = .bool_type } };
 
 fn typeExpr(gz: *GenZir, scope: *Scope, type_node: Ast.Node.Index) InnerError!Zir.Inst.Ref {
-    return comptimeExpr(gz, scope, coerced_type_ri, type_node);
+    return comptimeExpr(gz, scope, coerced_type_ri, type_node, .type);
 }
 
 fn reachableTypeExpr(
@@ -388,7 +389,7 @@ fn reachableTypeExpr(
     type_node: Ast.Node.Index,
     reachable_node: Ast.Node.Index,
 ) InnerError!Zir.Inst.Ref {
-    return reachableExprComptime(gz, scope, coerced_type_ri, type_node, reachable_node, true);
+    return reachableExprComptime(gz, scope, coerced_type_ri, type_node, reachable_node, .type);
 }
 
 /// Same as `expr` but fails with a compile error if the result type is `noreturn`.
@@ -399,7 +400,7 @@ fn reachableExpr(
     node: Ast.Node.Index,
     reachable_node: Ast.Node.Index,
 ) InnerError!Zir.Inst.Ref {
-    return reachableExprComptime(gz, scope, ri, node, reachable_node, false);
+    return reachableExprComptime(gz, scope, ri, node, reachable_node, null);
 }
 
 fn reachableExprComptime(
@@ -408,10 +409,11 @@ fn reachableExprComptime(
     ri: ResultInfo,
     node: Ast.Node.Index,
     reachable_node: Ast.Node.Index,
-    force_comptime: bool,
+    /// If `null`, the expression is not evaluated in a comptime context.
+    comptime_reason: ?std.zig.SimpleComptimeReason,
 ) InnerError!Zir.Inst.Ref {
-    const result_inst = if (force_comptime)
-        try comptimeExpr(gz, scope, ri, node)
+    const result_inst = if (comptime_reason) |r|
+        try comptimeExpr(gz, scope, ri, node, r)
     else
         try expr(gz, scope, ri, node);
 
@@ -782,7 +784,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
             const result = try gz.addPlNode(.array_mul, node, Zir.Inst.ArrayMul{
                 .res_ty = if (try ri.rl.resultType(gz, node)) |t| t else .none,
                 .lhs = try expr(gz, scope, .{ .rl = .none }, node_datas[node].lhs),
-                .rhs = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, node_datas[node].rhs),
+                .rhs = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, node_datas[node].rhs, .array_mul_factor),
             });
             return rvalue(gz, ri, result, node);
         },
@@ -1453,7 +1455,7 @@ fn arrayInitExpr(
                     });
                     break :inst .{ array_type_inst, elem_type };
                 } else {
-                    const sentinel = try comptimeExpr(gz, scope, .{ .rl = .{ .ty = elem_type } }, array_type.ast.sentinel);
+                    const sentinel = try comptimeExpr(gz, scope, .{ .rl = .{ .ty = elem_type } }, array_type.ast.sentinel, .array_sentinel);
                     const array_type_inst = try gz.addPlNode(
                         .array_type_sentinel,
                         array_init.ast.type_expr,
@@ -1721,7 +1723,7 @@ fn structInitExpr(
                         .rhs = elem_type,
                     });
                 } else blk: {
-                    const sentinel = try comptimeExpr(gz, scope, .{ .rl = .{ .ty = elem_type } }, array_type.ast.sentinel);
+                    const sentinel = try comptimeExpr(gz, scope, .{ .rl = .{ .ty = elem_type } }, array_type.ast.sentinel, .array_sentinel);
                     break :blk try gz.addPlNode(
                         .array_type_sentinel,
                         struct_init.ast.type_expr,
@@ -1966,6 +1968,20 @@ fn comptimeExpr(
     scope: *Scope,
     ri: ResultInfo,
     node: Ast.Node.Index,
+    reason: std.zig.SimpleComptimeReason,
+) InnerError!Zir.Inst.Ref {
+    return comptimeExpr2(gz, scope, ri, node, node, reason);
+}
+
+/// Like `comptimeExpr`, but draws a distinction between `node`, the expression to evaluate at comptime,
+/// and `src_node`, the node to attach to the `block_comptime`.
+fn comptimeExpr2(
+    gz: *GenZir,
+    scope: *Scope,
+    ri: ResultInfo,
+    node: Ast.Node.Index,
+    src_node: Ast.Node.Index,
+    reason: std.zig.SimpleComptimeReason,
 ) InnerError!Zir.Inst.Ref {
     if (gz.is_comptime) {
         // No need to change anything!
@@ -2049,23 +2065,23 @@ fn comptimeExpr(
     block_scope.is_comptime = true;
     defer block_scope.unstack();
 
-    const block_inst = try gz.makeBlockInst(.block_comptime, node);
+    const block_inst = try gz.makeBlockInst(.block_comptime, src_node);
     // Replace result location and copy back later - see above.
     const ty_only_ri: ResultInfo = .{
         .ctx = ri.ctx,
-        .rl = if (try ri.rl.resultType(gz, node)) |res_ty|
+        .rl = if (try ri.rl.resultType(gz, src_node)) |res_ty|
             .{ .coerced_ty = res_ty }
         else
             .none,
     };
     const block_result = try fullBodyExpr(&block_scope, scope, ty_only_ri, node, .normal);
     if (!gz.refIsNoReturn(block_result)) {
-        _ = try block_scope.addBreak(.@"break", block_inst, block_result);
+        _ = try block_scope.addBreak(.break_inline, block_inst, block_result);
     }
-    try block_scope.setBlockBody(block_inst);
+    try block_scope.setBlockComptimeBody(block_inst, reason);
     try gz.instructions.append(gz.astgen.gpa, block_inst);
 
-    return rvalue(gz, ri, block_inst.toRef(), node);
+    return rvalue(gz, ri, block_inst.toRef(), src_node);
 }
 
 /// This one is for an actual `comptime` syntax, and will emit a compile error if
@@ -2084,7 +2100,7 @@ fn comptimeExprAst(
     const tree = astgen.tree;
     const node_datas = tree.nodes.items(.data);
     const body_node = node_datas[node].lhs;
-    return comptimeExpr(gz, scope, ri, body_node);
+    return comptimeExpr2(gz, scope, ri, body_node, node, .comptime_keyword);
 }
 
 /// Restore the error return trace index. Performs the restore only if the result is a non-error or
@@ -2494,10 +2510,10 @@ fn labeledBlockExpr(
 
     // Reserve the Block ZIR instruction index so that we can put it into the GenZir struct
     // so that break statements can reference it.
-    const block_tag: Zir.Inst.Tag = if (force_comptime) .block_comptime else .block;
-    const block_inst = try gz.makeBlockInst(block_tag, block_node);
+    const block_inst = try gz.makeBlockInst(if (force_comptime) .block_comptime else .block, block_node);
     try gz.instructions.append(astgen.gpa, block_inst);
     var block_scope = gz.makeSubBlock(parent_scope);
+    block_scope.is_inline = force_comptime;
     block_scope.label = GenZir.Label{
         .token = label_token,
         .block_inst = block_inst,
@@ -2511,14 +2527,20 @@ fn labeledBlockExpr(
         // As our last action before the return, "pop" the error trace if needed
         _ = try gz.addRestoreErrRetIndex(.{ .block = block_inst }, .always, block_node);
         const result = try rvalue(gz, block_scope.break_result_info, .void_value, block_node);
-        _ = try block_scope.addBreak(.@"break", block_inst, result);
+        const break_tag: Zir.Inst.Tag = if (force_comptime) .break_inline else .@"break";
+        _ = try block_scope.addBreak(break_tag, block_inst, result);
     }
 
     if (!block_scope.label.?.used) {
         try astgen.appendErrorTok(label_token, "unused block label", .{});
     }
 
-    try block_scope.setBlockBody(block_inst);
+    if (force_comptime) {
+        try block_scope.setBlockComptimeBody(block_inst, .comptime_keyword);
+    } else {
+        try block_scope.setBlockBody(block_inst);
+    }
+
     if (need_result_rvalue) {
         return rvalue(gz, ri, block_inst.toRef(), block_node);
     } else {
@@ -3255,7 +3277,7 @@ fn varDecl(
                 } else .{ .rl = .none, .ctx = .const_init };
                 const prev_anon_name_strategy = gz.anon_name_strategy;
                 gz.anon_name_strategy = .dbg_var;
-                const init_inst = try reachableExprComptime(gz, scope, result_info, var_decl.ast.init_node, node, force_comptime);
+                const init_inst = try reachableExprComptime(gz, scope, result_info, var_decl.ast.init_node, node, if (force_comptime) .comptime_keyword else null);
                 gz.anon_name_strategy = prev_anon_name_strategy;
 
                 try gz.addDbgVar(.dbg_var_val, ident_name, init_inst);
@@ -3321,7 +3343,7 @@ fn varDecl(
             const prev_anon_name_strategy = gz.anon_name_strategy;
             gz.anon_name_strategy = .dbg_var;
             defer gz.anon_name_strategy = prev_anon_name_strategy;
-            const init_inst = try reachableExprComptime(gz, scope, init_result_info, var_decl.ast.init_node, node, force_comptime);
+            const init_inst = try reachableExprComptime(gz, scope, init_result_info, var_decl.ast.init_node, node, if (force_comptime) .comptime_keyword else null);
 
             // The const init expression may have modified the error return trace, so signal
             // to Sema that it should save the new index for restoring later.
@@ -3393,7 +3415,14 @@ fn varDecl(
             };
             const prev_anon_name_strategy = gz.anon_name_strategy;
             gz.anon_name_strategy = .dbg_var;
-            _ = try reachableExprComptime(gz, scope, result_info, var_decl.ast.init_node, node, is_comptime);
+            _ = try reachableExprComptime(
+                gz,
+                scope,
+                result_info,
+                var_decl.ast.init_node,
+                node,
+                if (var_decl.comptime_token != null) .comptime_keyword else null,
+            );
             gz.anon_name_strategy = prev_anon_name_strategy;
             const final_ptr: Zir.Inst.Ref = if (resolve_inferred) ptr: {
                 break :ptr try gz.addUnNode(.resolve_inferred_alloc, alloc, node);
@@ -3501,8 +3530,8 @@ fn assignDestructure(gz: *GenZir, scope: *Scope, node: Ast.Node.Index) InnerErro
 
     if (full.comptime_token) |_| {
         const comptime_block_inst = try gz.makeBlockInst(.block_comptime, node);
-        _ = try inner_gz.addBreak(.@"break", comptime_block_inst, .void_value);
-        try inner_gz.setBlockBody(comptime_block_inst);
+        _ = try inner_gz.addBreak(.break_inline, comptime_block_inst, .void_value);
+        try inner_gz.setBlockComptimeBody(comptime_block_inst, .comptime_keyword);
         try gz.instructions.append(gz.astgen.gpa, comptime_block_inst);
     }
 }
@@ -3673,8 +3702,8 @@ fn assignDestructureMaybeDecls(
         // Finish the block_comptime. Inferred alloc resolution etc will occur
         // in the parent block.
         const comptime_block_inst = try gz.makeBlockInst(.block_comptime, node);
-        _ = try inner_gz.addBreak(.@"break", comptime_block_inst, .void_value);
-        try inner_gz.setBlockBody(comptime_block_inst);
+        _ = try inner_gz.addBreak(.break_inline, comptime_block_inst, .void_value);
+        try inner_gz.setBlockComptimeBody(comptime_block_inst, .comptime_keyword);
         try gz.instructions.append(gz.astgen.gpa, comptime_block_inst);
     }
 
@@ -3867,7 +3896,16 @@ fn ptrType(
         gz.astgen.source_line = source_line;
         gz.astgen.source_column = source_column;
 
-        sentinel_ref = try comptimeExpr(gz, scope, .{ .rl = .{ .ty = elem_type } }, ptr_info.ast.sentinel);
+        sentinel_ref = try comptimeExpr(
+            gz,
+            scope,
+            .{ .rl = .{ .ty = elem_type } },
+            ptr_info.ast.sentinel,
+            switch (ptr_info.size) {
+                .Slice => .slice_sentinel,
+                else => .pointer_sentinel,
+            },
+        );
         trailing_count += 1;
     }
     if (ptr_info.ast.addrspace_node != 0) {
@@ -3953,7 +3991,7 @@ fn arrayType(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) !
     {
         return astgen.failNode(len_node, "unable to infer array size", .{});
     }
-    const len = try expr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, len_node);
+    const len = try reachableExprComptime(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, len_node, node, .type);
     const elem_type = try typeExpr(gz, scope, node_datas[node].rhs);
 
     const result = try gz.addPlNode(.array_type, node, Zir.Inst.Bin{
@@ -3977,9 +4015,9 @@ fn arrayTypeSentinel(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.
     {
         return astgen.failNode(len_node, "unable to infer array size", .{});
     }
-    const len = try reachableExpr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, len_node, node);
+    const len = try reachableExprComptime(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, len_node, node, .array_length);
     const elem_type = try typeExpr(gz, scope, extra.elem_type);
-    const sentinel = try reachableExprComptime(gz, scope, .{ .rl = .{ .coerced_ty = elem_type } }, extra.sentinel, node, true);
+    const sentinel = try reachableExprComptime(gz, scope, .{ .rl = .{ .coerced_ty = elem_type } }, extra.sentinel, node, .array_sentinel);
 
     const result = try gz.addPlNode(.array_type_sentinel, node, Zir.Inst.ArrayTypeSentinel{
         .len = len,
@@ -5321,7 +5359,7 @@ fn tupleDecl(
         astgen.scratch.appendAssumeCapacity(@intFromEnum(field_type_ref));
 
         if (field.ast.value_expr != 0) {
-            const field_init_ref = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = field_type_ref } }, field.ast.value_expr);
+            const field_init_ref = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = field_type_ref } }, field.ast.value_expr, .tuple_field_default_value);
             astgen.scratch.appendAssumeCapacity(@intFromEnum(field_init_ref));
         } else {
             astgen.scratch.appendAssumeCapacity(@intFromEnum(Zir.Inst.Ref.none));
@@ -5693,7 +5731,7 @@ fn containerDecl(
             namespace.base.tag = .namespace;
 
             const arg_inst: Zir.Inst.Ref = if (container_decl.ast.arg != 0)
-                try comptimeExpr(&block_scope, &namespace.base, coerced_type_ri, container_decl.ast.arg)
+                try comptimeExpr(&block_scope, &namespace.base, coerced_type_ri, container_decl.ast.arg, .type)
             else
                 .none;
 
@@ -7573,7 +7611,7 @@ fn switchExprErrUnion(
                 if (node_tags[item_node] == .switch_range) continue;
                 items_len += 1;
 
-                const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node);
+                const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item);
                 try payloads.append(gpa, @intFromEnum(item_inst));
             }
 
@@ -7583,8 +7621,8 @@ fn switchExprErrUnion(
                 if (node_tags[range] != .switch_range) continue;
                 ranges_len += 1;
 
-                const first = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].lhs);
-                const last = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].rhs);
+                const first = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].lhs, .switch_item);
+                const last = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].rhs, .switch_item);
                 try payloads.appendSlice(gpa, &[_]u32{
                     @intFromEnum(first), @intFromEnum(last),
                 });
@@ -7602,7 +7640,7 @@ fn switchExprErrUnion(
             scalar_case_index += 1;
             try payloads.resize(gpa, header_index + 2); // item, body_len
             const item_node = case.ast.values[0];
-            const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node);
+            const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item);
             payloads.items[header_index] = @intFromEnum(item_inst);
             break :blk header_index + 1;
         };
@@ -8046,7 +8084,7 @@ fn switchExpr(
                 if (node_tags[item_node] == .switch_range) continue;
                 items_len += 1;
 
-                const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node);
+                const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item);
                 try payloads.append(gpa, @intFromEnum(item_inst));
             }
 
@@ -8056,8 +8094,8 @@ fn switchExpr(
                 if (node_tags[range] != .switch_range) continue;
                 ranges_len += 1;
 
-                const first = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].lhs);
-                const last = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].rhs);
+                const first = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].lhs, .switch_item);
+                const last = try comptimeExpr(parent_gz, scope, item_ri, node_datas[range].rhs, .switch_item);
                 try payloads.appendSlice(gpa, &[_]u32{
                     @intFromEnum(first), @intFromEnum(last),
                 });
@@ -8075,7 +8113,7 @@ fn switchExpr(
             scalar_case_index += 1;
             try payloads.resize(gpa, header_index + 2); // item, body_len
             const item_node = case.ast.values[0];
-            const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node);
+            const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item);
             payloads.items[header_index] = @intFromEnum(item_inst);
             break :blk header_index + 1;
         };
@@ -8836,7 +8874,7 @@ fn asmExpr(
         },
         else => .{
             .tag = .asm_expr,
-            .tmpl = @enumFromInt(@intFromEnum(try comptimeExpr(gz, scope, .{ .rl = .none }, full.ast.template))),
+            .tmpl = @enumFromInt(@intFromEnum(try comptimeExpr(gz, scope, .{ .rl = .none }, full.ast.template, .inline_assembly_code))),
         },
     };
 
@@ -8973,7 +9011,7 @@ fn unionInit(
     params: []const Ast.Node.Index,
 ) InnerError!Zir.Inst.Ref {
     const union_type = try typeExpr(gz, scope, params[0]);
-    const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1]);
+    const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .union_field_name);
     const field_type = try gz.addPlNode(.field_type_ref, node, Zir.Inst.FieldTypeRef{
         .container_type = union_type,
         .field_name = field_name,
@@ -9078,7 +9116,7 @@ fn ptrCast(
                     const flags_int: FlagsInt = @bitCast(flags);
                     const cursor = maybeAdvanceSourceCursorToMainToken(gz, root_node);
                     const parent_ptr_type = try ri.rl.resultTypeForCast(gz, root_node, "@alignCast");
-                    const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, node_datas[node].lhs);
+                    const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, node_datas[node].lhs, .field_name);
                     const field_ptr = try expr(gz, scope, .{ .rl = .none }, node_datas[node].rhs);
                     try emitDbgStmt(gz, cursor);
                     const result = try gz.addExtendedPayloadSmall(.field_parent_ptr, flags_int, Zir.Inst.FieldParentPtr{
@@ -9279,7 +9317,7 @@ fn builtinCall(
                 return astgen.failNode(node, "'@branchHint' must appear as the first statement in a function or conditional branch", .{});
             }
             const hint_ty = try gz.addBuiltinValue(node, .branch_hint);
-            const hint_val = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = hint_ty } }, params[0]);
+            const hint_val = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = hint_ty } }, params[0], .operand_branchHint);
             _ = try gz.addExtendedPayload(.branch_hint, Zir.Inst.UnNode{
                 .node = gz.nodeIndexToRelative(node),
                 .operand = hint_val,
@@ -9326,18 +9364,18 @@ fn builtinCall(
             if (ri.rl == .ref or ri.rl == .ref_coerced_ty) {
                 return gz.addPlNode(.field_ptr_named, node, Zir.Inst.FieldNamed{
                     .lhs = try expr(gz, scope, .{ .rl = .ref }, params[0]),
-                    .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1]),
+                    .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
                 });
             }
             const result = try gz.addPlNode(.field_val_named, node, Zir.Inst.FieldNamed{
                 .lhs = try expr(gz, scope, .{ .rl = .none }, params[0]),
-                .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1]),
+                .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
             });
             return rvalue(gz, ri, result, node);
         },
         .FieldType => {
             const ty_inst = try typeExpr(gz, scope, params[0]);
-            const name_inst = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1]);
+            const name_inst = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name);
             const result = try gz.addPlNode(.field_type_ref, node, Zir.Inst.FieldTypeRef{
                 .container_type = ty_inst,
                 .field_name = name_inst,
@@ -9358,7 +9396,7 @@ fn builtinCall(
         .@"export" => {
             const exported = try expr(gz, scope, .{ .rl = .none }, params[0]);
             const export_options_ty = try gz.addBuiltinValue(node, .export_options);
-            const options = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = export_options_ty } }, params[1]);
+            const options = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = export_options_ty } }, params[1], .export_options);
             _ = try gz.addPlNode(.@"export", node, Zir.Inst.Export{
                 .exported = exported,
                 .options = options,
@@ -9368,7 +9406,7 @@ fn builtinCall(
         .@"extern" => {
             const type_inst = try typeExpr(gz, scope, params[0]);
             const extern_options_ty = try gz.addBuiltinValue(node, .extern_options);
-            const options = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = extern_options_ty } }, params[1]);
+            const options = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = extern_options_ty } }, params[1], .extern_options);
             const result = try gz.addExtendedPayload(.builtin_extern, Zir.Inst.BinNode{
                 .node = gz.nodeIndexToRelative(node),
                 .lhs = type_inst,
@@ -9560,7 +9598,7 @@ fn builtinCall(
         // zig fmt: on
 
         .wasm_memory_size => {
-            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0]);
+            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0], .wasm_memory_index);
             const result = try gz.addExtendedPayload(.wasm_memory_size, Zir.Inst.UnNode{
                 .node = gz.nodeIndexToRelative(node),
                 .operand = operand,
@@ -9568,7 +9606,7 @@ fn builtinCall(
             return rvalue(gz, ri, result, node);
         },
         .wasm_memory_grow => {
-            const index_arg = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0]);
+            const index_arg = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0], .wasm_memory_index);
             const delta_arg = try expr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, params[1]);
             const result = try gz.addExtendedPayload(.wasm_memory_grow, Zir.Inst.BinNode{
                 .node = gz.nodeIndexToRelative(node),
@@ -9579,8 +9617,8 @@ fn builtinCall(
         },
         .c_define => {
             if (!gz.c_import) return gz.astgen.failNode(node, "C define valid only inside C import block", .{});
-            const name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[0]);
-            const value = try comptimeExpr(gz, scope, .{ .rl = .none }, params[1]);
+            const name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[0], .operand_cDefine_macro_name);
+            const value = try comptimeExpr(gz, scope, .{ .rl = .none }, params[1], .operand_cDefine_macro_value);
             const result = try gz.addExtendedPayload(.c_define, Zir.Inst.BinNode{
                 .node = gz.nodeIndexToRelative(node),
                 .lhs = name,
@@ -9666,7 +9704,7 @@ fn builtinCall(
         },
         .call => {
             const call_modifier_ty = try gz.addBuiltinValue(node, .call_modifier);
-            const modifier = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = call_modifier_ty } }, params[0]);
+            const modifier = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = call_modifier_ty } }, params[0], .call_modifier);
             const callee = try expr(gz, scope, .{ .rl = .none }, params[1]);
             const args = try expr(gz, scope, .{ .rl = .none }, params[2]);
             const result = try gz.addPlNode(.builtin_call, node, Zir.Inst.BuiltinCall{
@@ -9682,7 +9720,7 @@ fn builtinCall(
         },
         .field_parent_ptr => {
             const parent_ptr_type = try ri.rl.resultTypeForCast(gz, node, builtin_name);
-            const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[0]);
+            const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[0], .field_name);
             const result = try gz.addExtendedPayloadSmall(.field_parent_ptr, 0, Zir.Inst.FieldParentPtr{
                 .src_node = gz.nodeIndexToRelative(node),
                 .parent_ptr_type = parent_ptr_type,
@@ -9713,7 +9751,7 @@ fn builtinCall(
                 .elem_type = try typeExpr(gz, scope, params[0]),
                 .a = try expr(gz, scope, .{ .rl = .none }, params[1]),
                 .b = try expr(gz, scope, .{ .rl = .none }, params[2]),
-                .mask = try comptimeExpr(gz, scope, .{ .rl = .none }, params[3]),
+                .mask = try comptimeExpr(gz, scope, .{ .rl = .none }, params[3], .operand_shuffle_mask),
             });
             return rvalue(gz, ri, result, node);
         },
@@ -9739,7 +9777,7 @@ fn builtinCall(
         },
         .Vector => {
             const result = try gz.addPlNode(.vector_type, node, Zir.Inst.Bin{
-                .lhs = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0]),
+                .lhs = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0], .type),
                 .rhs = try typeExpr(gz, scope, params[1]),
             });
             return rvalue(gz, ri, result, node);
@@ -9747,7 +9785,7 @@ fn builtinCall(
         .prefetch => {
             const prefetch_options_ty = try gz.addBuiltinValue(node, .prefetch_options);
             const ptr = try expr(gz, scope, .{ .rl = .none }, params[0]);
-            const options = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = prefetch_options_ty } }, params[1]);
+            const options = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = prefetch_options_ty } }, params[1], .prefetch_options);
             _ = try gz.addExtendedPayload(.prefetch, Zir.Inst.BinNode{
                 .node = gz.nodeIndexToRelative(node),
                 .lhs = ptr,
@@ -9785,7 +9823,7 @@ fn builtinCall(
         },
 
         .work_item_id => {
-            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0]);
+            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0], .work_group_dim_index);
             const result = try gz.addExtendedPayload(.work_item_id, Zir.Inst.UnNode{
                 .node = gz.nodeIndexToRelative(node),
                 .operand = operand,
@@ -9793,7 +9831,7 @@ fn builtinCall(
             return rvalue(gz, ri, result, node);
         },
         .work_group_size => {
-            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0]);
+            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0], .work_group_dim_index);
             const result = try gz.addExtendedPayload(.work_group_size, Zir.Inst.UnNode{
                 .node = gz.nodeIndexToRelative(node),
                 .operand = operand,
@@ -9801,7 +9839,7 @@ fn builtinCall(
             return rvalue(gz, ri, result, node);
         },
         .work_group_id => {
-            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0]);
+            const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .u32_type } }, params[0], .work_group_dim_index);
             const result = try gz.addExtendedPayload(.work_group_id, Zir.Inst.UnNode{
                 .node = gz.nodeIndexToRelative(node),
                 .operand = operand,
@@ -9821,7 +9859,13 @@ fn hasDeclOrField(
     tag: Zir.Inst.Tag,
 ) InnerError!Zir.Inst.Ref {
     const container_type = try typeExpr(gz, scope, lhs_node);
-    const name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, rhs_node);
+    const name = try comptimeExpr(
+        gz,
+        scope,
+        .{ .rl = .{ .coerced_ty = .slice_const_u8_type } },
+        rhs_node,
+        if (tag == .has_decl) .decl_name else .field_name,
+    );
     const result = try gz.addPlNode(tag, node, Zir.Inst.Bin{
         .lhs = container_type,
         .rhs = name,
@@ -9874,7 +9918,7 @@ fn simpleUnOp(
 ) InnerError!Zir.Inst.Ref {
     const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
     const operand = if (tag == .compile_error)
-        try comptimeExpr(gz, scope, operand_ri, operand_node)
+        try comptimeExpr(gz, scope, operand_ri, operand_node, .compile_error_string)
     else
         try expr(gz, scope, operand_ri, operand_node);
     switch (tag) {
@@ -9972,7 +10016,13 @@ fn simpleCBuiltin(
 ) InnerError!Zir.Inst.Ref {
     const name: []const u8 = if (tag == .c_undef) "C undef" else "C include";
     if (!gz.c_import) return gz.astgen.failNode(node, "{s} valid only inside C import block", .{name});
-    const operand = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, operand_node);
+    const operand = try comptimeExpr(
+        gz,
+        scope,
+        .{ .rl = .{ .coerced_ty = .slice_const_u8_type } },
+        operand_node,
+        if (tag == .c_undef) .operand_cUndef_macro_name else .operand_cInclude_file_name,
+    );
     _ = try gz.addExtendedPayload(tag, Zir.Inst.UnNode{
         .node = gz.nodeIndexToRelative(node),
         .operand = operand,
@@ -9990,7 +10040,7 @@ fn offsetOf(
     tag: Zir.Inst.Tag,
 ) InnerError!Zir.Inst.Ref {
     const type_inst = try typeExpr(gz, scope, lhs_node);
-    const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, rhs_node);
+    const field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, rhs_node, .field_name);
     const result = try gz.addPlNode(tag, node, Zir.Inst.Bin{
         .lhs = type_inst,
         .rhs = field_name,
@@ -11996,11 +12046,16 @@ const GenZir = struct {
     }
 
     /// Assumes nothing stacked on `gz`. Unstacks `gz`.
+    /// Asserts `inst` is not a `block_comptime`.
     fn setBlockBody(gz: *GenZir, inst: Zir.Inst.Index) !void {
         const astgen = gz.astgen;
         const gpa = astgen.gpa;
         const body = gz.instructionsSlice();
         const body_len = astgen.countBodyLenAfterFixups(body);
+
+        const zir_tags = astgen.instructions.items(.tag);
+        assert(zir_tags[@intFromEnum(inst)] != .block_comptime); // use `setComptimeBlockBody` instead
+
         try astgen.extra.ensureUnusedCapacity(
             gpa,
             @typeInfo(Zir.Inst.Block).@"struct".fields.len + body_len,
@@ -12013,6 +12068,32 @@ const GenZir = struct {
         gz.unstack();
     }
 
+    /// Assumes nothing stacked on `gz`. Unstacks `gz`.
+    /// Asserts `inst` is a `block_comptime`.
+    fn setBlockComptimeBody(gz: *GenZir, inst: Zir.Inst.Index, comptime_reason: std.zig.SimpleComptimeReason) !void {
+        const astgen = gz.astgen;
+        const gpa = astgen.gpa;
+        const body = gz.instructionsSlice();
+        const body_len = astgen.countBodyLenAfterFixups(body);
+
+        const zir_tags = astgen.instructions.items(.tag);
+        assert(zir_tags[@intFromEnum(inst)] == .block_comptime); // use `setBlockBody` instead
+
+        try astgen.extra.ensureUnusedCapacity(
+            gpa,
+            @typeInfo(Zir.Inst.BlockComptime).@"struct".fields.len + body_len,
+        );
+        const zir_datas = astgen.instructions.items(.data);
+        zir_datas[@intFromEnum(inst)].pl_node.payload_index = astgen.addExtraAssumeCapacity(
+            Zir.Inst.BlockComptime{
+                .reason = comptime_reason,
+                .body_len = body_len,
+            },
+        );
+        astgen.appendBodyWithFixups(body);
+        gz.unstack();
+    }
+
     /// Assumes nothing stacked on `gz`. Unstacks `gz`.
     fn setTryBody(gz: *GenZir, inst: Zir.Inst.Index, operand: Zir.Inst.Ref) !void {
         const astgen = gz.astgen;
lib/std/zig/Zir.zig
@@ -78,6 +78,7 @@ pub fn extraData(code: Zir, comptime T: type, index: usize) ExtraData(T) {
             Inst.Ref,
             Inst.Index,
             Inst.Declaration.Name,
+            std.zig.SimpleComptimeReason,
             NullTerminatedString,
             => @enumFromInt(code.extra[i]),
 
@@ -291,7 +292,8 @@ pub const Inst = struct {
         /// Uses the `pl_node` union field. Payload is `Block`.
         block,
         /// Like `block`, but forces full evaluation of its contents at compile-time.
-        /// Uses the `pl_node` union field. Payload is `Block`.
+        /// Exited with `break_inline`.
+        /// Uses the `pl_node` union field. Payload is `BlockComptime`.
         block_comptime,
         /// A list of instructions which are analyzed in the parent context, without
         /// generating a runtime block. Must terminate with an "inline" variant of
@@ -2547,6 +2549,13 @@ pub const Inst = struct {
         body_len: u32,
     };
 
+    /// Trailing:
+    /// * inst: Index // for each `body_len`
+    pub const BlockComptime = struct {
+        reason: std.zig.SimpleComptimeReason,
+        body_len: u32,
+    };
+
     /// Trailing:
     /// * inst: Index // for each `body_len`
     pub const BoolBr = struct {
@@ -4517,7 +4526,6 @@ fn findTrackableInner(
         // Block instructions, recurse over the bodies.
 
         .block,
-        .block_comptime,
         .block_inline,
         .c_import,
         .typeof_builtin,
@@ -4528,6 +4536,12 @@ fn findTrackableInner(
             const body = zir.bodySlice(extra.end, extra.data.body_len);
             return zir.findTrackableBody(gpa, contents, defers, body);
         },
+        .block_comptime => {
+            const inst_data = datas[@intFromEnum(inst)].pl_node;
+            const extra = zir.extraData(Inst.BlockComptime, inst_data.payload_index);
+            const body = zir.bodySlice(extra.end, extra.data.body_len);
+            return zir.findTrackableBody(gpa, contents, defers, body);
+        },
         .condbr, .condbr_inline => {
             const inst_data = datas[@intFromEnum(inst)].pl_node;
             const extra = zir.extraData(Inst.CondBr, inst_data.payload_index);
lib/std/zig.zig
@@ -718,6 +718,165 @@ pub const EnvVar = enum {
     }
 };
 
+pub const SimpleComptimeReason = enum(u32) {
+    // Evaluating at comptime because a builtin operand must be comptime-known.
+    // These messages all mention a specific builtin.
+    operand_Type,
+    operand_setEvalBranchQuota,
+    operand_setFloatMode,
+    operand_branchHint,
+    operand_setRuntimeSafety,
+    operand_embedFile,
+    operand_cImport,
+    operand_cDefine_macro_name,
+    operand_cDefine_macro_value,
+    operand_cInclude_file_name,
+    operand_cUndef_macro_name,
+    operand_shuffle_mask,
+    operand_atomicRmw_operation,
+    operand_reduce_operation,
+
+    // Evaluating at comptime because an operand must be comptime-known.
+    // These messages do not mention a specific builtin (and may not be about a builtin at all).
+    export_target,
+    export_options,
+    extern_options,
+    prefetch_options,
+    call_modifier,
+    compile_error_string,
+    inline_assembly_code,
+    atomic_order,
+    array_mul_factor,
+    slice_cat_operand,
+    comptime_call_target,
+    wasm_memory_index,
+    work_group_dim_index,
+
+    // Evaluating at comptime because types must be comptime-known.
+    // Reasons other than `.type` are just more specific messages.
+    type,
+    array_sentinel,
+    pointer_sentinel,
+    slice_sentinel,
+    array_length,
+    vector_length,
+    error_set_contents,
+    struct_fields,
+    enum_fields,
+    union_fields,
+    function_ret_ty,
+    function_parameters,
+
+    // Evaluating at comptime because decl/field name must be comptime-known.
+    decl_name,
+    field_name,
+    struct_field_name,
+    enum_field_name,
+    union_field_name,
+    tuple_field_name,
+    tuple_field_index,
+
+    // Evaluating at comptime because it is an attribute of a global declaration.
+    container_var_init,
+    @"callconv",
+    @"align",
+    @"addrspace",
+    @"linksection",
+
+    // Miscellaneous reasons.
+    comptime_keyword,
+    comptime_call_modifier,
+    switch_item,
+    tuple_field_default_value,
+    struct_field_default_value,
+    enum_field_tag_value,
+    slice_single_item_ptr_bounds,
+    comptime_param_arg,
+    stored_to_comptime_field,
+    stored_to_comptime_var,
+    casted_to_comptime_enum,
+    casted_to_comptime_int,
+    casted_to_comptime_float,
+    panic_handler,
+
+    pub fn message(r: SimpleComptimeReason) []const u8 {
+        return switch (r) {
+            // zig fmt: off
+            .operand_Type                => "operand to '@Type' must be comptime-known",
+            .operand_setEvalBranchQuota  => "operand to '@setEvalBranchQuota' must be comptime-known",
+            .operand_setFloatMode        => "operand to '@setFloatMode' must be comptime-known",
+            .operand_branchHint          => "operand to '@branchHint' must be comptime-known",
+            .operand_setRuntimeSafety    => "operand to '@setRuntimeSafety' must be comptime-known",
+            .operand_embedFile           => "operand to '@embedFile' must be comptime-known",
+            .operand_cImport             => "operand to '@cImport' is evaluated at comptime",
+            .operand_cDefine_macro_name  => "'@cDefine' macro name must be comptime-known",
+            .operand_cDefine_macro_value => "'@cDefine' macro value must be comptime-known",
+            .operand_cInclude_file_name  => "'@cInclude' file name must be comptime-known",
+            .operand_cUndef_macro_name   => "'@cUndef' macro name must be comptime-known",
+            .operand_shuffle_mask        => "'@shuffle' mask must be comptime-known",
+            .operand_atomicRmw_operation => "'@atomicRmw' operation must be comptime-known",
+            .operand_reduce_operation    => "'@reduce' operation must be comptime-known",
+
+            .export_target        => "export target must be comptime-known",
+            .export_options       => "export options must be comptime-known",
+            .extern_options       => "extern options must be comptime-known",
+            .prefetch_options     => "prefetch options must be comptime-known",
+            .call_modifier        => "call modifier must be comptime-known",
+            .compile_error_string => "compile error string must be comptime-known",
+            .inline_assembly_code => "inline assembly code must be comptime-known",
+            .atomic_order         => "atomic order must be comptime-known",
+            .array_mul_factor     => "array multiplication factor must be comptime-known",
+            .slice_cat_operand    => "slice being concatenated must be comptime-known",
+            .comptime_call_target => "function being called at comptime must be comptime-known",
+            .wasm_memory_index    => "wasm memory index must be comptime-known",
+            .work_group_dim_index => "work group dimension index must be comptime-known",
+
+            .type                => "types must be comptime-known",
+            .array_sentinel      => "array sentinel value must be comptime-known",
+            .pointer_sentinel    => "pointer sentinel value must be comptime-known",
+            .slice_sentinel      => "slice sentinel value must be comptime-known",
+            .array_length        => "array length must be comptime-known",
+            .vector_length       => "vector length must be comptime-known",
+            .error_set_contents  => "error set contents must be comptime-known",
+            .struct_fields       => "struct fields must be comptime-known",
+            .enum_fields         => "enum fields must be comptime-known",
+            .union_fields        => "union fields must be comptime-known",
+            .function_ret_ty     => "function return type must be comptime-known",
+            .function_parameters => "function parameters must be comptime-known",
+
+            .decl_name         => "declaration name must be comptime-known",
+            .field_name        => "field name must be comptime-known",
+            .struct_field_name => "struct field name must be comptime-known",
+            .enum_field_name   => "enum field name must be comptime-known",
+            .union_field_name  => "union field name must be comptime-known",
+            .tuple_field_name  => "tuple field name must be comptime-known",
+            .tuple_field_index => "tuple field index must be comptime-known",
+
+            .container_var_init => "initializer of container-level variable must be comptime-known",
+            .@"callconv"        => "calling convention must be comptime-known",
+            .@"align"           => "alignment must be comptime-known",
+            .@"addrspace"       => "address space must be comptime-known",
+            .@"linksection"     => "linksection must be comptime-known",
+
+            .comptime_keyword             => "'comptime' keyword forces comptime evaluation",
+            .comptime_call_modifier       => "'.compile_time' call modifier forces comptime evaluation",
+            .switch_item                  => "switch prong values must be comptime-known",
+            .tuple_field_default_value    => "tuple field default value must be comptime-known",
+            .struct_field_default_value   => "struct field default value must be comptime-known",
+            .enum_field_tag_value         => "enum field tag value must be comptime-known",
+            .slice_single_item_ptr_bounds => "slice of single-item pointer must have comptime-known bounds",
+            .comptime_param_arg           => "argument to comptime parameter must be comptime-known",
+            .stored_to_comptime_field     => "value stored to a comptime field must be comptime-known",
+            .stored_to_comptime_var       => "value stored to a comptime variable must be comptime-known",
+            .casted_to_comptime_enum      => "value casted to enum with 'comptime_int' tag type must be comptime-known",
+            .casted_to_comptime_int       => "value casted to 'comptime_int' must be comptime-known",
+            .casted_to_comptime_float     => "value casted to 'comptime_float' must be comptime-known",
+            .panic_handler                => "panic handler must be comptime-known",
+            // zig fmt: on
+        };
+    }
+};
+
 test {
     _ = Ast;
     _ = AstRlAnnotate;
src/Zcu/PerThread.zig
@@ -682,7 +682,13 @@ fn analyzeComptimeUnit(pt: Zcu.PerThread, cu_id: InternPool.ComptimeUnit.Id) Zcu
         .namespace = comptime_unit.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = .{ .reason = .{
+            .src = .{
+                .base_node_inst = comptime_unit.zir_index,
+                .offset = .{ .token_offset = 0 },
+            },
+            .r = .{ .simple = .comptime_keyword },
+        } },
         .src_base_inst = comptime_unit.zir_index,
         .type_name_ctx = try ip.getOrPutStringFmt(gpa, pt.tid, "{}.comptime", .{
             Type.fromInterned(zcu.namespacePtr(comptime_unit.namespace).owner_type).containerTypeName(ip).fmt(ip),
@@ -878,7 +884,7 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
         .namespace = old_nav.analysis.?.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = undefined, // set below
         .src_base_inst = old_nav.analysis.?.zir_index,
         .type_name_ctx = old_nav.fqn,
     };
@@ -893,6 +899,11 @@ fn analyzeNavVal(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileErr
     const section_src = block.src(.{ .node_offset_var_decl_section = 0 });
     const addrspace_src = block.src(.{ .node_offset_var_decl_addrspace = 0 });
 
+    block.comptime_reason = .{ .reason = .{
+        .src = init_src,
+        .r = .{ .simple = .container_var_init },
+    } };
+
     const maybe_ty: ?Type = if (zir_decl.type_body != null) ty: {
         // Since we have a type body, the type is resolved separately!
         // Of course, we need to make sure we depend on it properly.
@@ -1253,7 +1264,7 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
         .namespace = old_nav.analysis.?.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = undefined, // set below
         .src_base_inst = old_nav.analysis.?.zir_index,
         .type_name_ctx = old_nav.fqn,
     };
@@ -1262,6 +1273,13 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
     const zir_decl = zir.getDeclaration(inst_resolved.inst);
     assert(old_nav.is_usingnamespace == (zir_decl.kind == .@"usingnamespace"));
 
+    const ty_src = block.src(.{ .node_offset_var_decl_ty = 0 });
+
+    block.comptime_reason = .{ .reason = .{
+        .src = ty_src,
+        .r = .{ .simple = .type },
+    } };
+
     const type_body = zir_decl.type_body orelse {
         // The type of this `Nav` is inferred from the value.
         // In other words, this `nav_ty` depends on the corresponding `nav_val`.
@@ -1279,8 +1297,6 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
         return .{ .type_changed = true };
     };
 
-    const ty_src = block.src(.{ .node_offset_var_decl_ty = 0 });
-
     const resolved_ty: Type = ty: {
         const uncoerced_type_ref = try sema.resolveInlineBody(&block, type_body, inst_resolved.inst);
         const type_ref = try sema.coerce(&block, .type, uncoerced_type_ref, ty_src);
@@ -2442,7 +2458,7 @@ fn analyzeFnBodyInner(pt: Zcu.PerThread, func_index: InternPool.Index) Zcu.SemaE
         .namespace = decl_nav.analysis.?.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = false,
+        .comptime_reason = null,
         .src_base_inst = decl_nav.analysis.?.zir_index,
         .type_name_ctx = func_nav.fqn,
     };
src/Compilation.zig
@@ -3435,6 +3435,7 @@ pub fn addModuleErrorMsg(
     var notes: std.ArrayHashMapUnmanaged(ErrorBundle.ErrorMessage, void, ErrorNoteHashContext, true) = .empty;
     defer notes.deinit(gpa);
 
+    var last_note_loc: ?std.zig.Loc = null;
     for (module_err_msg.notes) |module_note| {
         const note_src_loc = module_note.src_loc.upgrade(zcu);
         const source = try note_src_loc.file_scope.getSource(gpa);
@@ -3443,6 +3444,9 @@ pub fn addModuleErrorMsg(
         const note_file_path = try note_src_loc.file_scope.fullPath(gpa);
         defer gpa.free(note_file_path);
 
+        const omit_source_line = loc.eql(err_loc) or (last_note_loc != null and loc.eql(last_note_loc.?));
+        last_note_loc = loc;
+
         const gop = try notes.getOrPutContext(gpa, .{
             .msg = try eb.addString(module_note.msg),
             .src_loc = try eb.addSourceLocation(.{
@@ -3452,7 +3456,7 @@ pub fn addModuleErrorMsg(
                 .span_end = span.end,
                 .line = @intCast(loc.line),
                 .column = @intCast(loc.column),
-                .source_line = if (err_loc.eql(loc)) 0 else try eb.addString(loc.source_line),
+                .source_line = if (omit_source_line) 0 else try eb.addString(loc.source_line),
             }),
         }, .{ .eb = eb });
         if (gop.found_existing) {
src/print_zir.zig
@@ -437,7 +437,6 @@ const Writer = struct {
             .field_call => try self.writeCall(stream, inst, .field),
 
             .block,
-            .block_comptime,
             .block_inline,
             .suspend_block,
             .loop,
@@ -445,6 +444,8 @@ const Writer = struct {
             .typeof_builtin,
             => try self.writeBlock(stream, inst),
 
+            .block_comptime => try self.writeBlockComptime(stream, inst),
+
             .condbr,
             .condbr_inline,
             => try self.writeCondBr(stream, inst),
@@ -1343,16 +1344,21 @@ const Writer = struct {
 
     fn writeBlock(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void {
         const inst_data = self.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;
-        try self.writePlNodeBlockWithoutSrc(stream, inst);
+        const extra = self.code.extraData(Zir.Inst.Block, inst_data.payload_index);
+        const body = self.code.bodySlice(extra.end, extra.data.body_len);
+        try self.writeBracedBody(stream, body);
+        try stream.writeAll(") ");
         try self.writeSrcNode(stream, inst_data.src_node);
     }
 
-    fn writePlNodeBlockWithoutSrc(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void {
+    fn writeBlockComptime(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void {
         const inst_data = self.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;
-        const extra = self.code.extraData(Zir.Inst.Block, inst_data.payload_index);
+        const extra = self.code.extraData(Zir.Inst.BlockComptime, inst_data.payload_index);
         const body = self.code.bodySlice(extra.end, extra.data.body_len);
+        try stream.print("reason={s}, ", .{@tagName(extra.data.reason)});
         try self.writeBracedBody(stream, body);
         try stream.writeAll(") ");
+        try self.writeSrcNode(stream, inst_data.src_node);
     }
 
     fn writeCondBr(self: *Writer, stream: anytype, inst: Zir.Inst.Index) !void {
src/Sema.zig
@@ -377,9 +377,7 @@ pub const Block = struct {
     runtime_index: RuntimeIndex = .zero,
     inline_block: Zir.Inst.OptionalIndex = .none,
 
-    comptime_reason: ?*const ComptimeReason = null,
-    // TODO is_comptime and comptime_reason should probably be merged together.
-    is_comptime: bool,
+    comptime_reason: ?BlockComptimeReason = null,
     is_typeof: bool = false,
 
     /// Keep track of the active error return trace index around blocks so that we can correctly
@@ -419,6 +417,10 @@ pub const Block = struct {
         };
     }
 
+    fn isComptime(block: Block) bool {
+        return block.comptime_reason != null;
+    }
+
     fn builtinCallArgSrc(block: *Block, builtin_call_node: i32, arg_index: u32) LazySrcLoc {
         return block.src(.{ .node_offset_builtin_call_arg = .{
             .builtin_call_node = builtin_call_node,
@@ -434,44 +436,6 @@ pub const Block = struct {
         return block.src(.{ .token_offset = tok_offset });
     }
 
-    const ComptimeReason = union(enum) {
-        c_import: struct {
-            src: LazySrcLoc,
-        },
-        comptime_ret_ty: struct {
-            func: Air.Inst.Ref,
-            func_src: LazySrcLoc,
-            return_ty: Type,
-        },
-
-        fn explain(cr: ComptimeReason, sema: *Sema, msg: ?*Zcu.ErrorMsg) !void {
-            const parent = msg orelse return;
-            const pt = sema.pt;
-            const prefix = "expression is evaluated at comptime because ";
-            switch (cr) {
-                .c_import => |ci| {
-                    try sema.errNote(ci.src, parent, prefix ++ "it is inside a @cImport", .{});
-                },
-                .comptime_ret_ty => |rt| {
-                    const ret_ty_src: LazySrcLoc = if (try sema.funcDeclSrcInst(rt.func)) |fn_decl_inst| .{
-                        .base_node_inst = fn_decl_inst,
-                        .offset = .{ .node_offset_fn_type_ret_ty = 0 },
-                    } else rt.func_src;
-                    if (rt.return_ty.isGenericPoison()) {
-                        return sema.errNote(ret_ty_src, parent, prefix ++ "the generic function was instantiated with a comptime-only return type", .{});
-                    }
-                    try sema.errNote(
-                        ret_ty_src,
-                        parent,
-                        prefix ++ "the function returns a comptime-only type '{}'",
-                        .{rt.return_ty.fmt(pt)},
-                    );
-                    try sema.explainWhyTypeIsComptime(parent, ret_ty_src, rt.return_ty);
-                },
-            }
-        }
-    };
-
     const Param = struct {
         /// `none` means `anytype`.
         ty: InternPool.Index,
@@ -539,7 +503,6 @@ pub const Block = struct {
             .instructions = .{},
             .label = null,
             .inlining = parent.inlining,
-            .is_comptime = parent.is_comptime,
             .comptime_reason = parent.comptime_reason,
             .is_typeof = parent.is_typeof,
             .runtime_cond = parent.runtime_cond,
@@ -860,6 +823,77 @@ pub const Block = struct {
             .inst = inst,
         });
     }
+
+    /// Returns the `*Block` that should be passed to `Sema.failWithOwnedErrorMsg`, because all inline
+    /// calls below it have already been reported with "called at comptime from here" notes.
+    fn explainWhyBlockIsComptime(start_block: *Block, err_msg: *Zcu.ErrorMsg) !*Block {
+        const sema = start_block.sema;
+        var block = start_block;
+        while (true) {
+            switch (block.comptime_reason.?) {
+                .inlining_parent => {
+                    const inlining = block.inlining.?;
+                    try sema.errNote(inlining.call_src, err_msg, "called at comptime from here", .{});
+                    block = inlining.call_block;
+                },
+                .reason => |r| {
+                    try r.r.explain(sema, r.src, err_msg);
+                    return block;
+                },
+            }
+        }
+    }
+};
+
+const ComptimeReason = union(enum) {
+    /// Evaluating at comptime for a reason in the `std.zig.SimpleComptimeReason` enum.
+    simple: std.zig.SimpleComptimeReason,
+
+    /// Evaluating at comptime because of a comptime-only type.
+    /// The format string looks like "foo '{}' bar", where "{}" is the comptime-only type.
+    /// We will then explain why this type is comptime-only.
+    comptime_only: struct {
+        ty: Type,
+        msg: enum {
+            union_init,
+            struct_init,
+            tuple_init,
+            param_ty_arg,
+            ret_ty_call,
+            ret_ty_generic_call,
+        },
+    },
+
+    fn explain(reason: ComptimeReason, sema: *Sema, src: LazySrcLoc, err_msg: *Zcu.ErrorMsg) !void {
+        switch (reason) {
+            .simple => |simple| {
+                try sema.errNote(src, err_msg, "{s}", .{simple.message()});
+            },
+            .comptime_only => |co| {
+                const pre, const post = switch (co.msg) {
+                    .union_init => .{ "initializer of comptime-only union", "must be comptime-known" },
+                    .struct_init => .{ "initializer of comptime-only struct", "must be comptime-known" },
+                    .tuple_init => .{ "initializer of comptime-only tuple", "must be comptime-known" },
+                    .param_ty_arg => .{ "argument to parameter with comptime-only type", "must be comptime-known" },
+                    .ret_ty_call => .{ "function with comptime-only return type", "is evaluated at comptime" },
+                    .ret_ty_generic_call => .{ "generic function instantiated with comptime-only return type", "is evaluated at comptime" },
+                };
+                try sema.errNote(src, err_msg, "{s} '{}' {s}", .{ pre, co.ty.fmt(sema.pt), post });
+                try sema.explainWhyTypeIsComptime(err_msg, src, co.ty);
+            },
+        }
+    }
+};
+
+const BlockComptimeReason = union(enum) {
+    /// This block inherits being comptime-only from the `inlining` call site.
+    inlining_parent,
+
+    /// This block is comptime for the given reason at the given source location.
+    reason: struct {
+        src: LazySrcLoc,
+        r: ComptimeReason,
+    },
 };
 
 const LabeledBlock = struct {
@@ -885,12 +919,6 @@ const InferredAlloc = struct {
     prongs: std.ArrayListUnmanaged(Air.Inst.Index) = .empty,
 };
 
-const NeededComptimeReason = struct {
-    needed_comptime_reason: []const u8,
-    value_comptime_reason: ?[]const u8 = null,
-    block_comptime_reason: ?*const Block.ComptimeReason = null,
-};
-
 pub fn deinit(sema: *Sema) void {
     const gpa = sema.gpa;
     sema.air_instructions.deinit(gpa);
@@ -954,7 +982,7 @@ pub fn analyzeFnBody(
 /// we are evaluating at comptime, semantically analyze the body and return the result from it.
 /// Returns `null` if control flow did not break from this block, but instead terminated with some
 /// other runtime noreturn instruction. Compile-time breaks to blocks further up the stack still
-/// return `error.ComptimeBreak`. If `block.is_comptime`, this function will never return `null`.
+/// return `error.ComptimeBreak`. If `block.isComptime()`, this function will never return `null`.
 fn analyzeInlineBody(
     sema: *Sema,
     block: *Block,
@@ -1003,7 +1031,7 @@ pub fn resolveInlineBody(
 /// If this function returns normally, the merges of `block` were populated with all possible
 /// (runtime) results of this block. Peer type resolution should be performed on the result,
 /// and relevant runtime instructions written to perform necessary coercions and breaks. See
-/// `resolveAnalyzedBlock`. This form of return is impossible if `block.is_comptime == true`.
+/// `resolveAnalyzedBlock`. This form of return is impossible if `block.isComptime()`.
 ///
 /// Alternatively, this function may return `error.ComptimeBreak`. This indicates that comptime
 /// control flow is happening, and we are breaking at comptime from a block indicated by the
@@ -1340,7 +1368,7 @@ fn analyzeBodyInner(
                         continue;
                     },
                     .breakpoint => {
-                        if (!block.is_comptime) {
+                        if (!block.isComptime()) {
                             _ = try block.addNoOp(.breakpoint);
                         }
                         i += 1;
@@ -1515,7 +1543,7 @@ fn analyzeBodyInner(
                 continue;
             },
             .check_comptime_control_flow => {
-                if (!block.is_comptime) {
+                if (!block.isComptime()) {
                     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
                     const src = block.nodeOffset(inst_data.src_node);
                     const inline_block = inst_data.operand.toIndex().?;
@@ -1562,7 +1590,7 @@ fn analyzeBodyInner(
 
             // Special case instructions to handle comptime control flow.
             .@"break" => {
-                if (block.is_comptime) {
+                if (block.isComptime()) {
                     sema.comptime_break_inst = inst;
                     return error.ComptimeBreak;
                 } else {
@@ -1575,7 +1603,7 @@ fn analyzeBodyInner(
                 return error.ComptimeBreak;
             },
             .repeat => {
-                if (block.is_comptime) {
+                if (block.isComptime()) {
                     // Send comptime control flow back to the beginning of this block.
                     const src = block.nodeOffset(datas[@intFromEnum(inst)].node);
                     try sema.emitBackwardBranch(block, src);
@@ -1597,7 +1625,7 @@ fn analyzeBodyInner(
                 i = 0;
                 continue;
             },
-            .switch_continue => if (block.is_comptime) {
+            .switch_continue => if (block.isComptime()) {
                 sema.comptime_break_inst = inst;
                 return error.ComptimeBreak;
             } else {
@@ -1605,17 +1633,40 @@ fn analyzeBodyInner(
                 break;
             },
 
-            .loop => if (block.is_comptime) {
+            .loop => if (block.isComptime()) {
                 continue :inst .block_inline;
             } else try sema.zirLoop(block, inst),
 
-            .block => if (block.is_comptime) {
+            .block => if (block.isComptime()) {
                 continue :inst .block_inline;
-            } else try sema.zirBlock(block, inst, false),
+            } else try sema.zirBlock(block, inst),
 
-            .block_comptime => if (block.is_comptime) {
-                continue :inst .block_inline;
-            } else try sema.zirBlock(block, inst, true),
+            .block_comptime => {
+                const pl_node = datas[@intFromEnum(inst)].pl_node;
+                const src = block.nodeOffset(pl_node.src_node);
+                const extra = sema.code.extraData(Zir.Inst.BlockComptime, pl_node.payload_index);
+                const block_body = sema.code.bodySlice(extra.end, extra.data.body_len);
+
+                if (block.isComptime()) {
+                    // No need for a sub-block; just resolve the other body directly!
+                    break :inst try sema.resolveInlineBody(block, block_body, inst);
+                }
+
+                var child_block = block.makeSubBlock();
+                defer child_block.instructions.deinit(sema.gpa);
+                child_block.comptime_reason = .{ .reason = .{
+                    .src = src,
+                    .r = .{ .simple = extra.data.reason },
+                } };
+
+                const result = try sema.resolveInlineBody(&child_block, block_body, inst);
+
+                if (!try sema.isComptimeKnown(result)) {
+                    return sema.failWithNeededComptime(&child_block, src, null);
+                }
+
+                break :inst result;
+            },
 
             .block_inline => blk: {
                 // Directly analyze the block body without introducing a new block.
@@ -1725,7 +1776,7 @@ fn analyzeBodyInner(
                     return error.ComptimeBreak;
                 }
             },
-            .condbr => if (block.is_comptime) {
+            .condbr => if (block.isComptime()) {
                 continue :inst .condbr_inline;
             } else {
                 try sema.zirCondbr(block, inst);
@@ -1742,10 +1793,7 @@ fn analyzeBodyInner(
                 );
                 const uncasted_cond = try sema.resolveInst(extra.data.condition);
                 const cond = try sema.coerce(block, Type.bool, uncasted_cond, cond_src);
-                const cond_val = try sema.resolveConstDefinedValue(block, cond_src, cond, .{
-                    .needed_comptime_reason = "condition in comptime branch must be comptime-known",
-                    .block_comptime_reason = block.comptime_reason,
-                });
+                const cond_val = try sema.resolveConstDefinedValue(block, cond_src, cond, null);
                 const inline_body = if (cond_val.toBool()) then_body else else_body;
 
                 try sema.maybeErrorUnwrapCondbr(block, inline_body, extra.data.condition, cond_src);
@@ -1756,7 +1804,7 @@ fn analyzeBodyInner(
                 break :inst result;
             },
             .@"try" => blk: {
-                if (!block.is_comptime) break :blk try sema.zirTry(block, inst);
+                if (!block.isComptime()) break :blk try sema.zirTry(block, inst);
                 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;
                 const src = block.nodeOffset(inst_data.src_node);
                 const operand_src = block.src(.{ .node_offset_bin_lhs = inst_data.src_node });
@@ -1771,10 +1819,7 @@ fn analyzeBodyInner(
                 }
                 const is_non_err = try sema.analyzeIsNonErrComptimeOnly(block, operand_src, err_union);
                 assert(is_non_err != .none);
-                const is_non_err_val = try sema.resolveConstDefinedValue(block, operand_src, is_non_err, .{
-                    .needed_comptime_reason = "try operand inside comptime block must be comptime-known",
-                    .block_comptime_reason = block.comptime_reason,
-                });
+                const is_non_err_val = try sema.resolveConstDefinedValue(block, operand_src, is_non_err, null);
                 if (is_non_err_val.toBool()) {
                     break :blk try sema.analyzeErrUnionPayload(block, src, err_union_ty, err_union, operand_src, false);
                 }
@@ -1782,7 +1827,7 @@ fn analyzeBodyInner(
                 break :blk result;
             },
             .try_ptr => blk: {
-                if (!block.is_comptime) break :blk try sema.zirTryPtr(block, inst);
+                if (!block.isComptime()) break :blk try sema.zirTryPtr(block, inst);
                 const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;
                 const src = block.nodeOffset(inst_data.src_node);
                 const operand_src = block.src(.{ .node_offset_bin_lhs = inst_data.src_node });
@@ -1792,10 +1837,7 @@ fn analyzeBodyInner(
                 const err_union = try sema.analyzeLoad(block, src, operand, operand_src);
                 const is_non_err = try sema.analyzeIsNonErrComptimeOnly(block, operand_src, err_union);
                 assert(is_non_err != .none);
-                const is_non_err_val = try sema.resolveConstDefinedValue(block, operand_src, is_non_err, .{
-                    .needed_comptime_reason = "try operand inside comptime block must be comptime-known",
-                    .block_comptime_reason = block.comptime_reason,
-                });
+                const is_non_err_val = try sema.resolveConstDefinedValue(block, operand_src, is_non_err, null);
                 if (is_non_err_val.toBool()) {
                     break :blk try sema.analyzeErrUnionPayloadPtr(block, src, operand, false, false);
                 }
@@ -1873,7 +1915,7 @@ fn resolveConstBool(
     block: *Block,
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) !bool {
     const air_inst = try sema.resolveInst(zir_ref);
     const wanted_type = Type.bool;
@@ -1887,7 +1929,7 @@ fn resolveConstString(
     block: *Block,
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) ![]u8 {
     const air_inst = try sema.resolveInst(zir_ref);
     return sema.toConstString(block, src, air_inst, reason);
@@ -1898,7 +1940,7 @@ pub fn toConstString(
     block: *Block,
     src: LazySrcLoc,
     air_inst: Air.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) ![]u8 {
     const pt = sema.pt;
     const coerced_inst = try sema.coerce(block, Type.slice_const_u8, air_inst, src);
@@ -1912,7 +1954,7 @@ pub fn resolveConstStringIntern(
     block: *Block,
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) !InternPool.NullTerminatedString {
     const air_inst = try sema.resolveInst(zir_ref);
     const wanted_type = Type.slice_const_u8;
@@ -2063,9 +2105,7 @@ fn analyzeAsType(
 ) !Type {
     const wanted_type = Type.type;
     const coerced_inst = try sema.coerce(block, wanted_type, air_inst, src);
-    const val = try sema.resolveConstDefinedValue(block, src, coerced_inst, .{
-        .needed_comptime_reason = "types must be comptime-known",
-    });
+    const val = try sema.resolveConstDefinedValue(block, src, coerced_inst, .{ .simple = .type });
     return val.toType();
 }
 
@@ -2077,7 +2117,7 @@ pub fn setupErrorReturnTrace(sema: *Sema, block: *Block, last_arg_index: usize)
     const ip = &zcu.intern_pool;
     if (!comp.config.any_error_tracing) return;
 
-    assert(!block.is_comptime);
+    assert(!block.isComptime());
     var err_trace_block = block.makeSubBlock();
     defer err_trace_block.instructions.deinit(gpa);
 
@@ -2148,7 +2188,7 @@ fn resolveConstValue(
     block: *Block,
     src: LazySrcLoc,
     inst: Air.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ?ComptimeReason,
 ) CompileError!Value {
     return try sema.resolveValue(inst) orelse {
         return sema.failWithNeededComptime(block, src, reason);
@@ -2177,7 +2217,7 @@ fn resolveConstDefinedValue(
     block: *Block,
     src: LazySrcLoc,
     air_ref: Air.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ?ComptimeReason,
 ) CompileError!Value {
     const val = try sema.resolveConstValue(block, src, air_ref, reason);
     if (val.isUndef(sema.pt.zcu)) return sema.failWithUseOfUndef(block, src);
@@ -2217,15 +2257,16 @@ pub fn resolveFinalDeclValue(
             const val: Value = .fromInterned(ip_index);
             break :rt_ptr val.isPtrRuntimeValue(zcu);
         };
-        const value_comptime_reason: ?[]const u8 = if (is_runtime_ptr)
-            "thread local and dll imported variables have runtime-known addresses"
-        else
-            null;
 
-        return sema.failWithNeededComptime(block, src, .{
-            .needed_comptime_reason = "global variable initializer must be comptime-known",
-            .value_comptime_reason = value_comptime_reason,
-        });
+        switch (sema.failWithNeededComptime(block, src, .{ .simple = .container_var_init })) {
+            error.AnalysisFail => |e| {
+                if (sema.err != null and is_runtime_ptr) {
+                    try sema.errNote(src, sema.err.?, "threadlocal and dll imported variables have runtime-known addresses", .{});
+                }
+                return e;
+            },
+            else => |e| return e,
+        }
     };
 
     if (val.canMutateComptimeVarState(zcu)) {
@@ -2235,21 +2276,19 @@ pub fn resolveFinalDeclValue(
     return val;
 }
 
-fn failWithNeededComptime(sema: *Sema, block: *Block, src: LazySrcLoc, reason: NeededComptimeReason) CompileError {
-    const msg = msg: {
+fn failWithNeededComptime(sema: *Sema, block: *Block, src: LazySrcLoc, reason: ?ComptimeReason) CompileError {
+    const msg, const fail_block = msg: {
         const msg = try sema.errMsg(src, "unable to resolve comptime value", .{});
         errdefer msg.destroy(sema.gpa);
-        try sema.errNote(src, msg, "{s}", .{reason.needed_comptime_reason});
-        if (reason.value_comptime_reason) |value_comptime_reason| {
-            try sema.errNote(src, msg, "{s}", .{value_comptime_reason});
-        }
-
-        if (reason.block_comptime_reason) |block_comptime_reason| {
-            try block_comptime_reason.explain(sema, msg);
-        }
-        break :msg msg;
+        const fail_block = if (reason) |r| b: {
+            try r.explain(sema, src, msg);
+            break :b block;
+        } else b: {
+            break :b try block.explainWhyBlockIsComptime(msg);
+        };
+        break :msg .{ msg, fail_block };
     };
-    return sema.failWithOwnedErrorMsg(block, msg);
+    return sema.failWithOwnedErrorMsg(fail_block, msg);
 }
 
 fn failWithUseOfUndef(sema: *Sema, block: *Block, src: LazySrcLoc) CompileError {
@@ -2578,9 +2617,7 @@ pub fn analyzeAsAlign(
     src: LazySrcLoc,
     air_ref: Air.Inst.Ref,
 ) !Alignment {
-    const alignment_big = try sema.analyzeAsInt(block, src, air_ref, align_ty, .{
-        .needed_comptime_reason = "alignment must be comptime-known",
-    });
+    const alignment_big = try sema.analyzeAsInt(block, src, air_ref, align_ty, .{ .simple = .@"align" });
     return sema.validateAlign(block, src, alignment_big);
 }
 
@@ -2615,7 +2652,7 @@ fn resolveInt(
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
     dest_ty: Type,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) !u64 {
     const air_ref = try sema.resolveInst(zir_ref);
     return sema.analyzeAsInt(block, src, air_ref, dest_ty, reason);
@@ -2627,7 +2664,7 @@ fn analyzeAsInt(
     src: LazySrcLoc,
     air_ref: Air.Inst.Ref,
     dest_ty: Type,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) !u64 {
     const coerced = try sema.coerce(block, dest_ty, air_ref, src);
     const val = try sema.resolveConstDefinedValue(block, src, coerced, reason);
@@ -2687,9 +2724,7 @@ fn zirTupleDecl(
             if (zir_field_init != .none) {
                 const uncoerced_field_init = try sema.resolveInst(zir_field_init);
                 const coerced_field_init = try sema.coerce(block, field_type, uncoerced_field_init, init_src);
-                const field_init_val = try sema.resolveConstDefinedValue(block, init_src, coerced_field_init, .{
-                    .needed_comptime_reason = "tuple field default value must be comptime-known",
-                });
+                const field_init_val = try sema.resolveConstDefinedValue(block, init_src, coerced_field_init, .{ .simple = .tuple_field_default_value });
                 if (field_init_val.canMutateComptimeVarState(zcu)) {
                     return sema.fail(block, init_src, "field default value contains reference to comptime-mutable memory", .{});
                 }
@@ -3414,7 +3449,7 @@ fn zirRetPtr(sema: *Sema, block: *Block) CompileError!Air.Inst.Ref {
 
     const pt = sema.pt;
 
-    if (block.is_comptime or try sema.fn_ret_ty.comptimeOnlySema(pt)) {
+    if (block.isComptime() or try sema.fn_ret_ty.comptimeOnlySema(pt)) {
         try sema.fn_ret_ty.resolveFields(pt);
         return sema.analyzeComptimeAlloc(block, sema.fn_ret_ty, .none);
     }
@@ -3608,7 +3643,7 @@ fn zirAllocExtended(
         break :blk try sema.resolveAlign(block, align_src, align_ref);
     } else .none;
 
-    if (block.is_comptime or small.is_comptime) {
+    if (block.isComptime() or small.is_comptime) {
         if (small.has_type) {
             return sema.analyzeComptimeAlloc(block, var_ty, alignment);
         } else {
@@ -4075,7 +4110,7 @@ fn zirAlloc(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.I
     const ty_src = block.src(.{ .node_offset_var_decl_ty = inst_data.src_node });
 
     const var_ty = try sema.resolveType(block, ty_src, inst_data.operand);
-    if (block.is_comptime or try var_ty.comptimeOnlySema(pt)) {
+    if (block.isComptime() or try var_ty.comptimeOnlySema(pt)) {
         return sema.analyzeComptimeAlloc(block, var_ty, .none);
     }
     if (sema.func_is_naked and try var_ty.hasRuntimeBitsSema(pt)) {
@@ -4103,7 +4138,7 @@ fn zirAllocMut(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
     const ty_src = block.src(.{ .node_offset_var_decl_ty = inst_data.src_node });
     const var_ty = try sema.resolveType(block, ty_src, inst_data.operand);
-    if (block.is_comptime) {
+    if (block.isComptime()) {
         return sema.analyzeComptimeAlloc(block, var_ty, .none);
     }
     if (sema.func_is_naked and try var_ty.hasRuntimeBitsSema(pt)) {
@@ -4129,7 +4164,7 @@ fn zirAllocInferred(
 
     const gpa = sema.gpa;
 
-    if (block.is_comptime) {
+    if (block.isComptime()) {
         try sema.air_instructions.append(gpa, .{
             .tag = .inferred_alloc_comptime,
             .data = .{ .inferred_alloc_comptime = .{
@@ -4778,7 +4813,7 @@ fn validateUnionInit(
         return sema.failWithOwnedErrorMsg(block, msg);
     }
 
-    if (block.is_comptime and
+    if (block.isComptime() and
         (try sema.resolveDefinedValue(block, init_src, union_ptr)) != null)
     {
         // In this case, comptime machinery already did everything. No work to do here.
@@ -4897,9 +4932,11 @@ fn validateUnionInit(
         try sema.storePtr2(block, init_src, union_ptr, init_src, union_init, init_src, .store);
         return;
     } else if (try union_ty.comptimeOnlySema(pt)) {
-        return sema.failWithNeededComptime(block, block.nodeOffset(field_ptr_data.src_node), .{
-            .needed_comptime_reason = "initializer of comptime only union must be comptime-known",
-        });
+        const src = block.nodeOffset(field_ptr_data.src_node);
+        return sema.failWithNeededComptime(block, src, .{ .comptime_only = .{
+            .ty = union_ty,
+            .msg = .union_init,
+        } });
     }
     if (init_ref) |v| try sema.validateRuntimeValue(block, block.nodeOffset(field_ptr_data.src_node), v);
 
@@ -4953,7 +4990,7 @@ fn validateStructInit(
     errdefer if (root_msg) |msg| msg.destroy(sema.gpa);
 
     const struct_ptr = try sema.resolveInst(struct_ptr_zir_ref);
-    if (block.is_comptime and
+    if (block.isComptime() and
         (try sema.resolveDefinedValue(block, init_src, struct_ptr)) != null)
     {
         try struct_ty.resolveLayout(pt);
@@ -5081,9 +5118,11 @@ fn validateStructInit(
                     field_values[i] = val.toIntern();
                 } else if (require_comptime) {
                     const field_ptr_data = sema.code.instructions.items(.data)[@intFromEnum(field_ptr)].pl_node;
-                    return sema.failWithNeededComptime(block, block.nodeOffset(field_ptr_data.src_node), .{
-                        .needed_comptime_reason = "initializer of comptime only struct must be comptime-known",
-                    });
+                    const src = block.nodeOffset(field_ptr_data.src_node);
+                    return sema.failWithNeededComptime(block, src, .{ .comptime_only = .{
+                        .ty = struct_ty,
+                        .msg = .struct_init,
+                    } });
                 } else {
                     struct_is_comptime = false;
                 }
@@ -5253,7 +5292,7 @@ fn zirValidatePtrArrayInit(
         else => unreachable,
     };
 
-    if (block.is_comptime and
+    if (block.isComptime() and
         (try sema.resolveDefinedValue(block, init_src, array_ptr)) != null)
     {
         // In this case the comptime machinery will have evaluated the store instructions
@@ -5629,9 +5668,7 @@ fn storeToInferredAllocComptime(
     // There will be only one store_to_inferred_ptr because we are running at comptime.
     // The alloc will turn into a Decl or a ComptimeAlloc.
     const operand_val = try sema.resolveValue(operand) orelse {
-        return sema.failWithNeededComptime(block, src, .{
-            .needed_comptime_reason = "value being stored to a comptime variable must be comptime-known",
-        });
+        return sema.failWithNeededComptime(block, src, .{ .simple = .stored_to_comptime_var });
     };
     const alloc_ty = try pt.ptrTypeSema(.{
         .child = operand_ty.toIntern(),
@@ -5663,9 +5700,7 @@ fn storeToInferredAllocComptime(
 fn zirSetEvalBranchQuota(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
     const src = block.nodeOffset(inst_data.src_node);
-    const quota: u32 = @intCast(try sema.resolveInt(block, src, inst_data.operand, Type.u32, .{
-        .needed_comptime_reason = "eval branch quota must be comptime-known",
-    }));
+    const quota: u32 = @intCast(try sema.resolveInt(block, src, inst_data.operand, .u32, .{ .simple = .operand_setEvalBranchQuota }));
     sema.branch_quota = @max(sema.branch_quota, quota);
     sema.allow_memoize = false;
 }
@@ -5794,9 +5829,7 @@ fn zirCompileError(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
     const src = block.nodeOffset(inst_data.src_node);
     const operand_src = block.builtinCallArgSrc(inst_data.src_node, 0);
-    const msg = try sema.resolveConstString(block, operand_src, inst_data.operand, .{
-        .needed_comptime_reason = "compile error string must be comptime-known",
-    });
+    const msg = try sema.resolveConstString(block, operand_src, inst_data.operand, .{ .simple = .compile_error_string });
     return sema.fail(block, src, "{s}", .{msg});
 }
 
@@ -5848,7 +5881,7 @@ fn zirPanic(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void
     // source location if we do it here.
     const coerced_msg = try sema.coerce(block, Type.slice_const_u8, msg_inst, block.builtinCallArgSrc(inst_data.src_node, 0));
 
-    if (block.is_comptime) {
+    if (block.isComptime()) {
         return sema.fail(block, src, "encountered @panic at comptime", .{});
     }
 
@@ -5864,7 +5897,7 @@ fn zirPanic(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void
 fn zirTrap(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
     const src_node = sema.code.instructions.items(.data)[@intFromEnum(inst)].node;
     const src = block.nodeOffset(src_node);
-    if (block.is_comptime)
+    if (block.isComptime())
         return sema.fail(block, src, "encountered @trap at comptime", .{});
     _ = try block.addNoOp(.trap);
 }
@@ -5974,15 +6007,16 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr
     var c_import_buf = std.ArrayList(u8).init(gpa);
     defer c_import_buf.deinit();
 
-    const comptime_reason: Block.ComptimeReason = .{ .c_import = .{ .src = src } };
     var child_block: Block = .{
         .parent = parent_block,
         .sema = sema,
         .namespace = parent_block.namespace,
         .instructions = .{},
         .inlining = parent_block.inlining,
-        .is_comptime = true,
-        .comptime_reason = &comptime_reason,
+        .comptime_reason = .{ .reason = .{
+            .src = src,
+            .r = .{ .simple = .operand_cImport },
+        } },
         .c_import_buf = &c_import_buf,
         .runtime_cond = parent_block.runtime_cond,
         .runtime_loop = parent_block.runtime_loop,
@@ -6073,7 +6107,7 @@ fn zirSuspendBlock(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) Comp
     return sema.failWithUseOfAsync(parent_block, src);
 }
 
-fn zirBlock(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index, force_comptime: bool) CompileError!Air.Inst.Ref {
+fn zirBlock(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref {
     const tracy = trace(@src());
     defer tracy.end();
 
@@ -6109,7 +6143,6 @@ fn zirBlock(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index, force_compt
         .instructions = .{},
         .label = &label,
         .inlining = parent_block.inlining,
-        .is_comptime = parent_block.is_comptime or force_comptime,
         .comptime_reason = parent_block.comptime_reason,
         .is_typeof = parent_block.is_typeof,
         .want_safety = parent_block.want_safety,
@@ -6143,7 +6176,7 @@ fn resolveBlockBody(
     body_inst: Zir.Inst.Index,
     merges: *Block.Merges,
 ) CompileError!Air.Inst.Ref {
-    if (child_block.is_comptime) {
+    if (child_block.isComptime()) {
         return sema.resolveInlineBody(child_block, body, body_inst);
     } else {
         assert(sema.air_instructions.items(.tag)[@intFromEnum(merges.block_inst)] == .block);
@@ -6303,7 +6336,7 @@ fn resolveAnalyzedBlock(
         }
     }
     // 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.
+    assert(!child_block.isComptime()); // 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.
 
@@ -6425,9 +6458,7 @@ fn zirExport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void
     const options_src = block.builtinCallArgSrc(inst_data.src_node, 1);
 
     const ptr = try sema.resolveInst(extra.exported);
-    const ptr_val = try sema.resolveConstDefinedValue(block, ptr_src, ptr, .{
-        .needed_comptime_reason = "export target must be comptime-known",
-    });
+    const ptr_val = try sema.resolveConstDefinedValue(block, ptr_src, ptr, .{ .simple = .export_target });
     const ptr_ty = ptr_val.typeOf(zcu);
 
     const options = try sema.resolveExportOptions(block, options_src, extra.options);
@@ -6553,17 +6584,13 @@ fn zirDisableInstrumentation(sema: *Sema) CompileError!void {
 fn zirSetFloatMode(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!void {
     const extra = sema.code.extraData(Zir.Inst.UnNode, extended.operand).data;
     const src = block.builtinCallArgSrc(extra.node, 0);
-    block.float_mode = try sema.resolveBuiltinEnum(block, src, extra.operand, "FloatMode", .{
-        .needed_comptime_reason = "operand to @setFloatMode must be comptime-known",
-    });
+    block.float_mode = try sema.resolveBuiltinEnum(block, src, extra.operand, "FloatMode", .{ .simple = .operand_setFloatMode });
 }
 
 fn zirSetRuntimeSafety(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
     const operand_src = block.builtinCallArgSrc(inst_data.src_node, 0);
-    block.want_safety = try sema.resolveConstBool(block, operand_src, inst_data.operand, .{
-        .needed_comptime_reason = "operand to @setRuntimeSafety must be comptime-known",
-    });
+    block.want_safety = try sema.resolveConstBool(block, operand_src, inst_data.operand, .{ .simple = .operand_setRuntimeSafety });
 }
 
 fn zirBreak(sema: *Sema, start_block: *Block, inst: Zir.Inst.Index) CompileError!void {
@@ -6650,7 +6677,7 @@ fn zirSwitchContinue(sema: *Sema, start_block: *Block, inst: Zir.Inst.Index) Com
 }
 
 fn zirDbgStmt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
-    if (block.is_comptime or block.ownerModule().strip) return;
+    if (block.isComptime() or block.ownerModule().strip) return;
 
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].dbg_stmt;
 
@@ -6676,7 +6703,7 @@ fn zirDbgStmt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!voi
 }
 
 fn zirDbgEmptyStmt(_: *Sema, block: *Block, _: Zir.Inst.Index) CompileError!void {
-    if (block.is_comptime or block.ownerModule().strip) return;
+    if (block.isComptime() or block.ownerModule().strip) return;
     _ = try block.addNoOp(.dbg_empty_stmt);
 }
 
@@ -6699,7 +6726,7 @@ fn addDbgVar(
     air_tag: Air.Inst.Tag,
     name: []const u8,
 ) CompileError!void {
-    if (block.is_comptime or block.ownerModule().strip) return;
+    if (block.isComptime() or block.ownerModule().strip) return;
 
     const pt = sema.pt;
     const zcu = pt.zcu;
@@ -6931,7 +6958,7 @@ pub fn analyzeSaveErrRetIndex(sema: *Sema, block: *Block) SemaError!Air.Inst.Ref
     const zcu = pt.zcu;
     const gpa = sema.gpa;
 
-    if (block.is_comptime or block.is_typeof) {
+    if (block.isComptime() or block.is_typeof) {
         const index_val = try pt.intValue_u64(Type.usize, sema.comptime_err_ret_trace.items.len);
         return Air.internedToRef(index_val.toIntern());
     }
@@ -7134,7 +7161,7 @@ fn zirCall(
     }
 
     if (block.ownerModule().error_tracing and
-        !block.is_comptime and !block.is_typeof and (input_is_error or pop_error_return_trace))
+        !block.isComptime() and !block.is_typeof and (input_is_error or pop_error_return_trace))
     {
         const return_ty = sema.typeOf(call_inst);
         if (modifier != .always_tail and return_ty.isNoReturn(zcu))
@@ -7404,12 +7431,14 @@ const CallArgsInfo = union(enum) {
                 };
 
                 // Generate args to comptime params in comptime block
-                const parent_comptime = block.is_comptime;
-                defer block.is_comptime = parent_comptime;
+                const parent_comptime = block.comptime_reason;
+                defer block.comptime_reason = parent_comptime;
                 // Note that we are indexing into parameters, not arguments, so use `arg_index` instead of `real_arg_idx`
                 if (arg_index < @min(param_count, 32) and func_ty_info.paramIsComptime(@intCast(arg_index))) {
-                    block.is_comptime = true;
-                    // TODO set comptime_reason
+                    block.comptime_reason = .{ .reason = .{
+                        .src = cai.argSrc(block, arg_index),
+                        .r = .{ .simple = .comptime_param_arg },
+                    } };
                 }
                 // Give the arg its result type
                 const provide_param_ty = if (maybe_param_ty) |t| t else Type.generic_poison;
@@ -7611,23 +7640,37 @@ fn analyzeCall(
 
     const gpa = sema.gpa;
 
+    const func_ret_ty_src: LazySrcLoc = if (try sema.funcDeclSrcInst(func)) |fn_decl_inst| .{
+        .base_node_inst = fn_decl_inst,
+        .offset = .{ .node_offset_fn_type_ret_ty = 0 },
+    } else func_src;
+
+    // If this is not `null`, the call is comptime.
+    var comptime_call_reason: ?BlockComptimeReason = cr: {
+        if (block.comptime_reason) |r| break :cr r;
+        if (modifier == .compile_time) break :cr .{ .reason = .{
+            .src = call_src,
+            .r = .{ .simple = .comptime_call_modifier },
+        } };
+        break :cr null;
+    };
+
     const is_generic_call = func_ty_info.is_generic;
-    var is_comptime_call = block.is_comptime or modifier == .compile_time;
-    var is_inline_call = is_comptime_call or modifier == .always_inline or func_ty_info.cc == .@"inline";
-    var comptime_reason: ?*const Block.ComptimeReason = null;
-    if (!is_inline_call and !is_comptime_call) {
+    var is_inline_call = comptime_call_reason != null or modifier == .always_inline or func_ty_info.cc == .@"inline";
+    if (!is_inline_call) {
         if (try Type.fromInterned(func_ty_info.return_type).comptimeOnlySema(pt)) {
-            is_comptime_call = true;
             is_inline_call = true;
-            comptime_reason = &.{ .comptime_ret_ty = .{
-                .func = func,
-                .func_src = func_src,
-                .return_ty = Type.fromInterned(func_ty_info.return_type),
+            comptime_call_reason = .{ .reason = .{
+                .src = func_ret_ty_src,
+                .r = .{ .comptime_only = .{
+                    .ty = .fromInterned(func_ty_info.return_type),
+                    .msg = .ret_ty_call,
+                } },
             } };
         }
     }
 
-    if (sema.func_is_naked and !is_inline_call and !is_comptime_call) {
+    if (sema.func_is_naked and !is_inline_call) {
         const msg = msg: {
             const msg = try sema.errMsg(call_src, "runtime {s} not allowed in naked function", .{@tagName(operation)});
             errdefer msg.destroy(sema.gpa);
@@ -7642,6 +7685,7 @@ fn analyzeCall(
     }
 
     if (!is_inline_call and is_generic_call) {
+        var comptime_ret_ty: Type = undefined;
         if (sema.instantiateGenericCall(
             block,
             func,
@@ -7651,6 +7695,7 @@ fn analyzeCall(
             args_info,
             call_tag,
             call_dbg_node,
+            &comptime_ret_ty,
         )) |some| {
             return some;
         } else |err| switch (err) {
@@ -7659,26 +7704,34 @@ fn analyzeCall(
             },
             error.ComptimeReturn => {
                 is_inline_call = true;
-                is_comptime_call = true;
-                comptime_reason = &.{ .comptime_ret_ty = .{
-                    .func = func,
-                    .func_src = func_src,
-                    .return_ty = Type.fromInterned(func_ty_info.return_type),
+                comptime_call_reason = .{ .reason = .{
+                    .src = func_ret_ty_src,
+                    .r = .{
+                        .comptime_only = .{
+                            .ty = comptime_ret_ty,
+                            .msg = .ret_ty_generic_call,
+                        },
+                    },
                 } };
             },
             else => |e| return e,
         }
     }
 
+    const is_comptime_call = comptime_call_reason != null;
+    // `comptime_call_reason` shouldn't be mutated again
+    defer assert(is_comptime_call == (comptime_call_reason != null));
+
     if (is_comptime_call and modifier == .never_inline) {
         return sema.fail(block, call_src, "unable to perform 'never_inline' call at compile-time", .{});
     }
 
     const result: Air.Inst.Ref = if (is_inline_call) res: {
-        const func_val = try sema.resolveConstDefinedValue(block, func_src, func, .{
-            .needed_comptime_reason = "function being called at comptime must be comptime-known",
-            .block_comptime_reason = comptime_reason,
-        });
+        const old_comptime_reason = block.comptime_reason;
+        block.comptime_reason = comptime_call_reason;
+        defer block.comptime_reason = old_comptime_reason;
+
+        const func_val = try sema.resolveConstDefinedValue(block, func_src, func, .{ .simple = .comptime_call_target });
         const module_fn_index = switch (zcu.intern_pool.indexToKey(func_val.toIntern())) {
             .@"extern" => return sema.fail(block, call_src, "{s} call of extern function", .{
                 @as([]const u8, if (is_comptime_call) "comptime" else "inline"),
@@ -7767,8 +7820,7 @@ fn analyzeCall(
             .label = null,
             .inlining = &inlining,
             .is_typeof = block.is_typeof,
-            .is_comptime = is_comptime_call,
-            .comptime_reason = comptime_reason,
+            .comptime_reason = if (is_comptime_call) .inlining_parent else null,
             .error_return_trace_index = block.error_return_trace_index,
             .runtime_cond = block.runtime_cond,
             .runtime_loop = block.runtime_loop,
@@ -7857,11 +7909,16 @@ fn analyzeCall(
         // on parameters, we must now do the same for the return type as we just did with
         // each of the parameters, resolving the return type and providing it to the child
         // `Sema` so that it can be used for the `ret_ptr` instruction.
-        const ret_ty_inst = if (fn_info.ret_ty_body.len != 0)
-            try sema.resolveInlineBody(&child_block, fn_info.ret_ty_body, module_fn.zir_body_inst.resolve(ip) orelse return error.AnalysisFail)
-        else
-            try sema.resolveInst(fn_info.ret_ty_ref);
         const ret_ty_src: LazySrcLoc = .{ .base_node_inst = module_fn.zir_body_inst, .offset = .{ .node_offset_fn_type_ret_ty = 0 } };
+        const ret_ty_inst = if (fn_info.ret_ty_body.len != 0) r: {
+            const old_child_comptime_reason = child_block.comptime_reason;
+            defer child_block.comptime_reason = old_child_comptime_reason;
+            child_block.comptime_reason = .{ .reason = .{
+                .src = ret_ty_src,
+                .r = .{ .simple = .function_ret_ty },
+            } };
+            break :r try sema.resolveInlineBody(&child_block, fn_info.ret_ty_body, module_fn.zir_body_inst.resolve(ip) orelse return error.AnalysisFail);
+        } else try sema.resolveInst(fn_info.ret_ty_ref);
         sema.fn_ret_ty = try sema.analyzeAsType(&child_block, ret_ty_src, ret_ty_inst);
         if (module_fn.analysisUnordered(ip).inferred_error_set) {
             // Create a fresh inferred error set type for inline/comptime calls.
@@ -8136,23 +8193,18 @@ fn analyzeInlineCallArg(
                 return casted_arg;
             }
             const arg_src = args_info.argSrc(arg_block, arg_i.*);
-            if (try Type.fromInterned(param_ty).comptimeOnlySema(ics.callee().pt)) {
-                _ = try ics.caller().resolveConstValue(arg_block, arg_src, casted_arg, .{
-                    .needed_comptime_reason = "argument to parameter with comptime-only type must be comptime-known",
-                    .block_comptime_reason = param_block.comptime_reason,
-                });
-            } else if (!is_comptime_call and zir_tags[@intFromEnum(inst)] == .param_comptime) {
-                _ = try ics.caller().resolveConstValue(arg_block, arg_src, casted_arg, .{
-                    .needed_comptime_reason = "parameter is comptime",
-                });
+            if (zir_tags[@intFromEnum(inst)] == .param_comptime) {
+                _ = try ics.caller().resolveConstValue(arg_block, arg_src, casted_arg, .{ .simple = .comptime_param_arg });
+            } else if (!is_comptime_call and try Type.fromInterned(param_ty).comptimeOnlySema(ics.callee().pt)) {
+                _ = try ics.caller().resolveConstValue(arg_block, arg_src, casted_arg, .{ .comptime_only = .{
+                    .ty = .fromInterned(param_ty),
+                    .msg = .param_ty_arg,
+                } });
             }
 
             if (is_comptime_call) {
                 ics.callee().inst_map.putAssumeCapacityNoClobber(inst, casted_arg);
-                const arg_val = try ics.caller().resolveConstValue(arg_block, arg_src, casted_arg, .{
-                    .needed_comptime_reason = "argument to function being called at comptime must be comptime-known",
-                    .block_comptime_reason = param_block.comptime_reason,
-                });
+                const arg_val = try ics.caller().resolveConstValue(arg_block, arg_src, casted_arg, null);
                 switch (arg_val.toIntern()) {
                     .generic_poison, .generic_poison_type => {
                         // This function is currently evaluated as part of an as-of-yet unresolvable
@@ -8188,10 +8240,7 @@ fn analyzeInlineCallArg(
 
             if (is_comptime_call) {
                 ics.callee().inst_map.putAssumeCapacityNoClobber(inst, uncasted_arg);
-                const arg_val = try ics.caller().resolveConstValue(arg_block, arg_src, uncasted_arg, .{
-                    .needed_comptime_reason = "argument to function being called at comptime must be comptime-known",
-                    .block_comptime_reason = param_block.comptime_reason,
-                });
+                const arg_val = try ics.caller().resolveConstValue(arg_block, arg_src, uncasted_arg, null);
                 switch (arg_val.toIntern()) {
                     .generic_poison, .generic_poison_type => {
                         // This function is currently evaluated as part of an as-of-yet unresolvable
@@ -8208,9 +8257,7 @@ fn analyzeInlineCallArg(
                 memoized_arg_values[arg_i.*] = resolved_arg_val.toIntern();
             } else {
                 if (zir_tags[@intFromEnum(inst)] == .param_anytype_comptime) {
-                    _ = try ics.caller().resolveConstValue(arg_block, arg_src, uncasted_arg, .{
-                        .needed_comptime_reason = "parameter is comptime",
-                    });
+                    _ = try ics.caller().resolveConstValue(arg_block, arg_src, uncasted_arg, .{ .simple = .comptime_param_arg });
                 }
                 ics.callee().inst_map.putAssumeCapacityNoClobber(inst, uncasted_arg);
             }
@@ -8237,15 +8284,18 @@ fn instantiateGenericCall(
     args_info: CallArgsInfo,
     call_tag: Air.Inst.Tag,
     call_dbg_node: ?Zir.Inst.Index,
+    /// Populated when `error.ComptimeReturn` is returned.
+    comptime_ret_ty: *Type,
 ) CompileError!Air.Inst.Ref {
     const pt = sema.pt;
     const zcu = pt.zcu;
     const gpa = sema.gpa;
     const ip = &zcu.intern_pool;
 
-    const func_val = try sema.resolveConstDefinedValue(block, func_src, func, .{
-        .needed_comptime_reason = "generic function being called must be comptime-known",
-    });
+    // Generic function pointers are comptime-only types, so `func` is definitely comptime-known.
+    const func_val = (sema.resolveValue(func) catch unreachable).?;
+    if (func_val.isUndef(zcu)) return sema.failWithUseOfUndef(block, func_src);
+
     const generic_owner = switch (zcu.intern_pool.indexToKey(func_val.toIntern())) {
         .func => func_val.toIntern(),
         .ptr => |ptr| ip.getNav(ptr.base_addr.nav).status.fully_resolved.val,
@@ -8310,7 +8360,7 @@ fn instantiateGenericCall(
         .namespace = fn_nav.analysis.?.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = undefined, // set as needed
         .src_base_inst = fn_nav.analysis.?.zir_index,
         .type_name_ctx = fn_nav.fqn,
     };
@@ -8354,12 +8404,13 @@ fn instantiateGenericCall(
                             child_sema.generic_call_src = prev_generic_call_src;
                         }
 
+                        const param_ty_src = child_block.tokenOffset(param_data.src_tok);
+                        child_block.comptime_reason = .{ .reason = .{
+                            .src = param_ty_src,
+                            .r = .{ .simple = .type },
+                        } };
                         const param_ty_inst = try child_sema.resolveInlineBody(&child_block, param_ty_body, param_inst);
-                        break :param_ty try child_sema.analyzeAsType(
-                            &child_block,
-                            child_block.tokenOffset(param_data.src_tok),
-                            param_ty_inst,
-                        );
+                        break :param_ty try child_sema.analyzeAsType(&child_block, param_ty_src, param_ty_inst);
                     },
                     else => unreachable,
                 }
@@ -8452,6 +8503,10 @@ fn instantiateGenericCall(
 
     // We've already handled parameters, so don't resolve the whole body. Instead, just
     // do the instructions after the params (i.e. the func itself).
+    child_block.comptime_reason = .{ .reason = .{
+        .src = call_src,
+        .r = .{ .simple = .type },
+    } };
     const new_func_inst = try child_sema.resolveInlineBody(&child_block, fn_info.param_body[args_info.count()..], fn_info.param_body_inst);
     const callee_index = (child_sema.resolveConstDefinedValue(&child_block, LazySrcLoc.unneeded, new_func_inst, undefined) catch unreachable).toIntern();
 
@@ -8465,6 +8520,7 @@ fn instantiateGenericCall(
     // If the call evaluated to a return type that requires comptime, never mind
     // our generic instantiation. Instead we need to perform a comptime call.
     if (try Type.fromInterned(func_ty_info.return_type).comptimeOnlySema(pt)) {
+        comptime_ret_ty.* = .fromInterned(func_ty_info.return_type);
         return error.ComptimeReturn;
     }
     // Similarly, if the call evaluated to a generic type we need to instead
@@ -8622,9 +8678,7 @@ fn zirVectorType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!
     const len_src = block.builtinCallArgSrc(inst_data.src_node, 0);
     const elem_type_src = block.builtinCallArgSrc(inst_data.src_node, 1);
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
-    const len: u32 = @intCast(try sema.resolveInt(block, len_src, extra.lhs, Type.u32, .{
-        .needed_comptime_reason = "vector length must be comptime-known",
-    }));
+    const len: u32 = @intCast(try sema.resolveInt(block, len_src, extra.lhs, Type.u32, .{ .simple = .vector_length }));
     const elem_type = try sema.resolveType(block, elem_type_src, extra.rhs);
     try sema.checkVectorElemType(block, elem_type_src, elem_type);
     const vector_type = try sema.pt.vectorType(.{
@@ -8642,9 +8696,7 @@ fn zirArrayType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
     const len_src = block.src(.{ .node_offset_array_type_len = inst_data.src_node });
     const elem_src = block.src(.{ .node_offset_array_type_elem = inst_data.src_node });
-    const len = try sema.resolveInt(block, len_src, extra.lhs, Type.usize, .{
-        .needed_comptime_reason = "array length must be comptime-known",
-    });
+    const len = try sema.resolveInt(block, len_src, extra.lhs, Type.usize, .{ .simple = .array_length });
     const elem_type = try sema.resolveType(block, elem_src, extra.rhs);
     try sema.validateArrayElemType(block, elem_type, elem_src);
     const array_ty = try sema.pt.arrayType(.{
@@ -8664,16 +8716,12 @@ fn zirArrayTypeSentinel(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Compil
     const len_src = block.src(.{ .node_offset_array_type_len = inst_data.src_node });
     const sentinel_src = block.src(.{ .node_offset_array_type_sentinel = inst_data.src_node });
     const elem_src = block.src(.{ .node_offset_array_type_elem = inst_data.src_node });
-    const len = try sema.resolveInt(block, len_src, extra.len, Type.usize, .{
-        .needed_comptime_reason = "array length must be comptime-known",
-    });
+    const len = try sema.resolveInt(block, len_src, extra.len, Type.usize, .{ .simple = .array_length });
     const elem_type = try sema.resolveType(block, elem_src, extra.elem_type);
     try sema.validateArrayElemType(block, elem_type, elem_src);
     const uncasted_sentinel = try sema.resolveInst(extra.sentinel);
     const sentinel = try sema.coerce(block, elem_type, uncasted_sentinel, sentinel_src);
-    const sentinel_val = try sema.resolveConstDefinedValue(block, sentinel_src, sentinel, .{
-        .needed_comptime_reason = "array sentinel value must be comptime-known",
-    });
+    const sentinel_val = try sema.resolveConstDefinedValue(block, sentinel_src, sentinel, .{ .simple = .array_sentinel });
     const array_ty = try sema.pt.arrayType(.{
         .len = len,
         .sentinel = sentinel_val.toIntern(),
@@ -9071,9 +9119,7 @@ fn zirEnumFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     }
 
     if (dest_ty.intTagType(zcu).zigTypeTag(zcu) == .comptime_int) {
-        return sema.failWithNeededComptime(block, operand_src, .{
-            .needed_comptime_reason = "value being casted to enum with 'comptime_int' tag type must be comptime-known",
-        });
+        return sema.failWithNeededComptime(block, operand_src, .{ .simple = .casted_to_comptime_enum });
     }
 
     if (try sema.typeHasOnePossibleValue(dest_ty)) |opv| {
@@ -9487,9 +9533,7 @@ fn zirFunc(
             const ret_ty_body = sema.code.bodySlice(extra_index, extra.data.ret_body_len);
             extra_index += ret_ty_body.len;
 
-            const ret_ty_val = try sema.resolveGenericBody(block, ret_ty_src, ret_ty_body, inst, Type.type, .{
-                .needed_comptime_reason = "return type must be comptime-known",
-            });
+            const ret_ty_val = try sema.resolveGenericBody(block, ret_ty_src, ret_ty_body, inst, Type.type, .{ .simple = .function_ret_ty });
             break :blk ret_ty_val.toType();
         },
     };
@@ -9556,7 +9600,7 @@ fn resolveGenericBody(
     body: []const Zir.Inst.Index,
     func_inst: Zir.Inst.Index,
     dest_ty: Type,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) !Value {
     assert(body.len != 0);
 
@@ -9894,7 +9938,7 @@ fn funcCommon(
             };
             return sema.failWithOwnedErrorMsg(block, msg);
         }
-        if (is_source_decl and requires_comptime and !param_is_comptime and has_body and !block.is_comptime) {
+        if (is_source_decl and requires_comptime and !param_is_comptime and has_body and !block.isComptime()) {
             const msg = msg: {
                 const msg = try sema.errMsg(param_src, "parameter of type '{}' must be declared comptime", .{
                     param_ty.fmt(pt),
@@ -10132,7 +10176,7 @@ fn finishFunc(
 
     // If the return type is comptime-only but not dependent on parameters then
     // all parameter types also need to be comptime.
-    if (is_source_decl and opt_func_index != .none and ret_ty_requires_comptime and !block.is_comptime) comptime_check: {
+    if (is_source_decl and opt_func_index != .none and ret_ty_requires_comptime and !block.isComptime()) comptime_check: {
         for (block.params.items(.is_comptime)) |is_comptime| {
             if (!is_comptime) break;
         } else break :comptime_check;
@@ -10547,9 +10591,7 @@ fn zirFieldValNamed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErr
     const field_name_src = block.builtinCallArgSrc(inst_data.src_node, 1);
     const extra = sema.code.extraData(Zir.Inst.FieldNamed, inst_data.payload_index).data;
     const object = try sema.resolveInst(extra.lhs);
-    const field_name = try sema.resolveConstStringIntern(block, field_name_src, extra.field_name, .{
-        .needed_comptime_reason = "field name must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, field_name_src, extra.field_name, .{ .simple = .field_name });
     return sema.fieldVal(block, src, object, field_name, field_name_src);
 }
 
@@ -10562,9 +10604,7 @@ fn zirFieldPtrNamed(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErr
     const field_name_src = block.builtinCallArgSrc(inst_data.src_node, 1);
     const extra = sema.code.extraData(Zir.Inst.FieldNamed, inst_data.payload_index).data;
     const object_ptr = try sema.resolveInst(extra.lhs);
-    const field_name = try sema.resolveConstStringIntern(block, field_name_src, extra.field_name, .{
-        .needed_comptime_reason = "field name must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, field_name_src, extra.field_name, .{ .simple = .field_name });
     return sema.fieldPtr(block, src, object_ptr, field_name, field_name_src, false);
 }
 
@@ -11923,7 +11963,6 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
         .instructions = .{},
         .label = &label,
         .inlining = block.inlining,
-        .is_comptime = block.is_comptime,
         .comptime_reason = block.comptime_reason,
         .is_typeof = block.is_typeof,
         .c_import_buf = block.c_import_buf,
@@ -12027,11 +12066,8 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
         };
     }
 
-    if (child_block.is_comptime) {
-        _ = try sema.resolveConstDefinedValue(&child_block, main_operand_src, raw_operand_val, .{
-            .needed_comptime_reason = "condition in comptime switch must be comptime-known",
-            .block_comptime_reason = child_block.comptime_reason,
-        });
+    if (child_block.isComptime()) {
+        _ = try sema.resolveConstDefinedValue(&child_block, main_operand_src, raw_operand_val, null);
         unreachable;
     }
 
@@ -12148,7 +12184,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
 
         const operand_ty = sema.typeOf(val);
 
-        if (extra.data.bits.has_continue and !block.is_comptime) {
+        if (extra.data.bits.has_continue and !block.isComptime()) {
             // Even if the operand is comptime-known, this `switch` is runtime.
             if (try operand_ty.comptimeOnlySema(pt)) {
                 return sema.failWithOwnedErrorMsg(block, msg: {
@@ -12707,7 +12743,6 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         .instructions = .{},
         .label = &label,
         .inlining = block.inlining,
-        .is_comptime = block.is_comptime,
         .comptime_reason = block.comptime_reason,
         .is_typeof = block.is_typeof,
         .c_import_buf = block.c_import_buf,
@@ -12790,11 +12825,8 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         },
     }
 
-    if (child_block.is_comptime) {
-        _ = try sema.resolveConstDefinedValue(&child_block, operand_src, operand.simple.cond, .{
-            .needed_comptime_reason = "condition in comptime switch must be comptime-known",
-            .block_comptime_reason = child_block.comptime_reason,
-        });
+    if (child_block.isComptime()) {
+        _ = try sema.resolveConstDefinedValue(&child_block, operand_src, operand.simple.cond, null);
         unreachable;
     }
 
@@ -13582,10 +13614,7 @@ fn resolveSwitchComptimeLoop(
 
                 const cond_ref = try sema.switchCond(child_block, src, val);
 
-                cond_val = try sema.resolveConstDefinedValue(child_block, src, cond_ref, .{
-                    .needed_comptime_reason = "condition in comptime switch must be comptime-known",
-                    .block_comptime_reason = child_block.comptime_reason,
-                });
+                cond_val = try sema.resolveConstDefinedValue(child_block, src, cond_ref, null);
                 spa.operand = .{ .simple = .{
                     .by_val = val,
                     .by_ref = ref,
@@ -13825,9 +13854,7 @@ fn resolveSwitchItemVal(
 
     const item = try sema.coerce(block, coerce_ty, uncoerced_item, item_src);
 
-    const maybe_lazy = try sema.resolveConstDefinedValue(block, item_src, item, .{
-        .needed_comptime_reason = "switch prong values must be comptime-known",
-    });
+    const maybe_lazy = try sema.resolveConstDefinedValue(block, item_src, item, .{ .simple = .switch_item });
 
     const val = try sema.resolveLazyValue(maybe_lazy);
     const new_item = if (val.toIntern() != maybe_lazy.toIntern()) blk: {
@@ -14295,9 +14322,7 @@ fn zirHasField(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
     const ty_src = block.builtinCallArgSrc(inst_data.src_node, 0);
     const name_src = block.builtinCallArgSrc(inst_data.src_node, 1);
     const ty = try sema.resolveType(block, ty_src, extra.lhs);
-    const field_name = try sema.resolveConstStringIntern(block, name_src, extra.rhs, .{
-        .needed_comptime_reason = "field name must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, name_src, extra.rhs, .{ .simple = .field_name });
     try ty.resolveFields(pt);
     const ip = &zcu.intern_pool;
 
@@ -14344,9 +14369,7 @@ fn zirHasDecl(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
     const lhs_src = block.builtinCallArgSrc(inst_data.src_node, 0);
     const rhs_src = block.builtinCallArgSrc(inst_data.src_node, 1);
     const container_type = try sema.resolveType(block, lhs_src, extra.lhs);
-    const decl_name = try sema.resolveConstStringIntern(block, rhs_src, extra.rhs, .{
-        .needed_comptime_reason = "decl name must be comptime-known",
-    });
+    const decl_name = try sema.resolveConstStringIntern(block, rhs_src, extra.rhs, .{ .simple = .decl_name });
 
     try sema.checkNamespaceType(block, lhs_src, container_type);
 
@@ -14399,9 +14422,7 @@ fn zirEmbedFile(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     const pt = sema.pt;
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_node;
     const operand_src = block.builtinCallArgSrc(inst_data.src_node, 0);
-    const name = try sema.resolveConstString(block, operand_src, inst_data.operand, .{
-        .needed_comptime_reason = "file path name must be comptime-known",
-    });
+    const name = try sema.resolveConstString(block, operand_src, inst_data.operand, .{ .simple = .operand_embedFile });
 
     if (name.len == 0) {
         return sema.fail(block, operand_src, "file path name cannot be empty", .{});
@@ -14985,7 +15006,7 @@ fn zirArrayCat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
 
     const resolved_elem_ty = t: {
         var trash_block = block.makeSubBlock();
-        trash_block.is_comptime = false;
+        trash_block.comptime_reason = null;
         defer trash_block.instructions.deinit(sema.gpa);
 
         const instructions = [_]Air.Inst.Ref{
@@ -15268,9 +15289,7 @@ fn getArrayCatInfo(sema: *Sema, block: *Block, src: LazySrcLoc, operand: Air.Ins
             const ptr_info = operand_ty.ptrInfo(zcu);
             switch (ptr_info.flags.size) {
                 .Slice => {
-                    const val = try sema.resolveConstDefinedValue(block, src, operand, .{
-                        .needed_comptime_reason = "slice value being concatenated must be comptime-known",
-                    });
+                    const val = try sema.resolveConstDefinedValue(block, src, operand, .{ .simple = .slice_cat_operand });
                     return Type.ArrayInfo{
                         .elem_type = Type.fromInterned(ptr_info.child),
                         .sentinel = switch (ptr_info.sentinel) {
@@ -15431,9 +15450,7 @@ fn zirArrayMul(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
 
     if (lhs_ty.isTuple(zcu)) {
         // In `**` rhs must be comptime-known, but lhs can be runtime-known
-        const factor = try sema.resolveInt(block, rhs_src, extra.rhs, Type.usize, .{
-            .needed_comptime_reason = "array multiplication factor must be comptime-known",
-        });
+        const factor = try sema.resolveInt(block, rhs_src, extra.rhs, Type.usize, .{ .simple = .array_mul_factor });
         const factor_casted = try sema.usizeCast(block, rhs_src, factor);
         return sema.analyzeTupleMul(block, inst_data.src_node, lhs, factor_casted);
     }
@@ -15455,9 +15472,7 @@ fn zirArrayMul(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Ai
     };
 
     // In `**` rhs must be comptime-known, but lhs can be runtime-known
-    const factor = try sema.resolveInt(block, rhs_src, extra.rhs, Type.usize, .{
-        .needed_comptime_reason = "array multiplication factor must be comptime-known",
-    });
+    const factor = try sema.resolveInt(block, rhs_src, extra.rhs, Type.usize, .{ .simple = .array_mul_factor });
 
     const result_len_u64 = std.math.mul(u64, lhs_info.len, factor) catch
         return sema.fail(block, rhs_src, "operation results in overflow", .{});
@@ -17635,12 +17650,9 @@ fn zirAsm(
     const is_global_assembly = sema.func_index == .none;
     const zir_tags = sema.code.instructions.items(.tag);
 
-    const asm_source: []const u8 = if (tmpl_is_expr) blk: {
+    const asm_source: []const u8 = if (tmpl_is_expr) s: {
         const tmpl: Zir.Inst.Ref = @enumFromInt(@intFromEnum(extra.data.asm_source));
-        const s: []const u8 = try sema.resolveConstString(block, src, tmpl, .{
-            .needed_comptime_reason = "assembly code must be comptime-known",
-        });
-        break :blk s;
+        break :s try sema.resolveConstString(block, src, tmpl, .{ .simple = .inline_assembly_code });
     } else sema.code.nullTerminatedString(extra.data.asm_source);
 
     if (is_global_assembly) {
@@ -18203,7 +18215,7 @@ fn zirClosureGet(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstDat
         return sema.failWithOwnedErrorMsg(block, msg);
     }
 
-    if (!block.is_typeof and !block.is_comptime and sema.func_index != .none) {
+    if (!block.is_typeof and !block.isComptime() and sema.func_index != .none) {
         const msg = msg: {
             const name = name: {
                 const file, const src_base_node = Zcu.LazySrcLoc.resolveBaseNode(block.src_base_inst, zcu).?;
@@ -18244,7 +18256,7 @@ fn zirRetAddr(
     extended: Zir.Inst.Extended.InstData,
 ) CompileError!Air.Inst.Ref {
     _ = extended;
-    if (block.is_comptime) {
+    if (block.isComptime()) {
         // TODO: we could give a meaningful lazy value here. #14938
         return sema.pt.intRef(Type.usize, 0);
     } else {
@@ -19342,7 +19354,7 @@ fn zirTypeofBuiltin(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErr
         .namespace = block.namespace,
         .instructions = .{},
         .inlining = block.inlining,
-        .is_comptime = false,
+        .comptime_reason = null,
         .is_typeof = true,
         .want_safety = false,
         .error_return_trace_index = block.error_return_trace_index,
@@ -19422,7 +19434,7 @@ fn zirTypeofPeer(
         .namespace = block.namespace,
         .instructions = .{},
         .inlining = block.inlining,
-        .is_comptime = false,
+        .comptime_reason = null,
         .is_typeof = true,
         .runtime_cond = block.runtime_cond,
         .runtime_loop = block.runtime_loop,
@@ -19980,7 +19992,7 @@ fn ensurePostHoc(sema: *Sema, block: *Block, dest_block: Zir.Inst.Index) !*Label
             .instructions = .{},
             .label = &labeled_block.label,
             .inlining = block.inlining,
-            .is_comptime = block.is_comptime,
+            .comptime_reason = block.comptime_reason,
             .src_base_inst = block.src_base_inst,
             .type_name_ctx = block.type_name_ctx,
         },
@@ -20013,7 +20025,7 @@ fn zirUnreachable(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].@"unreachable";
     const src = block.nodeOffset(inst_data.src_node);
 
-    if (block.is_comptime) {
+    if (block.isComptime()) {
         return sema.fail(block, src, "reached unreachable code", .{});
     }
     // TODO Add compile error for @optimizeFor occurring too late in a scope.
@@ -20066,7 +20078,7 @@ fn zirRetImplicit(
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].un_tok;
     const r_brace_src = block.tokenOffset(inst_data.src_tok);
     if (block.inlining == null and sema.func_is_naked) {
-        assert(!block.is_comptime);
+        assert(!block.isComptime());
         if (block.wantSafety()) {
             // Calling a safety function from a naked function would not be legal.
             _ = try block.addNoOp(.trap);
@@ -20123,7 +20135,7 @@ fn zirRetLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!voi
     const src = block.nodeOffset(inst_data.src_node);
     const ret_ptr = try sema.resolveInst(inst_data.operand);
 
-    if (block.is_comptime or block.inlining != null or sema.func_is_naked) {
+    if (block.isComptime() or block.inlining != null or sema.func_is_naked) {
         const operand = try sema.analyzeLoad(block, src, ret_ptr, src);
         return sema.analyzeRet(block, operand, src, block.src(.{ .node_offset_return_operand = inst_data.src_node }));
     }
@@ -20215,7 +20227,7 @@ fn zirSaveErrRetIndex(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileE
     if (!block.ownerModule().error_tracing) return;
 
     // This is only relevant at runtime.
-    if (block.is_comptime or block.is_typeof) return;
+    if (block.isComptime() or block.is_typeof) return;
 
     const save_index = inst_data.operand == .none or b: {
         const operand = try sema.resolveInst(inst_data.operand);
@@ -20268,7 +20280,7 @@ fn restoreErrRetIndex(sema: *Sema, start_block: *Block, src: LazySrcLoc, target_
 
     const operand = try sema.resolveInstAllowNone(operand_zir);
 
-    if (start_block.is_comptime or start_block.is_typeof) {
+    if (start_block.isComptime() or start_block.is_typeof) {
         const is_non_error = if (operand != .none) blk: {
             const is_non_error_inst = try sema.analyzeIsNonErr(start_block, src, operand);
             const cond_val = try sema.resolveDefinedValue(start_block, src, is_non_error_inst);
@@ -20345,10 +20357,8 @@ fn analyzeRet(
     };
 
     if (block.inlining) |inlining| {
-        if (block.is_comptime) {
-            const ret_val = try sema.resolveConstValue(block, operand_src, operand, .{
-                .needed_comptime_reason = "value being returned at comptime must be comptime-known",
-            });
+        if (block.isComptime()) {
+            const ret_val = try sema.resolveConstValue(block, operand_src, operand, null);
             inlining.comptime_result = operand;
 
             if (sema.fn_ret_ty.isError(zcu) and ret_val.getErrorName(zcu) != .none) {
@@ -20362,7 +20372,7 @@ fn analyzeRet(
         try inlining.merges.br_list.append(sema.gpa, br_inst.toIndex().?);
         try inlining.merges.src_locs.append(sema.gpa, operand_src);
         return;
-    } else if (block.is_comptime) {
+    } else if (block.isComptime()) {
         return sema.fail(block, src, "function called at runtime cannot return value at comptime", .{});
     } else if (sema.func_is_naked) {
         const msg = msg: {
@@ -20436,9 +20446,7 @@ fn zirPtrType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
         const ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_i]);
         extra_i += 1;
         const coerced = try sema.coerce(block, elem_ty, try sema.resolveInst(ref), sentinel_src);
-        const val = try sema.resolveConstDefinedValue(block, sentinel_src, coerced, .{
-            .needed_comptime_reason = "pointer sentinel value must be comptime-known",
-        });
+        const val = try sema.resolveConstDefinedValue(block, sentinel_src, coerced, .{ .simple = .pointer_sentinel });
         try checkSentinelType(sema, block, sentinel_src, elem_ty);
         break :blk val.toIntern();
     } else .none;
@@ -20447,9 +20455,7 @@ fn zirPtrType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
         const ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_i]);
         extra_i += 1;
         const coerced = try sema.coerce(block, Type.u32, try sema.resolveInst(ref), align_src);
-        const val = try sema.resolveConstDefinedValue(block, align_src, coerced, .{
-            .needed_comptime_reason = "pointer alignment must be comptime-known",
-        });
+        const val = try sema.resolveConstDefinedValue(block, align_src, coerced, .{ .simple = .@"align" });
         // Check if this happens to be the lazy alignment of our element type, in
         // which case we can make this 0 without resolving it.
         switch (zcu.intern_pool.indexToKey(val.toIntern())) {
@@ -20472,18 +20478,14 @@ fn zirPtrType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
     const bit_offset: u16 = if (inst_data.flags.has_bit_range) blk: {
         const ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_i]);
         extra_i += 1;
-        const bit_offset = try sema.resolveInt(block, bitoffset_src, ref, Type.u16, .{
-            .needed_comptime_reason = "pointer bit-offset must be comptime-known",
-        });
+        const bit_offset = try sema.resolveInt(block, bitoffset_src, ref, Type.u16, .{ .simple = .type });
         break :blk @intCast(bit_offset);
     } else 0;
 
     const host_size: u16 = if (inst_data.flags.has_bit_range) blk: {
         const ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_i]);
         extra_i += 1;
-        const host_size = try sema.resolveInt(block, hostsize_src, ref, Type.u16, .{
-            .needed_comptime_reason = "pointer host size must be comptime-known",
-        });
+        const host_size = try sema.resolveInt(block, hostsize_src, ref, Type.u16, .{ .simple = .type });
         break :blk @intCast(host_size);
     } else 0;
 
@@ -20671,9 +20673,7 @@ fn zirUnionInit(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     if (union_ty.zigTypeTag(pt.zcu) != .@"union") {
         return sema.fail(block, ty_src, "expected union type, found '{}'", .{union_ty.fmt(pt)});
     }
-    const field_name = try sema.resolveConstStringIntern(block, field_src, extra.field_name, .{
-        .needed_comptime_reason = "name of field being initialized must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, field_src, extra.field_name, .{ .simple = .union_field_name });
     const init = try sema.resolveInst(extra.init);
     return sema.unionInit(block, init, init_src, union_ty, ty_src, field_name, field_src);
 }
@@ -20800,9 +20800,7 @@ fn zirStructInit(
                 try resolved_ty.resolveStructFieldInits(pt);
                 if (try resolved_ty.structFieldValueComptime(pt, field_index)) |default_value| {
                     const init_val = (try sema.resolveValue(field_inits[field_index])) orelse {
-                        return sema.failWithNeededComptime(block, field_src, .{
-                            .needed_comptime_reason = "value stored in comptime field must be comptime-known",
-                        });
+                        return sema.failWithNeededComptime(block, field_src, .{ .simple = .stored_to_comptime_field });
                     };
 
                     if (!init_val.eql(default_value, resolved_ty.fieldType(field_index, zcu), zcu)) {
@@ -20862,9 +20860,10 @@ fn zirStructInit(
         }
 
         if (try resolved_ty.comptimeOnlySema(pt)) {
-            return sema.failWithNeededComptime(block, field_src, .{
-                .needed_comptime_reason = "initializer of comptime only union must be comptime-known",
-            });
+            return sema.failWithNeededComptime(block, field_src, .{ .comptime_only = .{
+                .ty = resolved_ty,
+                .msg = .union_init,
+            } });
         }
 
         try sema.validateRuntimeValue(block, field_src, init_inst);
@@ -21003,9 +21002,10 @@ fn finishStructInit(
         return sema.failWithNeededComptime(block, block.src(.{ .init_elem = .{
             .init_node_offset = init_src.offset.node_offset.x,
             .elem_index = @intCast(runtime_index),
-        } }), .{
-            .needed_comptime_reason = "initializer of comptime only struct must be comptime-known",
-        });
+        } }), .{ .comptime_only = .{
+            .ty = struct_ty,
+            .msg = .struct_init,
+        } });
     }
 
     for (field_inits) |field_init| {
@@ -21315,11 +21315,7 @@ fn zirArrayInit(
             if (array_ty.structFieldIsComptime(i, zcu))
                 try array_ty.resolveStructFieldInits(pt);
             if (try array_ty.structFieldValueComptime(pt, i)) |field_val| {
-                const init_val = try sema.resolveValue(dest.*) orelse {
-                    return sema.failWithNeededComptime(block, elem_src, .{
-                        .needed_comptime_reason = "value stored in comptime field must be comptime-known",
-                    });
-                };
+                const init_val = try sema.resolveConstValue(block, elem_src, dest.*, .{ .simple = .stored_to_comptime_field });
                 if (!field_val.eql(init_val, elem_ty, zcu)) {
                     return sema.failWithInvalidComptimeFieldStore(block, elem_src, array_ty, i);
                 }
@@ -21508,9 +21504,7 @@ fn zirFieldTypeRef(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
     const ty_src = block.builtinCallArgSrc(inst_data.src_node, 0);
     const field_src = block.builtinCallArgSrc(inst_data.src_node, 1);
     const aggregate_ty = try sema.resolveType(block, ty_src, extra.container_type);
-    const field_name = try sema.resolveConstStringIntern(block, field_src, extra.field_name, .{
-        .needed_comptime_reason = "field name must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, field_src, extra.field_name, .{ .simple = .field_name });
     return sema.fieldType(block, aggregate_ty, field_name, field_src, ty_src);
 }
 
@@ -21890,9 +21884,7 @@ fn zirReify(
     const type_info_ty = try sema.getBuiltinType("Type");
     const uncasted_operand = try sema.resolveInst(extra.operand);
     const type_info = try sema.coerce(block, type_info_ty, uncasted_operand, operand_src);
-    const val = try sema.resolveConstDefinedValue(block, operand_src, type_info, .{
-        .needed_comptime_reason = "operand to @Type must be comptime-known",
-    });
+    const val = try sema.resolveConstDefinedValue(block, operand_src, type_info, .{ .simple = .operand_Type });
     const union_val = ip.indexToKey(val.toIntern()).un;
     if (try sema.anyUndef(block, operand_src, Value.fromInterned(union_val.val))) {
         return sema.failWithUseOfUndef(block, operand_src);
@@ -22136,9 +22128,7 @@ fn zirReify(
             const payload_val = Value.fromInterned(union_val.val).optionalValue(zcu) orelse
                 return Air.internedToRef(Type.anyerror.toIntern());
 
-            const names_val = try sema.derefSliceAsArray(block, src, payload_val, .{
-                .needed_comptime_reason = "error set contents must be comptime-known",
-            });
+            const names_val = try sema.derefSliceAsArray(block, src, payload_val, .{ .simple = .error_set_contents });
 
             const len = try sema.usizeCast(block, src, names_val.typeOf(zcu).arrayLen(zcu));
             var names: InferredErrorSet.NameMap = .{};
@@ -22151,9 +22141,7 @@ fn zirReify(
                     try ip.getOrPutString(gpa, pt.tid, "name", .no_embedded_nulls),
                 ).?);
 
-                const name = try sema.sliceToIpString(block, src, name_val, .{
-                    .needed_comptime_reason = "error set contents must be comptime-known",
-                });
+                const name = try sema.sliceToIpString(block, src, name_val, .{ .simple = .error_set_contents });
                 _ = try pt.getErrorValue(name);
                 const gop = names.getOrPutAssumeCapacity(name);
                 if (gop.found_existing) {
@@ -22200,9 +22188,7 @@ fn zirReify(
                 return sema.fail(block, src, "non-packed struct does not support backing integer type", .{});
             }
 
-            const fields_arr = try sema.derefSliceAsArray(block, operand_src, fields_val, .{
-                .needed_comptime_reason = "struct fields must be comptime-known",
-            });
+            const fields_arr = try sema.derefSliceAsArray(block, operand_src, fields_val, .{ .simple = .struct_fields });
 
             if (is_tuple_val.toBool()) {
                 switch (layout) {
@@ -22238,9 +22224,7 @@ fn zirReify(
                 return sema.fail(block, src, "reified enums must have no decls", .{});
             }
 
-            const fields_arr = try sema.derefSliceAsArray(block, operand_src, fields_val, .{
-                .needed_comptime_reason = "enum fields must be comptime-known",
-            });
+            const fields_arr = try sema.derefSliceAsArray(block, operand_src, fields_val, .{ .simple = .enum_fields });
 
             return sema.reifyEnum(block, inst, src, tag_type_val.toType(), is_exhaustive_val.toBool(), fields_arr, name_strategy);
         },
@@ -22311,9 +22295,7 @@ fn zirReify(
             }
             const layout = zcu.toEnum(std.builtin.Type.ContainerLayout, layout_val);
 
-            const fields_arr = try sema.derefSliceAsArray(block, operand_src, fields_val, .{
-                .needed_comptime_reason = "union fields must be comptime-known",
-            });
+            const fields_arr = try sema.derefSliceAsArray(block, operand_src, fields_val, .{ .simple = .union_fields });
 
             return sema.reifyUnion(block, inst, src, layout, tag_type_val, fields_arr, name_strategy);
         },
@@ -22354,9 +22336,7 @@ fn zirReify(
             const return_type = return_type_val.optionalValue(zcu) orelse
                 return sema.fail(block, src, "Type.Fn.return_type must be non-null for @Type", .{});
 
-            const params_val = try sema.derefSliceAsArray(block, operand_src, params_slice_val, .{
-                .needed_comptime_reason = "function parameters must be comptime-known",
-            });
+            const params_val = try sema.derefSliceAsArray(block, operand_src, params_slice_val, .{ .simple = .function_parameters });
 
             const args_len = try sema.usizeCast(block, src, params_val.typeOf(zcu).arrayLen(zcu));
             const param_types = try sema.arena.alloc(InternPool.Index, args_len);
@@ -22444,9 +22424,7 @@ fn reifyEnum(
         const field_name_val = try field_info.fieldValue(pt, 0);
         const field_value_val = try sema.resolveLazyValue(try field_info.fieldValue(pt, 1));
 
-        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{
-            .needed_comptime_reason = "enum field name must be comptime-known",
-        });
+        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{ .simple = .enum_field_name });
 
         std.hash.autoHash(&hasher, .{
             field_name,
@@ -22591,9 +22569,7 @@ fn reifyUnion(
         const field_type_val = try field_info.fieldValue(pt, 1);
         const field_align_val = try sema.resolveLazyValue(try field_info.fieldValue(pt, 2));
 
-        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{
-            .needed_comptime_reason = "union field name must be comptime-known",
-        });
+        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{ .simple = .union_field_name });
 
         std.hash.autoHash(&hasher, .{
             field_name,
@@ -22835,9 +22811,7 @@ fn reifyTuple(
         const field_is_comptime_val = try field_info.fieldValue(pt, 3);
         const field_alignment_val = try sema.resolveLazyValue(try field_info.fieldValue(pt, 4));
 
-        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{
-            .needed_comptime_reason = "tuple field name must be comptime-known",
-        });
+        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{ .simple = .tuple_field_name });
         const field_type = field_type_val.toType();
         const field_default_value: InternPool.Index = if (field_default_value_val.optionalValue(zcu)) |ptr_val| d: {
             const ptr_ty = try pt.singleConstPtrType(field_type_val.toType());
@@ -22845,7 +22819,7 @@ fn reifyTuple(
             const val = try sema.pointerDeref(block, src, ptr_val, ptr_ty) orelse return sema.failWithNeededComptime(
                 block,
                 src,
-                .{ .needed_comptime_reason = "tuple field default value must be comptime-known" },
+                .{ .simple = .tuple_field_default_value },
             );
             // Resolve the value so that lazy values do not create distinct types.
             break :d (try sema.resolveLazyValue(val)).toIntern();
@@ -22951,9 +22925,7 @@ fn reifyStruct(
         const field_is_comptime_val = try field_info.fieldValue(pt, 3);
         const field_alignment_val = try sema.resolveLazyValue(try field_info.fieldValue(pt, 4));
 
-        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{
-            .needed_comptime_reason = "struct field name must be comptime-known",
-        });
+        const field_name = try sema.sliceToIpString(block, src, field_name_val, .{ .simple = .struct_field_name });
         const field_is_comptime = field_is_comptime_val.toBool();
         const field_default_value: InternPool.Index = if (field_default_value_val.optionalValue(zcu)) |ptr_val| d: {
             const ptr_ty = try pt.singleConstPtrType(field_type_val.toType());
@@ -22961,7 +22933,7 @@ fn reifyStruct(
             const val = try sema.pointerDeref(block, src, ptr_val, ptr_ty) orelse return sema.failWithNeededComptime(
                 block,
                 src,
-                .{ .needed_comptime_reason = "struct field default value must be comptime-known" },
+                .{ .simple = .struct_field_default_value },
             );
             // Resolve the value so that lazy values do not create distinct types.
             break :d (try sema.resolveLazyValue(val)).toIntern();
@@ -23285,9 +23257,7 @@ fn zirIntFromFloat(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
         const result_val = try sema.intFromFloat(block, operand_src, operand_val, operand_ty, dest_ty, .truncate);
         return Air.internedToRef(result_val.toIntern());
     } else if (dest_scalar_ty.zigTypeTag(zcu) == .comptime_int) {
-        return sema.failWithNeededComptime(block, operand_src, .{
-            .needed_comptime_reason = "value being casted to 'comptime_int' must be comptime-known",
-        });
+        return sema.failWithNeededComptime(block, operand_src, .{ .simple = .casted_to_comptime_int });
     }
 
     try sema.requireRuntimeBlock(block, src, operand_src);
@@ -23368,9 +23338,7 @@ fn zirFloatFromInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileErro
         const result_val = try operand_val.floatFromIntAdvanced(sema.arena, operand_ty, dest_ty, pt, .sema);
         return Air.internedToRef(result_val.toIntern());
     } else if (dest_scalar_ty.zigTypeTag(zcu) == .comptime_float) {
-        return sema.failWithNeededComptime(block, operand_src, .{
-            .needed_comptime_reason = "value being casted to 'comptime_float' must be comptime-known",
-        });
+        return sema.failWithNeededComptime(block, operand_src, .{ .simple = .casted_to_comptime_float });
     }
 
     try sema.requireRuntimeBlock(block, src, operand_src);
@@ -24394,9 +24362,7 @@ fn bitOffsetOf(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!u6
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
 
     const ty = try sema.resolveType(block, lhs_src, extra.lhs);
-    const field_name = try sema.resolveConstStringIntern(block, rhs_src, extra.rhs, .{
-        .needed_comptime_reason = "name of field must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, rhs_src, extra.rhs, .{ .simple = .field_name });
 
     const pt = sema.pt;
     const zcu = pt.zcu;
@@ -24850,31 +24816,21 @@ fn resolveExportOptions(
     const visibility_src = block.src(.{ .init_field_visibility = src.offset.node_offset_builtin_call_arg.builtin_call_node });
 
     const name_operand = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "name", .no_embedded_nulls), name_src);
-    const name = try sema.toConstString(block, name_src, name_operand, .{
-        .needed_comptime_reason = "name of exported value must be comptime-known",
-    });
+    const name = try sema.toConstString(block, name_src, name_operand, .{ .simple = .export_options });
 
     const linkage_operand = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "linkage", .no_embedded_nulls), linkage_src);
-    const linkage_val = try sema.resolveConstDefinedValue(block, linkage_src, linkage_operand, .{
-        .needed_comptime_reason = "linkage of exported value must be comptime-known",
-    });
+    const linkage_val = try sema.resolveConstDefinedValue(block, linkage_src, linkage_operand, .{ .simple = .export_options });
     const linkage = zcu.toEnum(std.builtin.GlobalLinkage, linkage_val);
 
     const section_operand = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "section", .no_embedded_nulls), section_src);
-    const section_opt_val = try sema.resolveConstDefinedValue(block, section_src, section_operand, .{
-        .needed_comptime_reason = "linksection of exported value must be comptime-known",
-    });
+    const section_opt_val = try sema.resolveConstDefinedValue(block, section_src, section_operand, .{ .simple = .export_options });
     const section = if (section_opt_val.optionalValue(zcu)) |section_val|
-        try sema.toConstString(block, section_src, Air.internedToRef(section_val.toIntern()), .{
-            .needed_comptime_reason = "linksection of exported value must be comptime-known",
-        })
+        try sema.toConstString(block, section_src, Air.internedToRef(section_val.toIntern()), .{ .simple = .export_options })
     else
         null;
 
     const visibility_operand = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "visibility", .no_embedded_nulls), visibility_src);
-    const visibility_val = try sema.resolveConstDefinedValue(block, visibility_src, visibility_operand, .{
-        .needed_comptime_reason = "visibility of exported value must be comptime-known",
-    });
+    const visibility_val = try sema.resolveConstDefinedValue(block, visibility_src, visibility_operand, .{ .simple = .export_options });
     const visibility = zcu.toEnum(std.builtin.SymbolVisibility, visibility_val);
 
     if (name.len < 1) {
@@ -24901,7 +24857,7 @@ fn resolveBuiltinEnum(
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
     comptime name: []const u8,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) CompileError!@field(std.builtin, name) {
     const pt = sema.pt;
     const ty = try sema.getBuiltinType(name);
@@ -24916,7 +24872,7 @@ fn resolveAtomicOrder(
     block: *Block,
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) CompileError!std.builtin.AtomicOrder {
     return sema.resolveBuiltinEnum(block, src, zir_ref, "AtomicOrder", reason);
 }
@@ -24927,9 +24883,7 @@ fn resolveAtomicRmwOp(
     src: LazySrcLoc,
     zir_ref: Zir.Inst.Ref,
 ) CompileError!std.builtin.AtomicRmwOp {
-    return sema.resolveBuiltinEnum(block, src, zir_ref, "AtomicRmwOp", .{
-        .needed_comptime_reason = "@atomicRmW operation must be comptime-known",
-    });
+    return sema.resolveBuiltinEnum(block, src, zir_ref, "AtomicRmwOp", .{ .simple = .operand_atomicRmw_operation });
 }
 
 fn zirCmpxchg(
@@ -24967,12 +24921,8 @@ fn zirCmpxchg(
     const uncasted_ptr = try sema.resolveInst(extra.ptr);
     const ptr = try sema.checkAtomicPtrOperand(block, elem_ty, elem_ty_src, uncasted_ptr, ptr_src, false);
     const new_value = try sema.coerce(block, elem_ty, try sema.resolveInst(extra.new_value), new_value_src);
-    const success_order = try sema.resolveAtomicOrder(block, success_order_src, extra.success_order, .{
-        .needed_comptime_reason = "atomic order of cmpxchg success must be comptime-known",
-    });
-    const failure_order = try sema.resolveAtomicOrder(block, failure_order_src, extra.failure_order, .{
-        .needed_comptime_reason = "atomic order of cmpxchg failure must be comptime-known",
-    });
+    const success_order = try sema.resolveAtomicOrder(block, success_order_src, extra.success_order, .{ .simple = .atomic_order });
+    const failure_order = try sema.resolveAtomicOrder(block, failure_order_src, extra.failure_order, .{ .simple = .atomic_order });
 
     if (@intFromEnum(success_order) < @intFromEnum(std.builtin.AtomicOrder.monotonic)) {
         return sema.fail(block, success_order_src, "success atomic ordering must be monotonic or stricter", .{});
@@ -25113,9 +25063,7 @@ fn zirReduce(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.
     const extra = sema.code.extraData(Zir.Inst.Bin, inst_data.payload_index).data;
     const op_src = block.builtinCallArgSrc(inst_data.src_node, 0);
     const operand_src = block.builtinCallArgSrc(inst_data.src_node, 1);
-    const operation = try sema.resolveBuiltinEnum(block, op_src, extra.lhs, "ReduceOp", .{
-        .needed_comptime_reason = "@reduce operation must be comptime-known",
-    });
+    const operation = try sema.resolveBuiltinEnum(block, op_src, extra.lhs, "ReduceOp", .{ .simple = .operand_reduce_operation });
     const operand = try sema.resolveInst(extra.rhs);
     const operand_ty = sema.typeOf(operand);
     const pt = sema.pt;
@@ -25204,9 +25152,7 @@ fn zirShuffle(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air
         .child = .i32_type,
     });
     mask = try sema.coerce(block, mask_ty, mask, mask_src);
-    const mask_val = try sema.resolveConstValue(block, mask_src, mask, .{
-        .needed_comptime_reason = "shuffle mask must be comptime-known",
-    });
+    const mask_val = try sema.resolveConstValue(block, mask_src, mask, .{ .simple = .operand_shuffle_mask });
     return sema.analyzeShuffle(block, inst_data.src_node, elem_ty, a, b, mask_val, @intCast(mask_len));
 }
 
@@ -25474,9 +25420,7 @@ fn zirAtomicLoad(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!
     const elem_ty = try sema.resolveType(block, elem_ty_src, extra.elem_type);
     const uncasted_ptr = try sema.resolveInst(extra.ptr);
     const ptr = try sema.checkAtomicPtrOperand(block, elem_ty, elem_ty_src, uncasted_ptr, ptr_src, true);
-    const order = try sema.resolveAtomicOrder(block, order_src, extra.ordering, .{
-        .needed_comptime_reason = "atomic order of @atomicLoad must be comptime-known",
-    });
+    const order = try sema.resolveAtomicOrder(block, order_src, extra.ordering, .{ .simple = .atomic_order });
 
     switch (order) {
         .release, .acq_rel => {
@@ -25542,9 +25486,7 @@ fn zirAtomicRmw(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
         },
         else => {},
     }
-    const order = try sema.resolveAtomicOrder(block, order_src, extra.ordering, .{
-        .needed_comptime_reason = "atomic order of @atomicRmW must be comptime-known",
-    });
+    const order = try sema.resolveAtomicOrder(block, order_src, extra.ordering, .{ .simple = .atomic_order });
 
     if (order == .unordered) {
         return sema.fail(block, order_src, "@atomicRmw atomic ordering must not be unordered", .{});
@@ -25611,9 +25553,7 @@ fn zirAtomicStore(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     const elem_ty = sema.typeOf(operand);
     const uncasted_ptr = try sema.resolveInst(extra.ptr);
     const ptr = try sema.checkAtomicPtrOperand(block, elem_ty, elem_ty_src, uncasted_ptr, ptr_src, false);
-    const order = try sema.resolveAtomicOrder(block, order_src, extra.ordering, .{
-        .needed_comptime_reason = "atomic order of @atomicStore must be comptime-known",
-    });
+    const order = try sema.resolveAtomicOrder(block, order_src, extra.ordering, .{ .simple = .atomic_order });
 
     const air_tag: Air.Inst.Tag = switch (order) {
         .acquire, .acq_rel => {
@@ -25716,14 +25656,12 @@ fn zirBuiltinCall(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
     const modifier_ty = try sema.getBuiltinType("CallModifier");
     const air_ref = try sema.resolveInst(extra.modifier);
     const modifier_ref = try sema.coerce(block, modifier_ty, air_ref, modifier_src);
-    const modifier_val = try sema.resolveConstDefinedValue(block, modifier_src, modifier_ref, .{
-        .needed_comptime_reason = "call modifier must be comptime-known",
-    });
+    const modifier_val = try sema.resolveConstDefinedValue(block, modifier_src, modifier_ref, .{ .simple = .call_modifier });
     var modifier = zcu.toEnum(std.builtin.CallModifier, modifier_val);
     switch (modifier) {
         // These can be upgraded to comptime or nosuspend calls.
         .auto, .never_tail, .no_async => {
-            if (block.is_comptime) {
+            if (block.isComptime()) {
                 if (modifier == .never_tail) {
                     return sema.fail(block, modifier_src, "unable to perform 'never_tail' call at compile-time", .{});
                 }
@@ -25738,12 +25676,12 @@ fn zirBuiltinCall(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
                 return sema.fail(block, func_src, "modifier '{s}' requires a comptime-known function", .{@tagName(modifier)});
             };
 
-            if (block.is_comptime) {
+            if (block.isComptime()) {
                 modifier = .compile_time;
             }
         },
         .always_tail => {
-            if (block.is_comptime) {
+            if (block.isComptime()) {
                 modifier = .compile_time;
             }
         },
@@ -25751,12 +25689,12 @@ fn zirBuiltinCall(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError
             if (extra.flags.is_nosuspend) {
                 return sema.fail(block, modifier_src, "modifier 'async_kw' cannot be used inside nosuspend block", .{});
             }
-            if (block.is_comptime) {
+            if (block.isComptime()) {
                 return sema.fail(block, modifier_src, "modifier 'async_kw' cannot be used in combination with comptime function call", .{});
             }
         },
         .never_inline => {
-            if (block.is_comptime) {
+            if (block.isComptime()) {
                 return sema.fail(block, modifier_src, "unable to perform 'never_inline' call at compile-time", .{});
             }
         },
@@ -25820,9 +25758,7 @@ fn zirFieldParentPtr(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.Ins
     }
     try parent_ty.resolveLayout(pt);
 
-    const field_name = try sema.resolveConstStringIntern(block, field_name_src, extra.field_name, .{
-        .needed_comptime_reason = "field name must be comptime-known",
-    });
+    const field_name = try sema.resolveConstStringIntern(block, field_name_src, extra.field_name, .{ .simple = .field_name });
     const field_index = switch (parent_ty.zigTypeTag(zcu)) {
         .@"struct" => blk: {
             if (parent_ty.isTuple(zcu)) {
@@ -26680,9 +26616,7 @@ fn zirFuncFancy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
         extra_index += body.len;
 
         const cc_ty = try sema.getBuiltinType("CallingConvention");
-        const val = try sema.resolveGenericBody(block, cc_src, body, inst, cc_ty, .{
-            .needed_comptime_reason = "calling convention must be comptime-known",
-        });
+        const val = try sema.resolveGenericBody(block, cc_src, body, inst, cc_ty, .{ .simple = .@"callconv" });
         break :blk try sema.analyzeValueAsCallconv(block, cc_src, val);
     } else if (extra.data.bits.has_cc_ref) blk: {
         const cc_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]);
@@ -26690,9 +26624,7 @@ fn zirFuncFancy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
         const cc_ty = try sema.getBuiltinType("CallingConvention");
         const uncoerced_cc = try sema.resolveInst(cc_ref);
         const coerced_cc = try sema.coerce(block, cc_ty, uncoerced_cc, cc_src);
-        const cc_val = try sema.resolveConstDefinedValue(block, cc_src, coerced_cc, .{
-            .needed_comptime_reason = "calling convention must be comptime-known",
-        });
+        const cc_val = try sema.resolveConstDefinedValue(block, cc_src, coerced_cc, .{ .simple = .@"callconv" });
         break :blk try sema.analyzeValueAsCallconv(block, cc_src, cc_val);
     } else cc: {
         if (has_body) {
@@ -26730,9 +26662,7 @@ fn zirFuncFancy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
         const body = sema.code.bodySlice(extra_index, body_len);
         extra_index += body.len;
 
-        const val = try sema.resolveGenericBody(block, ret_src, body, inst, Type.type, .{
-            .needed_comptime_reason = "return type must be comptime-known",
-        });
+        const val = try sema.resolveGenericBody(block, ret_src, body, inst, Type.type, .{ .simple = .function_ret_ty });
         const ty = val.toType();
         break :blk ty;
     } else if (extra.data.bits.has_ret_ty_ref) blk: {
@@ -26742,9 +26672,7 @@ fn zirFuncFancy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
             error.GenericPoison => break :blk Type.generic_poison,
             else => |e| return e,
         };
-        const ret_ty_val = sema.resolveConstDefinedValue(block, ret_src, ret_ty_air_ref, .{
-            .needed_comptime_reason = "return type must be comptime-known",
-        }) catch |err| switch (err) {
+        const ret_ty_val = sema.resolveConstDefinedValue(block, ret_src, ret_ty_air_ref, .{ .simple = .function_ret_ty }) catch |err| switch (err) {
             error.GenericPoison => break :blk Type.generic_poison,
             else => |e| return e,
         };
@@ -26790,9 +26718,7 @@ fn zirCUndef(
     const extra = sema.code.extraData(Zir.Inst.UnNode, extended.operand).data;
     const src = block.builtinCallArgSrc(extra.node, 0);
 
-    const name = try sema.resolveConstString(block, src, extra.operand, .{
-        .needed_comptime_reason = "name of macro being undefined must be comptime-known",
-    });
+    const name = try sema.resolveConstString(block, src, extra.operand, .{ .simple = .operand_cUndef_macro_name });
     try block.c_import_buf.?.writer().print("#undef {s}\n", .{name});
     return .void_value;
 }
@@ -26805,9 +26731,7 @@ fn zirCInclude(
     const extra = sema.code.extraData(Zir.Inst.UnNode, extended.operand).data;
     const src = block.builtinCallArgSrc(extra.node, 0);
 
-    const name = try sema.resolveConstString(block, src, extra.operand, .{
-        .needed_comptime_reason = "path being included must be comptime-known",
-    });
+    const name = try sema.resolveConstString(block, src, extra.operand, .{ .simple = .operand_cInclude_file_name });
     try block.c_import_buf.?.writer().print("#include <{s}>\n", .{name});
     return .void_value;
 }
@@ -26823,14 +26747,10 @@ fn zirCDefine(
     const name_src = block.builtinCallArgSrc(extra.node, 0);
     const val_src = block.builtinCallArgSrc(extra.node, 1);
 
-    const name = try sema.resolveConstString(block, name_src, extra.lhs, .{
-        .needed_comptime_reason = "name of macro being undefined must be comptime-known",
-    });
+    const name = try sema.resolveConstString(block, name_src, extra.lhs, .{ .simple = .operand_cDefine_macro_name });
     const rhs = try sema.resolveInst(extra.rhs);
     if (sema.typeOf(rhs).zigTypeTag(zcu) != .void) {
-        const value = try sema.resolveConstString(block, val_src, extra.rhs, .{
-            .needed_comptime_reason = "value of macro being undefined must be comptime-known",
-        });
+        const value = try sema.resolveConstString(block, val_src, extra.rhs, .{ .simple = .operand_cDefine_macro_value });
         try block.c_import_buf.?.writer().print("#define {s} {s}\n", .{ name, value });
     } else {
         try block.c_import_buf.?.writer().print("#define {s}\n", .{name});
@@ -26851,9 +26771,7 @@ fn zirWasmMemorySize(
         return sema.fail(block, builtin_src, "builtin @wasmMemorySize is available when targeting WebAssembly; targeted CPU architecture is {s}", .{@tagName(target.cpu.arch)});
     }
 
-    const index: u32 = @intCast(try sema.resolveInt(block, index_src, extra.operand, Type.u32, .{
-        .needed_comptime_reason = "wasm memory size index must be comptime-known",
-    }));
+    const index: u32 = @intCast(try sema.resolveInt(block, index_src, extra.operand, Type.u32, .{ .simple = .wasm_memory_index }));
     try sema.requireRuntimeBlock(block, builtin_src, null);
     return block.addInst(.{
         .tag = .wasm_memory_size,
@@ -26878,9 +26796,7 @@ fn zirWasmMemoryGrow(
         return sema.fail(block, builtin_src, "builtin @wasmMemoryGrow is available when targeting WebAssembly; targeted CPU architecture is {s}", .{@tagName(target.cpu.arch)});
     }
 
-    const index: u32 = @intCast(try sema.resolveInt(block, index_src, extra.lhs, Type.u32, .{
-        .needed_comptime_reason = "wasm memory size index must be comptime-known",
-    }));
+    const index: u32 = @intCast(try sema.resolveInt(block, index_src, extra.lhs, Type.u32, .{ .simple = .wasm_memory_index }));
     const delta = try sema.coerce(block, Type.usize, try sema.resolveInst(extra.rhs), delta_src);
 
     try sema.requireRuntimeBlock(block, builtin_src, null);
@@ -26911,19 +26827,13 @@ fn resolvePrefetchOptions(
     const cache_src = block.src(.{ .init_field_cache = src.offset.node_offset_builtin_call_arg.builtin_call_node });
 
     const rw = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "rw", .no_embedded_nulls), rw_src);
-    const rw_val = try sema.resolveConstDefinedValue(block, rw_src, rw, .{
-        .needed_comptime_reason = "prefetch read/write must be comptime-known",
-    });
+    const rw_val = try sema.resolveConstDefinedValue(block, rw_src, rw, .{ .simple = .prefetch_options });
 
     const locality = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "locality", .no_embedded_nulls), locality_src);
-    const locality_val = try sema.resolveConstDefinedValue(block, locality_src, locality, .{
-        .needed_comptime_reason = "prefetch locality must be comptime-known",
-    });
+    const locality_val = try sema.resolveConstDefinedValue(block, locality_src, locality, .{ .simple = .prefetch_options });
 
     const cache = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "cache", .no_embedded_nulls), cache_src);
-    const cache_val = try sema.resolveConstDefinedValue(block, cache_src, cache, .{
-        .needed_comptime_reason = "prefetch cache must be comptime-known",
-    });
+    const cache_val = try sema.resolveConstDefinedValue(block, cache_src, cache, .{ .simple = .prefetch_options });
 
     return std.builtin.PrefetchOptions{
         .rw = zcu.toEnum(std.builtin.PrefetchOptions.Rw, rw_val),
@@ -26945,7 +26855,7 @@ fn zirPrefetch(
 
     const options = try sema.resolvePrefetchOptions(block, opts_src, extra.rhs);
 
-    if (!block.is_comptime) {
+    if (!block.isComptime()) {
         _ = try block.addInst(.{
             .tag = .prefetch,
             .data = .{ .prefetch = .{
@@ -26987,30 +26897,20 @@ fn resolveExternOptions(
     const dll_import_src = block.src(.{ .init_field_dll_import = src.offset.node_offset_builtin_call_arg.builtin_call_node });
 
     const name_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "name", .no_embedded_nulls), name_src);
-    const name = try sema.toConstString(block, name_src, name_ref, .{
-        .needed_comptime_reason = "name of the extern symbol must be comptime-known",
-    });
+    const name = try sema.toConstString(block, name_src, name_ref, .{ .simple = .extern_options });
 
     const library_name_inst = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "library_name", .no_embedded_nulls), library_src);
-    const library_name_val = try sema.resolveConstDefinedValue(block, library_src, library_name_inst, .{
-        .needed_comptime_reason = "library in which extern symbol is must be comptime-known",
-    });
+    const library_name_val = try sema.resolveConstDefinedValue(block, library_src, library_name_inst, .{ .simple = .extern_options });
 
     const linkage_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "linkage", .no_embedded_nulls), linkage_src);
-    const linkage_val = try sema.resolveConstDefinedValue(block, linkage_src, linkage_ref, .{
-        .needed_comptime_reason = "linkage of the extern symbol must be comptime-known",
-    });
+    const linkage_val = try sema.resolveConstDefinedValue(block, linkage_src, linkage_ref, .{ .simple = .extern_options });
     const linkage = zcu.toEnum(std.builtin.GlobalLinkage, linkage_val);
 
     const is_thread_local = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "is_thread_local", .no_embedded_nulls), thread_local_src);
-    const is_thread_local_val = try sema.resolveConstDefinedValue(block, thread_local_src, is_thread_local, .{
-        .needed_comptime_reason = "threadlocality of the extern symbol must be comptime-known",
-    });
+    const is_thread_local_val = try sema.resolveConstDefinedValue(block, thread_local_src, is_thread_local, .{ .simple = .extern_options });
 
     const library_name = if (library_name_val.optionalValue(zcu)) |library_name_payload| library_name: {
-        const library_name = try sema.toConstString(block, library_src, Air.internedToRef(library_name_payload.toIntern()), .{
-            .needed_comptime_reason = "library in which extern symbol is must be comptime-known",
-        });
+        const library_name = try sema.toConstString(block, library_src, Air.internedToRef(library_name_payload.toIntern()), .{ .simple = .extern_options });
         if (library_name.len == 0) {
             return sema.fail(block, library_src, "library name cannot be empty", .{});
         }
@@ -27019,9 +26919,7 @@ fn resolveExternOptions(
     } else null;
 
     const is_dll_import_ref = try sema.fieldVal(block, src, options, try ip.getOrPutString(gpa, pt.tid, "is_dll_import", .no_embedded_nulls), dll_import_src);
-    const is_dll_import_val = try sema.resolveConstDefinedValue(block, dll_import_src, is_dll_import_ref, .{
-        .needed_comptime_reason = "it must be comptime-known if the symbol is imported from a dll",
-    });
+    const is_dll_import_val = try sema.resolveConstDefinedValue(block, dll_import_src, is_dll_import_ref, .{ .simple = .extern_options });
 
     if (name.len == 0) {
         return sema.fail(block, name_src, "extern symbol name cannot be empty", .{});
@@ -27134,9 +27032,7 @@ fn zirWorkItem(
         },
     }
 
-    const dimension: u32 = @intCast(try sema.resolveInt(block, dimension_src, extra.operand, Type.u32, .{
-        .needed_comptime_reason = "dimension must be comptime-known",
-    }));
+    const dimension: u32 = @intCast(try sema.resolveInt(block, dimension_src, extra.operand, Type.u32, .{ .simple = .work_group_dim_index }));
     try sema.requireRuntimeBlock(block, builtin_src, null);
 
     return block.addInst(.{
@@ -27158,7 +27054,7 @@ fn zirInComptime(
     block: *Block,
 ) CompileError!Air.Inst.Ref {
     _ = sema;
-    return if (block.is_comptime) .bool_true else .bool_false;
+    return if (block.isComptime()) .bool_true else .bool_false;
 }
 
 fn zirBuiltinValue(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstData) CompileError!Air.Inst.Ref {
@@ -27249,9 +27145,7 @@ fn zirBranchHint(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstDat
 
     const hint_ty = try sema.getBuiltinType("BranchHint");
     const coerced_hint = try sema.coerce(block, hint_ty, uncoerced_hint, operand_src);
-    const hint_val = try sema.resolveConstDefinedValue(block, operand_src, coerced_hint, .{
-        .needed_comptime_reason = "operand to '@branchHint' must be comptime-known",
-    });
+    const hint_val = try sema.resolveConstDefinedValue(block, operand_src, coerced_hint, .{ .simple = .operand_branchHint });
 
     // We only apply the first hint in a branch.
     // This allows user-provided hints to override implicit cold hints.
@@ -27261,20 +27155,20 @@ fn zirBranchHint(sema: *Sema, block: *Block, extended: Zir.Inst.Extended.InstDat
 }
 
 fn requireRuntimeBlock(sema: *Sema, block: *Block, src: LazySrcLoc, runtime_src: ?LazySrcLoc) !void {
-    if (block.is_comptime) {
-        const msg = msg: {
+    if (block.isComptime()) {
+        const msg, const fail_block = msg: {
             const msg = try sema.errMsg(src, "unable to evaluate comptime expression", .{});
             errdefer msg.destroy(sema.gpa);
 
             if (runtime_src) |some| {
                 try sema.errNote(some, msg, "operation is runtime due to this operand", .{});
             }
-            if (block.comptime_reason) |some| {
-                try some.explain(sema, msg);
-            }
-            break :msg msg;
+
+            const fail_block = try block.explainWhyBlockIsComptime(msg);
+
+            break :msg .{ msg, fail_block };
         };
-        return sema.failWithOwnedErrorMsg(block, msg);
+        return sema.failWithOwnedErrorMsg(fail_block, msg);
     }
 }
 
@@ -27759,7 +27653,7 @@ fn addSafetyCheck(
     panic_id: Zcu.PanicId,
 ) !void {
     const gpa = sema.gpa;
-    assert(!parent_block.is_comptime);
+    assert(!parent_block.isComptime());
 
     var fail_block: Block = .{
         .parent = parent_block,
@@ -27767,7 +27661,7 @@ fn addSafetyCheck(
         .namespace = parent_block.namespace,
         .instructions = .{},
         .inlining = parent_block.inlining,
-        .is_comptime = false,
+        .comptime_reason = null,
         .src_base_inst = parent_block.src_base_inst,
         .type_name_ctx = parent_block.type_name_ctx,
     };
@@ -27874,7 +27768,7 @@ fn addSafetyCheckUnwrapError(
     unwrap_err_tag: Air.Inst.Tag,
     is_non_err_tag: Air.Inst.Tag,
 ) !void {
-    assert(!parent_block.is_comptime);
+    assert(!parent_block.isComptime());
     const ok = try parent_block.addUnOp(is_non_err_tag, operand);
     const gpa = sema.gpa;
 
@@ -27884,7 +27778,7 @@ fn addSafetyCheckUnwrapError(
         .namespace = parent_block.namespace,
         .instructions = .{},
         .inlining = parent_block.inlining,
-        .is_comptime = false,
+        .comptime_reason = null,
         .src_base_inst = parent_block.src_base_inst,
         .type_name_ctx = parent_block.type_name_ctx,
     };
@@ -27918,7 +27812,7 @@ fn addSafetyCheckIndexOob(
     len: Air.Inst.Ref,
     cmp_op: Air.Inst.Tag,
 ) !void {
-    assert(!parent_block.is_comptime);
+    assert(!parent_block.isComptime());
     const ok = try parent_block.addBinOp(cmp_op, index, len);
     return addSafetyCheckCall(sema, parent_block, src, ok, "outOfBounds", &.{ index, len });
 }
@@ -27930,7 +27824,7 @@ fn addSafetyCheckInactiveUnionField(
     active_tag: Air.Inst.Ref,
     wanted_tag: Air.Inst.Ref,
 ) !void {
-    assert(!parent_block.is_comptime);
+    assert(!parent_block.isComptime());
     const ok = try parent_block.addBinOp(.cmp_eq, active_tag, wanted_tag);
     return addSafetyCheckCall(sema, parent_block, src, ok, "inactiveUnionField", &.{ active_tag, wanted_tag });
 }
@@ -27944,7 +27838,7 @@ fn addSafetyCheckSentinelMismatch(
     ptr: Air.Inst.Ref,
     sentinel_index: Air.Inst.Ref,
 ) !void {
-    assert(!parent_block.is_comptime);
+    assert(!parent_block.isComptime());
     const pt = sema.pt;
     const zcu = pt.zcu;
     const expected_sentinel_val = maybe_sentinel orelse return;
@@ -27986,7 +27880,7 @@ fn addSafetyCheckCall(
     func_name: []const u8,
     args: []const Air.Inst.Ref,
 ) !void {
-    assert(!parent_block.is_comptime);
+    assert(!parent_block.isComptime());
     const gpa = sema.gpa;
     const pt = sema.pt;
     const zcu = pt.zcu;
@@ -27997,7 +27891,7 @@ fn addSafetyCheckCall(
         .namespace = parent_block.namespace,
         .instructions = .{},
         .inlining = parent_block.inlining,
-        .is_comptime = false,
+        .comptime_reason = null,
         .src_base_inst = parent_block.src_base_inst,
         .type_name_ctx = parent_block.type_name_ctx,
     };
@@ -29168,9 +29062,7 @@ fn elemPtr(
         .array, .vector => try sema.elemPtrArray(block, src, indexable_ptr_src, indexable_ptr, elem_index_src, elem_index, init, oob_safety),
         .@"struct" => blk: {
             // Tuple field access.
-            const index_val = try sema.resolveConstDefinedValue(block, elem_index_src, elem_index, .{
-                .needed_comptime_reason = "tuple field access index must be comptime-known",
-            });
+            const index_val = try sema.resolveConstDefinedValue(block, elem_index_src, elem_index, .{ .simple = .tuple_field_index });
             const index: u32 = @intCast(try index_val.toUnsignedIntSema(pt));
             break :blk try sema.tupleFieldPtr(block, src, indexable_ptr, elem_index_src, index, init);
         },
@@ -29225,9 +29117,7 @@ fn elemPtrOneLayerOnly(
                 .array, .vector => try sema.elemPtrArray(block, src, indexable_src, indexable, elem_index_src, elem_index, init, oob_safety),
                 .@"struct" => blk: {
                     assert(child_ty.isTuple(zcu));
-                    const index_val = try sema.resolveConstDefinedValue(block, elem_index_src, elem_index, .{
-                        .needed_comptime_reason = "tuple field access index must be comptime-known",
-                    });
+                    const index_val = try sema.resolveConstDefinedValue(block, elem_index_src, elem_index, .{ .simple = .tuple_field_index });
                     const index: u32 = @intCast(try index_val.toUnsignedIntSema(pt));
                     break :blk try sema.tupleFieldPtr(block, indexable_src, indexable, elem_index_src, index, false);
                 },
@@ -29305,9 +29195,7 @@ fn elemVal(
         },
         .@"struct" => {
             // Tuple field access.
-            const index_val = try sema.resolveConstDefinedValue(block, elem_index_src, elem_index, .{
-                .needed_comptime_reason = "tuple field access index must be comptime-known",
-            });
+            const index_val = try sema.resolveConstDefinedValue(block, elem_index_src, elem_index, .{ .simple = .tuple_field_index });
             const index: u32 = @intCast(try index_val.toUnsignedIntSema(pt));
             return sema.tupleField(block, indexable_src, indexable, elem_index_src, index);
         },
@@ -30093,9 +29981,7 @@ fn coerceExtra(
                 const val = maybe_inst_val orelse {
                     if (dest_ty.zigTypeTag(zcu) == .comptime_int) {
                         if (!opts.report_err) return error.NotCoercible;
-                        return sema.failWithNeededComptime(block, inst_src, .{
-                            .needed_comptime_reason = "value being casted to 'comptime_int' must be comptime-known",
-                        });
+                        return sema.failWithNeededComptime(block, inst_src, .{ .simple = .casted_to_comptime_int });
                     }
                     break :float;
                 };
@@ -30120,9 +30006,7 @@ fn coerceExtra(
                 if (dest_ty.zigTypeTag(zcu) == .comptime_int) {
                     if (!opts.report_err) return error.NotCoercible;
                     if (opts.no_cast_to_comptime_int) return inst;
-                    return sema.failWithNeededComptime(block, inst_src, .{
-                        .needed_comptime_reason = "value being casted to 'comptime_int' must be comptime-known",
-                    });
+                    return sema.failWithNeededComptime(block, inst_src, .{ .simple = .casted_to_comptime_int });
                 }
 
                 // integer widening
@@ -30158,9 +30042,7 @@ fn coerceExtra(
                     return Air.internedToRef(result_val.toIntern());
                 } else if (dest_ty.zigTypeTag(zcu) == .comptime_float) {
                     if (!opts.report_err) return error.NotCoercible;
-                    return sema.failWithNeededComptime(block, inst_src, .{
-                        .needed_comptime_reason = "value being casted to 'comptime_float' must be comptime-known",
-                    });
+                    return sema.failWithNeededComptime(block, inst_src, .{ .simple = .casted_to_comptime_float });
                 }
 
                 // float widening
@@ -30175,9 +30057,7 @@ fn coerceExtra(
                 const val = maybe_inst_val orelse {
                     if (dest_ty.zigTypeTag(zcu) == .comptime_float) {
                         if (!opts.report_err) return error.NotCoercible;
-                        return sema.failWithNeededComptime(block, inst_src, .{
-                            .needed_comptime_reason = "value being casted to 'comptime_float' must be comptime-known",
-                        });
+                        return sema.failWithNeededComptime(block, inst_src, .{ .simple = .casted_to_comptime_float });
                     }
                     break :int;
                 };
@@ -32435,9 +32315,7 @@ fn coerceTupleToStruct(
         field_refs[struct_field_index] = coerced;
         if (struct_type.fieldIsComptime(ip, struct_field_index)) {
             const init_val = try sema.resolveValue(coerced) orelse {
-                return sema.failWithNeededComptime(block, field_src, .{
-                    .needed_comptime_reason = "value stored in comptime field must be comptime-known",
-                });
+                return sema.failWithNeededComptime(block, field_src, .{ .simple = .stored_to_comptime_field });
             };
 
             const field_init = Value.fromInterned(struct_type.field_inits.get(ip)[struct_field_index]);
@@ -32550,9 +32428,7 @@ fn coerceTupleToTuple(
         field_refs[field_index] = coerced;
         if (default_val != .none) {
             const init_val = (try sema.resolveValue(coerced)) orelse {
-                return sema.failWithNeededComptime(block, field_src, .{
-                    .needed_comptime_reason = "value stored in comptime field must be comptime-known",
-                });
+                return sema.failWithNeededComptime(block, field_src, .{ .simple = .stored_to_comptime_field });
             };
 
             if (!init_val.eql(Value.fromInterned(default_val), Type.fromInterned(field_ty), pt.zcu)) {
@@ -32816,12 +32692,12 @@ fn analyzeRef(
     // In a comptime context, the store would fail, since the operand is runtime-known. But that's
     // okay; we don't actually need this store to succeed, since we're creating a runtime value in a
     // comptime scope, so the value can never be used aside from to get its type.
-    if (!block.is_comptime) {
+    if (!block.isComptime()) {
         try sema.storePtr(block, src, alloc, operand);
     }
 
     // Cast to the constant pointer type. We do this directly rather than going via `coerce` to
-    // avoid errors in the `block.is_comptime` case.
+    // avoid errors in the `block.isComptime()` case.
     return block.addBitCast(ptr_type, alloc);
 }
 
@@ -33184,24 +33060,24 @@ fn analyzeSlice(
                     array_ty = double_child_ty;
                     elem_ty = double_child_ty.childType(zcu);
                 } else {
-                    const bounds_error_message = "slice of single-item pointer must have comptime-known bounds [0..0], [0..1], or [1..1]";
                     if (uncasted_end_opt == .none) {
-                        return sema.fail(block, src, bounds_error_message, .{});
+                        return sema.fail(block, src, "slice of single-item pointer must be bounded", .{});
                     }
                     const start_value = try sema.resolveConstDefinedValue(
                         block,
                         start_src,
                         uncasted_start,
-                        .{ .needed_comptime_reason = bounds_error_message },
+                        .{ .simple = .slice_single_item_ptr_bounds },
                     );
 
                     const end_value = try sema.resolveConstDefinedValue(
                         block,
                         end_src,
                         uncasted_end_opt,
-                        .{ .needed_comptime_reason = bounds_error_message },
+                        .{ .simple = .slice_single_item_ptr_bounds },
                     );
 
+                    const bounds_error_message = "slice of single-item pointer must have bounds [0..0], [0..1], or [1..1]";
                     if (try sema.compareScalar(start_value, .neq, end_value, Type.comptime_int)) {
                         if (try sema.compareScalar(start_value, .neq, Value.zero_comptime_int, Type.comptime_int)) {
                             const msg = msg: {
@@ -33416,9 +33292,7 @@ fn analyzeSlice(
         if (sentinel_opt != .none) {
             const casted = try sema.coerce(block, elem_ty, sentinel_opt, sentinel_src);
             try checkSentinelType(sema, block, sentinel_src, elem_ty);
-            break :s try sema.resolveConstDefinedValue(block, sentinel_src, casted, .{
-                .needed_comptime_reason = "slice sentinel must be comptime-known",
-            });
+            break :s try sema.resolveConstDefinedValue(block, sentinel_src, casted, .{ .simple = .slice_sentinel });
         }
         // If we are slicing to the end of something that is sentinel-terminated
         // then the resulting slice type is also sentinel-terminated.
@@ -33499,9 +33373,9 @@ fn analyzeSlice(
         runtime_src = end_src;
     }
 
-    if (!checked_start_lte_end and block.wantSafety() and !block.is_comptime) {
+    if (!checked_start_lte_end and block.wantSafety() and !block.isComptime()) {
         // requirement: start <= end
-        assert(!block.is_comptime);
+        assert(!block.isComptime());
         try sema.requireRuntimeBlock(block, src, runtime_src.?);
         const ok = try block.addBinOp(.cmp_lte, start, end);
         try sema.addSafetyCheckCall(block, src, ok, "startGreaterThanEnd", &.{ start, end });
@@ -35859,7 +35733,7 @@ fn backingIntType(
         .namespace = struct_type.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = null, // set below if needed
         .src_base_inst = struct_type.zir_index,
         .type_name_ctx = struct_type.name,
     };
@@ -35899,6 +35773,10 @@ fn backingIntType(
             .base_node_inst = struct_type.zir_index,
             .offset = .{ .node_offset_container_tag = 0 },
         };
+        block.comptime_reason = .{ .reason = .{
+            .src = backing_int_src,
+            .r = .{ .simple = .type },
+        } };
         const backing_int_ty = blk: {
             if (backing_int_body_len == 0) {
                 const backing_int_ref: Zir.Inst.Ref = @enumFromInt(zir.extra[extra_index]);
@@ -36512,7 +36390,13 @@ fn structFields(
         .namespace = namespace_index,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = .{ .reason = .{
+            .src = .{
+                .base_node_inst = struct_type.zir_index,
+                .offset = .nodeOffset(0),
+            },
+            .r = .{ .simple = .struct_fields },
+        } },
         .src_base_inst = struct_type.zir_index,
         .type_name_ctx = struct_type.name,
     };
@@ -36698,7 +36582,7 @@ fn structFieldInits(
         .namespace = namespace_index,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = undefined, // set when `block_scope` is used
         .src_base_inst = struct_type.zir_index,
         .type_name_ctx = struct_type.name,
     };
@@ -36776,13 +36660,13 @@ fn structFieldInits(
                 .offset = .{ .container_field_value = @intCast(field_i) },
             };
 
+            block_scope.comptime_reason = .{ .reason = .{
+                .src = init_src,
+                .r = .{ .simple = .struct_field_default_value },
+            } };
             const init = try sema.resolveInlineBody(&block_scope, body, zir_index);
             const coerced = try sema.coerce(&block_scope, field_ty, init, init_src);
-            const default_val = try sema.resolveValue(coerced) orelse {
-                return sema.failWithNeededComptime(&block_scope, init_src, .{
-                    .needed_comptime_reason = "struct field default value must be comptime-known",
-                });
-            };
+            const default_val = try sema.resolveConstValue(&block_scope, init_src, coerced, null);
 
             if (default_val.canMutateComptimeVarState(zcu)) {
                 return sema.fail(&block_scope, init_src, "field default value contains reference to comptime-mutable memory", .{});
@@ -36850,20 +36734,26 @@ fn unionFields(
     const body = zir.bodySlice(extra_index, body_len);
     extra_index += body.len;
 
+    const src: LazySrcLoc = .{
+        .base_node_inst = union_type.zir_index,
+        .offset = .nodeOffset(0),
+    };
+
     var block_scope: Block = .{
         .parent = null,
         .sema = sema,
         .namespace = union_type.namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = .{ .reason = .{
+            .src = src,
+            .r = .{ .simple = .union_fields },
+        } },
         .src_base_inst = union_type.zir_index,
         .type_name_ctx = union_type.name,
     };
     defer assert(block_scope.instructions.items.len == 0);
 
-    const src = block_scope.nodeOffset(0);
-
     if (body.len != 0) {
         _ = try sema.analyzeInlineBody(&block_scope, body, zir_index);
     }
@@ -36993,9 +36883,7 @@ fn unionFields(
         if (enum_field_vals.capacity() > 0) {
             const enum_tag_val = if (tag_ref != .none) blk: {
                 const coerced = try sema.coerce(&block_scope, int_tag_ty, tag_ref, value_src);
-                const val = try sema.resolveConstDefinedValue(&block_scope, value_src, coerced, .{
-                    .needed_comptime_reason = "enum tag value must be comptime-known",
-                });
+                const val = try sema.resolveConstDefinedValue(&block_scope, value_src, coerced, .{ .simple = .enum_field_tag_value });
                 last_tag_val = val;
 
                 break :blk val;
@@ -37669,9 +37557,7 @@ pub fn analyzeAsAddressSpace(
     const zcu = pt.zcu;
     const addrspace_ty = try sema.getBuiltinType("AddressSpace");
     const coerced = try sema.coerce(block, addrspace_ty, air_ref, src);
-    const addrspace_val = try sema.resolveConstDefinedValue(block, src, coerced, .{
-        .needed_comptime_reason = "address space must be comptime-known",
-    });
+    const addrspace_val = try sema.resolveConstDefinedValue(block, src, coerced, .{ .simple = .@"addrspace" });
     const address_space = zcu.toEnum(std.builtin.AddressSpace, addrspace_val);
     const target = pt.zcu.getTarget();
     const arch = target.cpu.arch;
@@ -38560,7 +38446,7 @@ fn sliceToIpString(
     block: *Block,
     src: LazySrcLoc,
     slice_val: Value,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) CompileError!InternPool.NullTerminatedString {
     const pt = sema.pt;
     const zcu = pt.zcu;
@@ -38580,7 +38466,7 @@ fn derefSliceAsArray(
     block: *Block,
     src: LazySrcLoc,
     slice_val: Value,
-    reason: NeededComptimeReason,
+    reason: ComptimeReason,
 ) CompileError!Value {
     return try sema.maybeDerefSliceAsArray(block, src, slice_val) orelse {
         return sema.failWithNeededComptime(block, src, reason);
@@ -38734,7 +38620,10 @@ pub fn resolveDeclaredEnum(
         .namespace = namespace,
         .instructions = .{},
         .inlining = null,
-        .is_comptime = true,
+        .comptime_reason = .{ .reason = .{
+            .src = src,
+            .r = .{ .simple = .enum_fields },
+        } },
         .src_base_inst = tracked_inst,
         .type_name_ctx = type_name,
     };
@@ -38798,9 +38687,7 @@ pub fn resolveDeclaredEnum(
             last_tag_val = try sema.resolveConstDefinedValue(&block, .{
                 .base_node_inst = tracked_inst,
                 .offset = .{ .container_field_name = field_i },
-            }, tag_inst, .{
-                .needed_comptime_reason = "enum tag value must be comptime-known",
-            });
+            }, tag_inst, .{ .simple = .enum_field_tag_value });
             if (!(try sema.intFitsInType(last_tag_val.?, int_tag_ty, null))) break :overflow true;
             last_tag_val = try pt.getCoerced(last_tag_val.?, int_tag_ty);
             if (wip_ty.nextField(ip, field_name, last_tag_val.?.toIntern())) |conflict| {
@@ -38879,9 +38766,7 @@ fn getPanicInnerFn(
     const inner_name_ip = try ip.getOrPutString(gpa, pt.tid, inner_name, .no_embedded_nulls);
     const opt_fn_ref = try namespaceLookupVal(sema, block, src, outer_ty.getNamespaceIndex(zcu), inner_name_ip);
     const fn_ref = opt_fn_ref orelse return sema.fail(block, src, "std.builtin.Panic missing {s}", .{inner_name});
-    const fn_val = try sema.resolveConstValue(block, src, fn_ref, .{
-        .needed_comptime_reason = "panic handler must be comptime-known",
-    });
+    const fn_val = try sema.resolveConstValue(block, src, fn_ref, .{ .simple = .panic_handler });
     if (fn_val.typeOf(zcu).zigTypeTag(zcu) != .@"fn") {
         return sema.fail(block, src, "std.builtin.Panic.{s} is not a function", .{inner_name});
     }
@@ -38963,9 +38848,7 @@ pub fn resolveNavPtrModifiers(
     const @"linksection": InternPool.OptionalNullTerminatedString = ls: {
         const linksection_body = zir_decl.linksection_body orelse break :ls .none;
         const linksection_ref = try sema.resolveInlineBody(block, linksection_body, decl_inst);
-        const bytes = try sema.toConstString(block, section_src, linksection_ref, .{
-            .needed_comptime_reason = "linksection must be comptime-known",
-        });
+        const bytes = try sema.toConstString(block, section_src, linksection_ref, .{ .simple = .@"linksection" });
         if (std.mem.indexOfScalar(u8, bytes, 0) != null) {
             return sema.fail(block, section_src, "linksection cannot contain null bytes", .{});
         } else if (bytes.len == 0) {