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