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

[C++20][Coroutines] Lambda-coroutine with operator new in promise_type #84193

Merged
merged 1 commit into from
Mar 8, 2024

Conversation

andreasfertig
Copy link
Contributor

Fix #84064

According to http://eel.is/c++draft/dcl.fct.def.coroutine#9 the first parameter for overload resolution of operator new is size_t followed by the arguments of the coroutine function.
http://eel.is/c++draft/dcl.fct.def.coroutine#4 states that the first argument is the lvalue of *this if the coroutine is a member function.

Before this patch, Clang handled class types correctly but ignored lambdas. This patch adds support for lambda coroutines with a promise_type that implements a custom operator new.

The patch does consider C++23 static operator(), which already worked as there is no this parameter.

Copy link

github-actions bot commented Mar 6, 2024

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be
notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write
permissions for the repository. In which case you can instead tag reviewers by
name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review
by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate
is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" coroutines C++20 coroutines labels Mar 6, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Mar 6, 2024

@llvm/pr-subscribers-coroutines

@llvm/pr-subscribers-clang

Author: Andreas Fertig (andreasfertig)

Changes

Fix #84064

According to http://eel.is/c++draft/dcl.fct.def.coroutine#9 the first parameter for overload resolution of operator new is size_t followed by the arguments of the coroutine function.
http://eel.is/c++draft/dcl.fct.def.coroutine#4 states that the first argument is the lvalue of *this if the coroutine is a member function.

Before this patch, Clang handled class types correctly but ignored lambdas. This patch adds support for lambda coroutines with a promise_type that implements a custom operator new.

The patch does consider C++23 static operator(), which already worked as there is no this parameter.


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

5 Files Affected:

  • (modified) clang/include/clang/Sema/Sema.h (+10-2)
  • (modified) clang/lib/Sema/SemaCoroutine.cpp (+16-2)
  • (modified) clang/lib/Sema/SemaExprCXX.cpp (+10-5)
  • (added) clang/test/SemaCXX/gh84064-1.cpp (+54)
  • (added) clang/test/SemaCXX/gh84064-2.cpp (+53)
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 25c4c58ad4ae43..f6d81a15487a3e 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -6898,10 +6898,18 @@ class Sema final {
                                    BinaryOperatorKind Operator);
 
   //// ActOnCXXThis -  Parse 'this' pointer.
-  ExprResult ActOnCXXThis(SourceLocation loc);
+  ///
+  /// \param SkipLambdaCaptureCheck Whether to skip the 'this' check for a
+  /// lambda because 'this' is the lambda's 'this'-pointer.
+  ExprResult ActOnCXXThis(SourceLocation loc,
+                          bool SkipLambdaCaptureCheck = false);
 
   /// Build a CXXThisExpr and mark it referenced in the current context.
-  Expr *BuildCXXThisExpr(SourceLocation Loc, QualType Type, bool IsImplicit);
+  ///
+  /// \param SkipLambdaCaptureCheck Whether to skip the 'this' check for a
+  /// lambda because 'this' is the lambda's 'this'-pointer.
+  Expr *BuildCXXThisExpr(SourceLocation Loc, QualType Type, bool IsImplicit,
+                         bool SkipLambdaCaptureCheck = false);
   void MarkThisReferenced(CXXThisExpr *This);
 
   /// Try to retrieve the type of the 'this' pointer.
diff --git a/clang/lib/Sema/SemaCoroutine.cpp b/clang/lib/Sema/SemaCoroutine.cpp
index a969b9383563b2..d5655309d21f28 100644
--- a/clang/lib/Sema/SemaCoroutine.cpp
+++ b/clang/lib/Sema/SemaCoroutine.cpp
@@ -25,6 +25,7 @@
 #include "clang/Sema/Initialization.h"
 #include "clang/Sema/Overload.h"
 #include "clang/Sema/ScopeInfo.h"
+#include "clang/Sema/Sema.h"
 #include "clang/Sema/SemaInternal.h"
 #include "llvm/ADT/SmallSet.h"
 
