Skip to content

Implement sumtypes and matching - #23540

Draft
rikkimax wants to merge 1 commit into
dlang:masterfrom
rikkimax:sumtype-matching-llm
Draft

rikkimax wants to merge 1 commit into
dlang:masterfrom
rikkimax:sumtype-matching-llm

Conversation

@rikkimax

@rikkimax rikkimax commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Will update later.

@rikkimax rikkimax added the AI Generated Code that is generated by an LLM AI. label Aug 6, 2026
@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch 5 times, most recently from 00cf0ac to 767d042 Compare August 6, 2026 15:13
@rikkimax

rikkimax commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor Author

I just remembered more of the operator overloads need to be generated
Oh and is expressions is(T == __sumtype)
Could probably avoid some like toHash and just change runtime code for it.

@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch 6 times, most recently from 547dd3c to 2412122 Compare August 6, 2026 17:51
@rikkimax rikkimax added Review:Needs Changelog A changelog entry needs to be added to /changelog Review:Needs Spec PR A PR updating the language specification needs to be submitted to dlang.org labels Aug 6, 2026
@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch from 2412122 to 735bbc7 Compare August 6, 2026 18:53
@LightBender

LightBender commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

I understand that this is a massive pull request, but sumtypes/matching is a big new feature, and one we need to keep up with even stodgy old conservative languages like C#. If we can pile into this and get multiple people to review it should be manageable.

@WalterBright @tgehr

@rikkimax

rikkimax commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor Author

For reference, this design reduces to lowering to structs (literals + declarations), and ternary expressions (match).
It's fairly straightforward and is a pretty self-contained add-on in comparison to how it could've been done.

Overall I'm very impressed with the code quality MiMo V2.5 came up with, I couldn't have done it better (overall).

@Herringway

Copy link
Copy Markdown
Contributor

I don't see what the point is. The syntax isn't an improvement over the library solution, and I don't see any new functionality being offered. Is all this just to save an import?

@rikkimax

rikkimax commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Match expression support guard expressions and there are both named and unnamed variants.

@Herringway

Copy link
Copy Markdown
Contributor

Match expression support guard expressions and there are both named and unnamed variants.

The latter can be supported in the library solution. The former just looks awkward and inconsistent with the rest of the language. Why does this need a fancy new syntax that we can't use anywhere else?

@rikkimax

rikkimax commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

If you want to extend match expressions to other types that is fine. But I am not working on that right now, and the scope is sufficiently large.

Just because something can be done in library does not mean that it is the correct place to put it.
See fullyQualifiedName and bitfields as a good example of something that should never have existed.

Sumtypes are a primitive of data representation, same as tuples. Much more so than a map or a dynamic array.
The fact is, D is not keeping up with the mainstream understanding (which is lagging by a good 40 years of the literature).

@rikkimax

rikkimax commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Oh and another thing, match functions can't actually be safe if the argument is by-ref.
Its not possible to throw static analysis to sufficiently guarantee it either.

The match expressions, due to not using function calls can be analyzed with a borrow checker, and therefore the variables can be by-ref. Which is a pretty massive upgrade.

@Herringway

Copy link
Copy Markdown
Contributor

Just because something can be done in library does not mean that it is the correct place to put it. See fullyQualifiedName and bitfields as a good example of something that should never have existed.

It doesn't mean that it isn't the correct place to put it, either. And bitfields are far better than the C garbage that's supposedly replacing them. I used to think D's greatest strength was its metaprogramming, but it's doomed to be deficient, isn't it?

@rikkimax

rikkimax commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

And bitfields are far better than the C garbage that's supposedly replacing them.

Which can be fixed with a simple linting rule, which I implemented but wasn't accepted.

I used to think D's greatest strength was its metaprogramming, but it's doomed to be deficient, isn't it?

Yes, but not because of the design or implementation of metaprogramming stuff.
Templates themselves are designed and implemented fairly well; I haven't found anything I really want to change about it.
Some more traits to extract information would be helpful.

The core problem is overall compiler architecture wasn't designed to solve cyclicity in analysis, which prevents effects analysis, and the behaviour isn't correct. On the flip side, you get fast compile times, so it isn't all bad.

@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch from 735bbc7 to 01e98a9 Compare August 9, 2026 15:57
@github-actions

github-actions Bot commented Aug 9, 2026 •

Copy link
Copy Markdown

DMD perf check

