First Draft: Nominal Sum Types via `enum union` and `switch` Expressions
Nick Treleaven
nick at geany.org
Sat Sep 19 11:49:41 UTC 2026
On Monday, 14 September 2026 at 19:43:22 UTC, Meta wrote:
> On Monday, 14 September 2026 at 16:04:35 UTC, Nick Treleaven
> wrote:
>>> When it is ambiguous which type would be initialized by this
>>> assignment, the compiler requires the user to disambiguate:
>>> ```d
>>> enum union Nums
>>> {
>>> case int,
>>> case long,
>>> }
>>>
>>> Nums n = 0; // Error, 0 is ambiguous between variants `int`
>>> and `long` of enum union `Nums`
>>> Nums n = 0L; // Ok
>>> ```
>>
>> Why is 0 ambiguous? 0 has type `int`.
Did you mean to respond to this? Dukc also raised this point.
>> Surely that only works with one unit variant and one pointer?
>
> Generalized niche optimization can encode multiple unit
> variants without increasing the aggregate size. For an aligned
> pointer (e.g., int* aligned to 4 bytes on 32/64-bit systems),
> the lowest 2 bits are always 00, so that's 3 representable
> niche values.
OK, though the user may already be using those bits for non-GC
pointers. Perhaps it should be configurable? And sum types might
not work on bare metal - though I imagine at least
`*cast(byte*)null` would be reserved on all systems.
> Also for bools, technically only 0 and 1 are valid values,
> which leaves 254 other bit patterns for variant tags - although
> doesn't D define the only @safe bool bit patterns as 00000000
> and 11111111? I need to look into that.
OK. A `bool` can be `0x0` or `0x1`:
> Undefined Behavior:
> Interpreting a value with a byte representation other than 0
> or 1 as bool (e.g. an overlapped union field).
https://dlang.org/spec/type.html#bool
>> Why require a value? Actually I've just seen the example under
>> 'Side-Effects' - apparently it doesn't require a value.
>
> `writeln` still returns a value - a value of type `void`. You
> can't directly construct it, but it does exist and is 1 byte in
> size.
OK, though no byte is actually returned. Aside: Ideally instead
of returning void, D would error and suggest returning `()`, the
empty tuple. Though that would break too much code.
> A value is required for every arm because switch expressions
> are expressions.
OK.
>> That is not equivalent - in the first block `return` returns
>> from the function containing the `switch`, in the latter, the
>> `return` gives the result of the case arm.
>
> You're right, it's not equivalent, but it'd be very difficult
> to allow statement blocks in switch expressions.
>
>> I showed how the compiler could lower a declaration
>> initialized from a match/switch construct to a series of
>> statements here:
>> https://forum.dlang.org/post/gljmaiuvrjimeduohhuq@forum.dlang.org
>>
>> The same principle applies for a match/switch statement with
>> no result.
>
> It IS possible, and I also found a way to do it that uses a
> similar lowering when switch expressions appear in statement
> position. I just figured it wasn't worth the extra complexity
> and effort for this DIP.
I think it is a basic use case, and I'd like it to be included.
Not least because it may influence syntax and design of
switch/match expressions.
>>> Destructuring patterns allow fields to be omitted using `...`
>>> syntax:
>>
>> How often is it needed? This could be an enhancement DIP, as
>> it could apply to tuple declaration unpacking as well. Also
>> `_` syntax to ignore a single item could be a part of that DIP
>> too.
>
> Imagine an enum union defined in an external library:
> ```d
> enum union MyCoolUnion
> {
> case MyCoolStruct { int n; double d; },
> ...other cases
> }
>
> MyCoolUnion m = ...;
> switch (m)
> {
> case MyCoolStruct(n, d) => ...,
> ...handle other cases
> }
> ```
So the struct destructuring feature is now needing the `...`
pattern feature. Do we need struct destructuring in the first DIP?
> Now what happens if they add a field to MyCoolStruct?
> ```d
> case MyCoolStruct { int n; double d; bool b; /* NEW */ }
>
> switch (m)
> {
> case MyCoolStruct(n, d) => ..., // Error: pattern for
> variant `MyCoolStruct` has 2 argument(s), expected 3
> }
> ```
>
> It will break all switch expressions that handle MyCoolUnion.
> Discard and rest patterns avoid this:
> ```d
> switch (m)
> {
> case MyCoolStruct(n, d, rest...) => ..., // Or just discard
> unwanted fields with ...
> }
> ```
```d
case MyCoolStruct s => expr(s.n, s.d),
```
That works regardless of fields added to MyCoolStruct.
> Also I think rest... patterns could be very useful for
> serialization libraries. Look how simple it make serializing an
> enum union:
> ```d
> void serialize(U, Sink)(U unionVal, ref Sink sink)
> if (is(U == enum union))
> {
> sink.write(unionVal.__tag);
> switch (unionVal)
> {
> static foreach (V; __traits(allVariants, U))
> case V(fields...) => {
> static foreach (field; fields)
> sink.write(field); // 0 fields for unit
> cases, 1 for primitives, N for structs
> }(),
> }
> }
> ```
OK, but without that feature you can already get the fields of a
struct, and a tuple is itself a struct.
More information about the dip.development
mailing list