Repository navigation
Proposal: Inline Switch Cases #7224
Description
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).
Activity
zigazeljko commentedon Nov 26, 2020
Another usecase: dynamic dispatch for enums and tagged unions.
For example,
lib/std/zig/ast.zigcurrently does this:With this proposal, it would simplify to this:
rohlem commentedon Nov 26, 2020
I really like this idea, just not sure if I understand the point about
_requiringelse.I would still vote for supporting
inline _ =>, so the limitation doesn't quite make sense to me. If you use a specific enum size likeenum(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 trivialinline _ => {}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.
zigazeljko commentedon Nov 26, 2020
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.
frmdstryr commentedon Dec 2, 2020
Great idea!
Another thought would be to add special syntax for the single
inline elsesince that will probably be pretty common. Eg:Edit : updated to use the block style, the expression style below also would work.
Also instead of the
_ => ...I thinkundefined => ...is more clear.zigazeljko commentedon Dec 2, 2020
@frmdstryr I like the idea, but the syntax you are proposing looks a bit weird. Since this is the only case when a
switchwould have a payload capture, it could be directly followed by an expression. Eg:SpexGuy commentedon Jan 13, 2021
I really like that extension. Here are some real world cases where I would use it:
https://github.com/SpexGuy/Zig-GrailSort/blob/3e98eb224f004eb0dfd415ea700958d87c426939/src/grailsort.zig#L733-L737
https://github.com/SpexGuy/Zig-GrailSort/blob/3e98eb224f004eb0dfd415ea700958d87c426939/src/grailsort.zig#L804-L808
These cases are on the boundary of the design tradeoff between using runtime values to reduce code bloat and using comptime values to speed up inner loops, where I need to convert from one to the other.
andrewrk commentedon Jan 14, 2021
✔️ Main proposal
❌ No syntax sugar for inline else with no cases (example)
❌ No restrictions on using integer types, the user will have to use tools to help troubleshoot where binary bloat comes from, same as status quo
SpexGuy commentedon Jan 14, 2021
I'll expand on this a bit. While we agree that the sugared version looks cleaner for the exhaustive case, it makes it harder to special case specific items later. With the direct syntax,
But with the sugar, the natural modification is
From a maintainability standpoint, it's not clear that the sugared version is better. So we decided for language simplicity instead.
For the integer types, we felt that the limit was arbitrary. The language usually doesn't care about instantiating things like this, so introducing a new limit here seemed unintuitive. If this does become a problem, we can address it then, or introduce something different (some sort of instantiation limit, similar to the eval branch quota?) to help.
tau-dev commentedon Feb 13, 2021
This is great!
@SpexGuy Besides
elseand ranges, wouldinlinealso work on combined cases? For example, havingexpand to
would be quite nice.
SpexGuy commentedon Feb 13, 2021
Yes, all types of cases will support inline.