Hi everybody. I just had a thought about something that was bothering me for awhile. I’ve noticed frequently while learning Zig, that I am always receiving the same series of compiler errors when writing new code. For example errors like “unused local variable” or “variable not mutated consider using const”. Those are the first that come to mind but there are lots more. Maybe some other people are having this same issue? Well, I think it’s great that the compiler goes the extra mile for you. But it seems like I am always stuck in this same back and forth between the compiler and my code. Those errors can really add up, and the compiler will only output the first encounter of an error before it aborts. So I’m wondering if there might be a way to add some feature into the compiler which sort of scans over the code for any of these obvious errors, and prints them out all at once, to make it easier to fix. I’m sure it would be easy enough to write a scripted solution which does the same thing, but there might be other advantages to integrating this feature directly into the compiler. Any feedback or other suggestions on how to more effectively deal with this problem would be appreciated. Thanks!
I haven’t done so yet, because I am fairly satisfied with my current setup and switching between compile/fix hasn’t bothered me that much, so I am not sure about whether it would help, but I imagine using the zls feature to run the check step described by Loris would give you feedback more quickly and make the round trips a bit more immediate.
I also imagine that the --watch
feature and incremental compilation will lead towards more immediate / interactive feedback, but I think integrating that with editors might still take a while.
In Neovim with ZLS and the check step @Sze mentioned, I see all these errors in red text right there in the editor each time I save and sometimes even without saving. That gives me the chance to fix them all before doint the zig build run
.
Same here, this type of errors is a lot less painful with proper IDE integration (error squiggles and the language server (or is it zig fmt?) placing _ =
for unused variables). IMHO the Zig compiler should have an integrated language server instead of ZLS being a separate project tbh.
Yes, it is zig fmt
that places _ =
for unused variables.
By the way, @floooh welcome to ziggit
This has been the plan for a while now.
introduce file system watching features to the zig build system by andrewrk · Pull Request #20580 · ziglang/zig · GitHub was a big step in this direction. Next step is to introduce support for zig build
being a child process and speaking the Zig protocol to external projects.
Tracking issue is Implement a server in the compiler that serves information about the compilation · Issue #615 · ziglang/zig · GitHub
The compiler should just ignore these non-errors when optimize is debug
. It makes absolutely no sense to perform such checks with code that is obviously unfinished. Consider the following:
const std = @import("std");
fn foo(arg1: i32, arg2: i32) bool {}
fn bar(arg1: i32, arg2: i32) bool {
return arg1 < arg2;
}
test "bar" {
try std.testing.expect(bar(1, 2));
}
The programmer begin by first defining the interfaces of two functions. He chooses to work on bar()
first. He writes the implementation and a test case too. He tries to run it and is greeted by errors related to foo()
. No effing shumai! The function hasn’t been implemented yet! Our programmer would have immediately gotten around to doing so but for the compiler’s refusal to run the test for bar()
. In order to test bar()
, he has to change his code like so:
const std = @import("std");
fn foo(_: i32, _: i32) bool {}
fn bar(arg1: i32, arg2: i32) bool {
return arg1 < arg2;
}
test "bar" {
try std.testing.expect(bar(1, 2));
}
But is foo()
somehow more correct now than before? Of course not. foo()
is semantically broken. By raising strong objection before and remaining silent after the change the compiler is strongly implying that something has been fixed when nothing of that nature has actually occurred. This is very bad design.
No, it is not. There may be some things zig fmt
automatically fixes in the future, though: Formally introduce a class of errors that can be automatically fixed · Issue #17584 · ziglang/zig · GitHub
The better way to satisfy the compiler is this:
fn foo(arg1: i32, arg2: i32) bool {
_ = arg1;
_ = arg2;
}
Which the ZLS autofix will do for you. I find it kind of annoying actually, but not quite so annoying that I’ve turned it off.
What I do when it matters is this:
// Somewhere near the top of the file
const XXX = false;
fn foo(arg1: i32, arg2: i32) bool {
if (XXX) {
_ = arg1;
_ = arg2;
}
}
Which I use to block out anything I need to remove, whether debugging or just as scaffolding.
The advantage here is that eventually I remove the definition for const XXX
. Of course, I grep for XXX
first, but let’s say I forget: I now have compile errors for all the accidental dead code I need to remove.
I’ve seen that this is a divisive decision in the community, and I see why, although I come down firmly on the side of the language team and BDFL here.
Why? Well, I’ve spend hours, days, of my professional life, debugging shitty code with a bunch of dead stuff in it. It sucks. Also, Zig has conditional compilation, so it’s easy to use comptime known values, like my XXX
there, to rub out code. Unlike commented out code, and unlike unused parameters in long functions, and unlike ‘variables’ which are never mutated but not marked const
, an if (XXX)
branch is easy to grep for, and it’s trivial to remove all of it once the code is whipped into shape.
You can still easily scaffold stuff, knock out logic you don’t need or might not need, it’s just a slightly different technique, and one I got used to pretty quickly once I stopped fighting the language. My opinion is that what we get in return is worth it. YMMV.
I will say this though: the ZLS fixup I really want isn’t blocking out unused stuff, since I’m willing to ignore squiggly red lines for awhile while I work, but rather, something which will turn a const
into a var
and vice-versa. I’m also confident that either ZLS or some future build-in language server will do this, it’s an obvious win. I’ve yet to be saved from a bug by “oops I tried to mutate this variable but it’s a const
”, the advantage of the distinction for me is 100% that it makes the code easier to understand.
Thanks everybody for the great feedback. There are a lot of interesting ideas here worth looking at. It seems I need to do some work on improving my IDE and build environment as well.
Thank you for correcting me.
I thought that zig fmt
is fixing the unused parameters, variables and constants but it is the vscode plugin zig language server that autofix these if enable_autofix = true
This code:
pub fn foo(x: u8) void {
var y: u8 = 0;
}
On save becomes:
pub fn foo(x: u8) void {
_ = x; // autofix
var y: u8 = 0;
_ = y; // autofix
}
Then based on the // autofix
comment, when the parameter or the variable is used these lines are removed.