================
@@ -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
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to