Expected type '*T', found '*const T' in tagged union switch

Hello, I’m working on writing a gameboy emulator to learn zig. I’m trying to implement the game cartridge and need to model the different types of memory available. I’m trying to use a tagged union, but when switching over the values I keep getting this error and I cannot figure it out. Any help is appreciated.

Error:

src\mbc.zig:36:41: error: expected type '*mbc.Rom', found '*const mbc.Rom'
            Self.rom => |rom| return rom.read_byte(address),
                                     ~~~^~~~~~~~~~
src\mbc.zig:36:41: note: cast discards const qualifier
src\mbc.zig:64:28: note: parameter type declared here
    pub fn read_byte(self: *Rom, address: u16) u8 {

Code:

pub const MBC = union(enum) {
    rom: Rom,
    mbc1: MBC1,
    mbc2: MBC2,
    mbc3: MBC3,
    mbc5: MBC5,

    const Self = @This();

    pub fn read_byte(self: Self, address: u16) u8 {
        switch (self) {
            Self.rom => |rom| return rom.read_byte(address), <-- Error is on this line
            Self.mbc1 => |mbc1| return mbc1.read_byte(address),
            Self.mbc2 => |mbc2| return mbc2.read_byte(address),
            Self.mbc3 => |mbc3| return mbc3.read_byte(address),
            Self.mbc5 => |mbc5| return mbc5.read_byte(address),
        }
    }
};

pub const Rom = struct {
    data: []const u8,

    pub fn init(data: []const u8) Rom {
        return Rom{ .data = data };
    }

    pub fn read_byte(self: *Rom, address: u16) u8 {
        return self.data[address];
    }

    pub fn write_byte(_: *Rom, _: u16, _: u8) void {}

    pub fn size(self: *Rom) usize {
        return self.data.len;
    }

    pub fn save(_: *Rom) void {}
};
// other implementations omitted 

Have you tried using @constCast to avoid the error? Without the full stack trace or a working program, it is difficult to debug.

Adding the @constCast did fix it, but it feels weird to have to do this inside tagged unions. I added the full stack trace to the original message.

read_byte acceptsSelf by value. This means Self cannot be mutated in read_byte. You need to pass Self by reference by changing the function signature to read_byte(self: *Self...

All function parameters in zig are constant. You cannot mutate them. By passing by reference you can mutate the child data of a pointer, since you are not modifying the pointer itself, only the data it points to.

See Documentation - The Zig Programming Language

Edit:

Ok wait a second, you are just reading. So you don’t need to mutate at all. You need to change all your read_byte implementations to accept self by value.

1 Like

I’m not sure, but I think you can modify the switch to do:

    Self.rom => |*rom| return rom.read_byte(address),

That said, I think it would make more sense if read_byte’s signature was changed to take *const Rom instead, given that it doesn’t expect to mutate the value to begin with.

1 Like

I originally had it taking a pointer to Self, but then I cannot access the values and get the following errors. Can you explain how to setup the switch to access the fields?

src\mbc.zig:36:17: error: expected type '*mbc.MBC', found '@typeInfo(mbc.MBC).@"union".tag_type.?'
            Self.rom => |rom| return rom.read_byte(address),
            ~~~~^~~~
src\mbc.zig:4:17: note: enum declared here
pub const MBC = union(enum) {

I fixed out how to do this. Thank you everyone for your help. I needed to switch on the pointer like this

    pub fn read_byte(self: *const Self, address: u16) u8 {
        switch (self.*) { <-- this was the missing part
            .rom => |rom| return rom.read_byte(address),
            .mbc1 => |mbc1| return mbc1.read_byte(address),
            .mbc2 => |mbc2| return mbc2.read_byte(address),
            .mbc3 => |mbc3| return mbc3.read_byte(address),
            .mbc5 => |mbc5| return mbc5.read_byte(address),
        }
    }
1 Like

I’m relatively new to Zig as well, but I don’t think that is doing what you want. The .* will make a copy of self and switch on that.

If you are going to call a mutable function, then take *Self instead of *const Self. Alternatively, every read_byte needs to also take a *const Self and not modify self.

Also look into inline else, you can replace all those cases with a single inline else.

Hope that helps.

not every, just the ones that don’t need to mutate self, since they are using a tagged enum the types don’t need the same interface

Try this. Note |*rom|. This is the only change from your original code

Your problem was the fact that in zig all temporary variables like |rom| are const. But capture by pointer |*rom| avoids this issue.

1 Like

Your solution still wouldn’t work with the original code you posted so must have changed something else too. The recommended way to solve the problem stated in your original post would be to make the Rom.read_byte function take its first argument by value or const pointer instead, as the implementation shown above takes a mutable pointer while it doesn’t actually mutate anything. Given that your new code works, this is likely what you have already done and you can ignore the next paragraph. The original MBC.read_byte function would only work if all the field’s read_byte implementations took their first argument by value or const pointer.

If one of your read_byte implementations actually required a mutable pointer to its first argument, then you could handle that by making the MBC.read_byte function take its first argument by mutable pointer also, and capturing the switch prong by pointer, like so:

// scenario where one of the implementations
// *actually* needs to mutate their first argument.

// takes a mutable pointer to Self
pub fn read_byte(self: *Self, address: u16) u8 {
    return switch (self.*) {
        // rom.read_byte requires a mutable first argument,
        // so capture by pointer
        .rom => |*rom| rom.read_byte(address),
        // mbc1.read_byte doesn't require a mutable pointer,
        // so we can capture by value instead
        .mbc1 => |mbc1| mbc1.read_byte(address),
        // etc...
    };
}

Note that you can use type inference for enums in the switch prong (e.g. .rom instead of Self.rom)

You have some repitition in your switch prongs - the all have the same body. Wouldn’t it be nice if we could combine them all into a single prong?
You can combine multiple switch cases into a single prong by separating the cases with commas, e.g. rom, mbc1, mbc2 => {}. However, when you’re switching on a tagged union and capturing the payload in the prong, the payload type must be coercible between the cases. You can sidestep this requirement by using an inline prong, which essentially generates a separate prong for each case. This makes the type of the payload variable, and if you capture the enum tag it becomes comptime-known. This uses good 'ol duck typing, so as long your operations on the payload are valid for every type covered by the switch prong, it’ll work. For example, if every payload type in your tagged union has a read_byte method that takes its first argument by value or const pointer, you can call that method for the current active value like so:

return switch (self.*) {
    inline else => |payload| payload.read_byte(address),
};

If any of the fields don’t have a read_byte method, you’d get a compile error. You could special case these fields by handling them as separate switch cases.
If you need a mutable pointer, the first part of my post still applies.

3 Likes

Can you link to documentation where it says Self.* will make a copy and switch on that?

Thanks, I did not know about the inline else part. I’m using this as an interface and every implementation has the same function signatures.