Conditional switch cases

When handling errors in polymorphic code, sometimes we run into situations where particular errors are absent from an inferred error set. Consider the following:

const std = @import("std");

fn foo() !void {
    return error.FooHappened;
}

fn bar() !void {
    return error.BarHappened;
}

fn call(cb: anytype) void {
    cb() catch |err| {
        switch (err) {
            error.FooHappened => std.debug.print("foo happened\n", .{}),
            error.BarHappened => std.debug.print("bar happened\n", .{}),
        }
    };
}

pub fn main() !void {
    call(foo);
    call(bar);
}

This wouldn’t compile since the set returned by foo() doesn’t contain error.BarHappened and bar()'s set is missing error.FooHappened. While we can work around this by making foo() and bar() return the combined set, that’s undesirable since the resulting binary would end up with unreachable prongs.

Similar situations can arise when working with enums. The ability to disable particular switch cases based on comptime conditions is generally useful. In C, this is pretty easy:

    case TURTLE: return milkTurtle();
#ifdef UNICORN
    case UNICORN: return milkUnicorn();
#end
    case ZEBRA: return milkZebra();

One way we can implement the feature is to use undefined to denote the absence of a case:

fn call(cb: anytype) void {
    cb() catch |err| {
        const E = @TypeOf(err);
        switch (err) {
            ifPartOf(error.FooHappened, E) => std.debug.print("foo happened\n", .{}),
            ifPartOf(error.BarHappened, E) => std.debug.print("bar happened\n", .{}),
        }
    };
}

fn ifPartOf(comptime err: anytype, comptime Set: anytype) Set {
    const errors = @typeInfo(Set).error_set orelse return err;
    return inline for (errors) |e| {
        if (std.mem.eql(u8, @errorName(err), e.name)) break err;
    } else undefined;
}

I think this is fairly intuitive. undefined is not a legal value, ergo the case should be tossed out. The left side being left accidentally undefined is unlikely.

1 Like

Edit: Probably not as relevant as I thought initially, because something like => if (case_is_possible) comptime unreachable is not possible.

You can use a proper type instead of anytype without changing foo and bar

const std = @import("std");

pub fn main() !void {
	call(foo);
	call(bar);
}

fn foo() !void {
	return error.FooHappened;
}

fn bar() !void {
	return error.BarHappened;
}

const E = error{ FooHappened, BarHappened };

fn call(cb: fn () E!void) void { // function types cannot have an inferred error set.
	cb() catch |err| switch (err) {
		error.FooHappened => std.debug.print("foo happened\n", .{}),
		error.BarHappened => std.debug.print("bar happened\n", .{}),
	};
}

Heh, while I have this URL in my clipboard: #21507 would enable splatting switch prongs from comptime-known arrays.

At which point it’s dead easy, just do an if statement to assign one of two arrays to a comptime const, then use it.