First Draft: Nominal Sum Types via `enum union` and `switch` Expressions

Meta jared771 at gmail.com
Sun Sep 20 02:25:06 UTC 2026


On Saturday, 19 September 2026 at 11:49:41 UTC, Nick Treleaven 
wrote:
> 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

There shouldn't be anything stopping it. They lower to very 
simple struct declarations.

> - 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

Cool, in that case a bool variant in the enum union gives 254 
unused bit patterns that can be used for niche optimization.

>>> 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.

How switch expressions are lowered is an implementation detail 
which is why I avoided specifying it in the DIP. I think you're 
right that it's better to lower them to a regular switch 
statement, rather than a series of ternary expressions that my 
POC is currently doing. I'll consider adding a section on it to 
the DIP.

>>>> 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?

It's needed for tuple variants, because they're don't have named 
fields that can be accessed. The only way to access their fields 
is through destructuring.

>> 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.

This is arguably worse, because what if a new field is added 
somewhere down the line that needs to be handled? All switch 
expressions handling MyCoolStruct silently break. At least with 
the ... syntax, it's clear when you're intentionally ignoring 
additional fields. Also with a `rest...` pattern that transforms 
the captured fields into an AliasSeq, the added field will be 
automatically and transparently handled for you.

>> 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.

That's true, but also note that tuple variants aren't actually 
tuples.


More information about the dip.development mailing list