Skip to content

[CoroCleanup] Noop coroutine elision for load-and-call pattern#179154

Merged
NewSigma merged 3 commits intollvm:mainfrom
NewSigma:noop-coro-load
Feb 2, 2026
Merged

[CoroCleanup] Noop coroutine elision for load-and-call pattern#179154
NewSigma merged 3 commits intollvm:mainfrom
NewSigma:noop-coro-load

Conversation

@NewSigma
Copy link
Contributor

@NewSigma NewSigma commented Feb 2, 2026

We assume that the resume/destroy functions of coroutines follow the fastcc convention. If the convention mismatches, InstCombine would remove the function call. Specifically for the noop coroutine, the following code gives an inconsistent result between -O0 and -O1. This patch proposes that we carry out elision for this pattern.

void load_and_call() {
  using Fn = void (*)(void *);
  void *frame = __builtin_coro_noop();
  Fn x = *reinterpret_cast<Fn *>(frame);
  x(frame);
  __builtin_printf("1\n");
}

https://godbolt.org/z/5ovdEqa9f

@llvmbot
Copy link
Member

llvmbot commented Feb 2, 2026

@llvm/pr-subscribers-coroutines

@llvm/pr-subscribers-llvm-transforms

Author: Weibo He (NewSigma)

Changes

We assume that the resume/destroy functions of coroutines follow the fastcc convention. If the convention mismatches, InstCombine would remove the function call. Specifically for the noop coroutine, the following code gives an inconsistent result between -O0 and -O1. This patch proposes that we carry out elision for this pattern.

void load_and_call() {
  using Fn = void (*)(void *);
  void *frame = __builtin_coro_noop();
  Fn x = *reinterpret_cast&lt;Fn *&gt;(frame);
  x(frame);
  __builtin_printf("1\n");
}

https://godbolt.org/z/5ovdEqa9f


Full diff: https://github.com/llvm/llvm-project/pull/179154.diff

3 Files Affected:

  • (modified) llvm/lib/Transforms/Coroutines/CoroCleanup.cpp (+62-24)
  • (added) llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll (+51)
  • (removed) llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll (-24)
diff --git a/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp b/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
index 6b68cf5bc2c20..52386aa749bc9 100644
--- a/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroCleanup.cpp
@@ -8,6 +8,7 @@
 
 #include "llvm/Transforms/Coroutines/CoroCleanup.h"
 #include "CoroInternal.h"
+#include "llvm/Analysis/PtrUseVisitor.h"
 #include "llvm/IR/DIBuilder.h"
 #include "llvm/IR/Function.h"
 #include "llvm/IR/IRBuilder.h"
@@ -15,6 +16,7 @@
 #include "llvm/IR/Module.h"
 #include "llvm/IR/PassManager.h"
 #include "llvm/Transforms/Scalar/SimplifyCFG.h"
+#include "llvm/Transforms/Utils/Local.h"
 
 using namespace llvm;
 
@@ -30,9 +32,25 @@ struct Lowerer : coro::LowererBase {
   bool lower(Function &F);
 
 private:
-  void elideCoroNoop(IntrinsicInst *II);
   void lowerCoroNoop(IntrinsicInst *II);
 };
