Why does this result in a type mismatch?

I am building a library that utilizes a lot of floating-point arithmetic in performance-critical code, and was attempting to expose an enumeration value as a build-option to use @setFloatMode with, which I would use as the value of to set where needed.

Minimal example, nothing surprising:

build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // ---------------------------------------
    const options = b.addOptions();
    options.addOption(std.builtin.FloatMode, "float_mode", std.builtin.FloatMode.optimized);
    exe_mod.addOptions("config", options);
    // ---------------------------------------

    const exe = b.addExecutable(.{
        .name = "app",
        .root_module = exe_mod,
    });
    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

main.zig

const std = @import("std");
const config = @import("config");

pub fn main() !void {
    @setFloatMode(config.float_mode);
}

To my surprise:

❯ zig build run
run
└─ run temp
   └─ install
      └─ install temp
         └─ zig build-exe temp Debug native 1 errors
src/main.zig:5:25: error: expected type 'builtin.FloatMode', found 'options.builtin.FloatMode'
    @setFloatMode(config.float_mode);
                  ~~~~~~^~~~~~~~~~~
.zig-cache/c/00ba311644cb64fb948884833c869798/options.zig:1:34: note: enum declared here
pub const @"builtin.FloatMode" = enum (u1) {
                                 ^~~~
/home/eric/.local/share/zvm/0.14.0/lib/std/builtin.zig:806:23: note: enum declared here
pub const FloatMode = enum {
                      ^~~~
error: the following command failed with 1 compilation errors:
/home/eric/.local/share/zvm/0.14.0/zig build-exe -ODebug --dep config -Mroot=/home/eric/downloads/temp/src/main.zig -Mconfig=/home/eric/downloads/temp/.zig-cache/c/00ba311644cb64fb948884833c869798/options.zig --ca
che-dir /home/eric/downloads/temp/.zig-cache --global-cache-dir /home/eric/.cache/zig --name temp --zig-lib-dir /home/eric/.local/share/zvm/0.14.0/lib/ --listen=- 
Build Summary: 1/6 steps succeeded; 1 failed
run transitive failure
└─ run temp transitive failure
   β”œβ”€ zig build-exe temp Debug native 1 errors
   └─ install transitive failure
      └─ install temp transitive failure
         └─ zig build-exe temp Debug native (+1 more reused dependencies)
error: the following build command failed with exit code 1:
/home/eric/downloads/temp/.zig-cache/o/03d6290b99f12aa1397fa251451bf735/build /home/eric/.local/share/zvm/0.14.0/zig /home/eric/.local/share/zvm/0.14.0/lib /home/eric/downloads/temp /home/eric/downloads/temp/.zig-
cache /home/eric/.cache/zig --seed 0x1856d50d -Zd8bac23338536ccf run

Pertinent line:

error: expected type 'builtin.FloatMode', found 'options.builtin.FloatMode'

What? Why?

I have come up with an acceptable workaround using a bool, though it is less than ideal for my purposes. i was hoping someone would be willing to explain why this causes an error, or point out the obivous that I am missing.

It is because enums are nominal types, meaning that two enum types (even when they are declared with the same fields) are treated as distinct types.

Build options are implemented under the hood by generating a module that contains the data that was given to the build options, however that enum generated in there has an enum type which has a separate type identity, from the enum type that was used to model what should be stored there.

If you look at the generated module within the zig cache you will find it looks like this:

pub const @"builtin.FloatMode" = enum (u1) {
    strict = 0,
    optimized = 1,
};
pub const float_mode: @"builtin.FloatMode" = .optimized;

Where @"builtin.FloatMode" is the name of the new enum type, but we also see that it gets assigned explicit values, so we probably can rely on those numbers corresponding with the original enum values.

So by changing the program to this it works now:

const std = @import("std");
const config = @import("config");

const float_mode: std.builtin.FloatMode = @enumFromInt(@intFromEnum(config.float_mode));

pub fn main() !void {
    @setFloatMode(float_mode);
}
1 Like

That makes sense, I suspected something along those lines, but it did work correctly for an enum type/value that was defined within my project, which threw me off.

pub const Handedness = enum(u1) {
    right,
    left,
};

My build.zig was identical with the exception of something like this:

options.addOption(@import("local_file.zig").Handedness, "hand", .right);

This worked well, though now that I think about it and in light of your explanation, I only ever used the value for comptime switching on which implementation to use, never passing into a function that expected a specific type. I don’t have my project handy at this moment, but I assume I would be correct in assuming that it was actually of type options.Handedness and not just the Handedness relative to my project.

Thank you for the detailed explanation.

2 Likes