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 43 commits into
dlang:masterfrom
MetaLang:sumtype-poc
Open

MetaLang wants to merge 43 commits 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               => "Socket Error: %d".format(errCode),
            case ubyte[] data              => "Raw Frame (%d bytes)".format(data.length),
            case Disconnected              => "Connection Closed",
            case Heartbeat                 => "Keep-Alive ACK",
            case Ping(ts, seq)             => "Ping [seq=%d, ts=%d]".format(seq, ts),
            case HttpRequest { method, path, statusCode } => "%s %s -> %d".format(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
    auto e3 = NetworkEvent.Heartbeat;
    auto e4 = NetworkEvent.Ping(1_700_000_000, 42);
    auto 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

github-actions Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

DMD perf check

Metric Base PR Δ
compile hello.d (instr) 211.6 M 212.1 M +0.262%
compile hello.d -O -release (instr) 229.2 M 229.7 M +0.240%
compile Phobos (instr) 4,896.0 M 4,926.9 M +0.632%
compile Phobos codegen (instr) 1,423.3 M 1,427.1 M +0.268%
compile vibe.d (instr) 14,469.1 M 14,574.1 M +0.726%
dmd binary size (stripped) 8.12 MB 8.27 MB +1.85%
compile dmd itself (wall) 9.2 s 9.4 s +2.22%
Breakdown — compile hello.d
Phase (wall, self time) Base PR Δ
sema_other 12.6 ms 11.3 ms -10.03%
parse 23.7 ms 24.1 ms +1.75%
sema1 7.3 ms 7.3 ms -0.55%
codegen 1.6 ms 1.5 ms -1.41%
sema3 4.2 ms 4.2 ms -0.50%
Breakdown — compile Phobos

+30.9 M instructions: frontend +27.1 M (+0.78%), codegen +3.8 M (+0.27%)

Phase (wall, self time) Base PR Δ
sema1 144 ms 139 ms -2.97%
sema3 352 ms 354 ms +0.57%
codegen 301 ms 300 ms -0.26%
sema_other 159 ms 158 ms -0.41%
parse 80.3 ms 79.8 ms -0.64%
inline 2.9 ms 2.8 ms -3.46%
ctfe 7.5 ms 7.6 ms +0.92%
sema2 0.7 ms 0.7 ms +2.69%
All measurements
Metric Base PR Δ
compile hello.d (instr) 211.6 M 212.1 M +0.262%
compile hello.d -O -release (instr) 229.2 M 229.7 M +0.240%
compile Phobos (instr) 4,896.0 M 4,926.9 M +0.632%
compile Phobos codegen (instr) 1,423.3 M 1,427.1 M +0.268%
compile vibe.d (instr) 14,469.1 M 14,574.1 M +0.726%
dmd binary size (stripped) 8.12 MB 8.27 MB +1.85%
hello binary size (stripped) 0.72 MB 0.72 MB 0.00%
peak RSS (compile hello.d) 44.25 MB 44.46 MB +0.47%
peak RSS (compile Phobos) 614.1 MB 616.7 MB +0.44%
peak RSS (compile vibe.d) 1902 MB 1903 MB +0.07%
page faults (compile hello.d) 8,933 8,941 +0.09%
page faults (compile Phobos) 153,455 153,622 +0.11%
page faults (compile vibe.d) 476,416 476,241 -0.04%
compile dmd itself (wall) 9.2 s 9.4 s +2.22%
compile hello.d (wall) 49.4 ms 48.4 ms -1.89%
compile Phobos (wall) 1,047 ms 1,043 ms -0.40%

6fb3305 vs merge-base fb655c9 · about these metrics

Comment thread compiler/src/dmd/dcast.d Outdated
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.

Comment thread compiler/src/dmd/astbase.d Outdated
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.

MetaLang and others added 18 commits September 24, 2026 23:53
Add language-level tagged enum unions and switch-expression matching.\n\nFeatures:\n- Parse enum union declarations with named, tuple, record, alias, and bare-type variants.\n- Synthesize tagged storage, payload structs, variant factories, and destruction support.\n- Support implicit construction for unambiguous bare-type variants and reject duplicate or ambiguous cases.\n- Expand static foreach and static if enum-union cases in their correct semantic scope.\n- Add tuple and record pattern bindings, named/literal field checks, rest patterns, and guarded arms.\n- Diagnose non-union patterns, unmatched variants, invalid positional patterns, duplicate defaults, and the 256-variant limit.\n- Add frontend matrix-based usefulness checks for redundant arms, redundant defaults, guarded coverage, and exhaustiveness witnesses.\n- Add specification, runnable, compilable, and fail-compilation regression coverage.
Replace the obsolete  record discard spelling with the supported  syntax in the NetworkEvent example.
- add parser and AST support for enum union declarations, named/unit/record/bare variants, templates, static foreach, variant UDAs, and struct-equivalent declaration attributes
- synthesize tagged storage, named factories, bare-variant constructors, and lifecycle support for payloads
- add implicit bare-variant conversion with ambiguity diagnostics and preserve enum-union non-callability
- implement enum-union switch expressions with positional, record, type, rest, guard, default, redundancy, and exhaustiveness handling
- diagnose malformed switch patterns, duplicate cases, unknown bare types, oversized unions, and invalid variant declarations
- prevent user constructors from reading this before whole-value initialization, with branch-aware constructor flow tracking
- eliminate duplicate complex and imaginary transition deprecations from generated constructor parameters
- document enum union grammar, construction, pattern matching, member declarations, and limitations
- add runnable, compilable, and fail-compilation coverage for syntax, conversion, constructors, UDAs, attributes, diagnostics, patterns, and lifecycle behavior
Add enum-union pattern matching and construction refinements:

- resolve bare and qualified type patterns, including identifier bindings

- support payload bindings for named variants and share unit payload storage

- diagnose self aliases, unknown bare types, no-argument constructors, and ambiguous bare conversions

- resolve property-valued switch conditions and improve switch-expression formatting

- allow arrow-arm switch expressions in statement position and diagnose discarded no-effect switches

- preserve parser recovery for malformed switch expressions

- add focused compile, fail, and runnable regression coverage
Add compile-time reflection support for enum unions:
- recognize is(T == enum union)
- add allVariants, getTag, and variantConstructorParams traits
- preserve identifiers and UDAs for reflected variants

Model record and named alias variants as real nested declarations:
- synthesize nested struct declarations for record variants
- synthesize nested aliases for named type variants
- preserve explicit alias variant construction and record conversion

Improve conversion behavior and coverage:
- rank implicit candidates by MATCH quality
- prefer exact bool and other payload matches over weaker numeric conversions
- retain diagnostics for genuinely ambiguous conversions
- cover unit-variant separation from unrelated empty structs

Add enum union attribute, deprecation, conversion, reflection, and alias tests, and document the nested declaration lowering.
Add __traits(variantKind, V) for elements returned by allVariants.

Classify variants by declaration form:
- unit for zero-parameter named variants
- tuple for positional named variants
- struct for inline record variants
- alias for named type aliases
- bare for bare payload types, including externally declared structs

Document the reflection API and add positive coverage for every kind plus invalid input.
- decisiontree: format witness pattern correctly for unnamed/bare variants (e.g. typeof(null))
- expressionsem: disallow direct struct literal construction of enum union types, requiring a variant instead
- expressionsem: support unqualified enum union alias variant lookup inside with-statement scopes
- parse: improve error recovery in parseDeclDefs and enum union variant parsing to avoid cascading errors
- tests: update runnable/testenumunion.d and add test cases for direct construction, alias variant with-scopes, null variant witnesses, and parse recovery
…, and test updates

- parse: implement balanced curly brace tracking in parseDeclDefs error recovery so top-level declarations don't trigger spurious 'unmatched closing brace'
- parse: properly consume tokens up to matching closing brace upon switch expression arm error
- expressionsem: avoid speculative gagged expressionSemantic on dot.e1 in visit(CallExp) which swallowed diagnostics and caused ICE in test19790 / duplicate symbols in test11
- dsymbolsem: do not synthesize duplicate nested declarations for duplicate enum union variants to avoid cascading conflict errors
- unit tests: add enumUnion to ignoreTokens in location_offset.d
- runnable tests: replace deprecated cdouble and idouble in testenumunion.d with float and real
- fail tests: update TEST_OUTPUT for enum_union_const_mutation, enum_union_legacy_template_payload, enum_union_switch_expression_statement, and enum_union_unit_pattern_post_call_binding
MetaLang and others added 23 commits September 25, 2026 02:14
…ery, and test outputs

- expressionsem: implement findEnumUnionFromExp to resolve template instances, scope exps, and qualified identifiers during alias variant construction
- parse: restrict Lerror curly recovery to inEnumUnion bodies so standard top-level and struct declarations retain upstream error recovery
- parse: only bypass token advance on missing semicolon when expression statement is a switch expression
- runnable: remove obsolete cdouble/idouble deprecation TEST_OUTPUT block from testenumunion.d
- fail tests: align expected error line numbers in enum_union_legacy_template_payload, enum_union_switch_expression_statement, and enum_union_unit_pattern_post_call_binding
Stop enum union member synthesis after duplicate case diagnostics to avoid duplicate-symbol cascades. Refresh stale diagnostics and deprecation locations in the feature tests, and add the reviewed correctness and switch-lowering implementation plans.
Lower source-level __tag reads to discriminant rvalues while retaining direct backing-storage access for generated compiler code. Exclude the backing tag from tupleof and preserve the public reflection surface.

Cover qualified reads, rejected mutation and address-taking, member reflection, tuple filtering, and generated interface round trips.
Centralize factory and implicit-conversion initialization around a staged payload construction. Keep incomplete union temporaries non-destructible and commit the discriminant only after the payload is fully constructed.

Exercise payload copy construction, enum union copies, and throwing construction for named and bare variants.
Treat positional and record payload identifiers as fresh bindings and reject literal, call, operator, and other value expressions with a guard-oriented diagnostic. Remove synthesized payload equality checks from switch lowering and usefulness rows.

Make generated bindings CTFE-capable, cache manifest switch conditions, and cover guard-based tests across scalar, aggregate, reference, and custom equality payloads.
Keep the usefulness and literal-pattern machinery available for future full pattern matching while current binding-only enum union patterns contribute wildcard payload columns.

Insert source default arms as wildcard matrix rows so every later arm is diagnosed as unreachable, and document the retained architecture in the hardening plan.
Parse nested positional and record tuple bindings as UnpackDeclaration nodes without coupling switch patterns to the tuples preview flag. Feed selected payload fields through ordinary unpack semantic lowering and preserve binding order for guards and actions.

Print recursive tuple shapes in generated interfaces and cover nested bindings, qualifiers, single condition evaluation, alias-this tuples, arity diagnostics, value-pattern rejection, and header round trips.
Inspect arguments to generated enum union factories instead of treating the entire call as effect-free. Retain the source switch condition across semantic caching and include guard, action, and binding initializer sources in discarded-value checks.

Ignore trivial generated declaration and construction scaffolding while preserving real calls and copy-constructor effects, with once-only runtime coverage.
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