authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-03-15 18:18:38+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-03-15 18:24:32+00:00
log827a96b1efbd1d143752ac15c58b7e2152af133c
treeff96ee7be8594ee0a8f3691319cd702bf5fdb3e3
parent64f1c27332839f5dba9463aa2be3644da0cd5008
signaturelock-open Commit is signed but in an unrecognized format.

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

2 files changed, 89 insertions(+), 32 deletions(-)

lib/std/zig/AstGen.zig+75-32
......@@ -308,6 +308,9 @@ const ResultInfo = struct {
308308 /// The expression must generate a pointer rather than a value, and the pointer will be coerced
309309 /// by other code to this type, which is guaranteed by earlier instructions to be a pointer type.
310310 ref_coerced_ty: Zir.Inst.Ref,
311 /// Like `ref`, but the pointer will never be stored to, so local variables should not be
312 /// marked as possibly being mutated.
313 ref_const,
311314 /// The expression must store its result into this typed pointer. The result instruction
312315 /// from the expression must be ignored.
313316 ptr: PtrResultLoc,
......@@ -339,7 +342,7 @@ const ResultInfo = struct {
339342 /// If the location does not have a known result type, returns `null`.
340343 fn resultType(rl: Loc, gz: *GenZir, node: Ast.Node.Index) !?Zir.Inst.Ref {
341344 return switch (rl) {
342 .discard, .none, .ref, .inferred_ptr, .destructure => null,
345 .discard, .none, .ref, .ref_const, .inferred_ptr, .destructure => null,
343346 .ty, .coerced_ty => |ty_ref| ty_ref,
344347 .ref_coerced_ty => |ptr_ty| try gz.addUnNode(.elem_type, ptr_ty, node),
345348 .ptr => |ptr| {
......@@ -937,7 +940,11 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
937940 const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node);
938941 _ = try gz.addUnNode(.validate_deref, lhs, node);
939942 switch (ri.rl) {
940 .ref, .ref_coerced_ty => return lhs,
943 .ref,
944 .ref_coerced_ty,
945 .ref_const,
946 => return lhs,
947
941948 else => {
942949 const result = try gz.addUnNode(.load, lhs, node);
943950 return rvalue(gz, ri, result, node);
......@@ -973,6 +980,14 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
973980
974981 return gz.addUnNode(.optional_payload_safe_ptr, lhs, node);
975982 },
983 .ref_const => {
984 const lhs = try expr(gz, scope, .{ .rl = .ref_const }, tree.nodeData(node).node_and_token[0]);
985
986 const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
987 try emitDbgStmt(gz, cursor);
988
989 return gz.addUnNode(.optional_payload_safe_ptr, lhs, node);
990 },
976991 else => {
977992 const lhs = try expr(gz, scope, .{ .rl = .none }, tree.nodeData(node).node_and_token[0]);
978993
......@@ -998,7 +1013,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
9981013 .field_name_start = str_index,
9991014 });
10001015 switch (ri.rl) {
1001 .discard, .none, .ref, .inferred_ptr, .destructure => unreachable, // no result type
1016 .discard, .none, .ref, .ref_const, .inferred_ptr, .destructure => unreachable, // no result type
10021017 .ty, .coerced_ty => return res, // `decl_literal` does the coercion for us
10031018 .ref_coerced_ty, .ptr => return rvalue(gz, ri, res, node),
10041019 }
......@@ -1030,7 +1045,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
10301045 return switchExpr(gz, scope, ri.br(), node, switch_full, .@"catch");
10311046 }
10321047 switch (ri.rl) {
1033 .ref, .ref_coerced_ty => return orelseCatchExpr(
1048 .ref, .ref_const, .ref_coerced_ty => return orelseCatchExpr(
10341049 gz,
10351050 scope,
10361051 ri,
......@@ -1053,7 +1068,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
10531068 }
10541069 },
10551070 .@"orelse" => switch (ri.rl) {
1056 .ref, .ref_coerced_ty => return orelseCatchExpr(
1071 .ref, .ref_const, .ref_coerced_ty => return orelseCatchExpr(
10571072 gz,
10581073 scope,
10591074 ri,
......@@ -1511,7 +1526,7 @@ fn arrayInitExpr(
15111526 }
15121527 return .void_value;
15131528 },
1514 .ref => return arrayInitExprTyped(gz, scope, node, array_init.ast.elements, array_ty, elem_ty, true),
1529 .ref, .ref_const => return arrayInitExprTyped(gz, scope, node, array_init.ast.elements, array_ty, elem_ty, true),
15151530 else => {
15161531 const array_inst = try arrayInitExprTyped(gz, scope, node, array_init.ast.elements, array_ty, elem_ty, false);
15171532 return rvalue(gz, ri, array_inst, node);
......@@ -1527,7 +1542,7 @@ fn arrayInitExpr(
15271542 }
15281543 return Zir.Inst.Ref.void_value;
15291544 },
1530 .ref => {
1545 .ref, .ref_const => {
15311546 const result = try arrayInitExprAnon(gz, scope, node, array_init.ast.elements);
15321547 return gz.addUnTok(.ref, result, tree.firstToken(node));
15331548 },
......@@ -1701,7 +1716,7 @@ fn structInitExpr(
17011716 const val = try gz.addUnNode(.struct_init_empty_result, ty_inst, node);
17021717 return rvalue(gz, ri, val, node);
17031718 },
1704 .none, .ref, .inferred_ptr => {
1719 .none, .ref, .ref_const, .inferred_ptr => {
17051720 return rvalue(gz, ri, .empty_tuple, node);
17061721 },
17071722 .destructure => |destructure| {
......@@ -1817,7 +1832,7 @@ fn structInitExpr(
18171832 const ty_inst = try typeExpr(gz, scope, type_expr);
18181833 _ = try gz.addUnNode(.validate_struct_init_ty, ty_inst, node);
18191834 switch (ri.rl) {
1820 .ref => return structInitExprTyped(gz, scope, node, struct_init, ty_inst, true),
1835 .ref, .ref_const => return structInitExprTyped(gz, scope, node, struct_init, ty_inst, true),
18211836 else => {
18221837 const struct_inst = try structInitExprTyped(gz, scope, node, struct_init, ty_inst, false);
18231838 return rvalue(gz, ri, struct_inst, node);
......@@ -1834,7 +1849,7 @@ fn structInitExpr(
18341849 }
18351850 return .void_value;
18361851 },
1837 .ref => {
1852 .ref, .ref_const => {
18381853 const result = try structInitExprAnon(gz, scope, node, struct_init);
18391854 return gz.addUnTok(.ref, result, tree.firstToken(node));
18401855 },
......@@ -5832,6 +5847,7 @@ fn tryExpr(
58325847
58335848 const operand_rl: ResultInfo.Loc, const block_tag: Zir.Inst.Tag = switch (ri.rl) {
58345849 .ref, .ref_coerced_ty => .{ .ref, .try_ptr },
5850 .ref_const => .{ .ref_const, .try_ptr },
58355851 else => .{ .none, .@"try" },
58365852 };
58375853 const operand_ri: ResultInfo = .{ .rl = operand_rl, .ctx = .error_handling_expr };
......@@ -5856,7 +5872,7 @@ fn tryExpr(
58565872 defer else_scope.unstack();
58575873
58585874 const err_tag = switch (ri.rl) {
5859 .ref, .ref_coerced_ty => Zir.Inst.Tag.err_union_code_ptr,
5875 .ref, .ref_const, .ref_coerced_ty => Zir.Inst.Tag.err_union_code_ptr,
58605876 else => Zir.Inst.Tag.err_union_code,
58615877 };
58625878 const err_code = try else_scope.addUnNode(err_tag, operand, node);
......@@ -5867,7 +5883,7 @@ fn tryExpr(
58675883 try else_scope.setTryBody(try_inst, operand);
58685884 const result = try_inst.toRef();
58695885 switch (ri.rl) {
5870 .ref, .ref_coerced_ty => return result,
5886 .ref, .ref_const, .ref_coerced_ty => return result,
58715887 else => return rvalue(parent_gz, ri, result, node),
58725888 }
58735889}
......@@ -5909,6 +5925,7 @@ fn orelseCatchExpr(
59095925
59105926 const operand_ri: ResultInfo = switch (block_scope.break_result_info.rl) {
59115927 .ref, .ref_coerced_ty => .{ .rl = .ref, .ctx = if (do_err_trace) .error_handling_expr else .none },
5928 .ref_const => .{ .rl = .ref_const, .ctx = if (do_err_trace) .error_handling_expr else .none },
59125929 else => .{ .rl = .none, .ctx = if (do_err_trace) .error_handling_expr else .none },
59135930 };
59145931 // This could be a pointer or value depending on the `operand_ri` parameter.
......@@ -5930,7 +5947,7 @@ fn orelseCatchExpr(
59305947 // This could be a pointer or value depending on `unwrap_op`.
59315948 const unwrapped_payload = try then_scope.addUnNode(unwrap_op, operand, node);
59325949 const then_result = switch (ri.rl) {
5933 .ref, .ref_coerced_ty => unwrapped_payload,
5950 .ref, .ref_const, .ref_coerced_ty => unwrapped_payload,
59345951 else => try rvalue(&then_scope, block_scope.break_result_info, unwrapped_payload, node),
59355952 };
59365953 _ = try then_scope.addBreakWithSrcNode(.@"break", block, then_result, node);
......@@ -6026,8 +6043,9 @@ fn fieldAccess(
60266043) InnerError!Zir.Inst.Ref {
60276044 switch (ri.rl) {
60286045 .ref, .ref_coerced_ty => return addFieldAccess(.field_ptr, gz, scope, .{ .rl = .ref }, node),
6046 .ref_const => return addFieldAccess(.field_ptr, gz, scope, .{ .rl = .ref_const }, node),
60296047 else => {
6030 const access = try addFieldAccess(.field_ptr_load, gz, scope, .{ .rl = .ref }, node);
6048 const access = try addFieldAccess(.field_ptr_load, gz, scope, .{ .rl = .ref_const }, node);
60316049 return rvalue(gz, ri, access, node);
60326050 },
60336051 }
......@@ -6075,9 +6093,20 @@ fn arrayAccess(
60756093
60766094 return gz.addPlNode(.elem_ptr_node, node, Zir.Inst.Bin{ .lhs = lhs, .rhs = rhs });
60776095 },
6096 .ref_const => {
6097 const lhs_node, const rhs_node = tree.nodeData(node).node_and_node;
6098 const lhs = try expr(gz, scope, .{ .rl = .ref_const }, lhs_node);
6099
6100 const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
6101
6102 const rhs = try expr(gz, scope, .{ .rl = .{ .coerced_ty = .usize_type } }, rhs_node);
6103 try emitDbgStmt(gz, cursor);
6104
6105 return gz.addPlNode(.elem_ptr_node, node, Zir.Inst.Bin{ .lhs = lhs, .rhs = rhs });
6106 },
60786107 else => {
60796108 const lhs_node, const rhs_node = tree.nodeData(node).node_and_node;
6080 const lhs = try expr(gz, scope, .{ .rl = .ref }, lhs_node);
6109 const lhs = try expr(gz, scope, .{ .rl = .ref_const }, lhs_node);
60816110
60826111 const cursor = maybeAdvanceSourceCursorToMainToken(gz, node);
60836112
......@@ -7092,11 +7121,15 @@ fn switchExpr(
70927121
70937122 const catch_or_if_node = if (needs_non_err_handling) node else undefined;
70947123 const do_err_trace = needs_non_err_handling and astgen.fn_block != null;
7095 const non_err_is_ref: bool = switch (non_err) {
7124 const non_err_is_ref: enum { no, yes, yes_const } = switch (non_err) {
70967125 .none, .peer_break_target => undefined,
7097 .@"catch" => ri.rl == .ref or ri.rl == .ref_coerced_ty,
7098 .@"if" => |if_full| if_full.payload_token != null and
7099 tree.tokenTag(if_full.payload_token.?) == .asterisk,
7126 .@"catch" => switch (ri.rl) {
7127 .ref, .ref_coerced_ty => .yes,
7128 .ref_const => .yes_const,
7129 else => .no,
7130 },
7131 .@"if" => |if_full| if (if_full.payload_token != null and
7132 tree.tokenTag(if_full.payload_token.?) == .asterisk) .yes else .no,
71007133 };
71017134
71027135 if (switch_full.label_token) |label_token| {
......@@ -7285,8 +7318,12 @@ fn switchExpr(
72857318 block_scope.instructions_top = GenZir.unstacked_top;
72867319
72877320 const operand_ri: ResultInfo = .{
7288 .rl = if (any_payload_is_ref or
7289 (needs_non_err_handling and non_err_is_ref)) .ref else .none,
7321 .rl = loc: {
7322 if (any_payload_is_ref) break :loc .ref;
7323 if (needs_non_err_handling and non_err_is_ref == .yes) break :loc .ref;
7324 if (needs_non_err_handling and non_err_is_ref == .yes_const) break :loc .ref_const;
7325 break :loc .none;
7326 },
72907327 .ctx = if (do_err_trace) .error_handling_expr else .none,
72917328 };
72927329
......@@ -7454,10 +7491,10 @@ fn switchExpr(
74547491 .@"catch" => {
74557492 // We always effectively capture the error union payload; we use
74567493 // it to `break` from the entire `switch_block_err_union`.
7457 non_err_capture = if (non_err_is_ref) .by_ref else .by_val;
7494 non_err_capture = if (non_err_is_ref != .no) .by_ref else .by_val;
74587495
74597496 const then_result = switch (ri.rl) {
7460 .ref, .ref_coerced_ty => non_err_payload_inst.toRef(),
7497 .ref, .ref_const, .ref_coerced_ty => non_err_payload_inst.toRef(),
74617498 else => try rvalue(
74627499 &scratch_scope,
74637500 block_scope.break_result_info,
......@@ -7478,13 +7515,13 @@ fn switchExpr(
74787515 const then_node = if_full.ast.then_expr;
74797516 const then_sub_scope: *Scope = scope: {
74807517 if (if_full.payload_token) |payload_token| {
7481 const ident_token = payload_token + @intFromBool(non_err_is_ref);
7518 const ident_token = payload_token + @intFromBool(non_err_is_ref != .no);
74827519 const ident_name = try astgen.identAsString(ident_token);
74837520 const ident_name_str = tree.tokenSlice(ident_token);
74847521 if (mem.eql(u8, "_", ident_name_str)) {
74857522 break :scope &scratch_scope.base;
74867523 }
7487 non_err_capture = if (non_err_is_ref) .by_ref else .by_val;
7524 non_err_capture = if (non_err_is_ref != .no) .by_ref else .by_val;
74887525 try astgen.detectLocalShadowing(&scratch_scope.base, ident_name, ident_token, ident_name_str, .capture);
74897526 payload_val_scope = .{
74907527 .parent = &scratch_scope.base,
......@@ -7522,7 +7559,7 @@ fn switchExpr(
75227559 non_err_info = .{
75237560 .body_len = @intCast(body_len),
75247561 .capture = non_err_capture,
7525 .operand_is_ref = non_err_is_ref,
7562 .operand_is_ref = non_err_is_ref != .no,
75267563 };
75277564 }
75287565
......@@ -8245,7 +8282,7 @@ fn localVarRef(
82458282 }
82468283
82478284 switch (ri.rl) {
8248 .ref, .ref_coerced_ty => {
8285 .ref, .ref_const, .ref_coerced_ty => {
82498286 const ptr_inst = if (num_namespaces_out != 0) try tunnelThroughClosure(
82508287 gz,
82518288 ident,
......@@ -8254,7 +8291,7 @@ fn localVarRef(
82548291 .{ .token = local_ptr.token_src },
82558292 name_str_index,
82568293 ) else local_ptr.ptr;
8257 local_ptr.used_as_lvalue = true;
8294 if (ri.rl != .ref_const) local_ptr.used_as_lvalue = true;
82588295 return ptr_inst;
82598296 },
82608297 else => {
......@@ -8303,7 +8340,7 @@ fn localVarRef(
83038340
83048341 if (found_namespaces_out > 0 and found_needs_tunnel) {
83058342 switch (ri.rl) {
8306 .ref, .ref_coerced_ty => return tunnelThroughClosure(
8343 .ref, .ref_const, .ref_coerced_ty => return tunnelThroughClosure(
83078344 gz,
83088345 ident,
83098346 found_namespaces_out,
......@@ -8326,7 +8363,7 @@ fn localVarRef(
83268363 }
83278364
83288365 switch (ri.rl) {
8329 .ref, .ref_coerced_ty => return gz.addStrTok(.decl_ref, name_str_index, ident_token),
8366 .ref, .ref_const, .ref_coerced_ty => return gz.addStrTok(.decl_ref, name_str_index, ident_token),
83308367 else => {
83318368 const result = try gz.addStrTok(.decl_val, name_str_index, ident_token);
83328369 return rvalueNoCoercePreRef(gz, ri, result, ident);
......@@ -9108,9 +9145,15 @@ fn builtinCall(
91089145 .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
91099146 });
91109147 },
9148 .ref_const => {
9149 return gz.addPlNode(.field_ptr_named, node, Zir.Inst.FieldNamed{
9150 .lhs = try expr(gz, scope, .{ .rl = .ref_const }, params[0]),
9151 .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
9152 });
9153 },
91119154 else => {
91129155 const result = try gz.addPlNode(.field_ptr_named_load, node, Zir.Inst.FieldNamed{
9113 .lhs = try expr(gz, scope, .{ .rl = .ref }, params[0]),
9156 .lhs = try expr(gz, scope, .{ .rl = .ref_const }, params[0]),
91149157 .field_name = try comptimeExpr(gz, scope, .{ .rl = .{ .coerced_ty = .slice_const_u8_type } }, params[1], .field_name),
91159158 });
91169159 return rvalue(gz, ri, result, node);
......@@ -10530,7 +10573,7 @@ fn rvalueInner(
1053010573 _ = try gz.addUnNode(.ensure_result_non_error, result, src_node);
1053110574 return .void_value;
1053210575 },
10533 .ref, .ref_coerced_ty => {
10576 .ref, .ref_const, .ref_coerced_ty => {
1053410577 const coerced_result = if (allow_coerce_pre_ref and ri.rl == .ref_coerced_ty) res: {
1053510578 const ptr_ty = ri.rl.ref_coerced_ty;
1053610579 break :res try gz.addPlNode(.coerce_ptr_elem_ty, src_node, Zir.Inst.Bin{
test/cases/compile_errors/var_never_mutated.zig+14
......@@ -18,6 +18,16 @@ fn entry2() void {
1818
1919fn foo(_: u32) void {}
2020
21fn entry3() void {
22 var a: [1]u8 = .{0};
23 _ = a[0];
24}
25
26fn entry4() void {
27 var s: struct { a: u8 } = .{ .a = 0 };
28 _ = s.a;
29}
30
2131// error
2232//
2333// :2:9: error: local variable is never mutated
......@@ -26,3 +36,7 @@ fn foo(_: u32) void {}
2636// :9:9: note: consider using 'const'
2737// :15:9: error: local variable is never mutated
2838// :15:9: note: consider using 'const'
39// :22:9: error: local variable is never mutated
40// :22:9: note: consider using 'const'
41// :27:9: error: local variable is never mutated
42// :27:9: note: consider using 'const'