std.StaticStringMap infered type breaking change

From Zig 0.14.0 to 0.15.1 there seems to have been a breaking change in how types are inferred.

I used to have:

const presetMap = std.StaticStringMap(std.Target.Query).initComptime(.{
    .{ "arm-linux", .{ .cpu_arch = .aarch64, .os_tag = .linux } },
    .{ "native", .{} },
    .{ "linux", .{ .cpu_arch = .x86_64, .os_tag = .linux } }, // to be used for wsl on windows
});

But now I had to change it to:

const presetMap = std.StaticStringMap(std.Target.Query).initComptime(.{
    .{ "arm-linux", std.Target.Query{ .cpu_arch = .aarch64, .os_tag = .linux } },
    .{ "native", std.Target.Query{} },
    .{ "linux", std.Target.Query{ .cpu_arch = .x86_64, .os_tag = .linux } }, // to be used for wsl on windows
});

Anyone know what breaking change led to this?

there was a bug that anonymous structs could coerce to non anonymous structs if they had the same fields.

its not a regression in type inferences, its fixing an unintended type coercion.

1 Like

Interestingly this also used to work before (even though it was strange it managed to infer the type):

const Preset = struct {
    name: []const u8,
    target: std.Target.Query,
    optimization: std.builtin.OptimizeMode,
};

pub fn build(b: *std.Build) !void {
    const preset = .{ .name = build_preset, .target = presetMap.get(build_preset).?, .optimization = .ReleaseSmall };
}

Which makes sense, since the fix for the unintended type coercion.

So in this case we will always have to declare the type now? Feels like StaticStringMap should already know the type of the second argument as it accepts a key value pair of <[]const u8, T>, but I guess the compiler doesn’t know that this anonymous struct should be T?

Looking at implementation details the reason for the anonymous struct to not be coerced into T is because the function initComptime was designed to accept struct { []const u8 } too so the signature of the function looks like:

        /// Returns a map backed by static, comptime allocated memory.
        ///
        /// `kvs_list` must be either a list of `struct { []const u8, V }`
        /// (key-value pair) tuples, or a list of `struct { []const u8 }`
        /// (only keys) tuples if `V` is `void`.
        pub inline fn initComptime(comptime kvs_list: anytype) Self {

No it accepts an anytype, if it took a slice you’d need an & in front of the outer literal, if it took an array you’d need to pass the length as a separate comptime argument since that couldn’t be inferred.

Because its anytype it has no destination type to infer from, the result it an anonymous struct type.

Before, it could incorrectly be coerced to the actual type, an implicit changing of types. That is not allowed per the language design. It was a compiler bug.

I agree before it could incorrectly coerce to the wrong type. I was trying to ask about API design, because if the function signature was pub inline fn initComptime(comptime kvs_list: struct { []const u8, V }) Self { then the type could always be coerced correctly?

Mostly wondering about the use case of having a StaticStringMap with only keys and no values. Without that requirement the usage of it would be nicer.

that only accepts a single key and value.

It should be comptime kvs_list: []const KV
KV is already defined.

2 Likes

One problem with []const KV is that you couldn’t just specify the keys anymore, you also would have to specify the field names including the one for the void value field.

One alternative could be to use a tagged union to separate between the two valid cases accepted by the function:

const KVList = union(enum) {
    key_list: []const []const u8,
    key_value_list: []const struct { []const u8, V },

    pub fn keys(key_list:[]const []const u8) KVList {
        return .{ .key_list = key_list };
    }
    pub fn kvs(key_value_list: []const struct { []const u8, V }) KVList {
        return .{ .key_value_list = key_value_list };
    }
};

...
comptime kvs_list: KVList

That way the api user would still be able to only specify keys, but by using a concrete type that would be shown by Zls.

So the two cases would look like:

const presetMap = std.StaticStringMap(std.Target.Query).initComptime(.kvs(&.{
    .{ "arm-linux", .{ .cpu_arch = .aarch64, .os_tag = .linux } },
    .{ "native", .{} },
    .{ "linux", .{ .cpu_arch = .x86_64, .os_tag = .linux } }, // to be used for wsl on windows
}));

And something like

would look like:

pub const names = std.StaticStringMap(void).initComptime(.keys(&.{
    "anyerror",
    "anyframe",
    "anyopaque",
    "bool",
    "c_int",
    "c_long",
    "c_longdouble",
    "c_longlong",
    "c_char",
    "c_short",
    "c_uint",
    "c_ulong",
    "c_ulonglong",
    "c_ushort",
    "comptime_float",
    "comptime_int",
    "f128",
    "f16",
    "f32",
    "f64",
    "f80",
    "false",
    "isize",
    "noreturn",
    "null",
    "true",
    "type",
    "undefined",
    "usize",
    "void",
}));
1 Like

Hmm looking at this

It seems like the easier option is to just leave StaticStringMap the way it is and use an explicit array type, so @nm-remarkable’s example would become:

const presetMap = std.StaticStringMap(std.Target.Query).initComptime([_]struct{[]const u8, std.Target.Query}{
    .{ "arm-linux", .{ .cpu_arch = .aarch64, .os_tag = .linux } },
    .{ "native", .{} },
    .{ "linux", .{ .cpu_arch = .x86_64, .os_tag = .linux } }, // to be used for wsl on windows
});
2 Likes