Metric Base PR Δ
compile hello.d (instr) 214.9 M 224.8 M +4.601%
compile hello.d -O -release (instr) 233.2 M 243.1 M +4.238%
compile Phobos (instr) 5,121.8 M 5,160.2 M +0.750%
compile vibe.d (instr) 15,127.5 M 15,177.3 M +0.329%
dmd binary size (stripped) 6.86 MB 6.84 MB -0.27%
Breakdown — compile hello.d
Phase (wall, self time) Base PR Δ
sema_other 14.8 ms 13.8 ms -6.70%
parse 34.4 ms 33.8 ms -1.66%
sema1 10.6 ms 10.3 ms -2.83%
sema3 6.0 ms 6.0 ms -1.09%
codegen 1.9 ms 1.9 ms -1.03%
Breakdown — compile Phobos

+38.4 M instructions: frontend +39.3 M (+1.08%), codegen -0.9 M (-0.06%)

Phase (wall, self time) Base PR Δ
codegen 408 ms 400 ms -1.76%
parse 109 ms 114 ms +4.74%
sema3 616 ms 619 ms +0.39%
sema1 232 ms 234 ms +0.89%
sema_other 206 ms 207 ms +0.45%
ctfe 15.6 ms 15.0 ms -3.83%
inline 4.1 ms 4.2 ms +2.48%
sema2 1.4 ms 1.5 ms +3.07%
All measurements
Metric Base PR Δ
compile hello.d (instr) 214.9 M 224.8 M +4.601%
compile hello.d -O -release (instr) 233.2 M 243.1 M +4.238%
compile Phobos (instr) 5,121.8 M 5,160.2 M +0.750%
compile Phobos codegen (instr) 1,472.1 M 1,471.2 M -0.061%
compile vibe.d (instr) 15,127.5 M 15,177.3 M +0.329%
dmd binary size (stripped) 6.86 MB 6.84 MB -0.27%
hello binary size (stripped) 0.72 MB 0.72 MB 0.00%
peak RSS (compile hello.d) 43.43 MB 43.24 MB -0.43%
peak RSS (compile Phobos) 617.6 MB 617.2 MB -0.06%
peak RSS (compile vibe.d) 1917 MB 1918 MB +0.03%
compile dmd itself (wall) 12.5 s 12.5 s +0.10%
compile hello.d (wall) 67.7 ms 65.7 ms -2.88%
compile Phobos (wall) 1,592 ms 1,595 ms +0.18%

a8f423c vs merge-base d4c5f7a · about these metrics

@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch 3 times, most recently from 3753bec to 2edcc4e Compare August 10, 2026 17:30
@rikkimax

Copy link
Copy Markdown
Contributor Author

I don't see what the point is. The syntax isn't an improvement over the library solution, and I don't see any new functionality being offered. Is all this just to save an import?

I agree with that with a caveat.
Adding complexity to the compiler just to avoid an import is not justified, especially with this syntax.

Did you read my replies? I covered that it allows for things that a library solution is not able to do.

The proposed .match syntax is inconsistent with the rest of D.
In my opinion, if sumtypes are going into the language (and it definitely should), it should use the opportunity to properly modernize switch into first class pattern matching construct rather than introducing a separate/different/inconsistent syntax.
Rust and Zig did it right for example, there is just a unique construct for it, easy to learn and it's consistent.

let message = match maybe_digit {
    Some(x) if x < 10 => process_digit(x),
    Some(x) => process_other(x),
    None => panic!(),
};
x = switch(x) {
    0 ... 10 => process_digit(x)
    else => process_other(x);
};
auto message = maybe_digit.match {
   (int x) if (x < 10) => process_digit(x),
   (int x) => process_other(x),
   (None) => assert(0);
}

Zig isn't exactly equal here from what I can put together, but they aren't that far off what I put together.

The point is Rust has only match to handle primitives, enums (wich are sumtypes too) uniformly. Zig has only switch, they didn't invent a second mechanism for tagged unions, they extended switch to capture payloads.

D will ends up with two constructs: switch for basic types, and .match for sumtypes. one as statement, the other as expression.

If sumtypes are going into the compiler, switch should be modernized to support them (and to also be used as expression perhaps), adding a second, separate construct fragments the language, even if it looks better, it's not a good idea IMO.

It's the same with most other language, whether it is Swift or C#, they kept it coherent.

Don't get me wrong tho, I support the idea.

