[clang] [coroutines] Introduce [[clang::coro_lifetimebound]] (PR #72516)

via cfe-commits cfe-commits at lists.llvm.org
Thu Nov 16 06:28:45 PST 2023


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang

Author: Utkarsh Saxena (usx95)

<details>
<summary>Changes</summary>



---

Patch is 20.15 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/72516.diff


10 Files Affected:

- (modified) clang/docs/ReleaseNotes.rst (+5) 
- (modified) clang/include/clang/Basic/Attr.td (+24) 
- (modified) clang/include/clang/Basic/AttrDocs.td (+61) 
- (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+4) 
- (modified) clang/include/clang/Sema/Sema.h (+5) 
- (modified) clang/lib/Sema/SemaDecl.cpp (+21-2) 
- (modified) clang/lib/Sema/SemaInit.cpp (+6-1) 
- (modified) clang/test/Misc/pragma-attribute-supported-attributes-list.test (+2) 
- (added) clang/test/SemaCXX/coro-lifetimebound.cpp (+180) 
- (added) clang/test/SemaCXX/coro-return-type-and-wrapper.cpp (+117) 


``````````diff
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index 7a131cb520aa600..46b54836bae0b8d 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -300,6 +300,11 @@ Attribute Changes in Clang
   to reduce the size of the destroy functions for coroutines which are known to
   be destroyed after having reached the final suspend point.
 
+- Clang now introduced ``[[clang::coro_return_type]]`` and ``[[clang::coro_wrapper]]``
+  attributes. A function returning a type marked with ``[[clang::coro_return_type]]``
+  should be a coroutine. A non-coroutine function marked with ``[[clang::coro_wrapper]]``
+  is still allowed to return the such a type. This is helpful for analyzers to recognize coroutines from the function signatures.
+
 Improvements to Clang's diagnostics
 -----------------------------------
 - Clang constexpr evaluator now prints template arguments when displaying
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 31434565becaec6..2f8dbd94c14aa29 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -1094,6 +1094,30 @@ def CoroOnlyDestroyWhenComplete : InheritableAttr {
   let SimpleHandler = 1;
 }
 
+def CoroReturnType : InheritableAttr {
+  let Spellings = [Clang<"coro_return_type">];
+  let Subjects = SubjectList<[CXXRecord]>;
+  let LangOpts = [CPlusPlus];
+  let Documentation = [CoroReturnTypeAndWrapperDoc];
+  let SimpleHandler = 1;
+}
+
+def CoroWrapper : InheritableAttr {
+  let Spellings = [Clang<"coro_wrapper">];
+  let Subjects = SubjectList<[Function]>;
+  let LangOpts = [CPlusPlus];
+  let Documentation = [CoroReturnTypeAndWrapperDoc];
+  let SimpleHandler = 1;
+}
+
+def CoroLifetimeBound : InheritableAttr {
+  let Spellings = [Clang<"coro_wrapper">];
+  let Subjects = SubjectList<[CXXRecord]>;
+  let LangOpts = [CPlusPlus];
+  let Documentation = [CoroReturnTypeAndWrapperDoc];
+  let SimpleHandler = 1;
+}
+
 // OSObject-based attributes.
 def OSConsumed : InheritableParamAttr {
   let Spellings = [Clang<"os_consumed">];
diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td
index fa6f6acd0c30e88..ac385fe16880970 100644
--- a/clang/include/clang/Basic/AttrDocs.td
+++ b/clang/include/clang/Basic/AttrDocs.td
@@ -7482,3 +7482,64 @@ generation of the other destruction cases, optimizing the above `foo.destroy` to
 
   }];
 }
