I recently wanted to change which static string a var pointed to.
const std = @import("std");
pub fn main() !void {
var str = "first";
if (true) str = "second";
std.debug.print("str: {s}\n", .{str});
}
I got this error:
est.zig:5:21: error: expected type '*const [5:0]u8', found '*const [6:0]u8'
if (true) str = "second";
^~~~~~~~
test.zig:5:21: note: pointer type child 'u8' cannot cast into pointer type child '[5:0]u8'
referenced by:
callMain: /home/dvanderson/apps/zig-linux-x86_64-0.11.0/lib/std/start.zig:574:32
initEventLoopAndCallMain: /home/dvanderson/apps/zig-linux-x86_64-0.11.0/lib/std/start.zig:508:34
remaining reference traces hidden; use '-freference-trace' to see all reference traces
Zig inferred the type of str
to be a pointer to a const array (length 5, zero terminated) of u8. That is the type of string literals in zig.
When trying to assign "second", the types don't match because the length of the arrays is different.
The solution is to specify the type of str
to be a slice:
const std = @import("std");
pub fn main() !void {
// type can also be [:0]const u8
var str: []const u8 = "first";
if (true) str = "second";
std.debug.print("str: {s}\n", .{str});
}
This works because pointers to arrays coerce to slices. Each assignment coerces to the same str
slice type.
I've run into this a few times, so hope that writing it up can help people that find themselves in this situation.
Latest comments (5)
Couldn't you also use a literal
switch
statement for this, instead of an array?Yep! And usually that's what I do if the condition is simple. Like Loris said (I hope I'm saying this correctly), the type of the switch statement uses peer type resolution on "second" and "first", which resolves to a slice type.
The original formulation is useful if the conditions for which string to pick are larger and spread-out. Usually for me I want to set
str
to a default string, and then later in the function change it if needed.The article doesn't use arrays, but slices instead. The difference is critical and what you're implicitly doing in your code is defining
str
to be a slice, so it's another way of relying on the same principle explained in the original article.Short and sweet, thanks for sharing!
As a bit of trivia: a few versions ago (0.5 maybe?) Zig didn't have this, so it's a relatively recent introduction that I believe was aimed at making sure that we don't lose statically known type information, which could be leveraged to do more things at comptime (vs having to do them at runtime).
Looking at a string literal as a pointer to an array means that you know the length at comptime, which is information that you lose when representing it with a slice.
Another related trick that Zig does is this:
In this case the type of
window
is*const [window_size] u8
because even though the offset at which we begin slicing is runtime known, the size of that slice is fixed, and thus representable with an array pointer.Of course the downside of this approach is that sometimes the compiler will default to types that are less general than what you might expect, but the upside is that you don't lose any type information when doing those transformations, without requiring the compiler to implement an optimization pass dedicated to deducing this stuff (which then wouldn't even be usable in your comptime code).
Thanks for the context!
I love how zig has managed to pull off some great features in ways I didn't expect.
Like you say - slicing a known length is one of them. There was lots of ideas about how to syntactically do that, but directly recognizing
buf[offset..][0..len]
is beautiful.github.com/ziglang/zig/issues/15482
Same thing with multi-object for loops. So many options for how to special case the index, but once
for(aa, bb, 0..) |a,b,i|
was suggested it now feels natural.github.com/ziglang/zig/issues/7257...