Is there a compiler bug here? Or just intentional design?

Yes, I have found the same facts.

Now, do you mean it is not an intentional design? Switching on unions with non-exhaustive tags should not have the else prong (which captures no named tags), or should always have the else prong, or unions with non-exhaustive tags should be disallowed?

I am not entirely sure what is intentional, I think for that it would be good to have a comment from the core team or other compiler devs familiar with why status quo works like it does.

The forth option is:
status quo, switching on unions with non-exhaustive tags may have a else prong:

with else:

  • if all prongs are used, the else is dead code and ignored (for exhaustive tag this is a compile error)
  • otherwise used for the missing prongs

without else:

  • if all prongs are used, the code compiles
  • otherwise compile error for missing prong

To address this:

If you want to handle all unions (regardless of tag type) I think you can:

  • use an (inline) else prong without handling every other prong (requires having at least one not handled prong) to avoid the exhaustive compile error
  • convert the tag type to be always exhaustive or non-exhaustive based on the behavior you want and switch on that while accessing the payload manually (or via comptime), or convert the union type too
  • or use only inline else to handle all prongs

But this doesn’t seem very ergonomic.
I am not sure whether you meant this.

But maybe it is fine to require either non- or exhaustive enum tag types, instead of a mixture of both for any of your generic types?


maybe

1 Like

Looks like a wired inconsistency with Zig itself. Also wired that you can use ā€œ_ā€ and ā€œelseā€ both in switches, didn’t know that one

Accepted in this issue.

I also agree it’s a bit weird, but it is indeed useful.

It’s intentional to allow non-exhaustive enums as the tag type of a union. They still have distinct tags, so there’s no ambiguity there - all enum values that don’t correspond to a tag just won’t have a corresponding union tag either.

Allowing an unreachable else prong when switching on such unions is indeed an oversight though, thanks for noticing it! Since a tagged union is only instantiable for exactly the tags of its tag type a union with a nonexhaustive tag type is no different from a union with an exhaustive one in that regard.

For a const E = enum(u8) { a, b, _ }, const U = union(E) { a, b } this:

switch (u) {
    .a => { ... },
    .b => { ... },
    else => { ... },
}

is equivalent to this:

switch (u) {
    .a => { ... },
    .b => { ... },
    else => comptime unreachable,
}

which is equivalent to this:

switch (u) {
    .a => { ... },
    .b => { ... },
}

so there’s no compelling reason to allow any forms besides the last one. Especially the first form being disallowed helps avoiding dead code; without changing the definition of U there is literally no way to reach the else prong here.

EDIT: I’ve opened https://codeberg.org/ziglang/zig/issues/36304 to track this issue

3 Likes