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

Nick Treleaven nick at geany.org
Mon Sep 14 16:04:35 UTC 2026


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.

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

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

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

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

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.

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

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



More information about the dip.development mailing list