+
+
+def CoroReturnTypeAndWrapperDoc : Documentation {
+  let Category = DocCatDecl;
+  let Content = [{
+The `[[clang::coro_return_type]]` attribute is used to help static analyzers to recognize coroutines from the function signatures.
+
+The ``coro_return_type`` attribute should be marked on a C++ class to mark it as
+a **coroutine return type (CRT)**.
+
+A function ``R func(P1, .., PN)`` has a coroutine return type (CRT) ``R`` if ``R``
+is marked by ``[[clang::coro_return_type]]`` and  ``R`` has a promise type associated to it
+(i.e., std​::​coroutine_traits<R, P1, .., PN>​::​promise_type is a valid promise type).
+
+If the return type of a function is a ``CRT`` then the function must be a coroutine.
+Otherwise the program is invalid. It is allowed for a non-coroutine to return a ``CRT``
+if the function is marked with ``[[clang::coro_wrapper]]``.
+
+The ``coro_wrapper`` attribute should be marked on a C++ function to mark it as
+a **coroutine wrapper**. A coroutine wrapper is a function which returns a ``CRT``,
+is not a coroutine itself and is marked with ``[[clang::coro_wrapper]]``.
+
+From a language perspective, it is not possible to differentiate between a coroutine and a
+function returning a CRT by merely looking at the function signature.
+
+Clang will enforce that all functions that return a ``CRT``  are either coroutines or marked 
+with `[[clang::coro_wrapper]]`. Coroutine wrappers, in particular, are susceptible to capturing
+references to temporaries and other lifetime issues. This allows to avoid such lifetime
+issues with coroutine wrappers.
+
+For example,
+
+.. code-block:: c++
+
+  // This is a CRT.
+  template <typename T> struct [[clang::coro_return_type]] Task {
+    using promise_type = some_promise_type;
+  };
+
+  Task<int> increment(int a) { co_return a + 1; } // Fine. This is a coroutine.
+  Task<int> foo() { return increment(1); } // Error. foo is not a coroutine.
+
+  // Fine for a coroutine wrapper to return a CRT.
+  Task<int> [[clang::coro_wrapper]] foo() { return increment(1); }
+
+  void bar() {
+    // Invalid. This intantiates a function which returns a CRT but is not marked as
+    // a coroutine wrapper.
+    std::function<Task<int>(int)> f = increment;
+  }
+
+}];
+}
+
+def CoroLifetimeBoundDoc : Documentation {
+  let Category = DocCatDecl;
+  let Content = [{
+The `[[clang::coro_lifetimebound]]` attribute
+
+}];
+}
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 4614324babb1c91..cca5761bb374e4c 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -11591,6 +11591,10 @@ def err_conflicting_aligned_options : Error <
 def err_coro_invalid_addr_of_label : Error<
   "the GNU address of label extension is not allowed in coroutines."
 >;
+def err_coroutine_return_type : Error<
+  "function returns a type %0 makred with [[clang::coro_return_type]] but is neither a coroutine nor a coroutine wrapper; "
+  "non-coroutines should be marked with [[clang::coro_wrapper]] to allow returning coroutine return type"
+>;
 } // end of coroutines issue category
 
 let CategoryName = "Documentation Issue" in {
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index f69f366c1750918..7d029810f0ad67e 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -11183,6 +11183,11 @@ class Sema final {
   bool buildCoroutineParameterMoves(SourceLocation Loc);
   VarDecl *buildCoroutinePromise(SourceLocation Loc);
   void CheckCompletedCoroutineBody(FunctionDecl *FD, Stmt *&Body);
+
+  // As a clang extension, enforces that a function returning a type marked with
+  // [[clang::coro_return_type]] must be a coroutine. A function marked with
+  // [[clang::coro_wrapper]] are still allowed to return such a type.
+  void CheckCoroutineWrapper(FunctionDecl *FD);
   /// Lookup 'coroutine_traits' in std namespace and std::experimental
   /// namespace. The namespace found is recorded in Namespace.
   ClassTemplateDecl *lookupCoroutineTraits(SourceLocation KwLoc,
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 3876eb501083acb..8e2963a62c1ec16 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -17,6 +17,7 @@
 #include "clang/AST/CXXInheritance.h"
 #include "clang/AST/CharUnits.h"
 #include "clang/AST/CommentDiagnostic.h"
+#include "clang/AST/Decl.h"
 #include "clang/AST/DeclCXX.h"
 #include "clang/AST/DeclObjC.h"
 #include "clang/AST/DeclTemplate.h"
@@ -26,6 +27,7 @@
 #include "clang/AST/NonTrivialTypeVisitor.h"
 #include "clang/AST/Randstruct.h"
 #include "clang/AST/StmtCXX.h"
+#include "clang/AST/Type.h"
 #include "clang/Basic/Builtins.h"
 #include "clang/Basic/HLSLRuntime.h"
 #include "clang/Basic/PartialDiagnostic.h"
@@ -15811,6 +15813,20 @@ static void diagnoseImplicitlyRetainedSelf(Sema &S) {
           << FixItHint::CreateInsertion(P.first, "self->");
 }
 
+void Sema::CheckCoroutineWrapper(FunctionDecl *FD) {
+  if (!FD || getCurFunction()->isCoroutine())
+    return;
+  RecordDecl *RD = FD->getReturnType()->getAsRecordDecl();
+  if (!RD || !RD->getUnderlyingDecl()->hasAttr<CoroReturnTypeAttr>())
+    return;
+  // Allow `get_return_object()`.
+  if (FD->getDeclName().isIdentifier() &&
+      FD->getName().equals("get_return_object") && FD->param_empty())
+    return;
+  if (!FD->hasAttr<CoroWrapperAttr>())
+    Diag(FD->getLocation(), diag::err_coroutine_return_type) << RD;
+}
+
 Decl *Sema::ActOnFinishFunctionBody(Decl *dcl, Stmt *Body,
                                     bool IsInstantiation) {
   FunctionScopeInfo *FSI = getCurFunction();
@@ -15822,8 +15838,11 @@ Decl *Sema::ActOnFinishFunctionBody(Decl *dcl, Stmt *Body,
   sema::AnalysisBasedWarnings::Policy WP = AnalysisWarnings.getDefaultPolicy();
   sema::AnalysisBasedWarnings::Policy *ActivePolicy = nullptr;
 
-  if (getLangOpts().Coroutines && FSI->isCoroutine())
-    CheckCompletedCoroutineBody(FD, Body);
+  if (getLangOpts().Coroutines) {
+    if (FSI->isCoroutine())
+      CheckCompletedCoroutineBody(FD, Body);
+    CheckCoroutineWrapper(FD);
+  }
 
   {
     // Do not call PopExpressionEvaluationContext() if it is a lambda because
diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp
index 80b51b09bf5445f..14c45a69a244bc4 100644
--- a/clang/lib/Sema/SemaInit.cpp
+++ b/clang/lib/Sema/SemaInit.cpp
@@ -11,6 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "clang/AST/ASTContext.h"
+#include "clang/AST/Attrs.inc"
 #include "clang/AST/DeclObjC.h"
 #include "clang/AST/ExprCXX.h"
 #include "clang/AST/ExprObjC.h"
@@ -7580,10 +7581,14 @@ static void visitLifetimeBoundArguments(IndirectLocalPath &Path, Expr *Call,
   if (ObjectArg && implicitObjectParamIsLifetimeBound(Callee))
     VisitLifetimeBoundArg(Callee, ObjectArg);
 
+  bool checkCoroCall = false;
+  if (const auto *RD = Callee->getReturnType()->getAsRecordDecl()) {
+    checkCoroCall |= RD->hasAttr<CoroReturnTypeAttr>();
+  }
   for (unsigned I = 0,
                 N = std::min<unsigned>(Callee->getNumParams(), Args.size());
        I != N; ++I) {
-    if (Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
+    if (checkCoroCall || Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
       VisitLifetimeBoundArg(Callee->getParamDecl(I), Args[I]);
   }
 }
diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test
index 969794073a5f2e8..a57bc011c059483 100644
--- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test
+++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test
@@ -57,6 +57,8 @@
 // CHECK-NEXT: ConsumableSetOnRead (SubjectMatchRule_record)
 // CHECK-NEXT: Convergent (SubjectMatchRule_function)
 // CHECK-NEXT: CoroOnlyDestroyWhenComplete (SubjectMatchRule_record)
+// CHECK-NEXT: CoroReturnType (SubjectMatchRule_record)
+// CHECK-NEXT: CoroWrapper
 // CHECK-NEXT: CountedBy (SubjectMatchRule_field)
 // CHECK-NEXT: DLLExport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface)
 // CHECK-NEXT: DLLImport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface)
diff --git a/clang/test/SemaCXX/coro-lifetimebound.cpp b/clang/test/SemaCXX/coro-lifetimebound.cpp
new file mode 100644
index 000000000000000..52c2e8e147c7538
--- /dev/null
+++ b/clang/test/SemaCXX/coro-lifetimebound.cpp
@@ -0,0 +1,180 @@
+// RUN: %clang_cc1 -triple x86_64-apple-darwin9 %s -std=c++20 -fsyntax-only -verify -Wall -Wextra -Wno-error=unreachable-code -Wno-unused
+
+#include "Inputs/std-coroutine.h"
+
+using std::suspend_always;
+using std::suspend_never;
+
+template <typename T> struct [[clang::coro_lifetimebound, clang::coro_return_type]] Gen {
+  struct promise_type {
+    Gen<T> get_return_object() {
+      return {};
+    }
+    suspend_always initial_suspend();
+    suspend_always final_suspend() noexcept;
+    void unhandled_exception();
+    void return_value(const T &t);
+
+    template <typename U>
+    auto await_transform(const Gen<U> &) {
+      struct awaitable {
+        bool await_ready() noexcept { return false; }
+        void await_suspend(std::coroutine_handle<>) noexcept {}
+        U await_resume() noexcept { return {}; }
+      };
+      return awaitable{};
+    }
+  };
+};
+
+template <typename T> using Co = Gen<T>;
+
+Gen<int> foo_coro(const int& b);
+
+Gen<int> plain_return_foo_decl(int b) {
+  return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
+}
+
+Gen<int> foo_coro(const int& b) {
+  if (b > 0)
+    co_return 1;
+  co_return 2;
+}
+
+int getInt() { return 0; }
+
+Co<int> bar_coro(const int &b, int c) {
+  int x = co_await foo_coro(b);
+  int y = co_await foo_coro(1);
+  int z = co_await foo_coro(getInt());
+  auto unsafe1 = foo_coro(1); // expected-warning {{temporary whose address is used as value of local variable}}
+  auto unsafe2 = foo_coro(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
+  auto  safe1 = foo_coro(b);
+  auto  safe2 = foo_coro(c);
+  co_return co_await foo_coro(co_await foo_coro(1));
+}
+
+Gen<int> plain_return_co(int b) {
+  return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
+}
+
+Gen<int> safe_forwarding(const int& b) {
+  return foo_coro(b);
+}
+
+Gen<int> unsafe_wrapper(int b) {
+  return safe_forwarding(b); // expected-warning {{address of stack memory associated with parameter}}
+}
+
+Co<int> complex_plain_return(int b) {
+  return b > 0 
+      ? foo_coro(1)   // expected-warning {{returning address of local temporary object}}
+      : bar_coro(0, 1); // expected-warning {{returning address of local temporary object}}
+}
+
+void lambdas() {
+  auto unsafe_lambda = [](int b) {
+    return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
+  };
+  auto safe_lambda = [](int b) -> Co<int> {
+    int x = co_await foo_coro(1);
+    co_return x + co_await foo_coro(b);
+  };
+}
+// =============================================================================
+// Safe usage when parameters are value
+// =============================================================================
+namespace by_value {
+Gen<int> value_coro(int b) { co_return co_await foo_coro(b); }
+
+Gen<int> wrapper1(int b) { return value_coro(b); }
+Gen<int> wrapper2(const int& b) { return value_coro(b); }
+}
+
+// =============================================================================
+// std::function like wrappers. (Eg: https://godbolt.org/z/x3PfG3Gfb)
+// =============================================================================
+namespace std {
+
+template <class T>
+T&& forward(typename remove_reference<T>::type& t) noexcept;
+template <class T>
+T&& forward(typename remove_reference<T>::type&& t) noexcept;
+
+template <bool, typename>
+class function;
+
+template <bool UseFp, typename ReturnValue, typename... Args>
+class function<UseFp, ReturnValue(Args...)> {
+   public:
+    class Callable {
+       public:
+        ReturnValue operator()(Args&&...) const { return {}; }
+    };
+    Callable* callable_;
+    ReturnValue operator()(Args... args) const
+        requires (!UseFp)
+    {
+        return (*callable_)(std::forward<Args>(args)...); // expected-warning 3 {{address of stack memory}}
+    }
+
+    // Callable can also be a function pointer type.
+    using FpCallableType = ReturnValue(Args&&...);
+    FpCallableType* fp_callable_;
+    ReturnValue operator()(Args... args) const
+        requires(UseFp)
+    {
+        return fp_callable_(std::forward<Args>(args)...);
+    }
+
+    template <typename T>
+    function& operator=(T) {}
+    template <typename T>
+    function(T) {}
+    function() {}
+};
+}  // namespace std
+
+namespace without_function_pointers {
+template <typename T>
+using fn = std::function<false, T>;
+
+void use_std_function() {
+    fn<Co<int>(const int&, const int&)> pass;
+    pass(1, 1);
+    // Lifetime issue with one parameter.
+    fn<Co<int>(const int&, int)> fail;
+    fail(1, 1);        // expected-note {{in instantiation of}}
+    // Lifetime issue with both parameters.
+    fn<Co<int>(int, int)> fail_twice;
+    fail_twice(1, 1);  // expected-note {{in instantiation of}}
+}
+} // namespace without_function_pointers
+
+// =============================================================================
+// Future work: Function pointers needs to be fixed.
+// =============================================================================
+namespace with_function_pointers {
+template <typename T>
+using fn = std::function<true, T>;
+
+void use_std_function() {
+    fn<Co<int>(int, int)> fail;
+    fail(1, 1); // FIXME: Must error.
+}
+} // namespace function_pointers
+
+// =============================================================================
+// Future work: Reference wrappers needs to be fixed.
+// =============================================================================
+namespace with_reference_wrappers {
+struct RefWrapper {
+  RefWrapper(const int &a): b(a) {}
+  const int &b;
+};
+Co<int> RefWrapperCoro(RefWrapper a) { co_return a.b; }
+
+Co<int> UnsafeWrapper(int a) {
+  return RefWrapperCoro(a); // FIXME: Must error.
+}
+}
diff --git a/clang/test/SemaCXX/coro-return-type-and-wrapper.cpp b/clang/test/SemaCXX/coro-return-type-and-wrapper.cpp
new file mode 100644
index 000000000000000..71e28df31e56387
--- /dev/null
+++ b/clang/test/SemaCXX/coro-return-type-and-wrapper.cpp
@@ -0,0 +1,117 @@
+// RUN: %clang_cc1 -triple x86_64-apple-darwin9 %s -std=c++20 -fsyntax-only -verify -Wall -Wextra
+#include "Inputs/std-coroutine.h"
+
+using std::suspend_always;
+using std::suspend_never;
+
+
+template <typename T> struct [[clang::coro_return_type]] Gen {
+  struct promise_type {
+    Gen<T> get_return_object() {
+      return {};
+    }
+    suspend_always initial_suspend();
+    suspend_always final_suspend() noexcept;
+    void unhandled_exception();
+    void return_value(const T &t);
+
+    template <typename U>
+    auto await_transform(const Gen<U> &) {
+      struct awaitable {
+        bool await_ready() noexcept { return false; }
+        void await_suspend(std::coroutine_handle<>) noexcept {}
+        U await_resume() noexcept { return {}; }
+      };
+      return awaitable{};
+    }
+  };
+};
+
+Gen<int> foo_coro(int b);
+Gen<int> foo_coro(int b) { co_return b; }
+
+[[clang::coro_wrapper]] Gen<int> marked_wrapper1(int b) { return foo_coro(b); }
+
+// expected-error at +1 {{neither a coroutine nor a coroutine wrapper}}
+Gen<int> non_marked_wrapper(int b) { return foo_coro(b); }
+
+namespace using_decl {
+template <typename T> using Co = Gen<T>;
+
+[[clang::coro_wrapper]] Co<int> marked_wrapper1(int b) { return foo_coro(b); }
+
+// expected-error at +1 {{neither a coroutine nor a coroutine wrapper}}
+Co<int> non_marked_wrapper(int b) { return foo_coro(b); }
+} // namespace using_decl
+
+namespace lambdas {
+void foo() {
+  auto coro_lambda = []() -> Gen<int> {
+    co_return 1;
+  };
+  // expected-error at +1 {{neither a coroutine nor a coroutine wrapper}}
+  auto wrapper_lambda = []() -> Gen<int> {
+    return foo_coro(1);
+  };
+}
+}
+
+namespace std_function {
+namespace std {
+template <typename> class function;
+
+template <typename ReturnValue, typename... Args>
+class function<ReturnValue(Args...)> {
+public:
+  template <typename T> function &operator=(T) {}
+  template <typename T> function(T) {}
+  // expected-error at +1 {{neither a coroutine nor a coroutine wrapper}}
+  ReturnValue operator()(Args... args) const {
+    return callable_->Invoke(args...);  // expected-note {{in instantiation of member}}
+  }
+
+private:
+  class Callable {
+  public:
+    // expected-error at +1 {{neither a coroutine nor a coroutine wrapper}}
+    ReturnValue Invoke(Args...) const { return {}; }
+  };
+  Callable* callable_;
+};
+} // namespace std
+
+void use_std_function() {
+  std::function<int(bool)> foo = [](bool b) { return b ? 1 : 2; };
+  // expected-error at +1 {{neither a coroutine nor a coroutine wrapper}}
+  std::function<Gen<int>(bool)> test1 = [](bool b) {
+    return foo_coro(b);
+  };
+  std::function<Gen<int>(bool)> test2 = [](bool) -> Gen<int> {
+    co_return 1;
+  };
+  std::function<Gen<int>(bool)> test3 = foo_coro;
+
+  foo(true);   // Fine.
+  test1(true); // expected-note 2 {{in instantiation of member}}
+  test2(true);
+  test3(true);
+}
+} // namespace std_function
+
+// different_promise_type
+class [[clang::coro_return_type]] Task{};
+struct my_promise_type {
+  Task get_return_object() {
+    return {};
+  }
+  suspend_always initial_suspend();
+  suspend_always final_suspend() noexcept;
+  void unhandled_exception();
+};
+namespace std {
+template<> class coroutine_traits<Task, int> {
+    using promise_type = m...
[truncated]

``````````

</details>


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


More information about the cfe-commits mailing list