I occasionally run into situations where it seems like the compiler should be able to infer that particular switch cases are impossible. Here’s an example:
const Abc = enum { a, b, c };
fn example(abc: Abc) void {
switch (abc) {
.a => {},
.b, .c => |bc| {
//
// ...common code for b and c...
//
switch (bc) {
.a => unreachable,
.b => {
// b-specific code
},
.c => {
// c-specific code
},
}
},
}
}
In this example we know .a in the nested switch is impossible but, we’re switching on a full Abc enum value so the compiler doesn’t know that. Actually, if our Abc enum was instead an error type error {a, b, c}, then we could capture a new “sub error” value of type error{b, c}and we’d no longer need the .a => unreachable case.
const AbcError = error{ a, b, c };
fn errorExample(abc: AbcError) void {
switch (abc) {
error.a => {},
error.b, error.c => |bc| {
switch (bc) {
// compiler knows .a is impossible
error.b => {},
error.c => {},
}
},
}
}
Unlike enums, Zig can do this with errors because it’s able to generate error sub types. This is because errors are globally name-spaced so creating an error{ b, c } type from an error{ a, b, c} type is no problem. Not so with enums. Zig doesn’t have a way to create “sub types” of other types that limit the set of possible values, but, what if it did? Here’s what that could look like:
@SubType(comptime T: type, comptime Values: type) type
i.e.
const Abc = enum{ a, b, c };
@SubType(Abc, enum {b, c}) // a sub type of Abc that can only be b or c
The first example would become this:
const Abc = enum { a, b, c };
fn example(abc: Abc) void {
switch (abc) {
.a => {},
.b, .c => |bc| {
std.debug.assert(@TypeOf(bc) == @SubType(Abc, enum{ b, c }));
switch (bc) {
// .a is impossible, not a member of our sub type
.b => {},
.c => {},
}
},
}
}
Note that you could create a SubType function in userspace today, but, it would have to generate a new unique type with no association to the parent type. With @SubType, values are implicitly convertible to the parent type and, it has the same declarations as the parent type. And of course more importantly, without language support the compiler couldn’t integrate support into the switch statement.
Note that this ideas also applies to tagged unions:
const AbcTaggedUnion = union(enum) {
a: i32,
b: []const u8,
c: f32,
};
fn example(abc: AbcTaggedUnion ) void {
switch (abc) {
.a => |int| {},
.b, .c => |bc| {
std.debug.assert(@TypeOf(bc) == @SubType(AbcTaggedUnion, enum{ b, c }));
switch (bc) {
// .a is impossible, not a member of our sub type
.b => |string| { },
.c => |float| { },
}
},
}
}