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

Meta jared771 at gmail.com
Wed Sep 23 22:23:46 UTC 2026


On Wednesday, 23 September 2026 at 22:15:09 UTC, Meta wrote:
> On Wednesday, 23 September 2026 at 09:14:43 UTC, Dukc wrote:
>> On Monday, 21 September 2026 at 14:17:51 UTC, Meta wrote:
>>>> This means that `int*` is more specialised than `void*` and 
>>>> should therefore be picked.
>>>
>>> Ya, I meant for that to be long*, not void*.
>>
>> Now sounds right to me.
>>
>>>> Well you are already specifying your sum types are structs 
>>>> in disguise, which is likewise out of scope for what is 
>>>> minimally required for sum types.
>>>
>>> How else would they be represented? A struct that contains a 
>>> union has been the canonical way to do this for the past 50 
>>> years, and it's a very easy, simple lowering.
>>
>> Some inprecision from my part again. I don't actually care 
>> much how the compiler will lower the tagged union, that's an 
>> implementation detail only unless you spec it. I mean your 
>> specification that additional (non-`case`) non-`static` fields 
>> will behave as in `struct`s (or `class`es).
>>
>> It's not unreasonable, but it's not the only choice. The 
>> additional fields could alternatively behave as in a regular 
>> `union`s like I proposed, or the compiler might simply reject 
>> them. The latter choice would be the minimalist one.
>
> Ah I see. They're useful for having members that are valid for 
> every variant. Initially I had a "common field promotion" rule, 
> where fields that are common to every variant can be accessed 
> directly instead of needing to use a switch expression, but I 
> think allowing the enum union itself to have struct-style 
> fields is a better design.
>
>>>> I guess changing the keyword would be an okay alternative 
>>>> for me.
>>>
>>> `enum union` is just a placeholder name, but it also makes 
>>> the most sense to me. They have aspects of both enums, with 
>>> the enumerated cases, and unions, with the overlapping 
>>> storage. Simple.
>>
>> Except the storage stops overlapping if you use non-`case` 
>> fields.
>
> Yes.
>
>>>>
>>>> The question is, what are_those invariants in this case? You 
>>>> can specify that a sum type must have a valid tag on 
>>>> destruction or else undefined or unspecified behaviour will 
>>>> follow. But I don't think that's a good idea, since it isn't 
>>>> possible to skip the destructor call of a local variable
>>>
>>> Named Return Value Optimization.
>>>
>>
>> If you're returning the sumtyped value, that is. Not always 
>> the case.
>>
>>>> (except by things like `longjmp` or foreign language 
>>>> exceptions). Hence my suggestion to specify the destructor 
>>>> will do nothing if the tag is invalid.
>>>
>>> You're wildly over-complicating this. If the user wants to 
>>> mess around and do @system stuff with an enum union, they're 
>>> on their own.
>>
>> I'm not sure my suggestion is actually complicating anything. 
>> It is literally the simplest behaviour possible to do nothing 
>> on invalid tag. My suggestion would basically just ban the 
>> compiler from executing a random destructor on an invalid tag 
>> or the optimiser assuming the tag will always be valid.
>
> The tag can never be invalid. That's a core assumption of the 
> feature. If the user does some hacky stuff that messes with the 
> tag, they're responsible for making sure it's valid again 
> afterward.
>
>>>> I basically mean that if you have `switch (sum) { case x(y) 
>>>> => smth, ...}`, it could either mean that `x` is a variant 
>>>> and `y` is a variable declaration for `smth`, or that `x` is 
>>>> a function or a constructor called with value `y`, and `sum` 
>>>> is matched against the result (as in existing switch 
>>>> statements).
>>>
>>> It's invalid to have a function call here, so there's no 
>>> ambiguity.
>>
>> Doesn't that feel limiting? Consider matching a primitive 
>> type. Do you feel this should not be allowed?
>>
>> ```d
>> import std.conv;
>>
>> auto var = case(aString.front)
>> {
>>     case '\r' => fun(),
>>     case '\n' => gun(),
>>     // Unicode replacement character
>>     case to!dchar(0xFFFD) => hun(),
>> };
>> ```
>
> No, it shouldn't be allowed.
>
>> Remember, existing `switch` statements allow this. Why 
>> `switch` expressions wouldn't?
>
> Because switch statements match against values, while switch 
> expressions match against patterns. Patterns only exist at 
> compile time, so a function call cannot be a pattern.
>
>>>
>>> I'm fine with that. If you think it's worthwhile, you will 
>>> need to show me a very convincing example of why it's useful.
>>
>> ```d
>> enum union DynamicNumber
>> {   case long, case float, case double, case error(string);
>> }
>>
>> @safe sum(DynamicNumber a, DunamicNumber b) => switch(a)
>> {   case error(s) => a,
>>     case numA => switch(b)
>>     {   case error(s) => b,
>>         case numB => numA + numB,
>>     },
>> };
>> ```
>>
>> This is a lot more verbose to do manually with `static 
>> foreach`s, especially if you're going to stick with colon 
>> separators for the match cases as opposed to semicolon 
>> separators so that you can't place the `static foreach` inside 
>> the switch statement.
>
> The issue is that the only place in the language where this 
> exists is in foreach/static foreach, and in template lambdas. 
> In the foreach/static foreach case, the body is copied N times 
> for N different types. That makes some sense for a loop, but 
> IMO it makes *no* sense for a switch case. Template lambdas 
> infer the argument type, which can't be done in this case 
> because which variant is active is only known at runtime.
>
> Also, since you're omitting the types here, they would have to 
> be inferred by the compiler. I *think* it can infer them, and 
> the inferred type would have to be double, but that would be 
> very confusing for the reader. There are a lot of issues with 
> this suggestion.
>
> That being said, you're right that this would be tedious to do 
> with the proposal as it is now.
>
> For this specific DynamicNumber case, this *should* work 
> (should as in it doesn't right now because static foreach isn't 
> allowed in a switch expression, but should be made to work):
> ```d
> enum union DynamicNumber
> {
>     case long, float, double, error(string);
> }
>
> DynamicNumber sum(DynamicNumber a, DynamicNumber b) @safe
> {
>     enum errorTag = __traits(variantTag, DynamicNumber, 
> "error");
>     if (a.__tag == errorTag)
>         return a;
>
>     if (b.__tag == errorTag)
>         return b;
>
>     enum isBare(alias V) = __traits(variantKind, V) == "bare";
>     alias NumericVariants = Filter!(isBare, 
> __traits(allVariants, DynamicNumber));
>
>     return switch (a)
>     {
>         // Already handled the error case above, but this is 
> needed to satisfy the exhaustiveness check
>         default => assert(0),
>         static foreach (V1; NumericVariants)
>             static foreach (V2; NumericVariants)
>                 case V1 v1 => switch (other)
>                 {
>                     case V2 v2 => DynamicNumber(v1 + v2);
>                 }
>     }
> }
> ```

That can be simplified to:
```d
enum union DynamicNumber
{
     case long, float, double, error(string);
}

DynamicNumber sum(DynamicNumber a, DynamicNumber b) @safe
{
     enum isBare(alias V) = __traits(variantKind, V) == "bare";
     alias NumericVariants = Filter!(isBare, __traits(allVariants, 
DynamicNumber));
     return switch (a)
     {
         case error => a,
         static foreach (V1; NumericVariants)
             case V1 v1 => switch (b)
             {
                 case error => b,
                 static foreach (V2; NumericVariants)
                     case V2 v2 => DynamicNumber(v1 + v2),
             },
     };
}
```

Actually that's pretty straightforward for a fairly complex case. 
The only thing that's needed is static foreach support for 
generating switch arms, which is on my todo list already.




More information about the dip.development mailing list