Zig: Proposal: Inline Switch Cases

Created on 25 Nov 2020  路  5Comments  路  Source: ziglang/zig

This proposal allows switch cases to be inlined and instantiated for multiple values, similar to inline for. It allows for limited conversion of runtime values to comptime-known values, by generating separate code blocks for each possible value.

For example:

const SliceTypeA = extern struct {
    len: usize,
    ptr: [*]u8,
};
const SliceTypeB = extern struct {
    ptr: [*]u8,
    len: usize,
};
const AnySlice = union(enum) {
    a: SliceTypeA,
    b: SliceTypeB,
};
pub fn getLen(slice: AnySlice) usize {
    return switch (slice) { inline else => |val| val.len; };
}

In this code, the else clause of the switch is instantiated twice, once for SliceTypeA and once for SliceTypeB. It generates two cases, which grab the length from different offsets into the union. The type of val is comptime known, but may be different in different instantiations of the case.

Some more examples:

switch (@as(usize, c)) {
    // this case is instantiated 16 times, value is comptime known
    inline 0..15 => |value| {
        const IndexType = comptime getTypeFromIndex(value);
        // do something
    },
    // this case is generated only once, value is runtime known
    else => |value| reportErrorValue(value),
}

switch (@as(NonExhaustiveEnum, x)) {
    .a => handleA(),
    .b => handleB(),
    // only allowed because _ is specified, val is comptime known
    inline else => |val| handleDynamicNamed(val),
    // cannot be inline, val is runtime known.
    _ => |val| handleUnnamed(val),
}

To keep things sane, inline else is only allowed for tagged unions and exhaustive enums, or non-exhausted enums if the _ case is specified. In the enum case, the tag is comptime known in the else clause. In the union case, the comptime-known tag may also be specified as a second capture (else => |val, tag|). The type of the payload is comptime known but its value may be runtime known.

For the purposes of eval branch quota, each instantiation of a case (except the first one) counts as a backwards branch (just like inline for).

proposal

Most helpful comment

Another usecase: dynamic dispatch for enums and tagged unions.

For example, lib/std/zig/ast.zig currently does this:

pub fn iterate(base: *Node, index: usize) ?*Node {
    inline for (@typeInfo(Tag).Enum.fields) |field| {
        const tag = @intToEnum(Tag, field.value);
        if (base.tag == tag) {
            return @fieldParentPtr(tag.Type(), "base", base).iterate(index);
        }
    }
    unreachable;
}

With this proposal, it would simplify to this:

pub fn iterate(base: *Node, index: usize) ?*Node {
    return switch (base.tag) {
        inline else => |tag| @fieldParentPtr(tag.Type(), "base", base).iterate(index),
    };
}

All 5 comments

Another usecase: dynamic dispatch for enums and tagged unions.

For example, lib/std/zig/ast.zig currently does this:

pub fn iterate(base: *Node, index: usize) ?*Node {
    inline for (@typeInfo(Tag).Enum.fields) |field| {
        const tag = @intToEnum(Tag, field.value);
        if (base.tag == tag) {
            return @fieldParentPtr(tag.Type(), "base", base).iterate(index);
        }
    }
    unreachable;
}

With this proposal, it would simplify to this:

pub fn iterate(base: *Node, index: usize) ?*Node {
    return switch (base.tag) {
        inline else => |tag| @fieldParentPtr(tag.Type(), "base", base).iterate(index),
    };
}

I really like this idea, just not sure if I understand the point about _ requiring else.
I would still vote for supporting inline _ =>, so the limitation doesn't quite make sense to me. If you use a specific enum size like enum(i6) it might make perfect sense to generate all branches for some use cases.
Sure, for an enum(i32) it would be overkill, but even then the branch could contain something comptime-only like a @compileError, and allowing a trivial inline _ => {} also does no harm I think.

If we really want to straight-out disallow it for generating an absurd number of cases for _, we could just specify some limit, say 128.
But again, I don't think limiting edge cases just because they look unusual is necessary, that just forces them into using an uglier workaround.

If we really want to straight-out disallow it for generating an absurd number of cases for _, we could just specify some limit, say 128.

No need for that. Since each instantiation of a case (except the first one) counts as a backwards branch, the number of cases would already be limited by the eval branch quota.

Great idea!

Another thought would be to add special syntax for the single inline else since that will probably be pretty common. Eg:

pub fn iterate(base: *Node, index: usize) ?*Node {
    inline switch (base.tag) |tag| {
      return  @fieldParentPtr(tag.Type(), "base", base).iterate(index);
    };
}

Edit : updated to use the block style, the expression style below also would work.

Also instead of the _ => ... I think undefined => ... is more clear.

@frmdstryr I like the idea, but the syntax you are proposing looks a bit weird. Since this is the only case when a switch would have a payload capture, it could be directly followed by an expression. Eg:

pub fn iterate(base: *Node, index: usize) ?*Node {
    return inline switch (base.tag) |tag| @fieldParentPtr(tag.Type(), "base", base).iterate(index);
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

zimmi picture zimmi  路  3Comments

jorangreef picture jorangreef  路  3Comments

bheads picture bheads  路  3Comments

bronze1man picture bronze1man  路  3Comments

dobkeratops picture dobkeratops  路  3Comments