C# and Swift don't support sumtypes so they are not relevant.

I don't see a way to make the existing switch statements be turned into expressions. They operate on multiple fundamentally different principles. While they appear similar, they are not similar enough to make one do the other's job.

Statement vs expression.

Constant vs type with optional runtime guard.

@benjones

Copy link
Copy Markdown
Contributor

C# and Swift don't support sumtypes so they are not relevant.

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/enumerations

Swift enums are sumtypes and use switch to select among options

@MetaLang

Copy link
Copy Markdown
Member
  1. SumType Synthesis (Chaining): If arms evaluate to distinct types $T_1, T_2, \dots, T_n$, the result synthesizes a new __sumtype:

Why? No other language with sum types and pattern matching does this. They all require that the arms unify to a single type. This will lead to bugs like:

// User intended both arms to return an integer ID:
auto id = val.match!(
    case User u => u.id,        // int
    case Admin a => a.uuid      // string
); 
// Instead of a compile error, 'id' silently becomes `__sumtype(int | string)`.
// The error is only discovered 50 lines later when trying to do `id + 1`.

It doesn't seem worth the fluent method chaining.

And what happens in this case?

auto traverse(Node n) {
    return n.match!(
        case Leaf l   => l.value,              // int
        case Branch b => traverse(b.left)      // recurse
    );
}

Naively, it seems to me that this match will synthesize an infinitely recursive sum type.

Some bikeshedding:

Why invent new keywords like __sumtype and .match (the latter of which will be the first time that D has introduced a postfix keyword)? Can't we reuse union and/or enum, and introduce switch expressions, as @xoxorwr suggested?

enum union Option(T)
{
    case Some(T),
    case None, 
}

Option!int maybeInt;
auto n = switch (maybeInt) {
    case Some(int n) => n, // int
    case None => throw new Exception(...), // noreturn
}

// noreturn <: int therefore the expression unifies to int 

@rikkimax

rikkimax commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor Author
  1. SumType Synthesis (Chaining): If arms evaluate to distinct types

Why? No other language with sum types and pattern matching does this. They all require that the arms unify to a single type. This will lead to bugs like:

// User intended both arms to return an integer ID:
auto id = val.match!(
    case User u => u.id,        // int
    case Admin a => a.uuid      // string
); 
// Instead of a compile error, 'id' silently becomes `__sumtype(int | string)`.
// The error is only discovered 50 lines later when trying to do `id + 1`.

It doesn't seem worth the fluent method chaining.

Have you seen this wonderful library feature that D has, called input ranges?

And what happens in this case?

auto traverse(Node n) {
    return n.match!(
        case Leaf l   => l.value,              // int
        case Branch b => traverse(b.left)      // recurse
    );
}

Naively, it seems to me that this match will synthesize an infinitely recursive sum type.

EDIT: I put it into the wrong file, it does not compile.

P:\dmd\compiler\test\runnable\sumtypematching.d(1028): Error: forward reference to inferred return type of function call `traverse(__SumType27(cast(ubyte)0u, __matchArm29.left, ))`
        (Branch b) => traverse(Node(b.left))
                              ^
_error_
P:\dmd\compiler\test\runnable\sumtypematching.d(1033): Error: static assert:  `is(typeof(traverse(Node.init)) == int)` is false
static assert(is(typeof(traverse(Node.init)) == int));

Some bikeshedding:

Why invent new keywords like __sumtype and .match (the latter of which will be the first time that D has introduced a postfix keyword)? Can't we reuse union and/or enum, and introduce switch expressions, as @xoxorwr suggested?

enum union Option(T)
{
    case Some(T),
    case None, 
}

Option!int maybeInt;
auto n = switch (maybeInt) {
    case Some(int n) => n, // int
    case None => throw new Exception(...), // noreturn
}

// noreturn <: int therefore the expression unifies to int 

__sumtype is a transitionary identifier, in an edition it can be renamed to sumtype.
We can't do it up front without breaking code, including the standard library.

