Should D have control-flow expressions? IS it possible?

Meta jared771 at gmail.com
Thu Sep 10 06:56:25 UTC 2026


While working on the POC for my discriminated union proposal 
(https://github.com/dlang/dmd/pull/23744), which includes the 
addition of switch expressions, I realized that I would really 
like to allow break/continue/return - even goto - in expression 
position as the result of a match arm. Imagine you're reading 
data from a network stream in a loop and want to break out if you 
get an end of stream packet, or want just continue on and not do 
anything for a heartbeat packet?

```d
// Tagged union over wire packets: unboxed, @safe, and @nogc
enum union Packet
{
     case Data(const(ubyte)[] payload),
     case Ping(ulong timestamp),
     case Reset(ushort errorCode),
     case Heartbeat(),
     case EndOfStream(),
}

struct NetworkStream
{
     bool hasData();
     Packet readNext();
}

size_t handlePayload(const(ubyte)[] data);
size_t sendPong(ulong ts);
void finalizeSession(size_t total);

void processConnection(ref NetworkStream stream)
{
     size_t totalBytesProcessed = 0;
     scope(exit) finalizeSession(totalBytesProcess);

     // Systems loop processing incoming stream frames
     while (stream.hasData())
     {
         auto pkt = stream.readNext();

         // Switch expression with value-producing and diverging 
arms
         size_t bytesHandled = switch (pkt)
         {
             case Data(slice)    => handlePayload(slice),
             case Ping(ts)       => sendPong(ts),
             case Heartbeat()    => continue,           // Nothing 
to do for heartbeat packets
             case EndOfStream()  => break,              // 
EndOfStream stops reading and closes the connection
             case Reset(err)     => assert(0, "Reset"), // Already 
works in D today
         };

         totalBytesProcessed += bytesHandled;
     }
}
```

After some time with an LLM trying to figure out how it could be 
done, I've discovered that it IS possible when a switch 
expression is used in statement position like this - the compiler 
recognizes the specific `case ... => break,` pattern, and that 
arm gets transformed into a BreakExp of type noreturn. During 
semantic, if the compiler detects that the switch contains a 
break expression, it lowers the whole thing into a series of 
if-else blocks, with the EndOfStream one containing a break 
statement, and it all Just Works™ with no backend changes needed. 
I assume continue, return, and even goto would work pretty 
similarly.

Beyond switch expressions, there's no real reason why we can't 
have these control-flow statements embedded in *arbitrary* 
expressions:

```d
(true && break)
(cond()? return 42 : 10)
(1 + 1 == 2 || continue)
```

And so on.

But outside the hard-coded pattern outlined above that gets 
transformed by the compiler in a very specific case, it seems 
very difficult with the way the compiler is currently written. I 
don't completely understand all the technical details, but it 
seems that you can't just lower arbitrary expressions to 
statements.

This *does* work, but I assume it's a special case because 
exceptions unwind the stack. Hopefully I'm wrong:

```d
void main()
{
     for (auto i = 0; i < 10; i++) {
         (true && throw new Exception(""));
     }
}
```

Anyway, is this reasonably doable? I personally want it for 
switch expressions (or Rikki's proposed .match statement, which I 
think lowers to a tree of ternary expressions similarly to what 
my POC does).


More information about the Digitalmars-d mailing list