const std = @import("std");
const S = struct {
a: u8 = 1,
b: u8 = 2,
fn swap(self: *S) void {
const temp = self.a;
self.a = self.b;
self.b = temp;
}
};
var s: S = .{};
fn pointer() *S {
return &s;
}
pub fn main() void {
var ptr = pointer();
// std.debug.print("{any}\n", .{ptr}); // .{ a = 1, .b = 2 }
ptr.swap();
// std.debug.print("{any}\n", .{ptr}); // .{ a = 2, .b = 1 }
// var n: ?*u8 = null; // error: local variable is never mutated
// _ = n;
}
both const and var can be used to declare ptr, var is accepted even if the pointer itself is never mutated in that scope. Replacing it with const compiles and works just as well, so var is not required.
I would have expected to get the same error: local variable is never mutated.
The check for if a var is mutated happens in AST land, so it is quite limited in the information it has and counts anything that could do mutation (like a method call ptr.swap()) as if it certainly does mutation.
A method call foo.bar() could certainly mutate foo, the check doesn’t look beyond the scope the var was defined in, dong so would not be helpful as its in AST land, so it has no idea about types.
How could that even happen? It’s true that s lives outside that scope so it could be mutated by anything (even something that doesn’t work with ptr) but the question is about ptr itself that is never mutated in that scope. I don’t think foo.bar() can mutate foo?? It’s not like .bar() can mutate its own function argument (*foo).
Certainly I’m mistaken but I still don’t get it.
It can’t because of conditional compilation. Same reason we can’t have “error: unreachable code” here:
process.exit(0);
foo();
Even after analyzing exit and discovering that it has the noreturn type, the compiler can’t know for sure that under all possible configurations, the return type is the same.
Having the check against AST avoids this problem because it doesn’t matter what the types end up being - the syntax guarantees the property.
Then “error: unreachable code” would be false positive, because if you compile for Windows, in fact, the call to foo() is entirely reachable.
Of course doing this to the exit function would be ridiculous but there are plenty of real world examples where code is reachable in some configurations but unreachable in others. That’s the essential point of conditional compilation!
You can see me grappling with this problem space here:
Hmm. This statement of the problem feels at odds with other aspects of status quo Zig. For example, many declarations in the standard library are literally @compileError() for some targets and not others based on comptime logic. It seems actually more in line with the rest of Zig for the hypothetical you gave to be a compile error on targets other than Windows?
All of these examples have code afterwards that is meant to be unreachable and eliminated by semantic analysis - that’s the purpose of this function.
In general terms, we can bring this to the point of absurdity:
if (builtin.mode == .Debug) {
std.debug.print("my number is: {d}", .{x});
}
When you compile in release mode, you wouldn’t want this to be compile error: unreachable code, right?
Somehow I had missed this, I went under the assumption that AST had some understanding of types, so in reality I must assume it just makes some safe guesses based on syntax? If this is the case, then it’s clear why it’s happening.
This is especially difficult because the . method syntax is so overloaded. Zig could have different variants of . depending on whether a method is called on a value or a pointer (like . vs. -> in C++), and whether the methods gets “self” by value or by reference. This would allow more checks based on syntax alone, but would also make calling methods less convenient, because you would always have to choose the correct operator.