Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[coroutine] Suppress unreachable-code warning on coroutine statements. #77454

Merged
merged 6 commits into from
Feb 5, 2024

Conversation

hokein
Copy link
Collaborator

@hokein hokein commented Jan 9, 2024

This fixes #69219.

Consider an example:

CoTask my_coroutine() {
    std::abort();
    co_return 1; // unreachable code warning.
}

Clang emits a CFG-based unreachable warning on the co_return statement (precisely the 1 subexpr). If we remove this statement, the program semantic is changed (my_coroutine is not a coroutine anymore).

This patch fixes this issue by never considering coroutine statements as dead statements.

Copy link

github-actions bot commented Jan 9, 2024

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

// We suppress the unreachable warning for cases where an unreachable code is
// a substmt of the coroutine statement, becase removing it will change the
// function semantic if this is the only coroutine statement of the coroutine.
static bool isInCoroutineStmt(const CFGBlock *Block, const Stmt* S) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Some explanations to help understand the code:

  • Block is an unreachable block in CFG;
  • S is the first statement of the Block, which can be diagnosed as dead;

Given the code task test() { std::abort(); co_return 1; } (full CFG), the detected unreachable Block is B1, and S is a DeclRefExpr (substmt of the co_return stmt) which refers to the implicit __promise var decl.

 [B1]
   1: __promise
   2: [B1.1].return_value
   3: 1
   4: [B1.2]([B1.3])
   5: co_return [B1.3];   Preds (1): B2(Unreachable)
   Succs (1): B0

Copy link
Contributor

Choose a reason for hiding this comment

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

I do not think this works for more interesting cases when coroutine statements are before the abort.
Eg:

task test() {
  co_await 1;
  abort();
  co_await 2; // expected-warning {{code will never be executed}}
}

would fail. We would want a warning in this case.
co_await 2 has its own block which would be passed to this function (B1 in CFG)

More test ideas:

bool foo();
task test() {
  if (foo()) {
    std::abort();
    co_await 1; // expected-warning {{code will never be executed}}
  }
  co_await 2;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I discussed with @sam-mccall as well, we explored these options:

  1. Perform a global analysis on the CFG to make all these cases work perfect;
  2. Treat all coroutine statements as invalid dead statements, we never diagnose on these statements;
  3. Treat the current behaivor as intended, as technically these code are never executed during the runtime;

For 1), performing such global analysis (capturing all coroutine stmts in CFG, and figuring out all side effect of removing them) is sophisticated, and this model is not easy for human to understand and follow (as removing a coroutine statement may affect the whole analysis result).

For 3), users can suppress these diagnostics via the diagnostic pragma (the newly-introduced [[suppress]] is promising, but doesn't work for clang diagnostic).

2) is a simple model and solution. The tradeoff is that we might have some false negatives on some cases (as listed above), but I think it is fine.

Please take a look on the new change, and let me know what you think.

@hokein hokein marked this pull request as ready for review January 9, 2024 12:51
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:analysis labels Jan 9, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Jan 9, 2024

@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang

Author: Haojian Wu (hokein)

Changes

This fixes #69219.

Consider an example:

CoTask my_coroutine() {
    std::abort();
    co_return 1; // unreachable code warning.
}

Clang emits a CFG-based unreachable warning on the co_return statement (precisely the 1 subexpr). If we remove this statement, the program semantic is changed (my_coroutine is not a coroutine anymore).

The fix to suppress the warning for this particular case.


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

2 Files Affected:

  • (modified) clang/lib/Analysis/ReachableCode.cpp (+41-1)
  • (added) clang/test/SemaCXX/coroutine-unreachable-warning.cpp (+50)
diff --git a/clang/lib/Analysis/ReachableCode.cpp b/clang/lib/Analysis/ReachableCode.cpp
index 1bf0d9aec8620e..d839d2f999609d 100644
--- a/clang/lib/Analysis/ReachableCode.cpp
+++ b/clang/lib/Analysis/ReachableCode.cpp
@@ -17,6 +17,7 @@
 #include "clang/AST/ExprCXX.h"
 #include "clang/AST/ExprObjC.h"
 #include "clang/AST/ParentMap.h"
