Implementing switch on multiple enum values with comptime

That’s something I made today and I thought that some other people might want to use something like it. It works on enums, it doesn’t work on tagged unions when you want to capture.

Example of using it:

const EnumA = enum (u1) {
    Red,
    Black,
};
const EnumB = enum(u1) {
    Up,
    Down,
};

pub fn main() !void {
    const a: EnumA = .Red;
    const b: EnumB = .Down;
    const s = switch (match(.{a, b})) {
        .case(.{.Red, .Up}) => "red up",
        .case(.{.Red, .Down}) => "red down",
        .case(.{.Black, .Up}) => "black up",
        .case(.{.Black, .Down}) => "black down",
    };
    std.debug.print("{s}\n", .{s});
}

Implementation is like this:

fn MatcherStruct(types: []const type) type {
    for (types) |t| {
        if (@typeInfo(t) != .@"enum") @compileError("Types must be enums");
    }
    
    const N = types.len;
    const fieldNames: [N] []const u8 = comptime D: {
        var ans: [N][]const u8 = undefined;
        for (0..N) |i| {
            ans[i] = std.fmt.comptimePrint("{}", .{i});
        }
        break :D ans;
    };

    
    const Generated = @Struct(.@"packed", null, &fieldNames, types, &@splat(.{}));

    return packed struct {
        inner: Generated,

        inline fn case(args: @Tuple(types)) @This() {
            var ans: Generated = @bitCast(@as(@typeInfo(Generated).@"struct".backing_integer.?, 0));
            inline for (0..N) |i| {
                @field(ans, std.fmt.comptimePrint("{}", .{i})) = args[i];
            }
            return .{ .inner = ans };
        }
    };
}
fn match(args: anytype) MatcherStruct(D: {
    var ans: []const type = &.{};
    for (args) |arg| {
        ans = ans ++ &[_]type{@TypeOf(arg)};
    }
    break :D ans;
}) {
    return .case(args);
}

If you don’t want to type the .{} inside case, AFAIK there’s no way to solve it for the general case of “N” enum values because zig doesn’t support variadic function arguments. However you can hand roll implementations for any reasonable number of values:

fn MatcherStruct2(types: [2]type) type {
    return packed struct {
        @"0": types[0],
        @"1": types[1],

        inline fn case(@"0": types[0], @"1": types[1]) @This() {
            var ans: @This() = @bitCast(@as(@typeInfo(@This()).@"struct".backing_integer.?, 0));
            ans.@"0" = @"0";
            ans.@"1" = @"1";
            return ans;
        }
    };
}
fn MatcherStruct3(types: [3]type) type {
    return packed struct {
        @"0": types[0],
        @"1": types[1],
        @"2": types[2],

        inline fn case(@"0": types[0], @"1": types[1], @"2": types[2]) @This() {
            var ans: @This() = @bitCast(@as(@typeInfo(@This()).@"struct".backing_integer.?, 0));
            ans.@"0" = @"0";
            ans.@"1" = @"1";
            ans.@"2" = @"2";
            return ans;
        }
    };
}
fn MatcherStructN(args: anytype) type {
    var types: []const type = &.{};
    inline for (args) |arg| {
        types = types ++ &[_]type{@TypeOf(arg)};
    }
    return switch (types.len) {
        2 => MatcherStruct2(types[0..2].*),
        3 => MatcherStruct3(types[0..3].*),
        else => @compileError("Unsupported length"),
    };
}

fn otherMatch(args: anytype) MatcherStructN(args) {
    return @call(.auto, MatcherStructN(args).case, args);
}

pub fn main() !void {
    const a: EnumA = .Red;
    const b: EnumB = .Down;
    const s = switch (otherMatch(.{a, b})) {
        .case(.Red, .Up) => "red up",
        .case(.Red, .Down) => "red down",
        .case(.Black, .Up) => "black up",
        .case(.Black, .Down) => "black down",
    };
    std.debug.print("{s}\n", .{s});
}
17 Likes

Limitations:

  1. Doesn’t work on tagged unions
  2. Enums need to have a specified “int” type under them.
  3. The compiler sometimes incorrectly reports that you didn’t handle all possible cases when your enums don’t cover the full integer width. for example if you have a “u2” enum with 3 instead of 4 members. I think it’s possible in The future for the zig compiler to become smart enough to detect these.

Very clever trick, love it. I wonder what would happen to the compile times if these kinds of things used a lot though.

Gahhhh, I wanted to comment that Zig does technically support variadic functions, but looking at the docs it seems to be extern only.

So close yet so far…

1 Like

Oh that’s cool, I didn’t know we could do that at all!

I actually think that I might have a way to solve the general case then, will give it a go when I have more time.