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

Erich Keane via cfe-commits cfe-commits at lists.llvm.org
Fri Aug 21 06:13:25 PDT 2026


================
@@ -479,54 +420,142 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) {
                      .failed()) {
         return mlir::failure();
       }
-    }
 
-    mlir::Block &coroBodyBlock = coroBodyOp.getBody().back();
-    if (!coroBodyBlock.mightHaveTerminator()) {
-      mlir::OpBuilder::InsertionGuard guard(builder);
-      builder.setInsertionPointToEnd(&coroBodyBlock);
+      return mlir::success();
+    };
+
+    // Builds `final_suspend`: only emitted at all if the body can actually
+    // reach it (an explicit co_return, or falling off the end).
+    auto finalSuspendBuilder = [&]() -> mlir::LogicalResult {
+      // Note that LLVM checks CanFallthrough by looking into the availability
+      // of the insert block which is kinda brittle and unintuitive, seems to be
+      // related with how landing pads are handled.
+      //
+      // CIRGen handles this by checking pre-existing co_returns in the current
+      // scope instead.
+      //
+      // From LLVM IR Gen: const bool CanFallthrough = Builder.GetInsertBlock();
+      const bool canFallthrough = curLexScope->hasCoreturn();
+      const bool hasCoreturns = curCoro.data->coreturnCount > 0;
+      if (canFallthrough || hasCoreturns) {
+        curCoro.data->currentAwaitKind = cir::AwaitKind::Final;
+        if (emitStmt(s.getFinalSuspendStmt(), /*useCurrentScope=*/true)
+                .failed())
+          return mlir::failure();
+      }
       cir::YieldOp::create(builder, openCurlyLoc);
-    }
+      return mlir::success();
+    };
+
+    // Emit "if (coro.free(CoroId, CoroBegin)) Deallocate;"
+    auto destroyBuilder = [&]() -> mlir::LogicalResult {
+      Stmt *deallocate = s.getDeallocate();
+      if (emitStmt(deallocate, /*useCurrentScope=*/true).failed()) {
+        cgm.error(deallocate->getBeginLoc(),
+                  "failed to emit coroutine deallocation expression");
+        return mlir::failure();
+      }
+
+      cir::CoroFreeOp coroFree = curCoro.data->lastCoroFree;
 
-    // Note that LLVM checks CanFallthrough by looking into the availability
-    // of the insert block which is kinda brittle and unintuitive, seems to be
-    // related with how landing pads are handled.
-    //
-    // CIRGen handles this by checking pre-existing co_returns in the current
-    // scope instead.
-    //
-    // From LLVM IR Gen: const bool CanFallthrough = Builder.GetInsertBlock();
-    const bool canFallthrough = curLexScope->hasCoreturn();
-    const bool hasCoreturns = curCoro.data->coreturnCount > 0;
-    if (canFallthrough || hasCoreturns) {
-      curCoro.data->currentAwaitKind = cir::AwaitKind::Final;
+      if (!coroFree) {
+        cgm.error(deallocate->getBeginLoc(),
+                  "Deallocation expression does not refer to coro.free");
+        return mlir::failure();
+      }
       {
         mlir::OpBuilder::InsertionGuard guard(builder);
-        if (emitStmt(s.getFinalSuspendStmt(), /*useCurrentScope=*/true)
-                .failed())
+        builder.setInsertionPointAfter(coroFree);
+        mlir::Value isPtrNotNull =
+            builder.createPtrIsNotNull(coroFree.getResult());
+
+        llvm::SmallVector<mlir::Operation *> opsToMove;
+        mlir::Block *block = builder.getInsertionBlock();
+        mlir::Block::iterator it(isPtrNotNull.getDefiningOp());
+
+        for (++it; it != block->end(); ++it)
+          opsToMove.push_back(&*it);
+
+        auto ifOp = cir::IfOp::create(
+            builder, getLoc(deallocate->getSourceRange()), isPtrNotNull,
+            /*withElseRegion*/ false,
+            [&](mlir::OpBuilder &builder, mlir::Location loc) {
+              cir::YieldOp::create(builder, loc);
+            });
+
+        mlir::Operation *yieldOp = ifOp.getThenRegion().back().getTerminator();
+        for (auto *op : opsToMove)
+          op->moveBefore(yieldOp);
+      }
+
+      cir::YieldOp::create(builder, openCurlyLoc);
+      return mlir::success();
+    };
+
+    // Builds `exit`: coro.end(/*unwind*/ false) followed by the actual return
+    // to the caller.
+    auto exitBuilder = [&]() {
+      cir::ConstantOp nullHandler =
+          builder.getNullPtr(builder.getVoidPtrTy(), openCurlyLoc);
+      cir::ConstantOp noUnwind = builder.getBool(false, openCurlyLoc);
+      auto tkNone = cir::TokenNoneOp::create(builder, openCurlyLoc);
+      cir::CoroEndOp::create(builder, openCurlyLoc, nullHandler, noUnwind,
+                             tkNone);
+
+      if (auto *ret = cast_or_null<ReturnStmt>(s.getReturnStmt())) {
+        // Since we already emitted the return value above, so we shouldn't
+        // emit it again here.
+        Expr *previousRetValue = ret->getRetValue();
+        ret->setRetValue(nullptr);
+        if (emitStmt(ret, /*useCurrentScope=*/true).failed())
           return mlir::failure();
+        mlir::Block *block = builder.getInsertionBlock();
+        // emitReturnStmt() always creates a new insertion block after emitting
+        // the return. That block is unreachable in this case, so erase it.
+        block->erase();
+        // Set the return value back. The code generator, as the AST
+        // **Consumer**, shouldn't change the AST.
+        ret->setRetValue(previousRetValue);
       }
+
+      return mlir::success();
+    };
+
+    cir::CoroutineOp coro = cir::CoroutineOp::create(
+        builder, openCurlyLoc,
+        /*initialSuspendBuilder=*/
+        [&](mlir::OpBuilder &b, mlir::Location loc) {
+          res = initialSuspendBuilder();
+        },
+        /*bodyBuilder=*/
+        [&](mlir::OpBuilder &b, mlir::Location loc) {
+          if (res.succeeded())
----------------
erichkeane wrote:

I've not seen this pattern before, is there a reason we are doing it?  Is it really harmful to just always insert? 

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


More information about the cfe-commits mailing list