Skip to content

[clang][AST] Add 'IgnoreTemplateParmDepth' to structural equivalence cache #115518

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 1 commit into from
Nov 13, 2024

Conversation

balazske
Copy link
Collaborator

@balazske balazske commented Nov 8, 2024

Structural equivalence check uses a cache to store already found non-equivalent values. This cache can be reused for calls (ASTImporter does this). Value of "IgnoreTemplateParmDepth" can have an effect on the structural equivalence therefore it is wrong to reuse the same cache for checks with different values of 'IgnoreTemplateParmDepth'. The current change adds the 'IgnoreTemplateParmDepth' to the cache key to fix the problem.

…cache

Structural equivalence check uses a cache to store already found non-equivalent values.
This cache can be reused for calls (ASTImporter does this).
Value of "IgnoreTemplateParmDepth" can have an effect on the structural equivalence
therefore it is wrong to reuse the same cache for checks with different values of
'IgnoreTemplateParmDepth'. The current change adds the 'IgnoreTemplateParmDepth'
to the cache key to fix the problem.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:modules C++20 modules and Clang Header Modules labels Nov 8, 2024
@llvmbot
Copy link
Member

llvmbot commented Nov 8, 2024

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-modules

Author: Balázs Kéri (balazske)

Changes

Structural equivalence check uses a cache to store already found non-equivalent values. This cache can be reused for calls (ASTImporter does this). Value of "IgnoreTemplateParmDepth" can have an effect on the structural equivalence therefore it is wrong to reuse the same cache for checks with different values of 'IgnoreTemplateParmDepth'. The current change adds the 'IgnoreTemplateParmDepth' to the cache key to fix the problem.


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

7 Files Affected:

  • (modified) clang/include/clang/AST/ASTImporter.h (+2-1)
  • (modified) clang/include/clang/AST/ASTStructuralEquivalence.h (+12-7)
  • (modified) clang/lib/AST/ASTStructuralEquivalence.cpp (+4-2)
  • (modified) clang/lib/Sema/SemaType.cpp (+1-1)
  • (modified) clang/lib/Serialization/ASTReader.cpp (+1-1)
  • (modified) clang/lib/Serialization/ASTReaderDecl.cpp (+1-1)
  • (modified) clang/unittests/AST/StructuralEquivalenceTest.cpp (+81-7)
