Error: local variable is never mutated

This discussion comes up every time an error like this is implemented: there was a similar widespread reaction when we introduced errors for unused variables, for instance. I do understand those who think some kind of “sloppy” mode would work well, but I think it’s a flawed idea.

Firstly, it’s worth noting that Andrew’s mind probably isn’t going to change on this. This argument has been extensive, drawn-out, and gone on for years. In that time, Andrew has almost certainly heard every single argument you could possibly make, and his mind has not been changed. As BDFL/BDFN, he gets to make that call.

Now, let’s be clear: it’s pretty rare to make many changes to code during development that would trigger these errors. For sure, a block of code is an evolving thing, and I change my mind on how things should work as I write them; but it’s incredibly rare (I actually can’t think of a single time where this has happened) for me to explicitly decide a variable is going to be mutated, change my mind down the line, but not already have to modify the line it is declared on. This scenario simply doesn’t normally arise in the edit-build-run cycle, so requiring one more trivial modification (which your IDE can do for you anyway if you want it to!) doesn’t really change anything.

A tip for @gonzo and any others who have raised similar issues about commenting out blocks of code: if you want to comment out a big block of code in Zig, the closest equivalent to C’s #if 0 is to simply write if (false). It’s logically the exact same thing: what C does with the preprocessor, Zig tries to do with comptime. Not only does this nest well, it’s particularly useful in preventing errors like this from changing - the compiler doesn’t care that the code mutating your local variable is not actually analyzed, provided it exists. This is equally true for any error which uses “global information” like this - such errors are not dependant on semantic analysis, so wrapping the code in if (false) won’t introduce any error. For instance, if (false) _ = x; is still treated as a discard of x.

A big practical problem with the idea of a --sloppy mode (or similar) is - as has already been mentioned - that it can and will be found on code in the wild. This is not theoretical: look at the amount of code that exists which emits warnings on virtually every major C compiler, for instance. People will treat these “sloppy” errors just like warnings if they don’t block compilation, and when people forget to test their code without this option, code ends up on the internet with these issue. Worse still, if that’s a package, someone could use the package and not even notice that there’s an issue until they try to build their program in a release mode! Depending on how the option is implemented, the endgame here is either a. huge amounts of Zig code in the wild being useless to anything that isn’t a toy program, since it can’t be used in release mode, or b. the proliferation of --sloppy as a default, required to use large parts of the ecosystem. This code would also invariably appear in spaces like this forum, which makes helping people harder; code which makes these mistakes is naturally harder to read and understand.

That brings me on to the main, and perhaps most important, point. Errors like this do not just exist to make code easier to read - that’s a key goal, but not the only one. Another key goal is that this form of error helps prevent bugs. I really like how Andrew summarises this - in essence, Zig is designed to prevent bugs by strategically introducing friction to bug-prone areas of the language. When you consider it in this context, these errors are arguably more important during development, since that’s when you’re going to be writing code and so introducing bugs!

This is the case with unused variables for instance - having a variable which you have named but not used anywhere is a quite bug-prone pattern (it indicates that you have likely made a mistake), so Zig introduces the intentional friction of requiring a discard (_ = x) to acknowledge the intent here. This has two effects: it forces you as the author to acknowledge that you’ve used this pattern intentionally, and it highlights this fact to readers of your code (improving readability). I personally have had a significant amount of buggy code caught before I even hit build due to the existence of this error.

The new error is doing exactly the same thing. Writing var - indicating explicit intent to mutate a variable - and then never writing code which has the capability to do so is a clear indicator that you’ve gone wrong somewhere. Perhaps you changed your mind on exactly what the variable meant when writing code, or perhaps you restructured something; but more likely, you either should have written const in the first place, or you forgot to mutate something which definitely needs to be mutated. Don’t just take my word for this - when implementing this change, fixing the parts of the Zig codebase that flagged up this error identified 3 separate bugs (two in the standard library and one in the compiler). These bugs would have been tricky to spot otherwise, but were extremely obvious in the presence of this error. For instance, some Mach-O linker code initialized a var next_sym: usize = 0, and never changed it, which - even having zero familiarity with the relevant code - I could tell was clearly a bug. 3 bugs in a fairly well-audited and reviewed codebase used by a lot of people is not insignificant.

Also, as a quick closing note: const is significantly easier for the compiler to optimize. There isn’t much to say here, but the point is that a const often need not correspond to a stack allocation in the compiler’s first intermediate representation (ZIR), so the self-hosted backends can often lower it to being stored in a register even with no optimization passes whatsoever. Pretty neat!

TL;DR: this error message not only helps improve readability, but also contributes a lot to avoiding bugs in code, and is friendlier to the compiler (potentially improving runtime performance). Regardless, this issue is ultimately Andrew’s call as the BDFL, and I can gurantee you he’s heard every argument under the sun here.

16 Likes