compiler: fix missing "local variable is never mutated" error

This regressed back in https://github.com/ziglang/zig/pull/25154. I
didn't get around to fixing it until now, so a few instances of the
warning snuck into the repo over the past few months, which were fixed
in the previous commit. The regression has not appeared in a tagged
release though, so this is not a breaking change in 0.16.0.

Resolves: https://codeberg.org/ziglang/zig/issues/31049
This commit is contained in:
Matthew Lugg
2026-03-15 18:18:38 +00:00
parent 64f1c27332
commit 827a96b1ef
2 changed files with 89 additions and 32 deletions
+75 -32
View File
@@ -308,6 +308,9 @@ const ResultInfo = struct {
/// The expression must generate a pointer rather than a value, and the pointer will be coerced
/// by other code to this type, which is guaranteed by earlier instructions to be a pointer type.
ref_coerced_ty: Zir.Inst.Ref,
/// Like `ref`, but the pointer will never be stored to, so local variables should not be
/// marked as possibly being mutated.
ref_const,
/// The expression must store its result into this typed pointer. The result instruction
/// from the expression must be ignored.
ptr: PtrResultLoc,
@@ -339,7 +342,7 @@ const ResultInfo = struct {
/// If the location does not have a known result type, returns `null`.
fn resultType(rl: Loc, gz: *GenZir, node: Ast.Node.Index) !?Zir.Inst.Ref {
return switch (rl) {
.discard, .none, .ref, .inferred_ptr, .destructure => null,
.discard, .none, .ref, .ref_const, .inferred_ptr, .destructure => null,
.ty, .coerced_ty => |ty_ref| ty_ref,
.ref_coerced_ty => |ptr_ty| try gz.addUnNode(.elem_type, ptr_ty, node),
.ptr => |ptr| {
@@ -937,7 +940,11 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node);
_ = try gz.addUnNode(.validate_deref, lhs, node);
switch (ri.rl) {
.ref, .ref_coerced_ty => return lhs,
.ref,
.ref_coerced_ty,
.ref_const,
=> return lhs,
else => {
const result = try gz.addUnNode(.load, lhs, node);
return rvalue(gz, ri, result, node);
@@ -973,6 +980,14 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
return gz.addUnNode(.optional_payload_safe_ptr, lhs, node);
},
.ref_const => {
const lhs = try expr(gz, scope, .{ .rl = .ref_const }, tree.nodeData(node).node_and_token[0]);
const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
try emitDbgStmt(gz, cursor);
return gz.addUnNode(.optional_payload_safe_ptr, lhs, node);
},
else => {
const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node_and_token[0]);
@@ -998,7 +1013,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
.field_name_start = str_index,
});
switch (ri.rl) {
.discard, .none, .ref, .inferred_ptr, .destructure => unreachable, // no result type
.discard, .none, .ref, .ref_const, .inferred_ptr, .destructure => unreachable, // no result type
.ty, .coerced_ty => return res, // `decl_literal` does the coercion for us
.ref_coerced_ty, .ptr => return rvalue(gz, ri, res, node),
}
@@ -1030,7 +1045,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
return switchExpr(gz, scope, ri.br(), node, switch_full, .@"catch");
}
switch (ri.rl) {
.ref, .ref_coerced_ty => return orelseCatchExpr(
.ref, .ref_const, .ref_coerced_ty => return orelseCatchExpr(
gz,
scope,
ri,
@@ -1053,7 +1068,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
}
},
.@"orelse" => switch (ri.rl) {
.ref, .ref_coerced_ty => return orelseCatchExpr(
.ref, .ref_const, .ref_coerced_ty => return orelseCatchExpr(
gz,
scope,
ri,
@@ -1511,7 +1526,7 @@ fn arrayInitExpr(
}
return .void_value;
},
.ref => return arrayInitExprTyped(gz, scope, node, array_init.ast.elements, array_ty, elem_ty, true),
.ref, .ref_const => return arrayInitExprTyped(gz, scope, node, array_init.ast.elements, array_ty, elem_ty, true),
else => {
const array_inst = try arrayInitExprTyped(gz, scope, node, array_init.ast.elements, array_ty, elem_ty, false);
return rvalue(gz, ri, array_inst, node);
@@ -1527,7 +1542,7 @@ fn arrayInitExpr(
}
return Zir.Inst.Ref.void_value;
},
.ref => {
.ref, .ref_const => {
const result = try arrayInitExprAnon(gz, scope, node, array_init.ast.elements);
return gz.addUnTok(.ref, result, tree.firstToken(node));
},
@@ -1701,7 +1716,7 @@ fn structInitExpr(
const val = try gz.addUnNode(.struct_init_empty_result, ty_inst, node);
return rvalue(gz, ri, val, node);
},
.none, .ref, .inferred_ptr => {
.none, .ref, .ref_const, .inferred_ptr => {
return rvalue(gz, ri, .empty_tuple, node);
},
.destructure => |destructure| {
@@ -1817,7 +1832,7 @@ fn structInitExpr(
const ty_inst = try typeExpr(gz, scope, type_expr);
_ = try gz.addUnNode(.validate_struct_init_ty, ty_inst, node);
switch (ri.rl) {
.ref => return structInitExprTyped(gz, scope, node, struct_init, ty_inst, true),
.ref, .ref_const => return structInitExprTyped(gz, scope, node, struct_init, ty_inst, true),
else => {
const struct_inst = try structInitExprTyped(gz, scope, node, struct_init, ty_inst, false);
return rvalue(gz, ri, struct_inst, node);
@@ -1834,7 +1849,7 @@ fn structInitExpr(
}
return .void_value;
},
.ref => {
.ref, .ref_const => {
const result = try structInitExprAnon(gz, scope, node, struct_init);
return gz.addUnTok(.ref, result, tree.firstToken(node));
},
@@ -5832,6 +5847,7 @@ fn tryExpr(
const operand_rl: ResultInfo.Loc, const block_tag: Zir.Inst.Tag = switch (ri.rl) {
.ref, .ref_coerced_ty => .{ .ref, .try_ptr },
.ref_const => .{ .ref_const, .try_ptr },
else => .{ .none, .@"try" },
};
const operand_ri: ResultInfo = .{ .rl = operand_rl, .ctx = .error_handling_expr };
@@ -5856,7 +5872,7 @@ fn tryExpr(
defer else_scope.unstack();
const err_tag = switch (ri.rl) {
.ref, .ref_coerced_ty => Zir.Inst.Tag.err_union_code_ptr,
.ref, .ref_const, .ref_coerced_ty => Zir.Inst.Tag.err_union_code_ptr,
else => Zir.Inst.Tag.err_union_code,
};
const err_code = try else_scope.addUnNode(err_tag, operand, node);
@@ -5867,7 +5883,7 @@ fn tryExpr(
try else_scope.setTryBody(try_inst, operand);
const result = try_inst.toRef();
switch (ri.rl) {
.ref, .ref_coerced_ty => return result,
.ref, .ref_const, .ref_coerced_ty => return result,
else => return rvalue(parent_gz, ri, result, node),
}
}
@@ -5909,6 +5925,7 @@ fn orelseCatchExpr(
const operand_ri: ResultInfo = switch (block_scope.break_result_info.rl) {
.ref, .ref_coerced_ty => .{ .rl = .ref, .ctx = if (do_err_trace) .error_handling_expr else .none },
.ref_const => .{ .rl = .ref_const, .ctx = if (do_err_trace) .error_handling_expr else .none },
else => .{ .rl = .none, .ctx = if (do_err_trace) .error_handling_expr else .none },
};
// This could be a pointer or value depending on the `operand_ri` parameter.
@@ -5930,7 +5947,7 @@ fn orelseCatchExpr(
// This could be a pointer or value depending on `unwrap_op`.
const unwrapped_payload = try then_scope.addUnNode(unwrap_op, operand, node);
const then_result = switch (ri.rl) {
.ref, .ref_coerced_ty => unwrapped_payload,
.ref, .ref_const, .ref_coerced_ty => unwrapped_payload,
else => try rvalue(&then_scope, block_scope.break_result_info, unwrapped_payload, node),
};
_ = try then_scope.addBreakWithSrcNode(.@"break", block, then_result, node);
@@ -6026,8 +6043,9 @@ fn fieldAccess(
) InnerError!Zir.Inst.Ref {
switch (ri.rl) {
.ref, .ref_coerced_ty => return addFieldAccess(.field_ptr, gz, scope, .{ .rl = .ref }, node),
.ref_const => return addFieldAccess(.field_ptr, gz, scope, .{ .rl = .ref_const }, node),
else => {
const access = try addFieldAccess(.field_ptr_load, gz, scope, .{ .rl = .ref }, node);
const access = try addFieldAccess(.field_ptr_load, gz, scope, .{ .rl = .ref_const }, node);
return rvalue(gz, ri, access, node);
},
}
@@ -6075,9 +6093,20 @@ fn arrayAccess(
return gz.addPlNode(.elem_ptr_node, node, Zir.Inst.Bin{ .lhs = lhs, .rhs = rhs });
},
.ref_const => {
const lhs_node, const rhs_node = tree.nodeData(node).node_and_node;
const lhs = try expr(gz, scope, .{ .rl = .ref_const }, lhs_node);
const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
const rhs = try expr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, rhs_node);
try emitDbgStmt(gz, cursor);
return gz.addPlNode(.elem_ptr_node, node, Zir.Inst.Bin{ .lhs = lhs, .rhs = rhs });
},
else => {
const lhs_node, const rhs_node = tree.nodeData(node).node_and_node;
const lhs = try expr(gz, scope, .{ .rl = .ref }, lhs_node);
const lhs = try expr(gz, scope, .{ .rl = .ref_const }, lhs_node);
const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
@@ -7092,11 +7121,15 @@ fn switchExpr(
const catch_or_if_node = if (needs_non_err_handling) node else undefined;
const do_err_trace = needs_non_err_handling and astgen.fn_block != null;
const non_err_is_ref: bool = switch (non_err) {
const non_err_is_ref: enum { no, yes, yes_const } = switch (non_err) {
.none, .peer_break_target => undefined,
.@"catch" => ri.rl == .ref or ri.rl == .ref_coerced_ty,
.@"if" => |if_full| if_full.payload_token != null and
tree.tokenTag(if_full.payload_token.?) == .asterisk,
.@"catch" => switch (ri.rl) {
.ref, .ref_coerced_ty => .yes,
.ref_const => .yes_const,
else => .no,
},
.@"if" => |if_full| if (if_full.payload_token != null and
tree.tokenTag(if_full.payload_token.?) == .asterisk) .yes else .no,
};
if (switch_full.label_token) |label_token| {
@@ -7285,8 +7318,12 @@ fn switchExpr(
block_scope.instructions_top = GenZir.unstacked_top;
const operand_ri: ResultInfo = .{
.rl = if (any_payload_is_ref or
(needs_non_err_handling and non_err_is_ref)) .ref else .none,
.rl = loc: {
if (any_payload_is_ref) break :loc .ref;
if (needs_non_err_handling and non_err_is_ref == .yes) break :loc .ref;
if (needs_non_err_handling and non_err_is_ref == .yes_const) break :loc .ref_const;
break :loc .none;
},
.ctx = if (do_err_trace) .error_handling_expr else .none,
};
@@ -7454,10 +7491,10 @@ fn switchExpr(
.@"catch" => {
// We always effectively capture the error union payload; we use
// it to `break` from the entire `switch_block_err_union`.
non_err_capture = if (non_err_is_ref) .by_ref else .by_val;
non_err_capture = if (non_err_is_ref != .no) .by_ref else .by_val;
const then_result = switch (ri.rl) {
.ref, .ref_coerced_ty => non_err_payload_inst.toRef(),
.ref, .ref_const, .ref_coerced_ty => non_err_payload_inst.toRef(),
else => try rvalue(
&scratch_scope,
block_scope.break_result_info,
@@ -7478,13 +7515,13 @@ fn switchExpr(
const then_node = if_full.ast.then_expr;
const then_sub_scope: *Scope = scope: {
if (if_full.payload_token) |payload_token| {
const ident_token = payload_token + @intFromBool(non_err_is_ref);
const ident_token = payload_token + @intFromBool(non_err_is_ref != .no);
const ident_name = try astgen.identAsString(ident_token);
const ident_name_str = tree.tokenSlice(ident_token);
if (mem.eql(u8, "_", ident_name_str)) {
break :scope &scratch_scope.base;
}
non_err_capture = if (non_err_is_ref) .by_ref else .by_val;
non_err_capture = if (non_err_is_ref != .no) .by_ref else .by_val;
try astgen.detectLocalShadowing(&scratch_scope.base, ident_name, ident_token, ident_name_str, .capture);
payload_val_scope = .{
.parent = &scratch_scope.base,
@@ -7522,7 +7559,7 @@ fn switchExpr(
non_err_info = .{
.body_len = @intCast(body_len),
.capture = non_err_capture,
.operand_is_ref = non_err_is_ref,
.operand_is_ref = non_err_is_ref != .no,
};
}
@@ -8245,7 +8282,7 @@ fn localVarRef(
}
switch (ri.rl) {
.ref, .ref_coerced_ty => {
.ref, .ref_const, .ref_coerced_ty => {
const ptr_inst = if (num_namespaces_out != 0) try tunnelThroughClosure(
gz,
ident,
@@ -8254,7 +8291,7 @@ fn localVarRef(
.{ .token = local_ptr.token_src },
name_str_index,
) else local_ptr.ptr;
local_ptr.used_as_lvalue = true;
if (ri.rl != .ref_const) local_ptr.used_as_lvalue = true;
return ptr_inst;
},
else => {
@@ -8303,7 +8340,7 @@ fn localVarRef(
if (found_namespaces_out > 0 and found_needs_tunnel) {
switch (ri.rl) {
.ref, .ref_coerced_ty => return tunnelThroughClosure(
.ref, .ref_const, .ref_coerced_ty => return tunnelThroughClosure(
gz,
ident,
found_namespaces_out,
@@ -8326,7 +8363,7 @@ fn localVarRef(
}
switch (ri.rl) {
.ref, .ref_coerced_ty => return gz.addStrTok(.decl_ref, name_str_index, ident_token),
.ref, .ref_const, .ref_coerced_ty => return gz.addStrTok(.decl_ref, name_str_index, ident_token),
else => {
const result = try gz.addStrTok(.decl_val, name_str_index, ident_token);
return rvalueNoCoercePreRef(gz, ri, result, ident);
@@ -9108,9 +9145,15 @@ fn builtinCall(
.field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
});
},
.ref_const => {
return gz.addPlNode(.field_ptr_named, node, Zir.Inst.FieldNamed{
.lhs = try expr(gz, scope, .{ .rl = .ref_const }, params[0]),
.field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
});
},
else => {
const result = try gz.addPlNode(.field_ptr_named_load, node, Zir.Inst.FieldNamed{
.lhs = try expr(gz, scope, .{ .rl = .ref }, params[0]),
.lhs = try expr(gz, scope, .{ .rl = .ref_const }, params[0]),
.field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
});
return rvalue(gz, ri, result, node);
@@ -10530,7 +10573,7 @@ fn rvalueInner(
_ = try gz.addUnNode(.ensure_result_non_error, result, src_node);
return .void_value;
},
.ref, .ref_coerced_ty => {
.ref, .ref_const, .ref_coerced_ty => {
const coerced_result = if (allow_coerce_pre_ref and ri.rl == .ref_coerced_ty) res: {
const ptr_ty = ri.rl.ref_coerced_ty;
break :res try gz.addPlNode(.coerce_ptr_elem_ty, src_node, Zir.Inst.Bin{
@@ -18,6 +18,16 @@ fn entry2() void {
fn foo(_: u32) void {}
fn entry3() void {
var a: [1]u8 = .{0};
_ = a[0];
}
fn entry4() void {
var s: struct { a: u8 } = .{ .a = 0 };
_ = s.a;
}
// error
//
// :2:9: error: local variable is never mutated
@@ -26,3 +36,7 @@ fn foo(_: u32) void {}
// :9:9: note: consider using 'const'
// :15:9: error: local variable is never mutated
// :15:9: note: consider using 'const'
// :22:9: error: local variable is never mutated
// :22:9: note: consider using 'const'
// :27:9: error: local variable is never mutated
// :27:9: note: consider using 'const'