diff --git a/clang/include/clang/AST/ASTImporter.h b/clang/include/clang/AST/ASTImporter.h
index 088a2bd0fdd407..8c3fa842ab8b9d 100644
--- a/clang/include/clang/AST/ASTImporter.h
+++ b/clang/include/clang/AST/ASTImporter.h
@@ -62,7 +62,8 @@ class TypeSourceInfo;
   class ASTImporter {
     friend class ASTNodeImporter;
   public:
-    using NonEquivalentDeclSet = llvm::DenseSet<std::pair<Decl *, Decl *>>;
+    using NonEquivalentDeclSet =
+        llvm::DenseSet<std::tuple<Decl *, Decl *, int>>;
     using ImportedCXXBaseSpecifierMap =
         llvm::DenseMap<const CXXBaseSpecifier *, CXXBaseSpecifier *>;
 
diff --git a/clang/include/clang/AST/ASTStructuralEquivalence.h b/clang/include/clang/AST/ASTStructuralEquivalence.h
index 029439c8e9a3ac..67aa0023c25d08 100644
--- a/clang/include/clang/AST/ASTStructuralEquivalence.h
+++ b/clang/include/clang/AST/ASTStructuralEquivalence.h
@@ -39,6 +39,10 @@ enum class StructuralEquivalenceKind {
 };
 
 struct StructuralEquivalenceContext {
+  /// Store declaration pairs already found to be non-equivalent.
+  /// key: (from, to, IgnoreTemplateParmDepth)
+  using NonEquivalentDeclSet = llvm::DenseSet<std::tuple<Decl *, Decl *, int>>;
+
   /// AST contexts for which we are checking structural equivalence.
   ASTContext &FromCtx, &ToCtx;
 
@@ -52,7 +56,7 @@ struct StructuralEquivalenceContext {
 
   /// Declaration (from, to) pairs that are known not to be equivalent
   /// (which we have already complained about).
-  llvm::DenseSet<std::pair<Decl *, Decl *>> &NonEquivalentDecls;
+  NonEquivalentDeclSet &NonEquivalentDecls;
 
   StructuralEquivalenceKind EqKind;
 
@@ -72,12 +76,13 @@ struct StructuralEquivalenceContext {
   /// Whether to ignore comparing the depth of template param(TemplateTypeParm)
   bool IgnoreTemplateParmDepth;
 
-  StructuralEquivalenceContext(
-      ASTContext &FromCtx, ASTContext &ToCtx,
-      llvm::DenseSet<std::pair<Decl *, Decl *>> &NonEquivalentDecls,
-      StructuralEquivalenceKind EqKind, bool StrictTypeSpelling = false,
-      bool Complain = true, bool ErrorOnTagTypeMismatch = false,
-      bool IgnoreTemplateParmDepth = false)
+  StructuralEquivalenceContext(ASTContext &FromCtx, ASTContext &ToCtx,
+                               NonEquivalentDeclSet &NonEquivalentDecls,
+                               StructuralEquivalenceKind EqKind,
+                               bool StrictTypeSpelling = false,
+                               bool Complain = true,
+                               bool ErrorOnTagTypeMismatch = false,
+                               bool IgnoreTemplateParmDepth = false)
       : FromCtx(FromCtx), ToCtx(ToCtx), NonEquivalentDecls(NonEquivalentDecls),
         EqKind(EqKind), StrictTypeSpelling(StrictTypeSpelling),
         ErrorOnTagTypeMismatch(ErrorOnTagTypeMismatch), Complain(Complain),
diff --git a/clang/lib/AST/ASTStructuralEquivalence.cpp b/clang/lib/AST/ASTStructuralEquivalence.cpp
index 120ddc0f26c0d7..bf2f42932f25d4 100644
--- a/clang/lib/AST/ASTStructuralEquivalence.cpp
+++ b/clang/lib/AST/ASTStructuralEquivalence.cpp
@@ -2303,7 +2303,8 @@ static bool IsStructurallyEquivalent(StructuralEquivalenceContext &Context,
 
   // Check whether we already know that these two declarations are not
   // structurally equivalent.
-  if (Context.NonEquivalentDecls.count(P))
+  if (Context.NonEquivalentDecls.count(
+          std::make_tuple(D1, D2, Context.IgnoreTemplateParmDepth)))
     return false;
 
   // Check if a check for these declarations is already pending.
@@ -2511,7 +2512,8 @@ bool StructuralEquivalenceContext::Finish() {
     if (!Equivalent) {
       // Note that these two declarations are not equivalent (and we already
       // know about it).
-      NonEquivalentDecls.insert(P);
+      NonEquivalentDecls.insert(
+          std::make_tuple(D1, D2, IgnoreTemplateParmDepth));
 
       return true;
     }
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index e526a11973975d..e26d422a31b49e 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -9043,7 +9043,7 @@ bool Sema::RequireCompleteType(SourceLocation Loc, QualType T,
 }
 
 bool Sema::hasStructuralCompatLayout(Decl *D, Decl *Suggested) {
-  llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls;
+  StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls;
   if (!Suggested)
     return false;
 
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index 79615dc3c018ea..649bc318fa14c4 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -9905,7 +9905,7 @@ void ASTReader::finishPendingActions() {
       auto ExtensionsPair = PendingObjCExtensionIvarRedeclarations.back().first;
       auto DuplicateIvars =
           PendingObjCExtensionIvarRedeclarations.back().second;
-      llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls;
+      StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls;
       StructuralEquivalenceContext Ctx(
           ExtensionsPair.first->getASTContext(),
           ExtensionsPair.second->getASTContext(), NonEquivalentDecls,
diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp
index 33fcddbbdb2f15..6ece3ba7af9f4b 100644
--- a/clang/lib/Serialization/ASTReaderDecl.cpp
+++ b/clang/lib/Serialization/ASTReaderDecl.cpp
@@ -4447,7 +4447,7 @@ namespace {
         ObjCCategoryDecl *&Existing = NameCategoryMap[Cat->getDeclName()];
         if (Existing && Reader.getOwningModuleFile(Existing) !=
                             Reader.getOwningModuleFile(Cat)) {
-          llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls;
+          StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls;
           StructuralEquivalenceContext Ctx(
               Cat->getASTContext(), Existing->getASTContext(),
               NonEquivalentDecls, StructuralEquivalenceKind::Default,
diff --git a/clang/unittests/AST/StructuralEquivalenceTest.cpp b/clang/unittests/AST/StructuralEquivalenceTest.cpp
index e994086c99d041..7cf52df9b14d05 100644
--- a/clang/unittests/AST/StructuralEquivalenceTest.cpp
+++ b/clang/unittests/AST/StructuralEquivalenceTest.cpp
@@ -134,8 +134,8 @@ struct StructuralEquivalenceTest : ::testing::Test {
 
   bool testStructuralMatch(Decl *D0, Decl *D1,
                            bool IgnoreTemplateParmDepth = false) {
-    llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls01;
-    llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls10;
+    StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls01;
+    StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls10;
     StructuralEquivalenceContext Ctx01(
         D0->getASTContext(), D1->getASTContext(), NonEquivalentDecls01,
         StructuralEquivalenceKind::Default, /*StrictTypeSpelling=*/false,
@@ -153,8 +153,8 @@ struct StructuralEquivalenceTest : ::testing::Test {
   }
 
   bool testStructuralMatch(StmtWithASTContext S0, StmtWithASTContext S1) {
-    llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls01;
-    llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls10;
+    StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls01;
+    StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls10;
     StructuralEquivalenceContext Ctx01(
         *S0.Context, *S1.Context, NonEquivalentDecls01,
         StructuralEquivalenceKind::Default, false, false);
@@ -1792,7 +1792,7 @@ TEST_F(
   EXPECT_FALSE(testStructuralMatch(t));
 }
 struct StructuralEquivalenceCacheTest : public StructuralEquivalenceTest {
-  llvm::DenseSet<std::pair<Decl *, Decl *>> NonEquivalentDecls;
+  StructuralEquivalenceContext::NonEquivalentDeclSet NonEquivalentDecls;
 
   template <typename NodeType, typename MatcherType>
   std::pair<NodeType *, NodeType *>
@@ -1804,8 +1804,10 @@ struct StructuralEquivalenceCacheTest : public StructuralEquivalenceTest {
   }
 
   template <typename NodeType>
-  bool isInNonEqCache(std::pair<NodeType *, NodeType *> D) {
-    return NonEquivalentDecls.count(D) > 0;
+  bool isInNonEqCache(std::pair<NodeType *, NodeType *> D,
+                      bool IgnoreTemplateParmDepth = false) {
+    return NonEquivalentDecls.count(
+               std::make_tuple(D.first, D.second, IgnoreTemplateParmDepth)) > 0;
   }
 };
 
@@ -2015,6 +2017,78 @@ TEST_F(StructuralEquivalenceCacheTest, Cycle) {
       findDeclPair<FunctionDecl>(TU, functionDecl(hasName("x")))));
 }
 
+TEST_F(StructuralEquivalenceCacheTest, TemplateParmDepth) {
+  // In 'friend struct Y' ClassTemplateDecl has the TU as parent context.
+  // This declaration has template depth 1 (it is already inside a template).
+  // It has not a previous declaration and is an "undeclared" friend.
+  //
+  // Second TU has a specialization of 'struct X'.
+  // In this case 'friend struct Y' has the ClassTemplateSpecializationDecl as
+  // parent. It has template depth 0 (it is in the specialization). It has the
+  // first 'struct Y' declaration as previous declaration and canonical
+  // declaration.
+  //
+  // When these two 'friend struct Y' are compared, only the template depth is
+  // different.
+  // FIXME: Structural equivalence checks the depth only in types, not in
+  // TemplateParmDecl. For this reason the second 'A1' argument is needed (as a
+  // type) in the template to make the check fail.
+  auto TU = makeTuDecls(
+      R"(
+      template <class A1, A1>
+      struct Y;
+
+      template <class A>
+      struct X {
+        template <class A1, A1>
+        friend struct Y;
+      };
+      )",
+      R"(
+      template <class A1, A1>
+      struct Y;
+
+      template <class A>
+      struct X {
+        template <class A1, A1>
+        friend struct Y;
+      };
+
+      X<int> x;
+      )",
+      Lang_CXX03);
+
+  auto *D0 = LastDeclMatcher<ClassTemplateDecl>().match(
+      get<0>(TU), classTemplateDecl(hasName("Y"), unless(isImplicit())));
+  auto *D1 = LastDeclMatcher<ClassTemplateDecl>().match(
+      get<1>(TU), classTemplateDecl(hasName("Y"), unless(isImplicit())));
+  ASSERT_EQ(D0->getTemplateDepth(), 1u);
+  ASSERT_EQ(D1->getTemplateDepth(), 0u);
+
+  StructuralEquivalenceContext Ctx_NoIgnoreTemplateParmDepth(
+      get<0>(TU)->getASTContext(), get<1>(TU)->getASTContext(),
+      NonEquivalentDecls, StructuralEquivalenceKind::Default, false, false,
+      false, false);
+
+  EXPECT_FALSE(Ctx_NoIgnoreTemplateParmDepth.IsEquivalent(D0, D1));
+
+  Decl *NonEqDecl0 =
+      D0->getCanonicalDecl()->getTemplateParameters()->getParam(1);
+  Decl *NonEqDecl1 =
+      D1->getCanonicalDecl()->getTemplateParameters()->getParam(1);
+  EXPECT_TRUE(isInNonEqCache(std::make_pair(NonEqDecl0, NonEqDecl1), false));
+  EXPECT_FALSE(isInNonEqCache(std::make_pair(NonEqDecl0, NonEqDecl1), true));
+
+  StructuralEquivalenceContext Ctx_IgnoreTemplateParmDepth(
+      get<0>(TU)->getASTContext(), get<1>(TU)->getASTContext(),
+      NonEquivalentDecls, StructuralEquivalenceKind::Default, false, false,
+      false, true);
+
+  EXPECT_TRUE(Ctx_IgnoreTemplateParmDepth.IsEquivalent(D0, D1));
+
+  EXPECT_FALSE(isInNonEqCache(std::make_pair(NonEqDecl0, NonEqDecl1), true));
+}
+
 struct StructuralEquivalenceStmtTest : StructuralEquivalenceTest {};
 
 /// Fallback matcher to be used only when there is no specific matcher for a

@balazske
Copy link
Collaborator Author

balazske commented Nov 8, 2024

Another possible solution: Use two NonEquivalentDecls sets, one for IgnoreTemplateParmDepth = true and one for false. This may use less memory (no rarely used third value in the key) but requires more code changes. Probably use these two caches only in ASTImporter where the correct one can be passed to StructuralEquivalenceContext.

Copy link
Member

@Michael137 Michael137 left a comment

Choose a reason for hiding this comment

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

LGTM

@Michael137
Copy link
Member

Another possible solution: Use two NonEquivalentDecls sets, one for IgnoreTemplateParmDepth = true and one for false. This may use less memory (no rarely used third value in the key) but requires more code changes. Probably use these two caches only in ASTImporter where the correct one can be passed to StructuralEquivalenceContext.

I guess another alternative would be to not cache equivalence of friends (I think that's the only time we ignore the template depth?). But that might not be desirable either. Your current approach seems reasonable to me

@balazske
Copy link
Collaborator Author

I had another simple fix when the cache is used only if IgnoreTemplateParmDepth is false which is the often used case. During one structural equivalence comparison IgnoreTemplateParmDepth does not change, just one set of the cached values (for IgnoreTemplateParmDepth) is used.

Copy link
Contributor

@NagyDonat NagyDonat left a comment

Choose a reason for hiding this comment

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

Another possible solution: Use two NonEquivalentDecls sets, one for IgnoreTemplateParmDepth = true and one for false. This may use less memory (no rarely used third value in the key) but requires more code changes.

I like this idea, and I think it can be implemented without significant code changes -- see my inline comments for details.

(Disclaimer: I didn't check my change so there might be other locations where you didn't need to change between the old "set of pairs" and your "set of 3-tuples", but the suggested "two sets of pairs in an array" would behave differently -- but it would be a simplification even if you needed one or two lines of additional logic.)

However the current "set of 3-tuples" approach is also OK if you prefer it.

Comment on lines +65 to +66
using NonEquivalentDeclSet =
llvm::DenseSet<std::tuple<Decl *, Decl *, int>>;
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
using NonEquivalentDeclSet =
llvm::DenseSet<std::tuple<Decl *, Decl *, int>>;
using NonEquivalentDeclSet =
std::array<llvm::DenseSet<std::pair<Decl *, Decl *>>, 2>;

If you use two DenseSets and put them into an array, you can use them without significant code changes.

@@ -39,6 +39,10 @@ enum class StructuralEquivalenceKind {
};

struct StructuralEquivalenceContext {
/// Store declaration pairs already found to be non-equivalent.
/// key: (from, to, IgnoreTemplateParmDepth)
using NonEquivalentDeclSet = llvm::DenseSet<std::tuple<Decl *, Decl *, int>>;
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
using NonEquivalentDeclSet = llvm::DenseSet<std::tuple<Decl *, Decl *, int>>;
using NonEquivalentDeclSet =
std::array<llvm::DenseSet<std::pair<Decl *, Decl *>>, 2>;

As above.

Comment on lines +2306 to +2307
if (Context.NonEquivalentDecls.count(
std::make_tuple(D1, D2, Context.IgnoreTemplateParmDepth)))
Copy link
Contributor

@NagyDonat NagyDonat Nov 11, 2024

Choose a reason for hiding this comment

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

Suggested change
if (Context.NonEquivalentDecls.count(
std::make_tuple(D1, D2, Context.IgnoreTemplateParmDepth)))
if (Context.NonEquivalentDecls[Context.IgnoreTemplateParmDepth].count(P))

You can use a boolean value for indexing to select the right element in the array.

Comment on lines +2515 to +2516
NonEquivalentDecls.insert(
std::make_tuple(D1, D2, IgnoreTemplateParmDepth));
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
NonEquivalentDecls.insert(
std::make_tuple(D1, D2, IgnoreTemplateParmDepth));
NonEquivalentDecls[IgnoreTemplateParmDepth].insert(P);

Comment on lines +1809 to +1810
return NonEquivalentDecls.count(
std::make_tuple(D.first, D.second, IgnoreTemplateParmDepth)) > 0;
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
return NonEquivalentDecls.count(
std::make_tuple(D.first, D.second, IgnoreTemplateParmDepth)) > 0;
return NonEquivalentDecls[IgnoreTemplateParmDepth].count(D) > 0;

@Michael137
Copy link
Member

Michael137 commented Nov 11, 2024

Another possible solution: Use two NonEquivalentDecls sets, one for IgnoreTemplateParmDepth = true and one for false. This may use less memory (no rarely used third value in the key) but requires more code changes.

I like this idea, and I think it can be implemented without significant code changes -- see my inline comments for details.

(Disclaimer: I didn't check my change so there might be other locations where you didn't need to change between the old "set of pairs" and your "set of 3-tuples", but the suggested "two sets of pairs in an array" would behave differently -- but it would be a simplification even if you needed one or two lines of additional logic.)

However the current "set of 3-tuples" approach is also OK if you prefer it.

One thing I don't like about having two separate caches is the prospect of what this looks like if we add another parameter affecting the equivalence. For example, we already have another parameter StrictTypeSpelling, which by the looks of it could affect the result of an equivalence check. Would we need to add another cache for that too? That just doesn't feel like a future-proof solution. Encoding the flag that affects that hash of the cache entry in the entry itself (like @balazske did in the first iteration of this PR) would be my preferred approach.

@balazske
Copy link
Collaborator Author

It is a realistic requirement that new similar parameters are added, I had already an (experimental) fix where this is needed. Still I like better the solution with separate caches because efficiency reasons (IgnoreTemplateParmDepth = true is rarely used, not at all for C code). For new parameters we have the similar set of choices (add more new caches or extend cache key or not use the cache at all).

@Michael137
Copy link
Member

It is a realistic requirement that new similar parameters are added, I had already an (experimental) fix where this is needed. Still I like better the solution with separate caches because efficiency reasons (IgnoreTemplateParmDepth = true is rarely used, not at all for C code). For new parameters we have the similar set of choices (add more new caches or extend cache key or not use the cache at all).

We wouldn't just have to add a single extra cache, but each parameter would require us to add caches for all permutations of the parameters. Also, I'd be surprised if there's a noticeable difference in performance between the two to be honest. So I still prefer your first solution, but I don't want to block this if you prefer the other approach

@NagyDonat
Copy link
Contributor

NagyDonat commented Nov 12, 2024

The array-based solution can be extended to cover more than two separate cache sets by replacing the bool with a suitable enum (or a bitfield where independent flags are combined by bitwise or). It will stay more effective than the "use a single set and store 3-tuples instead of pairs" solution as long as (number of cache entries) * sizeof(int) > (number of separate cache sets) * sizeof(empty DenseSet) holds. (Nitpick: I'm saying sizeof(int), but the alignment of the 3-tuples with pointer fields probably means that the effective size of each set element is increased by a pointer.)

EDIT:

Also, I'd be surprised if there's a noticeable difference in performance between the two to be honest.

I agree that performance is not too important here; I'm favoring the array-based solution, because IMO it's more elegant that we don't need to re-pack the elements of the pair into a 3-tuple.

Also note that, as the set of my inline comments shows, it's really easy to convert between the two approaches if the evolution of the codebase makes that necessary -- there is no need to aggressively future-proof this decision.

Finally, this is all just bikeshedding and both potential solutions are better than spending more review rounds on this discussion. I'm fine with either implementation, let's merge one of them!

@balazske
Copy link
Collaborator Author

OK, I will merge the current version. Even then it is possible to use the third tuple member with bitwise-OR combined additional flags.

@balazske balazske merged commit 7a1fdbb into llvm:main Nov 13, 2024
12 checks passed
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:modules C++20 modules and Clang Header Modules clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants