================
@@ -2110,12 +2118,428 @@ class CIRTryOpFlattening : public 
mlir::OpRewritePattern<cir::TryOp> {
   }
 };
 
+static void
+collectCoReturnsOps(mlir::Region &region,
+                    llvm::SmallVectorImpl<cir::CoReturnOp> &coReturnOps) {
+  region.walk(
+      [&](cir::CoReturnOp coReturnOp) { coReturnOps.push_back(coReturnOp); });
+}
+
+// Flatten `cir.coroutine`. This runs once every nested op (awaits, cir.scope,
+// cleanup scopes, ...) has been flattened. The initialSuspend, body and
+// finalSuspend regions become the body of a cir.cleanup.scope whose cleanup is
+// the destroy region; the exit region follows the cleanup scope:
+//
+//   cir.cleanup.scope {
+//     initialSuspend ---> body ---> finalSuspend
+//                          |             ^
+//                          +-------------+  cir.co_return
+//   } cleanup all {
+//     destroy                         cir.coro.intrinsic.free
+//   }
+//   exit                              cir.coro.intrinsic.end, cir.return
+//
+// Leaving the cleanup scope body always runs the destroy region. In
+// particular, the cir.yield emitted for an await's destroy case leaves through
+// it. A suspended coroutine (cir.coro.exit) must keep its frame, so it
+// branches straight to the exit region.
+class CIRCoroutineFlattening : public mlir::OpRewritePattern<cir::CoroutineOp> 
{
+  void collectCoroExitsOps(
+      cir::CoroutineOp coroOp,
+      llvm::SmallVectorImpl<cir::CoroExitOp> &coroExitsOps) const {
+    // cir.coro.exit is produced by flattening a cir.await, which can only
+    // appear in the initialSuspend, body and finalSuspend regions.
+    auto collectCoroExit = [&](cir::CoroExitOp op) {
+      coroExitsOps.push_back(op);
+    };
+    coroOp.getInitialSuspend().walk(collectCoroExit);
+    coroOp.getBody().walk(collectCoroExit);
+    coroOp.getFinalSuspend().walk(collectCoroExit);
+  }
+
+  // Cleanup scope body: initialSuspend -> body -> finalSuspend. The three
+  // regions are moved, in this order, in front of the placeholder block the
+  // cleanup scope was created with, and the placeholder is erased once they
+  // are in place.
+  void inlineCleanUpScopeBody(mlir::PatternRewriter &rewriter,
+                              cir::CoroutineOp coroOp,
+                              cir::CleanupScopeOp scope) const {
+    mlir::Block *bodyPlaceholder = &scope.getBodyRegion().front();
+    // Take the entry blocks before the regions are moved.
+    mlir::Block *beforeBody = &coroOp.getBody().front();
+    mlir::Block *beforeFinal = &coroOp.getFinalSuspend().front();
+
+    inlineInitialSuspend(rewriter, coroOp, bodyPlaceholder, beforeBody);
+    inlineBody(rewriter, coroOp, bodyPlaceholder, beforeFinal);
+    inlineFinalSuspend(rewriter, coroOp, bodyPlaceholder);
+    rewriter.eraseBlock(bodyPlaceholder);
+  }
+
+  // initialSuspend ends in a cir.yield once the init await is done. Replace it
+  // with a branch to the body.
+  void inlineInitialSuspend(mlir::PatternRewriter &rewriter,
+                            cir::CoroutineOp coroOp, mlir::Block *insertBefore,
+                            mlir::Block *beforeBody) const {
+    mlir::Region &initialRegion = coroOp.getInitialSuspend();
+    auto initialYieldOp =
+        cast<cir::YieldOp>(initialRegion.back().getTerminator());
+    rewriter.setInsertionPoint(initialYieldOp);
+    rewriter.replaceOpWithNewOp<cir::BrOp>(
+        initialYieldOp, initialYieldOp.getArgs(), beforeBody);
+    rewriter.inlineRegionBefore(initialRegion, insertBefore);
+  }
+
+  // The body either falls off the end (cir.yield) or ends in a
+  // cir.co_return. Both, like every other cir.co_return in the body, branch
+  // to the final suspend.
+  void inlineBody(mlir::PatternRewriter &rewriter, cir::CoroutineOp coroOp,
+                  mlir::Block *insertBefore, mlir::Block *beforeFinal) const {
+    mlir::Region &bodyRegion = coroOp.getBody();
+
+    llvm::SmallVector<cir::CoReturnOp> coReturnsOps;
+    collectCoReturnsOps(bodyRegion, coReturnsOps);
+
+    mlir::Operation *bodyTerminator = bodyRegion.back().getTerminator();
+    if (auto bodyYield = dyn_cast<cir::YieldOp>(bodyTerminator)) {
+      rewriter.setInsertionPoint(bodyYield);
+      rewriter.replaceOpWithNewOp<cir::BrOp>(bodyYield, mlir::ValueRange{},
+                                             beforeFinal);
+    }
+
+    for (auto &coReturn : coReturnsOps) {
+      rewriter.setInsertionPoint(coReturn);
+      rewriter.replaceOpWithNewOp<cir::BrOp>(coReturn, mlir::ValueRange{},
+                                             beforeFinal);
+    }
+
+    rewriter.inlineRegionBefore(bodyRegion, insertBefore);
+  }
+
+  // finalSuspend keeps its cir.yield, which now ends the cleanup scope body:
+  // once the final await completes, the coroutine leaves the cleanup scope
+  // normally and runs the destroy region.
+  void inlineFinalSuspend(mlir::PatternRewriter &rewriter,
+                          cir::CoroutineOp coroOp,
+                          mlir::Block *insertBefore) const {
+    rewriter.inlineRegionBefore(coroOp.getFinalSuspend(), insertBefore);
+  }
+
+  // Cleanup scope cleanup region: the destroy region.
+  void inlineCleanUpScopeCleanup(mlir::PatternRewriter &rewriter,
+                                 cir::CoroutineOp coroOp,
+                                 cir::CleanupScopeOp scope) const {
+    mlir::Region &destroyRegion = coroOp.getDestroy();
+    mlir::Block *cleanupPlaceholder = &scope.getCleanupRegion().front();
+    rewriter.inlineRegionBefore(destroyRegion, cleanupPlaceholder);
+    rewriter.eraseBlock(cleanupPlaceholder);
+  }
+
+  // A suspended coroutine keeps its frame, so each cir.coro.exit branches
+  // straight to the exit region without running the destroy region.
+  void lowerCoroExits(mlir::PatternRewriter &rewriter,
+                      llvm::ArrayRef<cir::CoroExitOp> coroExitsOps,
+                      mlir::Block *beforeExit) const {
+    for (cir::CoroExitOp coroExit : coroExitsOps) {
+      // TODO(cir): once the GRO region is supported, this may need to branch
+      // there instead of to the exit region.
+      assert(!cir::MissingFeatures::coroutineGetRetObjManager());
+      rewriter.setInsertionPoint(coroExit);
+      rewriter.replaceOpWithNewOp<cir::BrOp>(coroExit, mlir::ValueRange{},
+                                             beforeExit);
+    }
+  }
+
+  // Exit region: inline it right after the cleanup scope, which falls through
+  // to it.
+  void inlineExitRegion(mlir::PatternRewriter &rewriter,
+                        cir::CoroutineOp coroOp, cir::CleanupScopeOp scope,
+                        mlir::Block *remainingOpsBlock) const {
+    mlir::Region &exitRegion = coroOp.getExit();
+    mlir::Block *beforeExit = &exitRegion.front();
+
+    rewriter.setInsertionPointAfter(scope);
+    mlir::Block *afterCleanup =
+        rewriter.splitBlock(remainingOpsBlock, rewriter.getInsertionPoint());
+    rewriter.inlineRegionBefore(exitRegion, afterCleanup);
+
+    rewriter.setInsertionPointAfter(scope);
+    cir::BrOp::create(rewriter, scope.getLoc(), mlir::ValueRange(), 
beforeExit);
+  }
+
+public:
+  using OpRewritePattern<cir::CoroutineOp>::OpRewritePattern;
+
+  mlir::LogicalResult
+  matchAndRewrite(cir::CoroutineOp coroOp,
+                  mlir::PatternRewriter &rewriter) const override {
+
+    assert(!cir::MissingFeatures::coroutineUnwindRegion());
+    for (mlir::Region &region : coroOp->getRegions())
+      if (hasNestedOpsToFlatten(region))
+        return mlir::failure();
+
+    // Collect the cir.coro.exit ops before the regions are moved. Each one
+    // becomes a branch to the entry of the exit region.
+    llvm::SmallVector<cir::CoroExitOp> coroExitsOps;
+    collectCoroExitsOps(coroOp, coroExitsOps);
+
+    mlir::Location loc = coroOp.getLoc();
+    mlir::Block *coroBlock = rewriter.getInsertionBlock();
+    // Split the current block before the CoroutineOp to create the inlining
+    // point.
+    mlir::Block *remainingOpsBlock =
+        rewriter.splitBlock(coroBlock, rewriter.getInsertionPoint());
+
+    rewriter.setInsertionPointToEnd(coroBlock);
+    cir::BrOp::create(rewriter, loc, mlir::ValueRange(), remainingOpsBlock);
+
+    // coroOp now lives in remainingOpsBlock. Build the cleanup scope in front
+    // of it.
+    //
+    // The coroutine becomes a cir.cleanup.scope so that its frame is freed
+    // (the destroy region, as its cleanup region) on every way out of the
+    // coroutine: completing the final suspend, an await's destroy case and,
+    // because the kind is All, an exception escaping the coroutine. The
+    // regular cleanup scope flattening then routes each of those exits
+    // through the destroy region. A suspended coroutine must keep its frame,
+    // so cir.coro.exit skips the cleanup scope (see lowerCoroExits).
+    rewriter.setInsertionPoint(coroOp);
+
+    auto emitYield = [](mlir::OpBuilder &b, mlir::Location l) {
+      cir::YieldOp::create(b, l);
+    };
+
+    auto scope = cir::CleanupScopeOp::create(rewriter, loc, CleanupKind::All,
+                                             emitYield, emitYield);
+
+    inlineCleanUpScopeBody(rewriter, coroOp, scope);
+    inlineCleanUpScopeCleanup(rewriter, coroOp, scope);
+
+    mlir::Block *beforeExit = &coroOp.getExit().front();
+    lowerCoroExits(rewriter, coroExitsOps, beforeExit);
+    inlineExitRegion(rewriter, coroOp, scope, remainingOpsBlock);
+
+    auto func = coroOp->getParentOfType<cir::FuncOp>();
+    // The coroutine regions have been flattened, so the original coroutine
+    // attribute no longer verifies. Preserve the information that this 
function
+    // originated from a coroutine for LLVM lowering.
+    func.setCoroutine(false);
+    func.setFlattenedCoroutine(true);
+
+    rewriter.eraseOp(coroOp);
+    return mlir::success();
+  }
+};
+
+class CIRAwaitOpFlattening : public mlir::OpRewritePattern<cir::AwaitOp> {
+  // The ready region ends in a cir.condition that holds the result of
+  // `await_ready()`. Replace it with a cir.brcond: if the awaiter is already
+  // ready, skip the suspension and go straight to the resume region
+  // otherwise go to the suspend region.
+  void flattenReadyRegion(mlir::PatternRewriter &rewriter, cir::AwaitOp 
awaitOp,
+                          mlir::Block *resumeEntry, mlir::Block *suspendEntry,
+                          mlir::Block *remainingOpsBlock) const {
+    mlir::Region &readyRegion = awaitOp.getReady();
+    auto conditionOp =
+        cast<cir::ConditionOp>(readyRegion.back().getTerminator());
+    rewriter.setInsertionPoint(conditionOp);
+    rewriter.replaceOpWithNewOp<cir::BrCondOp>(
+        conditionOp, conditionOp.getCondition(), resumeEntry, suspendEntry);
+    rewriter.inlineRegionBefore(readyRegion, remainingOpsBlock);
+  }
+
+  // The suspend region runs `await_suspend()` and ends in a
+  // cir.coro.suspend_point. Emit cir.coro.intrinsic.save and
+  // cir.coro.intrinsic.suspend around it, and replace the suspend point with a
+  // cir.switch.flat on the suspend result, which decides whether the coroutine
+  // resumes, is destroyed or stays suspended.
+  void flattenSuspendRegion(mlir::PatternRewriter &rewriter,
+                            cir::AwaitOp awaitOp, mlir::Block *resumeEntry,
+                            mlir::Block *remainingOpsBlock) const {
+    mlir::Region &suspendRegion = awaitOp.getSuspend();
+    // TODO(cir): support an await_suspend() that returns bool (it can veto
+    // the suspension and resume right away) by flattening it into a brcond.
+    auto suspendPoint =
+        cast<cir::CoroSuspendPoint>(suspendRegion.back().getTerminator());
+    cir::CoroSuspendOp coroSuspendIntri =
+        emitCoroSuspend(rewriter, awaitOp, suspendPoint);
+
+    // Inline the suspend region before creating the destroy and suspended
+    // blocks, so they are placed right after it, in front of the continuation.
+    rewriter.inlineRegionBefore(suspendRegion, remainingOpsBlock);
+
+    mlir::Location loc = awaitOp.getLoc();
+    mlir::Block *cleanupBlock =
+        createDestroyBlock(rewriter, loc, remainingOpsBlock);
+    mlir::Block *retBlock =
+        createSuspendedBlock(rewriter, loc, remainingOpsBlock);
+
+    // 0 resumes, 1 destroys and the default (-1) stays suspended.
+    llvm::SmallVector<mlir::APInt> caseValues{mlir::APInt(8, 0),
+                                              mlir::APInt(8, 1)};
+    llvm::SmallVector<mlir::Block *> caseDestinations{resumeEntry,
+                                                      cleanupBlock};
+    llvm::SmallVector<mlir::ValueRange> caseOperands{caseValues.size()};
+
+    // Dispatch to the appropriate coroutine path based on the result of
+    // `cir.coro.intrinsic.suspend`: resume, destroy, or suspend.
+    rewriter.setInsertionPoint(suspendPoint);
+    rewriter.replaceOpWithNewOp<cir::SwitchFlatOp>(
+        suspendPoint, coroSuspendIntri.getResult(), retBlock,
+        mlir::ValueRange{}, caseValues, caseDestinations, caseOperands);
+  }
+
+  // Emit cir.coro.intrinsic.save at the start of the suspend region and
+  // cir.coro.intrinsic.suspend right before its cir.coro.suspend_point.
+  cir::CoroSuspendOp emitCoroSuspend(mlir::PatternRewriter &rewriter,
+                                     cir::AwaitOp awaitOp,
+                                     cir::CoroSuspendPoint suspendPoint) const 
{
+    mlir::Location loc = awaitOp.getLoc();
+
+    // cir.coro.intrinsic.suspend takes the token produced by
+    // cir.coro.intrinsic.save as its first argument, so emit the save first.
+    rewriter.setInsertionPointToStart(&awaitOp.getSuspend().front());
+
+    // The save goes first in the suspend region, before await_suspend()
+    // runs, so the coroutine is already considered suspended (and can be
+    // resumed) from that point.
+    auto voidPtrTy = cir::PointerType::get(cir::VoidType::get(getContext()));
+    auto nullPtr = cir::ConstantOp::create(
+        rewriter, loc,
+        cir::ConstPtrAttr::get(voidPtrTy, rewriter.getI64IntegerAttr(0)));
+    auto coroSave = cir::CoroSaveOp::create(rewriter, loc, nullPtr);
+    rewriter.setInsertionPoint(suspendPoint);
+
+    // The second argument to `cir.coro.intrinsic.suspend` indicates whether
+    // this is the final suspend point. Coroutines suspended at a final suspend
+    // point are considered done (`cir.coro.intrinsic.done` returns true) and
+    // may only be destroyed; resuming them is undefined behavior.
+    bool isFinalSuspend = awaitOp.getKind() == cir::AwaitKind::Final;
+    auto isFinalCoroSuspend = cir::ConstantOp::create(
+        rewriter, loc, cir::BoolAttr::get(getContext(), isFinalSuspend));
+
+    // cir.coro.intrinsic.suspend returns:
+    //  -1 : coroutine suspended
+    //   0 : coroutine resumed
+    //   1 : coroutine destroyed
+    return cir::CoroSuspendOp::create(
+        rewriter, loc,
+        mlir::ValueRange{coroSave.getResult(), isFinalCoroSuspend});
+  }
+
+  // Destroy case. Classic codegen uses EmitBranchThroughCleanup() so every
+  // enclosing cleanup runs before the frame is freed. Here the destroy block
+  // is a single cir.yield, meant to leave through the coroutine cleanup scope
+  // (cir.coro.intrinsic.free) and then reach the exit region.
+  //
+  // FIXME: This only works when the await is directly in the coroutine
----------------
bcardosolopes wrote:

Until this is fixed, it's silent wrong code for very ordinary coroutines: a 
`co_await` in a loop, or with a local that has a destructor, keeps running the 
body after a destroy. The new tests (`silly_task_with_dtor`, `await_in_loop`, 
...) check that output in. Please emit an NYI error when the await is nested in 
anything other than the coroutine's own cleanup scope, and drop or `XFAIL` 
those checks, rather than landing the miscompile. Is the dedicated terminator 
much more work than that?

https://github.com/llvm/llvm-project/pull/203802
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to