+
+// Recursively walk and eliminate resume/destroy call on noop coro
+class NoopCoroElider : public PtrUseVisitor<NoopCoroElider> {
+  using Base = PtrUseVisitor<NoopCoroElider>;
+
+  IRBuilder<> Builder;
+public:
+  NoopCoroElider(const DataLayout &DL, LLVMContext &C) : Base(DL), Builder(C) {}
+
+  void run(IntrinsicInst *II);
+
+  void visitLoadInst(LoadInst &I) { enqueueUsers(I); }
+  void visitCallBase(CallBase &CB);
+  void visitIntrinsicInst(IntrinsicInst &II);
+private:
+  bool tryEraseCallInvoke(Instruction *I);
+};
 }
 
 static void lowerSubFn(IRBuilder<> &Builder, CoroSubFnInst *SubFn) {
@@ -73,6 +91,7 @@ bool Lowerer::lower(Function &F) {
   bool IsPrivateAndUnprocessed = F.isPresplitCoroutine() && F.hasLocalLinkage();
   bool Changed = false;
 
+  NoopCoroElider NCE(F.getDataLayout(), F.getContext());
   SmallPtrSet<Instruction *, 8> DeadInsts{};
   for (Instruction &I : instructions(F)) {
     if (auto *II = dyn_cast<IntrinsicInst>(&I)) {
@@ -100,7 +119,7 @@ bool Lowerer::lower(Function &F) {
         II->replaceAllUsesWith(ConstantTokenNone::get(Context));
         break;
       case Intrinsic::coro_noop:
-        elideCoroNoop(II);
+        NCE.run(II);
         if (!II->user_empty())
           lowerCoroNoop(II);
         break;
@@ -142,28 +161,6 @@ bool Lowerer::lower(Function &F) {
   return Changed;
 }
 
-void Lowerer::elideCoroNoop(IntrinsicInst *II) {
-  for (User *U : make_early_inc_range(II->users())) {
-    auto *Fn = dyn_cast<CoroSubFnInst>(U);
-    if (Fn == nullptr)
-      continue;
-
-    auto *User = Fn->getUniqueUndroppableUser();
-    if (auto *Call = dyn_cast<CallInst>(User)) {
-      Call->eraseFromParent();
-      Fn->eraseFromParent();
-      continue;
-    }
-
-    if (auto *I = dyn_cast<InvokeInst>(User)) {
-      Builder.SetInsertPoint(I);
-      Builder.CreateBr(I->getNormalDest());
-      I->eraseFromParent();
-      Fn->eraseFromParent();
-    }
-  }
-}
-
 void Lowerer::lowerCoroNoop(IntrinsicInst *II) {
   if (!NoopCoro) {
     LLVMContext &C = Builder.getContext();
@@ -200,6 +197,47 @@ void Lowerer::lowerCoroNoop(IntrinsicInst *II) {
   II->replaceAllUsesWith(NoopCoroVoidPtr);
 }
 
+void NoopCoroElider::run(IntrinsicInst *II) {
+  visitPtr(*II);
+
+  Worklist.clear();
+  VisitedUses.clear();
+}
+
+void NoopCoroElider::visitCallBase(CallBase &CB) {
+  auto *V = U->get();
+  bool ResumeOrDestroy = V == CB.getCalledOperand();
+  if (ResumeOrDestroy) {
+    bool Success = tryEraseCallInvoke(&CB);
+    assert(Success && "Unexpected CallBase");
+    RecursivelyDeleteTriviallyDeadInstructions(V);
+  }
+}
+
+void NoopCoroElider::visitIntrinsicInst(IntrinsicInst &II) {
+  if (auto *SubFn = dyn_cast<CoroSubFnInst>(&II)) {
+    auto *User = SubFn->getUniqueUndroppableUser();
+    if (!tryEraseCallInvoke(cast<Instruction>(User)))
+      return;
+    SubFn->eraseFromParent();
+  }
+}
+
+bool NoopCoroElider::tryEraseCallInvoke(Instruction *I) {
+  if (auto *Call = dyn_cast<CallInst>(I)) {
+    Call->eraseFromParent();
+    return true;
+  }
+
+  if (auto *II = dyn_cast<InvokeInst>(I)) {
+    Builder.SetInsertPoint(II);
+    Builder.CreateBr(II->getNormalDest());
+    II->eraseFromParent();
+    return true;
+  }
+  return false;
+}
+
 static bool declaresCoroCleanupIntrinsics(const Module &M) {
   return coro::declaresIntrinsics(
       M,
diff --git a/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll b/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll
new file mode 100644
index 0000000000000..6d9dd654b914a
--- /dev/null
+++ b/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll
@@ -0,0 +1,51 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -S -passes='coro-cleanup' | FileCheck %s
+
+; Tests that resume or destroy a no-op coroutine can be erased; Finally, erase coro.noop if it has no users.
+define void @erase() personality i32 0 {
+; CHECK-LABEL: define void @erase() personality i32 0 {
+; CHECK-NEXT:  [[DONE:.*:]]
+; CHECK-NEXT:    ret void
+;
+  %frame = call noundef ptr @llvm.coro.noop()
+  %resume = call ptr @llvm.coro.subfn.addr(ptr %frame, i8 0)
+  call fastcc void %resume(ptr %frame)
+  %destroy = call ptr @llvm.coro.subfn.addr(ptr %frame, i8 1)
+  invoke fastcc void %destroy(ptr %frame)
+  to label %done unwind label %unwind
+
+done:
+  ret void
+
+unwind:
+  %pad = landingpad { ptr, i32 }
+  catch ptr null
+  call void @terminate()
+  unreachable
+}
+
+; Tests the load-and-call pattern despite mismatched calling conventions. Prevent instcombine from breaking code.
+define void @load() personality i32 0 {
+; CHECK-LABEL: define void @load() personality i32 0 {
+; CHECK-NEXT:  [[DONE:.*:]]
+; CHECK-NEXT:    ret void
+;
+  %frame = call noundef ptr @llvm.coro.noop()
+  %resume = load ptr, ptr %frame, align 8
+  call void %resume(ptr %frame)
+  %destroy.addr = getelementptr inbounds nuw i8, ptr %frame, i64 8
+  %destroy = load ptr, ptr %destroy.addr, align 8
+  invoke void %destroy(ptr %frame)
+  to label %done unwind label %unwind
+
+done:
+  ret void
+
+unwind:
+  %pad = landingpad { ptr, i32 }
+  catch ptr null
+  call void @terminate()
+  unreachable
+}
+
+declare void @terminate() noreturn
diff --git a/llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll b/llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll
deleted file mode 100644
index 7fd9dc900ddb2..0000000000000
--- a/llvm/test/Transforms/Coroutines/coro-cleanup-noop-erase.ll
+++ /dev/null
@@ -1,24 +0,0 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
-; Tests that resume or destroy a no-op coroutine can be erased; Finally, erase coro.noop if it has no users.
-; RUN: opt < %s -S -passes='coro-cleanup' | FileCheck %s
-
-define void @fn() personality i32 0 {
-; CHECK-LABEL: define void @fn() personality i32 0 {
-; CHECK-NEXT:  [[DONE:.*:]]
-; CHECK-NEXT:    ret void
-;
-  %frame = call noundef ptr @llvm.coro.noop()
-  %resume = call ptr @llvm.coro.subfn.addr(ptr %frame, i8 0)
-  call fastcc void %resume(ptr %frame)
-  %destroy = call ptr @llvm.coro.subfn.addr(ptr %frame, i8 1)
-  invoke fastcc void %destroy(ptr %frame)
-  to label %done unwind label %unwind
-
-done:
-  ret void
-
-unwind:
-  %pad = landingpad { ptr, i32 }
-  catch ptr null
-  unreachable
-}

@github-actions
Copy link

github-actions bot commented Feb 2, 2026

✅ With the latest revision this PR passed the C/C++ code formatter.

Copy link
Member

@ChuanqiXu9 ChuanqiXu9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@NewSigma
Copy link
Contributor Author

NewSigma commented Feb 2, 2026

Thanks for the code review.

@NewSigma
Copy link
Contributor Author

NewSigma commented Feb 2, 2026

cc @bgra8 . I tested your repro. Could you please confirm if it resolved the problem?

@bgra8
Copy link
Contributor

bgra8 commented Feb 2, 2026

cc @bgra8 . I tested your repro. Could you please confirm if it resolved the problem?

Confirmed! Thanks for the fix @NewSigma !!

@NewSigma NewSigma merged commit 195a6d0 into llvm:main Feb 2, 2026
10 checks passed
@NewSigma NewSigma deleted the noop-coro-load branch February 2, 2026 14:03
@llvm-ci
Copy link

llvm-ci commented Feb 2, 2026

LLVM Buildbot has detected a new failure on builder llvm-clang-aarch64-darwin running on doug-worker-4 while building llvm at step 6 "test-build-unified-tree-check-all".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/190/builds/35545

Here is the relevant piece of the build log for the reference
Step 6 (test-build-unified-tree-check-all) failure: test (failure)
******************** TEST 'LLVM :: Transforms/Coroutines/coro-cleanup-noop-elide.ll' FAILED ********************
Exit Code: 2

Command Output (stdout):
--
# RUN: at line 2
/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt < /Users/buildbot/buildbot-root/llvm-project/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll -S -passes='coro-cleanup' | /Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/FileCheck /Users/buildbot/buildbot-root/llvm-project/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll
# executed command: /Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt -S -passes=coro-cleanup
# .---command stderr------------
# | Assertion failed: (Val && "isa<> used on a null pointer"), function doit, file Casting.h, line 109.
# | PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace and instructions to reproduce the bug.
# | Stack dump:
# | 0.	Program arguments: /Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt -S -passes=coro-cleanup
# | 1.	Running pass "coro-cleanup" on module "<stdin>"
# |  #0 0x00000001039dad78 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x101882d78)
# |  #1 0x00000001039d8b40 llvm::sys::RunSignalHandlers() (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x101880b40)
# |  #2 0x00000001039db878 SignalHandler(int, __siginfo*, void*) (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x101883878)
# |  #3 0x000000018f133584 (/usr/lib/system/libsystem_platform.dylib+0x18047b584)
# |  #4 0x000000018f102bc8 (/usr/lib/system/libsystem_pthread.dylib+0x18044abc8)
# |  #5 0x000000018f00fa40 (/usr/lib/system/libsystem_c.dylib+0x180357a40)
# |  #6 0x000000018f00ed30 (/usr/lib/system/libsystem_c.dylib+0x180356d30)
# |  #7 0x0000000104b4702c llvm::CoroCleanupPass::run(llvm::Module&, llvm::AnalysisManager<llvm::Module>&) (.cold.18) (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x1029ef02c)
# |  #8 0x00000001030ce458 llvm::CoroCleanupPass::run(llvm::Module&, llvm::AnalysisManager<llvm::Module>&) (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x100f76458)
# |  #9 0x0000000103059348 llvm::PassManager<llvm::Module, llvm::AnalysisManager<llvm::Module>>::run(llvm::Module&, llvm::AnalysisManager<llvm::Module>&) (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x100f01348)
# | #10 0x0000000103eea7c0 llvm::runPassPipeline(llvm::StringRef, llvm::Module&, llvm::TargetMachine*, llvm::TargetLibraryInfoImpl*, llvm::ToolOutputFile*, llvm::ToolOutputFile*, llvm::ToolOutputFile*, llvm::StringRef, llvm::ArrayRef<llvm::PassPlugin>, llvm::ArrayRef<std::__1::function<void (llvm::PassBuilder&)>>, llvm::opt_tool::OutputKind, llvm::opt_tool::VerifierKind, bool, bool, bool, bool, bool, bool, bool, bool) (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x101d927c0)
# | #11 0x0000000103ef5a84 optMain (/Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/opt+0x101d9da84)
# | #12 0x000000018ed77154
# `-----------------------------
# error: command failed with exit status: -6
# executed command: /Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/FileCheck /Users/buildbot/buildbot-root/llvm-project/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll
# .---command stderr------------
# | FileCheck error: '<stdin>' is empty.
# | FileCheck command line:  /Volumes/RAMDisk/buildbot-root/aarch64-darwin/build/bin/FileCheck /Users/buildbot/buildbot-root/llvm-project/llvm/test/Transforms/Coroutines/coro-cleanup-noop-elide.ll
# `-----------------------------
# error: command failed with exit status: 2

--

********************


@bgra8
Copy link
Contributor

bgra8 commented Feb 3, 2026

@NewSigma any ETA to reland the fix?

@NewSigma
Copy link
Contributor Author

NewSigma commented Feb 3, 2026

@NewSigma any ETA to reland the fix?

See #179431

NewSigma added a commit that referenced this pull request Feb 3, 2026
#179154)" (#179431)

There are multiple def-use paths from `coro.noop` to the calls of the
resume/destroy functions. We should update the worklist before erasing
instructions. The reversion passed tests with ASan enabled.

Reland #179154, which was reverted in #179289 due to ASan failures.
moar55 pushed a commit to moar55/llvm-project that referenced this pull request Feb 3, 2026
llvm#179154)" (llvm#179431)

There are multiple def-use paths from `coro.noop` to the calls of the
resume/destroy functions. We should update the worklist before erasing
instructions. The reversion passed tests with ASan enabled.

Reland llvm#179154, which was reverted in llvm#179289 due to ASan failures.
rishabhmadan19 pushed a commit to rishabhmadan19/llvm-project that referenced this pull request Feb 9, 2026
…179154)

We assume that the resume/destroy functions of coroutines follow the
`fastcc` convention. If the convention mismatches, `InstCombine` would
remove the function call. Specifically for the noop coroutine, the
following code gives an inconsistent result between -O0 and -O1. This
patch proposes that we carry out elision for this pattern.

``` C++
void load_and_call() {
  using Fn = void (*)(void *);
  void *frame = __builtin_coro_noop();
  Fn x = *reinterpret_cast<Fn *>(frame);
  x(frame);
  __builtin_printf("1\n");
}
```
rishabhmadan19 pushed a commit to rishabhmadan19/llvm-project that referenced this pull request Feb 9, 2026
rishabhmadan19 pushed a commit to rishabhmadan19/llvm-project that referenced this pull request Feb 9, 2026
llvm#179154)" (llvm#179431)

There are multiple def-use paths from `coro.noop` to the calls of the
resume/destroy functions. We should update the worklist before erasing
instructions. The reversion passed tests with ASan enabled.

Reland llvm#179154, which was reverted in llvm#179289 due to ASan failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants