authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2023-11-10 07:57:54+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2023-11-19 09:55:07+00:00
logbaabc6013ea4f44082e69375214e76b5d803c5cb
tree00f89e2c48f72b581bedd7b2c8a068d28839368f
parent325e0f5f0e8a9ce2540ec3ec5b7cbbecac15257a
signaturelock-open Commit is signed but in an unrecognized format.

compiler: add error for unnecessary use of 'var'

When a local variable is never used as an lvalue, we can determine that `const` would be sufficient for this variable, so emit an error in this case. More sophisticated checking is unfortunately not possible with Zig's current analysis model, since whether an lvalue is actually mutated depends on semantic analysis, in which some code paths may not be analyzed, so attempting to determine this would result in false positive compile errors. It's worth noting that an unfortunate consequence of this is that any field call `a.b()` will allow `a` to be `var`, even if `b` does not take a pointer as its first parameter - this is again a necessary compromise because the parameter type is not known until semantic analysis. Also update `translate-c` to not trigger these errors. This is done by replacing the `_ = @TypeOf(x)` emitted with `_ = &x` - the reference there means that the local is permitted to be `var`. A similar strategy will be used to prevent compile errors in the behavior tests, where we sometimes want to force a value to be runtime-known. Resolves: #224

2 files changed, 28 insertions(+), 9 deletions(-)

src/AstGen.zig+21-7
...@@ -1226,7 +1226,7 @@ fn awaitExpr(...@@ -1226,7 +1226,7 @@ fn awaitExpr(
1226 try astgen.errNoteNode(gz.suspend_node, "suspend block here", .{}),1226 try astgen.errNoteNode(gz.suspend_node, "suspend block here", .{}),
1227 });1227 });
1228 }1228 }
1229 const operand = try expr(gz, scope, .{ .rl = .none }, rhs_node);1229 const operand = try expr(gz, scope, .{ .rl = .ref }, rhs_node);
1230 const result = if (gz.nosuspend_node != 0)1230 const result = if (gz.nosuspend_node != 0)
1231 try gz.addExtendedPayload(.await_nosuspend, Zir.Inst.UnNode{1231 try gz.addExtendedPayload(.await_nosuspend, Zir.Inst.UnNode{
1232 .node = gz.nodeIndexToRelative(node),1232 .node = gz.nodeIndexToRelative(node),
...@@ -1248,7 +1248,7 @@ fn resumeExpr(...@@ -1248,7 +1248,7 @@ fn resumeExpr(
1248 const tree = astgen.tree;1248 const tree = astgen.tree;
1249 const node_datas = tree.nodes.items(.data);1249 const node_datas = tree.nodes.items(.data);
1250 const rhs_node = node_datas[node].lhs;1250 const rhs_node = node_datas[node].lhs;
1251 const operand = try expr(gz, scope, .{ .rl = .none }, rhs_node);1251 const operand = try expr(gz, scope, .{ .rl = .ref }, rhs_node);
1252 const result = try gz.addUnNode(.@"resume", operand, node);1252 const result = try gz.addUnNode(.@"resume", operand, node);
1253 return rvalue(gz, ri, result, node);1253 return rvalue(gz, ri, result, node);
1254}1254}
...@@ -2941,11 +2941,19 @@ fn checkUsed(gz: *GenZir, outer_scope: *Scope, inner_scope: *Scope) InnerError!v...@@ -2941,11 +2941,19 @@ fn checkUsed(gz: *GenZir, outer_scope: *Scope, inner_scope: *Scope) InnerError!v
2941 const s = scope.cast(Scope.LocalPtr).?;2941 const s = scope.cast(Scope.LocalPtr).?;
2942 if (s.used == 0 and s.discarded == 0) {2942 if (s.used == 0 and s.discarded == 0) {
2943 try astgen.appendErrorTok(s.token_src, "unused {s}", .{@tagName(s.id_cat)});2943 try astgen.appendErrorTok(s.token_src, "unused {s}", .{@tagName(s.id_cat)});
2944 } else if (s.used != 0 and s.discarded != 0) {2944 } else {
2945 try astgen.appendErrorTokNotes(s.discarded, "pointless discard of {s}", .{@tagName(s.id_cat)}, &[_]u32{2945 if (s.used != 0 and s.discarded != 0) {
2946 try gz.astgen.errNoteTok(s.used, "used here", .{}),2946 try astgen.appendErrorTokNotes(s.discarded, "pointless discard of {s}", .{@tagName(s.id_cat)}, &[_]u32{
2947 });2947 try astgen.errNoteTok(s.used, "used here", .{}),
2948 });
2949 }
2950 if (s.id_cat == .@"local variable" and !s.used_as_lvalue) {
2951 try astgen.appendErrorTokNotes(s.token_src, "local variable is never mutated", .{}, &.{
2952 try astgen.errNoteTok(s.token_src, "consider using 'const'", .{}),
2953 });
2954 }
2948 }2955 }
2956
2949 scope = s.parent;2957 scope = s.parent;
2950 },2958 },
2951 .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent,2959 .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent,
...@@ -7579,7 +7587,10 @@ fn localVarRef(...@@ -7579,7 +7587,10 @@ fn localVarRef(
7579 );7587 );
75807588
7581 switch (ri.rl) {7589 switch (ri.rl) {
7582 .ref, .ref_coerced_ty => return ptr_inst,7590 .ref, .ref_coerced_ty => {
7591 local_ptr.used_as_lvalue = true;
7592 return ptr_inst;
7593 },
7583 else => {7594 else => {
7584 const loaded = try gz.addUnNode(.load, ptr_inst, ident);7595 const loaded = try gz.addUnNode(.load, ptr_inst, ident);
7585 return rvalueNoCoercePreRef(gz, ri, loaded, ident);7596 return rvalueNoCoercePreRef(gz, ri, loaded, ident);
...@@ -10948,6 +10959,9 @@ const Scope = struct {...@@ -10948,6 +10959,9 @@ const Scope = struct {
10948 /// Track the identifier where it is discarded, like this `_ = foo;`.10959 /// Track the identifier where it is discarded, like this `_ = foo;`.
10949 /// 0 means never discarded.10960 /// 0 means never discarded.
10950 discarded: Ast.TokenIndex = 0,10961 discarded: Ast.TokenIndex = 0,
10962 /// Whether this value is used as an lvalue after inititialization.
10963 /// If not, we know it can be `const`, so will emit a compile error if it is `var`.
10964 used_as_lvalue: bool = false,
10951 /// String table index.10965 /// String table index.
10952 name: u32,10966 name: u32,
10953 id_cat: IdCat,10967 id_cat: IdCat,
src/translate_c/ast.zig+7-2
...@@ -1625,13 +1625,18 @@ fn renderNode(c: *Context, node: Node) Allocator.Error!NodeIndex {...@@ -1625,13 +1625,18 @@ fn renderNode(c: *Context, node: Node) Allocator.Error!NodeIndex {
1625 });1625 });
1626 const main_token = try c.addToken(.equal, "=");1626 const main_token = try c.addToken(.equal, "=");
1627 if (payload.value.tag() == .identifier) {1627 if (payload.value.tag() == .identifier) {
1628 // Render as `_ = @TypeOf(foo);` to avoid tripping "pointless discard" error.1628 // Render as `_ = &foo;` to avoid tripping "pointless discard" and "local variable never mutated" errors.
1629 var addr_of_pl: Payload.UnOp = .{
1630 .base = .{ .tag = .address_of },
1631 .data = payload.value,
1632 };
1633 const addr_of: Node = .{ .ptr_otherwise = &addr_of_pl.base };
1629 return c.addNode(.{1634 return c.addNode(.{
1630 .tag = .assign,1635 .tag = .assign,
1631 .main_token = main_token,1636 .main_token = main_token,
1632 .data = .{1637 .data = .{
1633 .lhs = lhs,1638 .lhs = lhs,
1634 .rhs = try renderBuiltinCall(c, "@TypeOf", &.{payload.value}),1639 .rhs = try renderNode(c, addr_of),
1635 },1640 },
1636 });1641 });
1637 } else {1642 } else {