Skip to content

[Clang] Implement P2747 constexpr placement new #104586

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

Merged
merged 7 commits into from
Aug 23, 2024

Conversation

cor3ntin
Copy link
Contributor

@cor3ntin cor3ntin commented Aug 16, 2024

The implementation follows the resolution of CWG2922

@cor3ntin cor3ntin requested a review from AaronBallman August 16, 2024 12:50
@cor3ntin cor3ntin requested a review from Endilll as a code owner August 16, 2024 12:50
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Aug 16, 2024
@llvmbot
Copy link
Member

llvmbot commented Aug 16, 2024

@llvm/pr-subscribers-clang

Author: cor3ntin (cor3ntin)

Changes

In C++26 and as an extension in C++20.
The implementation follows the resolution of CWG2922


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

12 Files Affected:

  • (modified) clang/docs/LanguageExtensions.rst (+1)
  • (modified) clang/docs/ReleaseNotes.rst (+4-1)
  • (modified) clang/include/clang/Basic/DiagnosticASTKinds.td (+1-1)
  • (modified) clang/lib/AST/ExprConstant.cpp (+37-23)
  • (modified) clang/lib/Frontend/InitPreprocessor.cpp (+1-1)
  • (modified) clang/test/AST/Interp/new-delete.cpp (+2-2)
  • (modified) clang/test/CXX/drs/cwg29xx.cpp (+26)
  • (modified) clang/test/Lexer/cxx-features.cpp (+1-1)
  • (modified) clang/test/SemaCXX/constant-expression-cxx2a.cpp (+1-1)
  • (modified) clang/test/SemaCXX/cxx2a-constexpr-dynalloc.cpp (+80-2)
  • (modified) clang/www/cxx_dr_status.html (+1-1)
  • (modified) clang/www/cxx_status.html (+1-1)
diff --git a/clang/docs/LanguageExtensions.rst b/clang/docs/LanguageExtensions.rst
index 114e742f3561b7..296bb85f5e72b2 100644
--- a/clang/docs/LanguageExtensions.rst
+++ b/clang/docs/LanguageExtensions.rst
@@ -1506,6 +1506,7 @@ Attributes on Structured Bindings            __cpp_structured_bindings        C+
 Pack Indexing                                __cpp_pack_indexing              C++26         C++03
 ``= delete ("should have a reason");``       __cpp_deleted_function           C++26         C++03
 Variadic Friends                             __cpp_variadic_friend            C++26         C++03
+``constexpr`` placement new                  __cpp_constexpr                  C++26         C++20
 -------------------------------------------- -------------------------------- ------------- -------------
 Designated initializers (N494)                                                C99           C89
 Array & element qualification (N2607)                                         C23           C89
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index ffdd063ec99037..22864062fba7fd 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -73,7 +73,7 @@ C++ Specific Potentially Breaking Changes
     template <> // error: extraneous template head
     template <typename T>
     void f();
-    
+
 ABI Changes in This Version
 ---------------------------
 
@@ -131,6 +131,9 @@ C++2c Feature Support
 
 - Implemented `P2893R3 Variadic Friends <https://wg21.link/P2893>`_
 
+- Implemented `P2747R2 constexpr placement new <https://wg21.link/P2747R2>`_.
+
+
 Resolutions to C++ Defect Reports
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
diff --git a/clang/include/clang/Basic/DiagnosticASTKinds.td b/clang/include/clang/Basic/DiagnosticASTKinds.td
index f317c5ac44f32b..569d2cc20a526c 100644
--- a/clang/include/clang/Basic/DiagnosticASTKinds.td
+++ b/clang/include/clang/Basic/DiagnosticASTKinds.td
@@ -335,7 +335,7 @@ def note_constexpr_new_non_replaceable : Note<
 def note_constexpr_new_placement : Note<
   "this placement new expression is not yet supported in constant expressions">;
 def note_constexpr_placement_new_wrong_type : Note<
-  "placement new would change type of storage from %0 to %1">;
+    "placement new would change type of storage from %0 to %1">;
 def note_constexpr_new_negative : Note<
   "cannot allocate array; evaluated array bound %0 is negative">;
 def note_constexpr_new_too_large : Note<
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index 09edbb6641650a..50cffa95fc01ce 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -44,18 +44,23 @@
 #include "clang/AST/CXXInheritance.h"
 #include "clang/AST/CharUnits.h"
 #include "clang/AST/CurrentSourceLocExprScope.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
 #include "clang/AST/Expr.h"
 #include "clang/AST/OSLog.h"
 #include "clang/AST/OptionalDiagnostic.h"
 #include "clang/AST/RecordLayout.h"
 #include "clang/AST/StmtVisitor.h"
+#include "clang/AST/Type.h"
 #include "clang/AST/TypeLoc.h"
 #include "clang/Basic/Builtins.h"
 #include "clang/Basic/DiagnosticSema.h"
+#include "clang/Basic/LangStandard.h"
 #include "clang/Basic/TargetInfo.h"
 #include "llvm/ADT/APFixedPoint.h"
 #include "llvm/ADT/SmallBitVector.h"
 #include "llvm/ADT/StringExtras.h"
+#include "llvm/Support/Casting.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/SaveAndRestore.h"
 #include "llvm/Support/SipHash.h"
