[clang] [CIR] Add structured control flow for coroutine suspend points (PR #213191)

via cfe-commits cfe-commits at lists.llvm.org
Sun Aug 2 18:36:06 PDT 2026


================
@@ -4818,13 +4702,107 @@ def CIR_CoReturnOp : CIR_Op<"co_return", [
   let hasLLVMLowering = false;
 }
 
+//===----------------------------------------------------------------------===//
+// CoroSuspendPoint
+//===----------------------------------------------------------------------===//
+
+def CIR_CoroSuspendPoint : CIR_Op<"coro.suspend_point", [
+  Pure, Terminator, HasParent<"AwaitOp">, HasAncestor<"CoroRetPointOp">
+]> {
+  let summary = "Marks the point where a coroutine actually suspends";
+  let description = [{
+    Terminates the `suspend` region of a `cir.await` op, marking the exact
+    point where control returns to the caller/resumer if the coroutine
+    decides to suspend.
+
+    This op must appear inside the `suspend` region of a `cir.await`, and
+    that `cir.await` must in turn be nested within a `cir.coro.ret_point`.
+    During lowering to FlattenCFG, `cir.coro.suspend_point` becomes the
+    branch target that routes control to one of three destinations
+    depending on how the coroutine resumes: the resume block (normal
+    continuation), the cleanup/destroy block, or the ret/exit block that
+    hands control back to the caller.
+
+    Example:
+    ```mlir
+    cir.await(user, ready : {
+      ...
+      cir.condition(%ready)
+    }, suspend : {
+      ...
+      cir.coro.suspend_point
+    }, resume : {
+      ...
+      cir.yield
+    },)
+    ```
+  }];
+
+  let assemblyFormat = [{
+    attr-dict
+  }];
+
+  let hasLLVMLowering = false;
+}
+
+//===----------------------------------------------------------------------===//
+// CoroRetPoint
+//===----------------------------------------------------------------------===//
+
+def CIR_CoroRetPointOp : CIR_Op<"coro.ret_point", [
----------------
Andres-Salamanca wrote:

Hi @andykaylor, the problem with making this a single op the way you're proposing is mainly `cir.cleanup`.
Every `cir.await` has 3 has three possible destinations, depending on what the coroutine's caller does with the handle. If they call `handle.resume()`, execution just continues. But if they call `handle.destroy()`, we need to run the cleanups, destroy the coroutine handle, and finally call `coro.end` and return.
If you look at how this is structured today: `cir.coro.ret_point` (name's up for discussion) wraps `cir.cleanup`, which wraps the rest of the coroutine initial suspend, user body, final suspend. So if the coroutine suspends normally, every suspend point already knows it needs to branch to `cir.coro.ret_point`'s ret region. But if it's a `destroy()`, it knows it first needs to go through the cleanup.

(**I put these diagrams together because I think a picture is worth a thousand words here. This is pretty hard to explain in text, so hopefully they make the control flow and the motivation behind the design a bit clearer.**)

<img width="664" height="693" alt="imagen" src="https://github.com/user-attachments/assets/d3844e5e-882e-45b5-9755-310b3ba345f3" />

**Note: Here I only drew the three possible destinations of the initial suspend. Every `cir.await` has the same destinations, but I didn't draw them because the diagram would become too cluttered.**

With the single-op idea you're proposing, what happens to the cleanup? If you look at the diagram, `cir.cleanup` would need to wrap the `cir.coroutine` op you're proposing,  which means a `destroy()` call would have to jump out to the cleanup, and then jump *back* into `epilog` to actually return the coroutine. I don't think that works well.
<img width="644" height="744" alt="imagen" src="https://github.com/user-attachments/assets/e30d078d-a8fc-476c-a2f9-209b5eb4c197" />

Here's another issue: making this a single op is also hard because of how much nesting there is. I think that's part of why you're proposing collapsing it into one op  but there's another piece we're missing: the GRO.

If you look at that diagram, it's very dependent on *who the predecessor is*: if it's the final suspend, we need to run the cleanup; if it's not the final suspend, we should jump to return instead. And if you notice, each `cir.await` no longer branches directly to `coro.end`/`return` on suspend instead, they jump to the GRO when suspended, and the GRO is what decides whether to go to return. So that idea wouldn't work there either.

<img width="702" height="794" alt="imagen" src="https://github.com/user-attachments/assets/396add26-56be-49d7-a04f-2be837ebddb1" />

Finally, if we want to keep this as a single op, here's what I'd propose: we do what you suggested a `cir.coroutine` op  but with more regions: `prolog` (where the initial suspend lives), `body` (where `co_return` lives, like the current `cir.coro.body`), `epilog` (the final suspend), a `destroy` region (where `coro.free` lives), and finally an `exit` region (`coro.end` + `return`). With this, we could drop `cir.cleanup` it wouldn't be necessary anymore. You can see this in the diagram.
<img width="649" height="726" alt="imagen" src="https://github.com/user-attachments/assets/f41d1462-5002-492d-b5f3-c4e8c12f9f06" />

This is better for two reasons: first, `destroy` becomes explicit instead of implicit before, it depended on `cir.cleanup`, so this way it's clearer that going through `destroy` is mandatory to actually destroy the coroutine. Second, the `exit` region is also explicit as well. And on top of that, we significantly reduce the nesting we had before.

You can also see an example with the GRO here  it would be an optional region in this diagram. Same idea: if we come from `epilog` (final suspend), we branch to the `destroy` region but if we come from any other region, we branch to `exit` instead.
<img width="698" height="842" alt="imagen" src="https://github.com/user-attachments/assets/1392f908-b81e-42f5-94db-9ef80a9da3c2" />

@erichkeane @bcardosolopes

https://github.com/llvm/llvm-project/pull/213191


More information about the cfe-commits mailing list