+#include "clang/AST/RecursiveASTVisitor.h"
 #include "clang/AST/StmtCXX.h"
 #include "clang/Analysis/AnalysisDeclContext.h"
 #include "clang/Analysis/CFG.h"
@@ -60,6 +61,45 @@ static bool isTrivialDoWhile(const CFGBlock *B, const Stmt *S) {
   return false;
 }
 
+// Check if the block starts with a coroutine statement and see if the given
+// unreachable 'S' is the substmt of the coroutine statement.
+//
+// We suppress the unreachable warning for cases where an unreachable code is
+// a substmt of the coroutine statement, becase removing it will change the
+// function semantic if this is the only coroutine statement of the coroutine.
+static bool isInCoroutineStmt(const CFGBlock *Block, const Stmt* S) {
+  // The coroutine statement, co_return, co_await, or co_yield.
+  const Stmt* CoroStmt = nullptr;
+  // Find the first coroutine statement in the block.
+  for (CFGBlock::const_iterator I = Block->begin(), E = Block->end(); I != E;
+       ++I)
+    if (std::optional<CFGStmt> CS = I->getAs<CFGStmt>()) {
+      const Stmt *S = CS->getStmt();
+      if (llvm::isa<CoreturnStmt>(S) || llvm::isa<CoroutineSuspendExpr>(S)) {
+        CoroStmt = S ;
+        break;
+      }
+    }
+  if (!CoroStmt)
+    return false;
+
+  struct Checker : RecursiveASTVisitor<Checker> {
+    const Stmt *StmtToCheck;
+    bool CoroutineSubStmt = false;
+    Checker(const Stmt *S) : StmtToCheck(S) {}
+    bool VisitStmt(const Stmt *S) {
+      if (S == StmtToCheck)
+        CoroutineSubStmt = true;
+      return true;
+    }
+    // The 'S' stmt captured in the CFG can be implicit.
+    bool shouldVisitImplicitCode() const { return true; }
+  };
+  Checker checker(S);
+  checker.TraverseStmt(const_cast<Stmt *>(CoroStmt));
+  return checker.CoroutineSubStmt;
+}
+
 static bool isBuiltinUnreachable(const Stmt *S) {
   if (const auto *DRE = dyn_cast<DeclRefExpr>(S))
     if (const auto *FDecl = dyn_cast<FunctionDecl>(DRE->getDecl()))
@@ -623,7 +663,7 @@ void DeadCodeScan::reportDeadCode(const CFGBlock *B,
   if (isa<BreakStmt>(S)) {
     UK = reachable_code::UK_Break;
   } else if (isTrivialDoWhile(B, S) || isBuiltinUnreachable(S) ||
-             isBuiltinAssumeFalse(B, S, C)) {
+             isBuiltinAssumeFalse(B, S, C) || isInCoroutineStmt(B, S)) {
     return;
   }
   else if (isDeadReturn(B, S)) {
diff --git a/clang/test/SemaCXX/coroutine-unreachable-warning.cpp b/clang/test/SemaCXX/coroutine-unreachable-warning.cpp
new file mode 100644
index 00000000000000..6ac5c34262b7e0
--- /dev/null
+++ b/clang/test/SemaCXX/coroutine-unreachable-warning.cpp
@@ -0,0 +1,50 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-unknown %s -std=c++20 -fsyntax-only -verify -Wunreachable-code
+
+#include "Inputs/std-coroutine.h"
+
+extern void abort (void) __attribute__ ((__noreturn__));
+
+struct task {
+  struct promise_type {
+    std::suspend_always initial_suspend();
+    std::suspend_always final_suspend() noexcept;
+    void return_void();
+    std::suspend_always yield_value(int) { return {}; }
+      task get_return_object();
+    void unhandled_exception();
+  };
+};
+
+task test1() {
+  abort();
+  co_yield 1;
+}
+
+task test2() {
+  abort();
+  1; // expected-warning {{code will never be executed}}
+  co_yield 1;
+}
+
+task test3() {
+  abort();
+  co_return;
+}
+
+task test4() {
+  abort();
+  1; // expected-warning {{code will never be executed}}
+  co_return;
+}
+
+
+task test5() {
+  abort();
+  co_await std::suspend_never{};
+}
+
+task test6() {
+  abort();
+  1; // expected-warning {{code will never be executed}}
+  co_await std::suspend_never{};
+}

Copy link
Contributor

@usx95 usx95 left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on. But this also disables the warning when it was actually correct. See comments.

// We suppress the unreachable warning for cases where an unreachable code is
// a substmt of the coroutine statement, becase removing it will change the
// function semantic if this is the only coroutine statement of the coroutine.
static bool isInCoroutineStmt(const CFGBlock *Block, const Stmt* S) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: rename S to Unreachable to avoid confusion with other variables named S.

// We suppress the unreachable warning for cases where an unreachable code is
// a substmt of the coroutine statement, becase removing it will change the
// function semantic if this is the only coroutine statement of the coroutine.
static bool isInCoroutineStmt(const CFGBlock *Block, const Stmt* S) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I do not think this works for more interesting cases when coroutine statements are before the abort.
Eg:

task test() {
  co_await 1;
  abort();
  co_await 2; // expected-warning {{code will never be executed}}
}

would fail. We would want a warning in this case.
co_await 2 has its own block which would be passed to this function (B1 in CFG)

More test ideas:

bool foo();
task test() {
  if (foo()) {
    std::abort();
    co_await 1; // expected-warning {{code will never be executed}}
  }
  co_await 2;
}

@hokein hokein force-pushed the coroutine-unreachable branch 2 times, most recently from c8a1951 to ee6fa40 Compare February 1, 2024 08:47
Copy link
Contributor

@usx95 usx95 left a comment

Choose a reason for hiding this comment

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

Skipping coroutine statements SG.

Change mostly LG. Left few comments.

clang/lib/Analysis/ReachableCode.cpp Outdated Show resolved Hide resolved
clang/lib/Analysis/ReachableCode.cpp Outdated Show resolved Hide resolved
clang/test/SemaCXX/coroutine-unreachable-warning.cpp Outdated Show resolved Hide resolved
clang/lib/Analysis/ReachableCode.cpp Show resolved Hide resolved
Copy link
Contributor

@usx95 usx95 left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks.

Comment on lines +15 to +21

struct Awaiter {
bool await_ready();
void await_suspend(auto);
int await_resume();
};
auto await_transform(const int& x) { return Awaiter{}; }
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: (Just to reduce the boilerplate not related to the test itself) we do not need to define a new awaitable here. Maybe just:
std::suspend_always await_transform(int) { return {}; }

Copy link
Collaborator Author

@hokein hokein Feb 2, 2024

Choose a reason for hiding this comment

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

We have a testcase int a = co_await 1; where the return type of await_resume needs to be int, std::suspend_always is not sufficient enough for it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh my bad. I thought this was targeting co_await some_integer;. Makes sense then.

clang/test/SemaCXX/coroutine-unreachable-warning.cpp Outdated Show resolved Hide resolved
clang/lib/Analysis/ReachableCode.cpp Show resolved Hide resolved
@hokein hokein merged commit a73baf6 into llvm:main Feb 5, 2024
4 checks passed
@hokein hokein deleted the coroutine-unreachable branch February 5, 2024 07:55
agozillon pushed a commit to agozillon/llvm-project that referenced this pull request Feb 5, 2024
llvm#77454)

This fixes llvm#69219.

Consider an example:

```
CoTask my_coroutine() {
    std::abort();
    co_return 1; // unreachable code warning.
}
```

Clang emits a CFG-based unreachable warning on the `co_return` statement
(precisely the `1` subexpr). If we remove this statement, the program
semantic is changed (my_coroutine is not a coroutine anymore).

This patch fixes this issue by never considering coroutine statements as
dead statements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:analysis clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

-Wunreachable-code and coroutines.
3 participants