Skip to content

[DO NOT MERGE] POC: Add language-level tagged unions (enum union) and pattern matching (switch expressions) - #23744

Open
MetaLang wants to merge 1 commit into
dlang:masterfrom
MetaLang:sumtype-poc
Open

[DO NOT MERGE] POC: Add language-level tagged unions (enum union) and pattern matching (switch expressions)#23744
MetaLang wants to merge 1 commit into
dlang:masterfrom
MetaLang:sumtype-poc

Conversation

@MetaLang

@MetaLang MetaLang commented Aug 29, 2026

Copy link
Copy Markdown
Member

This PR adds language-level support for Rust/Swift-style tagged unions (called enum union), and dedicated syntax for matching on their variants in the form of switch expressions.

I wasn't satisfied with @rikkimax's approach in #23540, which is more in line with the structural approach taken by ML/Haskell, so I decided to implement it myself. And by myself, I mean an LLM - I wrote the spec, but it wrote all of the code and tests, and it was pretty quick and dirty. Consequently and unsurprisingly, most of the test runners are failing.

module net.event_processor;

import core.stdc.stdio;
import std.format : format;

// 1. Hybrid Tagged Union: Primitives, Slices, Tuples, and Named Records
enum union NetworkEvent
{
    // Bare primitive & slice types (types act directly as discriminant tags)
    case int,                    // Raw error code
    case ubyte[],                // Unparsed raw payload buffer

    // Unit variants
    case Disconnected,
    case Heartbeat,

    // Positional (tuple-like) variants
    case Ping(ulong, ushort),

    // Named record variants
    case HttpRequest { string method; string path; ushort statusCode; };

    // Embedded methods
    string summary() const @safe
    {
        // Switch expression with fat-arrow arms and comma separators
        return switch (this)
        {
            case int errCode               => format("Socket Error: %d", errCode),
            case ubyte[] data              => format("Raw Frame (%d bytes)", data.length),
            case Disconnected              => "Connection Closed",
            case Heartbeat                 => "Keep-Alive ACK",
            case Ping(ts, seq)             => format("Ping [seq=%d, ts=%d]", seq, ts),
            case HttpRequest { method, path, statusCode } => format("%s %s -> %d", method, path, statusCode),
        };
    }
}

// 2. Request Dispatcher demonstrating elimination and return type unification
struct ConnectionHandler
{
    ulong activeSessionId;

    // Dispatches an incoming event and computes an action response code
    int handleEvent(NetworkEvent event) @safe
    {
        // All arms strictly unify via Least Upper Bound (LUB)
        return switch (event)
        {
            case int err => err < 0 ? err : -1,
            case ubyte[] frame => processFrame(frame),
            case Heartbeat => 0,
            case Ping(ts, seq) => sendPong(ts, seq),
            case HttpRequest { statusCode, .. } => statusCode, // Partial record destructuring
            case Disconnected => throw new Exception("Terminating disconnected session"),
        };
    }

    private int processFrame(const ubyte[] frame) @safe pure nothrow => 200;
    private int sendPong(ulong ts, ushort seq) @safe nothrow => 1;
}

void main() @safe
{
    // Supports assignment-style construction
    NetworkEvent e1 = 404;
    NetworkEvent e2 = [0xDE, 0xAD, 0xBE, 0xEF];

    // Labeled variant construction via synthesized static factories for named case variants
    NetworkEvent e3 = NetworkEvent.Heartbeat;
    NetworkEvent e4 = NetworkEvent.Ping(1_700_000_000, 42);
    NetworkEvent e5 = NetworkEvent.HttpRequest("GET", "/api/v1/status", 200);

    auto handler = ConnectionHandler(1001);

    assert(handler.handleEvent(e1) == -1);
    assert(handler.handleEvent(e3) == 0);
    assert(handler.handleEvent(e4) == 1);
    assert(handler.handleEvent(e5) == 200);
    assert(e5.summary() == "GET /api/v1/status -> 200");
}

See enum_union_guide.md for an explanation of how the features work.

Included

  • parser/semantic support for enum unions
  • bare-type variants
  • switch matching, guards, and exhaustiveness checks
  • validation for duplicate cases/types and lifecycle restrictions
  • runtime + regression tests

@xoxorwr @limepoutine @Herringway

@MetaLang MetaLang changed the title Add enum union support and tests Add language-level tagged unions (enum union) and pattern matching (switch expressions) Aug 29, 2026
@github-actions

Copy link
Copy Markdown

DMD perf check

Metric Base PR Δ
compile hello.d (instr) 214.9 M 223.3 M +3.900%
compile hello.d -O -release (instr) 233.2 M 241.6 M +3.591%
compile Phobos (instr) 5,123.7 M 5,167.2 M +0.848%
compile vibe.d (instr) 15,130.8 M 15,225.8 M +0.628%
dmd binary size (stripped) 6.86 MB 6.82 MB -0.61%
Breakdown — compile hello.d
Phase (wall, self time) Base PR Δ
parse 33.8 ms 34.9 ms +3.09%
sema1 10.2 ms 10.5 ms +2.19%
sema3 5.7 ms 5.7 ms -0.87%
sema_other 13.6 ms 13.6 ms +0.18%
codegen 1.9 ms 1.9 ms -0.90%
Breakdown — compile Phobos

+43.5 M instructions: frontend +43.7 M (+1.20%), codegen -0.3 M (-0.02%)

Phase (wall, self time) Base PR Δ
sema3 564 ms 569 ms +0.91%
codegen 394 ms 391 ms -0.90%
sema1 218 ms 215 ms -1.30%
sema_other 196 ms 194 ms -0.80%
ctfe 14.0 ms 13.8 ms -1.62%
inline 4.3 ms 4.1 ms -4.56%
parse 113 ms 113 ms +0.13%
sema2 0.9 ms 1.0 ms +2.22%
All measurements
Metric Base PR Δ
compile hello.d (instr) 214.9 M 223.3 M +3.900%
compile hello.d -O -release (instr) 233.2 M 241.6 M +3.591%
compile Phobos (instr) 5,123.7 M 5,167.2 M +0.848%
compile Phobos codegen (instr) 1,472.7 M 1,472.5 M -0.018%
compile vibe.d (instr) 15,130.8 M 15,225.8 M +0.628%
dmd binary size (stripped) 6.86 MB 6.82 MB -0.61%
hello binary size (stripped) 0.72 MB 0.72 MB 0.00%
peak RSS (compile hello.d) 43.38 MB 43.39 MB +0.04%
peak RSS (compile Phobos) 617.6 MB 618.5 MB +0.14%
peak RSS (compile vibe.d) 1917 MB 1918 MB +0.04%
compile dmd itself (wall) 11.9 s 12.0 s +0.96%
compile hello.d (wall) 65.3 ms 66.5 ms +1.88%
compile Phobos (wall) 1,504 ms 1,501 ms -0.20%

e174f31 vs merge-base 7e0b115 · about these metrics

Comment thread compiler/src/dmd/dcast.d
from.isFunction_Delegate_PtrToFunction()
? MATCH.convert : MATCH.exact;
const isNullUnitVariant = from.toBasetype().ty == Tnull &&
variant.payload.length == 0 && variant.ident == Identifier.idPool("None");

@rikkimax rikkimax Aug 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to avoid comparing identifiers by string, chuck None into the table and do a pointer comparison instead.

alias Parameters = Array!(Parameter);
alias Statements = Array!(Statement);
alias Catches = Array!(Catch);
inout(SwitchExp) isSwitchExp() { return op == EXP.switchExpression ? cast(typeof(return))this : null; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't look like the right place for it.

@rikkimax

Copy link
Copy Markdown
Contributor

Some of the implementation is certainly cleaner.

It doesn't handle integer confusion, which is why I banned that.

No handling of alias sequences and expansion into variants. Kinda important use case both in literature and in D code.

You did not solve for the overlapped error.

You support multiple values per variant, I assume that tuples will exist to define that into existance.

It does not support by-ref, vs by-value this is a killer feature over library.

It does not support chaining like it would via UFCS, this is a downgrade over library.

It requires a lot of redundent tokens, you don't need case inside of the declaration, nor in switch expression.

The extra combination of enum and union I very much dislike it. As we've learned from DIP1000 that is not a good thing.

@MetaLang MetaLang changed the title Add language-level tagged unions (enum union) and pattern matching (switch expressions) [DO NOT MERGE] Add language-level tagged unions (enum union) and pattern matching (switch expressions) Aug 29, 2026
@MetaLang MetaLang changed the title [DO NOT MERGE] Add language-level tagged unions (enum union) and pattern matching (switch expressions) [DO NOT MERGE] POC: Add language-level tagged unions (enum union) and pattern matching (switch expressions) Aug 29, 2026
@MetaLang

MetaLang commented Aug 29, 2026

Copy link
Copy Markdown
Member Author

Some of the implementation is certainly cleaner.

It's just vibe-coded LLM slop I banged out to show my vision for how such a feature would work.

It doesn't handle integer confusion, which is why I banned that.

I'm not completely sure what you're referring to. It will reject cases like:

enum union Num
{
    int,
    long,
}

Num n = 0;

Unless you disambiguate with a cast or literal syntax.

No handling of alias sequences and expansion into variants. Kinda important use case both in literature and in D code.

Ya, too complicated to implement for a POC, but it should be supported through .tupleof or something similar.

You did not solve for the overlapped error.

What's that?

You support multiple values per variant, I assume that tuples will exist to define that into existance.

The variants are "tuple-like", but currently don't have any relation to tuples. I don't think that's a necessity, but may be nice to have. The struct variants are really useful though.

It does not support by-ref, vs by-value this is a killer feature over library.

Ya I'm not sure what to do about that because of the safety issues. Maybe with your fast DFA it'd be safe to support.

It does not support chaining like it would via UFCS, this is a downgrade over library.

That feature is maybe a nice to have, but it's very easy to emulate with a switch expression inside a UFCS function and IMO doesn't add a whole lot.

It requires a lot of redundent tokens, you don't need case inside of the declaration, nor in switch expression.

Those are for readability/comprehensibility more than anything. It makes it more clear to the people reading and writing the code what's going on semantically, and I think that the case token before each variant might be necessary for disambiguation if you also wanna have member functions inside the body, but I may be misremembering.

The case tokens in switch expressions are to match how you declare cases in the enum union body, and to make it more familiar for programmers who are used to the regular switch. But yeah they're unnecessary in terms of parsing.

The extra combination of enum and union I very much dislike it. As we've learned from DIP1000 that is not a good thing.

I think it's good for signalling to the programmer what this construct does and how it works. It's like a union, but with an enumerated list of cases (and enums are traditionally used for the tag in a tagged union).

Also note that Rust, Swift, C#, Zig and Odin all use the keywords enum and/or union (in Zig it's literally union(enum)) for their versions of this concept, and half of those languages use switch expressions instead of match.

You could just as easily use sumtype instead, but IMO that term is loaded and means different things to different people.

@rikkimax

Copy link
Copy Markdown
Contributor

No handling of alias sequences and expansion into variants. Kinda important use case both in literature and in D code.

Ya, too complicated to implement for a POC, but it should be supported through .tupleof or something similar.

You did not solve for the overlapped error.

What's that?

Its an error for @safe code, to prevent accessing of union fields.

You support multiple values per variant, I assume that tuples will exist to define that into existance.

The variants are "tuple-like", but currently don't have any relation to tuples. I don't think that's a necessity, but may be nice to have. The struct variants are really useful though.

It does not support by-ref, vs by-value this is a killer feature over library.

Ya I'm not sure what to do about that because of the safety issues. Maybe with your fast DFA it'd be safe to support.

I've got to go do that on my PR.

It does not support chaining like it would via UFCS, this is a downgrade over library.

That feature is maybe a nice to have, but it's very easy to emulate with a switch expression inside a UFCS function and IMO doesn't add a whole lot.

Booo functions, not analyzable! Not clean chaining of input ranges.

It requires a lot of redundent tokens, you don't need case inside of the declaration, nor in switch expression.

Those are for readability/comprehensibility more than anything. It makes it more clear to the people reading and writing the code what's going on semantically, and I think that the case token before each variant might be necessary for disambiguation if you also wanna have member functions inside the body, but I may be misremembering.

The extra combination of enum and union I very much dislike it. As we've learned from DIP1000 that is not a good thing.

I think it's good for signalling to the programmer what this construct does and how it works. It's like a union, but with an enumerated list of cases (and enums are traditionally used for the tag in a tagged union). You could just as easily use sumtype instead, but IMO that term is loaded and means different things to different people.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants