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

Meta jared771 at gmail.com
Mon Sep 14 19:43:22 UTC 2026


On Monday, 14 September 2026 at 16:04:35 UTC, Nick Treleaven 
wrote:
> On Monday, 14 September 2026 at 06:35:13 UTC, Meta wrote:
>> I was inspired by Rikki's DIP 
>> (https://forum.dlang.org/post/nhbiwarfrlqqffegkhsf@forum.dlang.org) to write my own that introduces sum types from a different direction that I feel is more "D-like". It borrows heavily from Rust and Swift's version of the feature.
>
> Somewhat like Rikki's DIP, I think this is too complex for a 
> first DIP. It would be better to focus on the core 
> functionality and extend it later. Some of this could maybe be 
> marked as plans for later enhancements.
>
>> 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`.
>
>> ## Enum Union Members
>>
>> Enum unions are treated as struct declarations internally, 
>> which contain a union with the declared variant cases, and a 
>> `__tag` value to track which variant is currently active.
>>
>> Like other aggregates in D, enum unions can contain members, 
>> member functions, constructors, destructors, aliases, etc.
>> ```d
>> enum union NetworkMessage
>> {
>>     case Heartbeat(),
>>     case Text(string content, string encoding),
>>     case Binary(ubyte[]),
>>     case Status(int statusCode, string statusText); // 
>> Terminating semicolon delimits variants
>
> Why not require semi-colon after every case then, instead of 
> comma? That would be simpler, and consistent with field 
> declarations in a union/struct/class.

I've strongly considered it. Maybe I'll change it to be like 
field declarations, so you can do either:
```d
case int,
      double,
      Point(double, double);
```

OR single `case <variant>;` declarations.

>> ## Implicit Construction
>>
>> Enum unions are implicitly constructed in the following cases: 
>> the struct-style construction via assignment discussed 
>> previously, when a function takes an enum union as an 
>> argument, and when a function returns an enum union:
>> ```d
>> enum union Option(T)
>> {
>>     case None = typeof(null),
>>     case Some(T),
>> }
>>
>> Option!ConfigValue getConfigVal(string name) {
>>     string[string] config = readConfig("config.csv");
>>     if (auto val = name in config) with (typeof(return)) {
>>         return Some(ConfigValue(*val)); // Implicitly 
>> constructs an Option!ConfigValue
>>     }
>
> Above, how is that implicit construction? It just uses `with`.

You're right, it's not. THIS would be implicit construction:
```d
struct Some(T)
{
     T val;
}

enum union Option(T)
{
     case None = typeof(null),
     case Some = Some!T,
}

Option!ConfigValue getConfigVal(string name)
{
     string[string] config = readConfig("config.csv");
     if (auto val = name in config) with typeof(return)
     {
         return Some!ConfigValue(ConfigValue(*val));
     }

     return null;
}
```

The point is that for a function taking an enum union as an 
argument, or returning one, passing or returning a type contained 
in the enum union will implicitly wrap it in that union. This is 
in comparison to std.sumtype where you have to do:
```d
struct Some(T) { T val; }
alias None = typeof(null);
alias Option(T) = SumType!(Some!T, None);
Option!ConfigValue getConfigVal(string name)
{
     ...
     if (...)
         return Option!ConfigValue(Some!ConfigValue(*val));

     return Option!ConfigValue.None();
}

Which gets very tedious.

>> ```d
>>     return null;
>> }
>>
>> void applyConfigVal(Option!ConfigValue c);
>> applyConfigVal(null); // Implicitly constructs 
>> Option!ConfigValue.None
>> ```
>
> I like that, and it is a reason to have sum types be built-in 
> to the language. Rikki gave a list of reasons, but most of them 
> seem to be applicable to a library sum type at least in theory.
>
>> ## Niche Optimization (not yet implemented)
>>
>> When an enum union contains unit variants alongside 
>> non-nullable references, pointers (`T*`), class references, or 
>> bounded scalars (such as `bool`), the compiler exploits 
>> invalid bit patterns to encode the unit state:
>>
>> * `Option!(int*)`: The null pointer address `0x0` represents 
>> `None`.
>> * `sizeof(Option!(int*)) == 8` (on 64-bit platforms), 
>> incurring zero byte overhead for the tag.
>
> 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. 
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.

Also Gemini suggested this one:
```
Unmapped Virtual Address Space (The Zero Page): Modern operating 
systems reserve the first page of virtual memory (0x0000_0000 
through 0x0000_0FFF) as unmapped. Any address in that 4 KB range 
triggers a page fault and can never point to valid user data, 
providing 4,096 distinct niche representations.
```

That gives us pointer values of 0x0001 through 0x0FFF to work 
with, meaning that if there's at least 1 pointer in the union, it 
wouldn't need to store its own tag.


>> There may only be **one** pattern per variant. The following 
>> will not compile:
>> ```d
>> switch (pkt)
>> {
>>     case Data(bytes) => ...,
>>     case Data(bytes2) => ..., // Error: redundant match arm. 
>> Pattern is unreachable
>> }
>> ```
>>
>> Every arm must start with the `case` keyword, and every arm is 
>> required to produce a value.
>
> 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. A value is required for every arm because switch 
expressions are expressions. In the same way that `int n = cond ? 
0;` makes no sense, a case arm that doesn't return a value 
doesn't make sense either.

That's what I asked about in this thread: 
https://forum.dlang.org/thread/qeimvsincewaddhgqmfd@forum.dlang.org - basically, is there a way to allow break, return, continue, and goto to be expressions as well as statements and have their type be `noreturn`. It *is* possible in certain cases like my example in that post, but in general allowing expressions to transfer control in the middle of initializing a value is difficult, and will likely be very buggy. I just figured it's not worth it for this DIP.

>> Arms may not contain statements; only a single expression that 
>> produces the value for that arm. Thus, the following will not 
>> compile:
>> ```d
>>     case Data(bytes) => {
>>         writeln("Received Data payload");
>>         ...
>>         return format(...);
>>     }
>> ```
>>
>> However, statement blocks can be emulated using an 
>> immediately-called delegate literal:
>> ```d
>>     case Data(bytes) => {
>>         writeln(...);
>>         ...etc.
>>         return format(...);
>>     }(),
>> ```
>
> 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.

>> ### Destructuring Patterns
>> As shown above, destructuring patterns destructure the enum 
>> union's variants. Destructuring patterns can be used for unit, 
>> tuple, and structure variants.
>>
>> 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
}
```

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

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
             }(),
     }
}
```

>> The `...` syntax allows ALL fields to be omitted:
>> ```d
>>     case Unit(...) => 1, // This is valid because ... means "0 
>> or more fields"
>>     case Struct(...) => "No access to Struct's fields here",
>> ```
>
> Hold on, one case arm returns int and one string. What is the 
> result type of the switch expression? I assumed it was the 
> common type of all case arm result types.

That's a copy and paste error. I'll correct it in the next 
version. Note though that it's valid to mix arms that evaluate to 
`noreturn` with any other type, which is the case for assert and 
throw.



More information about the dip.development mailing list