digitalmars.D - Should D have control-flow expressions? IS it possible?
- Meta (91/91) Sep 09 While working on the POC for my discriminated union proposal
- monkyyy (7/10) Sep 10 https://forum.dlang.org/post/xsjudliyxcfnhoxqymgg@forum.dlang.org
- Basile B. (8/13) Sep 10 The problem is about algorithmic complexity. D does the flow
- Meta (3/16) Sep 10 Hmmm, do you remember anything in particular? From what I've
- Basile B. (5/23) Sep 10 for some reason I don't have access granteed to codeberg (running
- ABrightLight (10/20) Sep 11 I think that this works because `throw new Exception("")` is
- DanielG (41/41) Sep 11 (These are comments on your feature as a whole - let me know if
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).
Sep 09
On Thursday, 10 September 2026 at 06:56:25 UTC, Meta wrote:it lowers the whole thing into a series of if-else blocks... I assume continue, return, and even goto would work pretty similarly.https://forum.dlang.org/post/xsjudliyxcfnhoxqymgg forum.dlang.org Nah, goto, continue, break, is a some kind of stack; if else chains could not explain this behavior. adr was looking into it recently maybe ask him but maybe youd have to extend whatever the hell the goto stack is to allow access with expressions
Sep 10
On Thursday, 10 September 2026 at 06:56:25 UTC, Meta wrote:[...] 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).The problem is about algorithmic complexity. D does the flow analysis at the statement level. That way you dont have to explore the whole sub-expressions to create the "basic blocks". You can allow expressions to change the flow, but this is a serious shift. For example Zig does that. In the past they had a lot of bugs related to the way they mix expressions and statements.
Sep 10
On Friday, 11 September 2026 at 03:35:47 UTC, Basile B. wrote:On Thursday, 10 September 2026 at 06:56:25 UTC, Meta wrote:Hmmm, do you remember anything in particular? From what I've read, one of the big issues is around cleanup.[...] 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).The problem is about algorithmic complexity. D does the flow analysis at the statement level. That way you dont have to explore the whole sub-expressions to create the "basic blocks". You can allow expressions to change the flow, but this is a serious shift. For example Zig does that. In the past they had a lot of bugs related to the way they mix expressions and statements.
Sep 10
On Friday, 11 September 2026 at 04:07:20 UTC, Meta wrote:On Friday, 11 September 2026 at 03:35:47 UTC, Basile B. wrote:for some reason I don't have access granteed to codeberg (running joke), but basically in the past, every bug related to "terminator" (when they used LLVM) [are about the flow analysis](https://github.com/ziglang/zig/issues?q=is%3Aissue%20state%3Aclosed%2 terminator%20LLVM). You see I have no problem with that prog-lang but due to my own interest I've spent a lot of time reading bug reports. As a user of LLVM I can tell you frankly that that used to be one of their main problem.On Thursday, 10 September 2026 at 06:56:25 UTC, Meta wrote:Hmmm, do you remember anything in particular? From what I've read, one of the big issues is around cleanup.[...] 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).The problem is about algorithmic complexity. D does the flow analysis at the statement level. That way you dont have to explore the whole sub-expressions to create the "basic blocks". You can allow expressions to change the flow, but this is a serious shift. For example Zig does that. In the past they had a lot of bugs related to the way they mix expressions and statements.
Sep 10
On Thursday, 10 September 2026 at 06:56:25 UTC, Meta wrote:
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(""));
}
}
```
I think that this works because `throw new Exception("")` is
inferred `noreturn`, which is implicitly convertible to all
types. <https://dlang.org/spec/type.html#noreturn>
As for some of the rest there, D does have labeled break and
continue, but they cannot cross function scopes.
I agree that there is room for more escape continuations (besides
just exceptions and some limited labeling for gotos) than what is
currently in the language, but not sure which could safely be
added.
Sep 11
(These are comments on your feature as a whole - let me know if there's a better thread for it. I don't see one in the DIP Ideas / DIP Devel forums) 1. THIS is what sumtypes should look like!! I really hope you will create a competing DIP for this - I do NOT want type-unions-masquerading-as-sumtypes to be what we end up with. 2. IMO, drop the bare type cases. [It's cleaner.](https://i.imgur.com/nZ5q20o.jpeg) They don't really add anything, and bare type matching in `switch` should be the responsibility of whoever adds type unions to the language (versus vanilla sumtypes). Sumtypes and type unions should be orthogonal features, not mixed together. original statement-based switch, and the newer expression-based switch (`value switch { ... }` instead of `switch(value) { ... }`). Does your feature already work in a normal switch? If so, I don't think the extra complexity (of control-flow expressions) is justified. What I'm saying is, if somebody needs a break/continue, they can just use the normal statement-style switch. This would feel perfectly natural in D: ```d // resort to statement-style switch when you need break/continue size_t bytesHandled; switch (pkt) { case Data(slice): bytesHandled = handlePayload(slice); break; case Ping(ts): bytesHandled = sendPong(ts); break; case Heartbeat(): continue; case EndOfStream(): break; case Reset(err): assert(0, "Reset"); }; ```
Sep 11
(Whoops. My `EndOfStream()` break above should have been a labeled break or goto. But you get the idea)
Sep 11
On Saturday, 12 September 2026 at 05:27:01 UTC, DanielG wrote:1. THIS is what sumtypes should look like!! I really hope you will create a competing DIP for thisIt's your lucky day: https://forum.dlang.org/post/ppnebwqwqxgjdyaljmeq forum.dlang.org
Sep 13
On Monday, 14 September 2026 at 06:36:20 UTC, Meta wrote:It's your lucky day: https://forum.dlang.org/post/ppnebwqwqxgjdyaljmeq forum.dlang.org😮 Indeed it is! I will comment in that thread later today after I wake up.
Sep 14









monkyyy <crazymonkyyy gmail.com> 