@@ -1378,8 +1379,21 @@ bool CoroutineStmtBuilder::makeReturnOnAllocFailure() {
 static bool collectPlacementArgs(Sema &S, FunctionDecl &FD, SourceLocation Loc,
                                  SmallVectorImpl<Expr *> &PlacementArgs) {
   if (auto *MD = dyn_cast<CXXMethodDecl>(&FD)) {
-    if (MD->isImplicitObjectMemberFunction() && !isLambdaCallOperator(MD)) {
-      ExprResult ThisExpr = S.ActOnCXXThis(Loc);
+    if (MD->isImplicitObjectMemberFunction()) {
+      ExprResult ThisExpr{};
+
+      if (isLambdaCallOperator(MD) && !MD->isStatic()) {
+        Qualifiers ThisQuals = MD->getMethodQualifiers();
+        CXXRecordDecl *Record = MD->getParent();
+
+        Sema::CXXThisScopeRAII ThisScope(S, Record, ThisQuals,
+                                         Record != nullptr);
+
+        ThisExpr = S.ActOnCXXThis(Loc, /*SkipLambdaCaptureCheck*/ true);
+      } else {
+        ThisExpr = S.ActOnCXXThis(Loc);
+      }
+
       if (ThisExpr.isInvalid())
         return false;
       ThisExpr = S.CreateBuiltinUnaryOp(Loc, UO_Deref, ThisExpr.get());
diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp
index c34a40fa7c81ac..499c4943c23b01 100644
--- a/clang/lib/Sema/SemaExprCXX.cpp
+++ b/clang/lib/Sema/SemaExprCXX.cpp
@@ -1414,7 +1414,7 @@ bool Sema::CheckCXXThisCapture(SourceLocation Loc, const bool Explicit,
   return false;
 }
 
-ExprResult Sema::ActOnCXXThis(SourceLocation Loc) {
+ExprResult Sema::ActOnCXXThis(SourceLocation Loc, bool SkipLambdaCaptureCheck) {
   /// C++ 9.3.2: In the body of a non-static member function, the keyword this
   /// is a non-lvalue expression whose value is the address of the object for
   /// which the function is called.
@@ -1434,13 +1434,18 @@ ExprResult Sema::ActOnCXXThis(SourceLocation Loc) {
     return Diag(Loc, diag::err_invalid_this_use) << 0;
   }
 
-  return BuildCXXThisExpr(Loc, ThisTy, /*IsImplicit=*/false);
+  return BuildCXXThisExpr(Loc, ThisTy, /*IsImplicit=*/false,
+                          SkipLambdaCaptureCheck);
 }
 
-Expr *Sema::BuildCXXThisExpr(SourceLocation Loc, QualType Type,
-                             bool IsImplicit) {
+Expr *Sema::BuildCXXThisExpr(SourceLocation Loc, QualType Type, bool IsImplicit,
+                             bool SkipLambdaCaptureCheck) {
   auto *This = CXXThisExpr::Create(Context, Loc, Type, IsImplicit);
-  MarkThisReferenced(This);
+
+  if (!SkipLambdaCaptureCheck) {
+    MarkThisReferenced(This);
+  }
+
   return This;
 }
 
diff --git a/clang/test/SemaCXX/gh84064-1.cpp b/clang/test/SemaCXX/gh84064-1.cpp
new file mode 100644
index 00000000000000..dc7c475041094a
--- /dev/null
+++ b/clang/test/SemaCXX/gh84064-1.cpp
@@ -0,0 +1,54 @@
+// RUN: %clang_cc1 -fsyntax-only -verify -I%S/Inputs -std=c++20 %s
+
+// expected-no-diagnostics
+
+#include "std-coroutine.h"
+
+using size_t = decltype(sizeof(0));
+
+struct Generator {
+  struct promise_type {
+    int _val{};
+
+    Generator get_return_object() noexcept
+    {
+      return {};
+    }
+
+    std::suspend_never initial_suspend() noexcept
+    {
+      return {};
+    }
+
+    std::suspend_always final_suspend() noexcept
+    {
+      return {};
+    }
+
+    void return_void() noexcept {}
+    void unhandled_exception() noexcept {}
+
+    template<typename This, typename... TheRest>
+    static void*
+    operator new(size_t size,
+                 This&,
+                 TheRest&&...) noexcept
+    {
+        return nullptr;
+    }
+
+    static void operator delete(void*, size_t)
+    {
+    }
+  };
+};
+
+int main()
+{
+  auto lamb = []() -> Generator {
+    co_return;
+  };
+
+  static_assert(sizeof(decltype(lamb)) == 1);
+}
+
diff --git a/clang/test/SemaCXX/gh84064-2.cpp b/clang/test/SemaCXX/gh84064-2.cpp
new file mode 100644
index 00000000000000..457de43eab6d9e
--- /dev/null
+++ b/clang/test/SemaCXX/gh84064-2.cpp
@@ -0,0 +1,53 @@
+// RUN: %clang_cc1 -fsyntax-only -verify -I%S/Inputs -std=c++23 %s
+
+// expected-no-diagnostics
+
+#include "std-coroutine.h"
+
+using size_t = decltype(sizeof(0));
+
+struct GeneratorStatic {
+  struct promise_type {
+    int _val{};
+
+    GeneratorStatic get_return_object() noexcept
+    {
+      return {};
+    }
+
+    std::suspend_never initial_suspend() noexcept
+    {
+      return {};
+    }
+
+    std::suspend_always final_suspend() noexcept
+    {
+      return {};
+    }
+
+    void return_void() noexcept {}
+    void unhandled_exception() noexcept {}
+
+    template<typename... TheRest>
+    static void*
+    operator new(size_t  size,
+                 TheRest&&...) noexcept
+    {
+        return nullptr;
+    }
+
+    static void operator delete(void*, size_t)
+    {
+    }
+  };
+};
+
+
+int main()
+{
+  auto lambCpp23 = []() static -> GeneratorStatic {
+    co_return;
+  };
+
+  static_assert(sizeof(decltype(lambCpp23)) == 1);
+}

clang/lib/Sema/SemaCoroutine.cpp Outdated Show resolved Hide resolved
/// \param SkipLambdaCaptureCheck Whether to skip the 'this' check for a
/// lambda because 'this' is the lambda's 'this'-pointer.
ExprResult ActOnCXXThis(SourceLocation loc,
bool SkipLambdaCaptureCheck = false);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not thrilled with the name of this variable, I would like to see it bikeshed to be more clear what it means from a 'callers' perspective rather than an implementation perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, do you have any suggestions?
From what I understood, the previous implementation always checked whether capturing this was valid. My patch adds support for lambdas in coroutines with custom new. It needs a this'- pointer but not a captured one of the surrounding class; the lambdas own this'- pointer. Skipping the capture check sounded appropriate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe something like, IsNonStaticLambda? But I guess this only applies in a coroutine situation, so perhaps that is imperfect too. I might have to think it through.

Perhaps @ChuanqiXu9 can come up with something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe IsLambdasOwnThis?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can't parse that... can you describe that name a little more?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, have a look at this code (https://cppinsights.io/s/3212c7be)

class Test {
  int a;

public:
  Test(int x)
  : a{x}
  {
    int other{};
    auto l1 = [=] { return a + 2 + other; };
  }
};

The lambda l1 has two things that we refer to as this-pointer. It's very own, as every non-static member function has. Used to access other like this->other inside the lambdas body. That this-pointer isn't captured. Hence, no check.

Then the captured this-pointer from the enclosing scope of Test. In C++ Insights called __this. While being used it looks like this->__this->a.

The check is in place for the latter. For the use case of calling operator new in the case of a coroutine, we are talking about the first one, which isn't captured.

While writing about this, I haven't checked whether a promise_type with a constructor worked before or does so now. I can check that maybe tomorrow.

Copy link
Member

Choose a reason for hiding this comment

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

Excellent. Good. I suggest you to update this to the inline comments.

Copy link
Member

Choose a reason for hiding this comment

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

If there is any problem with the promise_type with a constructor, let's make it in other patches.

Copy link
Contributor

Choose a reason for hiding this comment

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

How about ThisRefersToClosureObject ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about ThisRefersToClosureObject ?

Sounds good! Thanks!

clang/test/SemaCXX/gh84064-1.cpp Show resolved Hide resolved
clang/lib/Sema/SemaExprCXX.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@Endilll Endilll left a comment

Choose a reason for hiding this comment

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

Sema.h changes look good.

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 with the adding the comments.

Fix llvm#84064

According to http://eel.is/c++draft/dcl.fct.def.coroutine#9 the first
parameter for overload resolution of `operator new` is `size_t` followed
by the arguments of the coroutine function.
http://eel.is/c++draft/dcl.fct.def.coroutine#4 states that the first argument
is the lvalue of `*this` if the coroutine is a member function.

Before this patch, Clang handled class types correctly but ignored lambdas.
This patch adds support for lambda coroutines with a `promise_type` that
implements a custom `operator new`.

The patch does consider C++23 `static operator()`, which already worked as
there is no `this` parameter.
@andreasfertig
Copy link
Contributor Author

The clang-format check fails due to a change in clang/docs/ReleaseNotes. rst, which isn't mine. What's the protocol? Shall I fix the format issue, or can my change be merged anyway?

@erichkeane
Copy link
Collaborator

The clang-format check fails due to a change in clang/docs/ReleaseNotes. rst, which isn't mine. What's the protocol? Shall I fix the format issue, or can my change be merged anyway?

Nope, don't worry about that one. That bot is being weird, feel free to merge without it.

@cor3ntin
Copy link
Contributor

cor3ntin commented Mar 8, 2024

@andreasfertig Will you need us to merge that for you?

@andreasfertig
Copy link
Contributor Author

@andreasfertig Will you need us to merge that for you?

@cor3ntin: It looks like I need you, yes. I can only close the PR :-/

@erichkeane erichkeane merged commit 35d3b33 into llvm:main Mar 8, 2024
3 of 4 checks passed
Copy link

github-actions bot commented Mar 8, 2024

@andreasfertig Congratulations on having your first Pull Request (PR) merged into the LLVM Project!

Your changes will be combined with recent changes from other authors, then tested
by our build bots. If there is a problem with a build, you may recieve a report in an email or a comment on this PR.

Please check whether problems have been caused by your change specifically, as
the builds can include changes from many authors. It is not uncommon for your
change to be included in a build that fails due to someone else's changes, or
infrastructure issues.

How to do this, and the rest of the post-merge process, is covered in detail here.

If your change does cause a problem, it may be reverted, or you can revert it yourself.
This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again.

If you don't get any reports, no action is required from you. Your changes are working as expected, well done!

@ChuanqiXu9
Copy link
Member

ChuanqiXu9 commented Mar 11, 2024

This commit breaks our coroutines library async_simple https://github.com/alibaba/async_simple . and here is a (relative) minimal reproducer: https://godbolt.org/z/sG5jzcGEz

The reproducer comes from an implementation for async_simple::Generator (https://github.com/alibaba/async_simple/blob/main/async_simple/coro/Generator.h), which is basically the same implementation from the paper of std::generator in c++23. So possibly this may make that crash too.

I'll revert this patch to avoid the regression now. Sorry for not testing it sufficiently.

ChuanqiXu9 added a commit that referenced this pull request Mar 11, 2024
…mise_type (#84193)"

This reverts commit 35d3b33.

See the comments in #84193 for
details
@andreasfertig
Copy link
Contributor Author

Interesting! I'll see what I can do.

@andreasfertig
Copy link
Contributor Author

Hello all,

I did more investigation and found another shortcoming. In some cases, my initial implementation picked the wrong this- type, for example, https://github.com/andreasfertig/llvm-project/blob/9c60fec6881cca7e7fed9dca5cf24a0bd1924eaa/clang/test/SemaCXX/coroutine-promise-ctor-lambda.cpp#L45.

I pushed a new branch (https://github.com/andreasfertig/llvm-project/tree/fixGH84064take2), which also fixes this behavior and requires fewer changes.

Regarding the crash, that was improper testing on my side. All tests let Clang crash, even the tests I checked in as part of the PR. The difference is that my tests used only static_assert to verify that the lambda's size did not increase, while the async_simple::Generator example executes the code. This step leads to code generation, the static_assert, I assume, is handled only on the constexpr evaluator.

This is where I'm stuck. The call stack for all cases is:

 #5 0x0000000105534f38 clang::CodeGen::CodeGenFunction::EmitCall(clang::CodeGen::CGFunctionInfo const&, clang::CodeGen::CGCallee const&, clang::CodeGen::ReturnValueSlot, clang::CodeGen::CallArgList const&, llvm::CallBase**, bool, clang::SourceLocation) (build/bin/clang-19+0x10262cf38)
 #6 0x00000001055e10d8 clang::CodeGen::CodeGenFunction::EmitCall(clang::QualType, clang::CodeGen::CGCallee const&, clang::CallExpr const*, clang::CodeGen::ReturnValueSlot, llvm::Value*) (build/bin/clang-19+0x1026d90d8)
 #7 0x00000001055e01b4 clang::CodeGen::CodeGenFunction::EmitCallExpr(clang::CallExpr const*, clang::CodeGen::ReturnValueSlot) (build/bin/clang-19+0x1026d81b4)
 #8 0x000000010561e5a8 (anonymous namespace)::ScalarExprEmitter::VisitCallExpr(clang::CallExpr const*) (build/bin/clang-19+0x1027165a8)
 #9 0x000000010560edec clang::CodeGen::CodeGenFunction::EmitScalarExpr(clang::Expr const*, bool) (build/bin/clang-19+0x102706dec)
#10 0x000000010554e22c clang::CodeGen::CodeGenFunction::EmitCoroutineBody(clang::CoroutineBodyStmt const&) (build/bin/clang-19+0x10264622c)

The crash is within CodeGenFunction::EmitCall at

V = I->getKnownRValue().getScalarVal();

I->getKnownRValue().getScalarVal() is nullptr, which leads to the crash later when V is deferenced. I don't understand why getKnownRValue is empty. It looks to me like I had to mark this to be not optimized away. However, I could not see any coding doing this for the non-lambda case. Any thoughts?

@ChuanqiXu9
Copy link
Member

Hi Andreas, thanks for looking into this. I am still confused about whether or not your new branch can fix the crash or not.

For the question about the crash itself, I don't have any insight though, I feel like this is a defect in the code generator. I didn't understand why mark a variable as referenced or not by the frontend may affect the code generation.

@andreasfertig
Copy link
Contributor Author

Hello @ChuanqiXu9,

Hi Andreas, thanks for looking into this. I am still confused about whether or not your new branch can fix the crash or not.

It's my pleasure! The new branch doesn't fix the crash. If I understand why it is crashing, this branch is a better change compared to the previous merged code.

For the question about the crash itself, I don't have any insight though, I feel like this is a defect in the code generator. I didn't understand why mark a variable as referenced or not by the frontend may affect the code generation.

I'm also struggling with that. As far as I could see, the coroutine part does nothing special for classes. I suspect that some other coroutine-unrelated part does something with the lambda. I also checked whether I holds a getKnownLValue, but that's not the case either.

Do you know who I could ping to get assistance with the CodeGen part?

@ChuanqiXu9
Copy link
Member

Hello @ChuanqiXu9,

Hi Andreas, thanks for looking into this. I am still confused about whether or not your new branch can fix the crash or not.

It's my pleasure! The new branch doesn't fix the crash. If I understand why it is crashing, this branch is a better change compared to the previous merged code.

For the question about the crash itself, I don't have any insight though, I feel like this is a defect in the code generator. I didn't understand why mark a variable as referenced or not by the frontend may affect the code generation.

I'm also struggling with that. As far as I could see, the coroutine part does nothing special for classes. I suspect that some other coroutine-unrelated part does something with the lambda. I also checked whether I holds a getKnownLValue, but that's not the case either.

Do you know who I could ping to get assistance with the CodeGen part?

Maybe @efriedma-quic @rjmccall

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category coroutines C++20 coroutines
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Clang error: lambda-coroutine with operator new in promise_type
7 participants