@@ -6691,7 +6696,7 @@ static bool HandleDestructionImpl(EvalInfo &Info, SourceRange CallRange,
     if (Size && Size > Value.getArrayInitializedElts())
       expandArray(Value, Value.getArraySize() - 1);
 
-    for (; Size != 0; --Size) {
+    for (Size = Value.getArraySize(); Size != 0; --Size) {
       APValue &Elem = Value.getArrayInitializedElt(Size - 1);
       if (!HandleLValueArrayAdjustment(Info, &LocE, ElemLV, ElemT, -1) ||
           !HandleDestructionImpl(Info, CallRange, ElemLV, Elem, ElemT))
@@ -10003,23 +10008,14 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
     return false;
 
   FunctionDecl *OperatorNew = E->getOperatorNew();
+  QualType AllocType = E->getAllocatedType();
+  QualType TargetType = AllocType;
 
   bool IsNothrow = false;
   bool IsPlacement = false;
-  if (OperatorNew->isReservedGlobalPlacementOperator() &&
-      Info.CurrentCall->isStdFunction() && !E->isArray()) {
-    // FIXME Support array placement new.
-    assert(E->getNumPlacementArgs() == 1);
-    if (!EvaluatePointer(E->getPlacementArg(0), Result, Info))
-      return false;
-    if (Result.Designator.Invalid)
-      return false;
-    IsPlacement = true;
-  } else if (!OperatorNew->isReplaceableGlobalAllocationFunction()) {
-    Info.FFDiag(E, diag::note_constexpr_new_non_replaceable)
-        << isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
-    return false;
-  } else if (E->getNumPlacementArgs()) {
+
+  if (E->getNumPlacementArgs() && E->getNumPlacementArgs() == 1 &&
+      E->getPlacementArg(0)->getType()->isNothrowT()) {
     // The only new-placement list we support is of the form (std::nothrow).
     //
     // FIXME: There is no restriction on this, but it's not clear that any
@@ -10030,14 +10026,25 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
     // (which should presumably be valid only if N is a multiple of
     // alignof(int), and in any case can't be deallocated unless N is
     // alignof(X) and X has new-extended alignment).
-    if (E->getNumPlacementArgs() != 1 ||
-        !E->getPlacementArg(0)->getType()->isNothrowT())
-      return Error(E, diag::note_constexpr_new_placement);
-
     LValue Nothrow;
     if (!EvaluateLValue(E->getPlacementArg(0), Nothrow, Info))
       return false;
     IsNothrow = true;
+  } else if (OperatorNew->isReservedGlobalPlacementOperator()) {
+    if (!EvaluatePointer(E->getPlacementArg(0), Result, Info))
+      return false;
+    if (Result.Designator.Invalid)
+      return false;
+    /// if(!lifetimeStartedInEvaluation(Info, Result.getLValueBase()))
+    //  return false;
+    TargetType = E->getPlacementArg(0)->getType();
+    IsPlacement = true;
+  } else if (E->getNumPlacementArgs()) {
+    return Error(E, diag::note_constexpr_new_placement);
+  } else if (!OperatorNew->isReplaceableGlobalAllocationFunction()) {
+    Info.FFDiag(E, diag::note_constexpr_new_non_replaceable)
+        << isa<CXXMethodDecl>(OperatorNew) << OperatorNew;
+    return false;
   }
 
   const Expr *Init = E->getInitializer();
@@ -10045,7 +10052,6 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
   const CXXConstructExpr *ResizedArrayCCE = nullptr;
   bool ValueInit = false;
 
-  QualType AllocType = E->getAllocatedType();
   if (std::optional<const Expr *> ArraySize = E->getArraySize()) {
     const Expr *Stripped = *ArraySize;
     for (; auto *ICE = dyn_cast<ImplicitCastExpr>(Stripped);
@@ -10139,9 +10145,17 @@ bool PointerExprEvaluator::VisitCXXNewExpr(const CXXNewExpr *E) {
       bool found(APValue &Subobj, QualType SubobjType) {
         // FIXME: Reject the cases where [basic.life]p8 would not permit the
         // old name of the object to be used to name the new object.
-        if (!Info.Ctx.hasSameUnqualifiedType(SubobjType, AllocType)) {
-          Info.FFDiag(E, diag::note_constexpr_placement_new_wrong_type) <<
-            SubobjType << AllocType;
+        unsigned SubobjectSize = 1;
+        unsigned AllocSize = 1;
+        if (auto *CAT = dyn_cast<ConstantArrayType>(AllocType))
+          AllocSize = CAT->getZExtSize();
+        if (auto *CAT = dyn_cast<ConstantArrayType>(SubobjType))
+          SubobjectSize = CAT->getZExtSize();
+        if (SubobjectSize < AllocSize ||
+            !Info.Ctx.hasSimilarType(Info.Ctx.getBaseElementType(SubobjType),
+                                     Info.Ctx.getBaseElementType(AllocType))) {
+          Info.FFDiag(E, diag::note_constexpr_placement_new_wrong_type)
+              << SubobjType << AllocType;
           return false;
         }
         Value = &Subobj;
diff --git a/clang/lib/Frontend/InitPreprocessor.cpp b/clang/lib/Frontend/InitPreprocessor.cpp
index 4f2856dd2247f8..61260a3379828d 100644
--- a/clang/lib/Frontend/InitPreprocessor.cpp
+++ b/clang/lib/Frontend/InitPreprocessor.cpp
@@ -660,7 +660,7 @@ static void InitializeCPlusPlusFeatureTestMacros(const LangOptions &LangOpts,
     Builder.defineMacro("__cpp_unicode_literals", "200710L");
     Builder.defineMacro("__cpp_user_defined_literals", "200809L");
     Builder.defineMacro("__cpp_lambdas", "200907L");
-    Builder.defineMacro("__cpp_constexpr", LangOpts.CPlusPlus26   ? "202306L"
+    Builder.defineMacro("__cpp_constexpr", LangOpts.CPlusPlus26   ? "202406L"
                                            : LangOpts.CPlusPlus23 ? "202211L"
                                            : LangOpts.CPlusPlus20 ? "201907L"
                                            : LangOpts.CPlusPlus17 ? "201603L"
diff --git a/clang/test/AST/Interp/new-delete.cpp b/clang/test/AST/Interp/new-delete.cpp
index 6bb30bc19f110c..0c823e7000f853 100644
--- a/clang/test/AST/Interp/new-delete.cpp
+++ b/clang/test/AST/Interp/new-delete.cpp
@@ -245,7 +245,7 @@ namespace std {
 namespace PlacementNew {
   constexpr int foo() { // both-error {{never produces a constant expression}}
     char c[sizeof(int)];
-    new (c) int{12}; // ref-note {{call to placement 'operator new'}} \
+    new (c) int{12}; // ref-note {{placement new would change type of storage from 'char' to 'int'}} \
                      // expected-note {{subexpression not valid in a constant expression}}
     return 0;
   }
@@ -309,7 +309,7 @@ namespace placement_new_delete {
   constexpr bool bad(int which) {
     switch (which) {
     case 0:
-      delete new (placement_new_arg{}) int; // ref-note {{call to placement 'operator new'}} \
+      delete new (placement_new_arg{}) int; // ref-note {{this placement new expression is not yet supported in constant expression}} \
                                             // expected-note {{subexpression not valid in a constant expression}}
       break;
 
diff --git a/clang/test/CXX/drs/cwg29xx.cpp b/clang/test/CXX/drs/cwg29xx.cpp
index 8cac9f283980b6..7e04aa0c3a4942 100644
--- a/clang/test/CXX/drs/cwg29xx.cpp
+++ b/clang/test/CXX/drs/cwg29xx.cpp
@@ -23,3 +23,29 @@ struct S {
   friend class C<Ts>::Nested...; // expected-error {{friend declaration expands pack 'Ts' that is declared it its own template parameter list}}
 };
 } // namespace cwg2917
+
+#if __cplusplus >= 202002
+
+namespace std {
+  using size_t = decltype(sizeof(0));
+};
+void *operator new(std::size_t, void *p) { return p; }
+void* operator new[] (std::size_t, void* p) {return p;}
+
+
+namespace cwg2922 { // cwg2922: 20 open 2024-07-10
+union U { int a, b; };
+constexpr U nondeterministic(bool i) {
+  if(i) {
+    U u;
+    new (&u) int();
+    // expected-note@-1 {{placement new would change type of storage from 'U' to 'int'}}
+    return u;
+  }
+  return {};
+}
+constexpr U _ = nondeterministic(true);
+// expected-error@-1 {{constexpr variable '_' must be initialized by a constant expression}} \
+// expected-note@-1 {{in call to 'nondeterministic(true)'}}
+}
+#endif
diff --git a/clang/test/Lexer/cxx-features.cpp b/clang/test/Lexer/cxx-features.cpp
index 1c51013ca06f77..4a06d29ae9dbc6 100644
--- a/clang/test/Lexer/cxx-features.cpp
+++ b/clang/test/Lexer/cxx-features.cpp
@@ -317,7 +317,7 @@
 #error "wrong value for __cpp_lambdas"
 #endif
 
-#if check(constexpr, 0, 200704, 201304, 201603, 201907, 202211, 202306)
+#if check(constexpr, 0, 200704, 201304, 201603, 201907, 202211, 202406L)
 #error "wrong value for __cpp_constexpr"
 #endif
 
diff --git a/clang/test/SemaCXX/constant-expression-cxx2a.cpp b/clang/test/SemaCXX/constant-expression-cxx2a.cpp
index e4d97dcb73562d..b8cbf723a96a76 100644
--- a/clang/test/SemaCXX/constant-expression-cxx2a.cpp
+++ b/clang/test/SemaCXX/constant-expression-cxx2a.cpp
@@ -994,7 +994,7 @@ namespace placement_new_delete {
   constexpr bool bad(int which) {
     switch (which) {
     case 0:
-      delete new (placement_new_arg{}) int; // expected-note {{call to placement 'operator new'}}
+      delete new (placement_new_arg{}) int; // expected-note {{this placement new expression is not yet supported in constant expressions}}
       break;
 
     case 1:
diff --git a/clang/test/SemaCXX/cxx2a-constexpr-dynalloc.cpp b/clang/test/SemaCXX/cxx2a-constexpr-dynalloc.cpp
index 357dc67bd5ad22..b02c90b88d1ac6 100644
--- a/clang/test/SemaCXX/cxx2a-constexpr-dynalloc.cpp
+++ b/clang/test/SemaCXX/cxx2a-constexpr-dynalloc.cpp
@@ -90,9 +90,10 @@ constexpr int no_deallocate_nonalloc = (std::allocator<int>().deallocate((int*)&
 // expected-note@-2 {{declared here}}
 
 void *operator new(std::size_t, void *p) { return p; }
-constexpr bool no_placement_new_in_user_code() { // expected-error {{never produces a constant expression}}
+void* operator new[] (std::size_t, void* p) {return p;}
+constexpr bool no_placement_new_in_user_code() {
   int a;
-  new (&a) int(42); // expected-note {{call to placement 'operator new'}}
+  new (&a) int(42);
   return a == 42;
 }
 
@@ -239,3 +240,80 @@ void f() {
 }
 
 }
+
+namespace placement_new {
+
+template <typename T, typename U>
+constexpr void f(T* t, U u) {
+    new (t) U(u);
+}
+
+consteval int ok() {
+    int i;
+    new (&i) int(0);
+    new (&i) int[1]{1};
+    new (static_cast<void*>(&i)) int(0);
+    return 0;
+}
+
+consteval int conversion() { // expected-error {{consteval function never produces a constant expression}}
+    int i;
+    new (static_cast<void*>(&i)) float(0);
+    // expected-note@-1 {{placement new would change type of storage from 'int' to 'float'}}
+    return 0;
+}
+
+consteval int indeterminate() {
+    int * indeterminate;
+    new (indeterminate) int(0);
+    // expected-note@-1 {{read of uninitialized object is not allowed in a constant expression}}
+    return 0;
+}
+
+consteval int array1() {
+    int i[2];
+    new (&i) int[]{1,2};
+    new (&i) int[]{1};
+    new (&i) int(0);
+    new (static_cast<void*>(&i)) int[]{1,2};
+    new (static_cast<void*>(&i)) int[]{1};
+    return 0;
+}
+
+consteval int array2() { // expected-error {{consteval function never produces a constant expression}}
+    int i[1];
+    new (&i) int[2];
+    //expected-note@-1 {{placement new would change type of storage from 'int[1]' to 'int[2]'}}
+    return 0;
+}
+
+struct S{
+    int* i;
+    constexpr S() : i(new int(42)) {} // expected-note {{allocation performed here was not deallocated}}
+    constexpr ~S() {delete i;}
+};
+
+consteval void alloc() {
+    S* s = new S();
+    s->~S();
+    new (s) S();
+    delete s;
+}
+
+
+consteval void alloc_err() {
+    S* s = new S();
+    new (s) S();
+    delete s;
+}
+
+
+
+int a = ok();
+int c = indeterminate(); // expected-error {{call to consteval function 'placement_new::indeterminate' is not a constant expression}} \
+             // expected-note {{in call to 'indeterminate()'}}
+int d = array1();
+int alloc1 = (alloc(), 0);
+int alloc2 = (alloc_err(), 0); // expected-error {{call to consteval function 'placement_new::alloc_err' is not a constant expression}}
+
+}
diff --git a/clang/www/cxx_dr_status.html b/clang/www/cxx_dr_status.html
index c901e1ce0e15b3..de84d94c20a152 100755
--- a/clang/www/cxx_dr_status.html
+++ b/clang/www/cxx_dr_status.html
@@ -17348,7 +17348,7 @@ <h2 id="cxxdr">C++ defect report implementation status</h2>
     <td><a href="https://cplusplus.github.io/CWG/issues/2922.html">2922</a></td>
     <td>open</td>
     <td>constexpr placement-new is too permissive</td>
-    <td align="center">Not resolved</td>
+    <td title="Clang 20 implements 2024-07-10 resolution" align="center">Not Resolved*</td>
   </tr></table>
 
 </div>
diff --git a/clang/www/cxx_status.html b/clang/www/cxx_status.html
index faee8b578b6242..58bbb12a76dd75 100755
--- a/clang/www/cxx_status.html
+++ b/clang/www/cxx_status.html
@@ -208,7 +208,7 @@ <h2 id="cxx26">C++2c implementation status</h2>
  <tr>
   <td><tt>constexpr</tt> placement new</td>
   <td><a href="https://wg21.link/P2747R2">P2747R2</a></td>
-  <td class="none" align="center">No</td>
+  <td class="unreleased" align="center">Clang 20</td>
  </tr>
  <tr>
   <td>Deleting a Pointer to an Incomplete Type Should be Ill-formed</td>

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.

DR testing side looks good

}
constexpr U _ = nondeterministic(true);
// expected-error@-1 {{constexpr variable '_' must be initialized by a constant expression}} \
// expected-note@-1 {{in call to 'nondeterministic(true)'}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// expected-note@-1 {{in call to 'nondeterministic(true)'}}
// expected-note@-1 {{in call to 'nondeterministic(true)'}}

Copy link
Contributor

Choose a reason for hiding this comment

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

This is still outstanding, albeit minor.

return false;
if (Result.Designator.Invalid)
return false;
/// if(!lifetimeStartedInEvaluation(Info, Result.getLValueBase()))
Copy link
Contributor

Choose a reason for hiding this comment

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

Debugging leftovers?

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

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

Thank you for working on this! Given that it's an extension in C++20 mode, we're missing diagnostics and test coverage for issuing extension/precompat diagnostics.


struct S{
int* i;
constexpr S() : i(new int(42)) {} // expected-note {{allocation performed here was not deallocated}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

What diagnostic is this note associated with?

// expected-note {{in call to 'indeterminate()'}}
int d = array1();
int alloc1 = (alloc(), 0);
int alloc2 = (alloc_err(), 0); // expected-error {{call to consteval function 'placement_new::alloc_err' is not a constant expression}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Other test cases to consider:

constexpr int *hmm() {
  return new int[2];
}

constexpr bool yay() {
  int *ptr = new (hmm()) int[2]{ 1, 2 };
  bool ret = ptr[0] == 1 && ptr[1] == 2;
  delete [] ptr;
  return ret;
}
static_assert(yay());

constexpr bool blah() {
  int *ptr = new (hmm()) int[3]{ 1, 2, 3 }; // you're a bad person who should feel bad
  bool ret = ptr[0] == 1 && ptr[1] == 2 && ptr[2] == 3;
  delete [] ptr;
  return ret;
}
static_assert(blah()); // Not a constant expression
int *oh_no() {
  int *evil;
  return evil;
}

constexpr bool blech() {
  int *ptr = new (oh_no()) int; // you're a bad person who should feel bad
  return true;
}
static_assert(blech()); // Not a constant expression

@cor3ntin
Copy link
Contributor Author

Thank you for working on this! Given that it's an extension in C++20 mode, we're missing diagnostics and test coverage for issuing extension/precompat diagnostics.

No longer an extension after our last chat - i just forgot to update the description

@cor3ntin
Copy link
Contributor Author

@AaronBallman ping :)

Copy link
Collaborator

@AaronBallman AaronBallman left a comment

Choose a reason for hiding this comment

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

LGTM!

@cor3ntin cor3ntin merged commit 6e78aef into llvm:main Aug 23, 2024
9 checks passed
cjdb pushed a commit to cjdb/llvm-project that referenced this pull request Aug 23, 2024
The implementation follows the resolution of CWG2922
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
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants