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