match is not a keyword, its a contextual keyword that must be followed by a {.

As for why not what you came up with syntax wise, that is a much newer syntax.
I'm basing it off of all the research papers I have read over the years and what they require, they have stood the test of time and it has been shown to be a very flexible primitive.

@MetaLang

Copy link
Copy Markdown
Member
  1. SumType Synthesis (Chaining): If arms evaluate to distinct types

Why? No other language with sum types and pattern matching does this. They all require that the arms unify to a single type. This will lead to bugs like:

// User intended both arms to return an integer ID:
auto id = val.match!(
    case User u => u.id,        // int
    case Admin a => a.uuid      // string
); 
// Instead of a compile error, 'id' silently becomes `__sumtype(int | string)`.
// The error is only discovered 50 lines later when trying to do `id + 1`.

It doesn't seem worth the fluent method chaining.

Have you seen this wonderful library feature that D has, called input ranges?

What do those have to do with sum types? Do you have a concrete example?

Some bikeshedding:

Why invent new keywords like __sumtype and .match (the latter of which will be the first time that D has introduced a postfix keyword)? Can't we reuse union and/or enum, and introduce switch expressions, as @xoxorwr suggested?

enum union Option(T)
{
    case Some(T),
    case None, 
}

Option!int maybeInt;
auto n = switch (maybeInt) {
    case Some(int n) => n, // int
    case None => throw new Exception(...), // noreturn
}

// noreturn <: int therefore the expression unifies to int 

match is not a keyword, its a contextual keyword that must be followed by a {.

Contextual or non contextual or whatever, D has never before had a control flow construct that is spelled <expr>.<keyword>. The Rust community has already had a huge fight over .await, and it's so easy to avoid by instead introducing a switch expression construct that supports pattern matching.

As for why not what you came up with syntax wise, that is a much newer syntax. I'm basing it off of all the research papers I have read over the years and what they require, they have stood the test of time and it has been shown to be a very flexible primitive.

I'm not talking about the primitive; I agree it's useful. I'm talking about the __sumtype(t1 | t2)/sumtype(t1 | t2) syntax. Why introduce a new keyword and specialized syntax when we can already easily add something like enum union that closely matches what exists in most other modern languages today?

@rikkimax

Copy link
Copy Markdown
Contributor Author
  1. SumType Synthesis (Chaining): If arms evaluate to distinct types

Why? No other language with sum types and pattern matching does this. They all require that the arms unify to a single type. This will lead to bugs like:

// User intended both arms to return an integer ID:
auto id = val.match!(
    case User u => u.id,        // int
    case Admin a => a.uuid      // string
); 
// Instead of a compile error, 'id' silently becomes `__sumtype(int | string)`.
// The error is only discovered 50 lines later when trying to do `id + 1`.

It doesn't seem worth the fluent method chaining.

Have you seen this wonderful library feature that D has, called input ranges?

What do those have to do with sum types? Do you have a concrete example?

It allows you to chain both input ranges and match expressions together.

Both are data processing transfer functions.

Some bikeshedding:

Why invent new keywords like __sumtype and .match (the latter of which will be the first time that D has introduced a postfix keyword)? Can't we reuse union and/or enum, and introduce switch expressions, as @xoxorwr suggested?

enum union Option(T)
{
    case Some(T),
    case None, 
}

Option!int maybeInt;
auto n = switch (maybeInt) {
    case Some(int n) => n, // int
    case None => throw new Exception(...), // noreturn
}

// noreturn <: int therefore the expression unifies to int 

match is not a keyword, its a contextual keyword that must be followed by a {.

Contextual or non contextual or whatever, D has never before had a control flow construct that is spelled <expr>.<keyword>. The Rust community has already had a huge fight over .await, and it's so easy to avoid by instead introducing a switch expression construct that supports pattern matching.

Right, it's not valid syntax which is why I prefer it over .match(), which would break code.

Not supporting it as an expression, and trying to force it into the role of a statement does mean that we lose to ability to chain them, like it is possible to do in library code.

However I do want to emphasize, its not a switch statement in its current form. They do not overlap in their features even though it appears that they may.

As for why not what you came up with syntax wise, that is a much newer syntax. I'm basing it off of all the research papers I have read over the years and what they require, they have stood the test of time and it has been shown to be a very flexible primitive.

I'm not talking about the primitive; I agree it's useful. I'm talking about the __sumtype(t1 | t2)/sumtype(t1 | t2) syntax. Why introduce a new keyword and specialized syntax when we can already easily add something like enum union that closely matches what exists in most other modern languages today?

Oh that particular bit of syntax is likely to be removed in favor of the other one.

I introduced it originally for mixing with tuples, but because it's going to explode in symbols if people use it, I want to remove it.

Waiting on borrow checker PR to be merged before I finish off this one.

@limepoutine

limepoutine commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Over chaining, it is possible to have polymorphic expressions (aka. lazy unification). No invisible type instantiations as per D's conventional style. Also makes optimization easier.

val.match {
    (User u) => u.id,        // int
    (Admin a) => a.uuid      // string
}                            // int or string, but not automatically __sumtype

// Error: can't unify two arms
auto id = val.match {
    (User u) => u.id,
    (Admin a) => a.uuid
};

// Generate both arms automatically
class Role
{
    this(int) { ...; }
    this(string) { ...; }
}
auto role = new Role(val.match {
    (User u) => u.id,
    (Admin a) => a.uuid
});

// Fine, IdType has both constructors
alias IdType = __sumtype(int | string);
IdType id = val.match {
    (User u) => u.id,
    (Admin a) => a.uuid
};

// Sure, with 200% template bloat
string id = to!string(val.match {
    (User u) => u.id,
    (Admin a) => a.uuid
});

This might be more manageable than unconditional chaining. If there is a chance to make CondExp polymorphic in a future edition, we can also have a sound (unintuitive maybe?) fix for #22078.

@rikkimax

rikkimax commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm not tieing matching into variable declarations, I want to get away from the entire idea of it acting as a statement.

A few features that may not be known:

  1. alias E6 = __sumtype(int | long); // error: sumtype cannot have more than one integer variant — found `int` and `long`
  2.  alias S1 = __sumtype(int | bool);
     S1 s = S1(21);
     auto r = s.match {
         (int x) => x * 2,
         (bool y) => y ? 1 : 0
     };
     assert(r == 42);
  3. A sumtype of 1 variant, lowers to an alias of that variant's type.

Hmm it does look that evaluating out to a sumtype isn't supported atm, I know I did do it so it must've got lost at some point. Thats another thing that needs fixing.

@MetaLang

Copy link
Copy Markdown
Member

Have you seen this wonderful library feature that D has, called input ranges?

What do those have to do with sum types? Do you have a concrete example?

It allows you to chain both input ranges and match expressions together.

Both are data processing transfer functions.

myRangeOfS1s.map!(s => s.match { ... }).filter!(...)...

Unless you've got a more specific example/use case in mind.

@rikkimax

Copy link
Copy Markdown
Contributor Author

Have you seen this wonderful library feature that D has, called input ranges?

What do those have to do with sum types? Do you have a concrete example?

It allows you to chain both input ranges and match expressions together.
Both are data processing transfer functions.

myRangeOfS1s.map!(s => s.match { ... }).filter!(...)...

Unless you've got a more specific example/use case in mind.

That's certainly getting closer to what I have in mind.

But think of match expressions as being a form of narrowing.

int result = source.match {
    (string) => X,
    (int) => Y,
    (bool) => Y
}.match {
    (int) => Z,
    (string) => U
};

Instead of ending there, the result type may instead be something compatible with input ranges like arrays.

source.match {
    (string[]) => X,
    (int[]) => Y,
    (bool[]) => Y
}.match {
    (int[]) => Z,
    (string[]) => U
}.filter!().map!().each!(
    (v) => v.match {
    }
);

@MetaLang

MetaLang commented Aug 27, 2026 •

Copy link
Copy Markdown
Member

Have you seen this wonderful library feature that D has, called input ranges?

What do those have to do with sum types? Do you have a concrete example?

It allows you to chain both input ranges and match expressions together.
Both are data processing transfer functions.

myRangeOfS1s.map!(s => s.match { ... }).filter!(...)...

Unless you've got a more specific example/use case in mind.

That's certainly getting closer to what I have in mind.

But think of match expressions as being a form of narrowing.

int result = source.match {
    (string) => X,
    (int) => Y,
    (bool) => Y
}.match {
    (int) => Z,
    (string) => U
};

Instead of ending there, the result type may instead be something compatible with input ranges like arrays.

source.match {
    (string[]) => X,
    (int[]) => Y,
    (bool[]) => Y
}.match {
    (int[]) => Z,
    (string[]) => U
}.filter!().map!().each!(
    (v) => v.match {
    }
);

I get it, but my question is why? No other language with pattern matching that I'm aware of does this, and I fail to see the value - especially when it'll likely be confusing for most programmers and has the potential to cause issues, and if you want pipelining you can very easily put your match inside a map() or a .filter(), as I showed. It seems to pointlessly introduce complexity for no real tangible benefit.

@limepoutine

Copy link
Copy Markdown
Contributor

Speaking in terms of range transformation, lazy unification is more capable because it also widens:

class Authenticator
{
    this(int);
    this(string);
    this(Certificate);
}

auto auth = new Authenticator(val.match {
    (User u) => u.cred.match {
        (int pinCode) => pinCode,
        (string password) => password
    },
    (Admin a) => a.certificate
});

It also interoperates with existing library solutions easily, just do Variant(val.match { ... }).

@MetaLang

MetaLang commented Aug 27, 2026 •

Copy link
Copy Markdown
Member

Speaking in terms of range transformation, lazy unification is more capable because it also widens:

class Authenticator
{
    this(int);
    this(string);
    this(Certificate);
}

auto auth = new Authenticator(val.match {
    (User u) => u.cred.match {
        (int pinCode) => pinCode,
        (string password) => password
    },
    (Admin a) => a.certificate
});

It also interoperates with existing library solutions easily, just do Variant(val.match { ... }).

I don't think your example will work. The result of the match expression will be sumtype(int | string | Certificate), which cannot implicitly convert to its variants (in fact, it's the opposite - the variants are subtypes of the sumtype), and thus none of Authenticator's constructors are a match. Your code should cause a compile error.

@limepoutine

Copy link
Copy Markdown
Contributor

Speaking in terms of range transformation, lazy unification is more capable because it also widens:

class Authenticator
{
    this(int);
    this(string);
    this(Certificate);
}

auto auth = new Authenticator(val.match {
    (User u) => u.cred.match {
        (int pinCode) => pinCode,
        (string password) => password
    },
    (Admin a) => a.certificate
});

It also interoperates with existing library solutions easily, just do Variant(val.match { ... }).

I don't think your example will work. The result of the match expression will be sumtype(int | string | Certificate), which cannot implicitly convert to its variants (in fact, it's the opposite - the variants are subtypes of the sumtype), and thus none of Authenticator's constructors are a match. Your code should cause a compile error.

I was proposing a structural typing alternative like Crystal, where sumtypes are automatically flattened. Rikki's proposal would give unflattened sumtype(sumtype(int | string) | Certificate).

@MetaLang

Copy link
Copy Markdown
Member

Speaking in terms of range transformation, lazy unification is more capable because it also widens:

class Authenticator
{
    this(int);
    this(string);
    this(Certificate);
}

auto auth = new Authenticator(val.match {
    (User u) => u.cred.match {
        (int pinCode) => pinCode,
        (string password) => password
    },
    (Admin a) => a.certificate
});

It also interoperates with existing library solutions easily, just do Variant(val.match { ... }).

I don't think your example will work. The result of the match expression will be sumtype(int | string | Certificate), which cannot implicitly convert to its variants (in fact, it's the opposite - the variants are subtypes of the sumtype), and thus none of Authenticator's constructors are a match. Your code should cause a compile error.

