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