I was proposing a structural typing alternative like Crystal, where sumtypes are automatically flattened. Rikki's proposal would give unflattened sumtype(sumtype(int | string) | Certificate).

Regardless, I'm pretty sure my point still stands.

@MetaLang

MetaLang commented Aug 29, 2026 •

Copy link
Copy Markdown
Member

I took a stab at my own (LLM-driven, very rough) implementation: #23744

@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch 10 times, most recently from 2d40c84 to b707d4e Compare September 3, 2026 11:22
@rikkimax
rikkimax force-pushed the sumtype-matching-llm branch from b707d4e to a8f423c Compare September 3, 2026 11:39
assert(rt.tag == 1);
rt.match
{
(int x) => 0, (ref y) => (y = false, 0) // catch-all ref on bool variant: y is ref bool

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.

Why does (y = false, 0) compile? I thought comma expression was an error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

It may parse but it should cause a semantic error:

comma.d(6): Error: using the result of a comma expression is not allowed
auto x = (y = false, 0);

So why doesn't this line?

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.

Because it isn't in a match expression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't know. It wasn't something I chose to support.
I expect it is some case that isn't disallowed.

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.

@Herringway I figured it out - using the result of a comma expression is an error, but the match expression result is not used. So actually the compiler should give "Error: 0 has no effect".

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

Labels

AI Generated Code that is generated by an LLM AI. Review:Needs Changelog A changelog entry needs to be added to /changelog Review:Needs Spec PR A PR updating the language specification needs to be submitted to dlang.org

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants