Skip to content

[C++20][Modules] Prevent premature calls to PassInterestingDeclsToConsumer() within FinishedDeserializing(). #129982

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 4 commits into from
Mar 16, 2025

Conversation

mpark
Copy link
Member

@mpark mpark commented Mar 6, 2025

Context

In ASTReader::finishPendingActions, there is a loop that clears out pending actions. After this loop however, more stuff happen, including a call to FD->hasBody(Defn) which ends up adding stuff back into PendingIncompleteDeclChains. As such, we leave finishPendingActions with pending actions remaining. This causes an assertion failure for the test case pr121245.cpp. The fix implemented in #121245 for this was to move the clearing of PendingIncompleteDeclChains to the end of finishPendingActions.

#126973 ended up reverting #121245 because it caused a breakage of bootstrap build on Mac, using XCode. The reduced test case for this is pr129982.cpp. The discovery here is that, in addition to FD->hasBody(Defn), the call to RD->addedMember(MD); can also add more decls to PendingIncompleteDeclChains. However, in this situation marking these newly added decls as incomplete causes pr129982.cpp to fail.

Big Picture

There are several places in this part of the code that mention something along the lines of:

we need the redeclaration chains to be fully wired

What does this mean really? and is that requirement actually satisfied by the current implementation?

The way in which a "complete redeclaration chain" is requested is by invoking getMostRecentDecl(). There is an underlying LazyGenerationalUpdatePtr that invokes ASTReader::CompleteRedeclChain.

The start of ASTReader::CompleteRedeclChain is:

  if (NumCurrentElementsDeserializing) {
    // We arrange to not care about the complete redeclaration chain while we're
    // deserializing. Just remember that the AST has marked this one as complete
    // but that it's not actually complete yet, so we know we still need to
    // complete it later.
    PendingIncompleteDeclChains.push_back(const_cast<Decl*>(D));
    return;
  }

The only place that ASTReader::finishPendingActions is called is here:

  if (NumCurrentElementsDeserializing == 1) {
    // We decrease NumCurrentElementsDeserializing only after pending actions
    // are finished, to avoid recursively re-calling finishPendingActions().
    finishPendingActions();
  }

So this means that:

  1. Within finishPendingActions, NumCurrentElementsDeserializing is always at least 1.
  2. Any call to getMostRecentDecl() (ultimately to CompleteRedeclChain) within finishPendingActions will always simply add the decl to PendingIncompleteDeclChains.

But we find these 2 comments within finishPendingActions after the clearing loop:

Example 1: PendingDefinitions

  // If we deserialized any C++ or Objective-C class definitions, any
  // Objective-C protocol definitions, or any redeclarable templates, make sure
  // that all redeclarations point to the definitions. Note that this can only
  // happen now, after the redeclaration chains have been fully wired.

Example 2: PendingBodies

  // Load the bodies of any functions or methods we've encountered. We do
  // this now (delayed) so that we can be sure that the declaration chains
  // have been fully wired up (hasBody relies on this).
  // FIXME: We shouldn't require complete redeclaration chains here.

But have the redeclaration chains been fully wired at either of these points...? The clearing loop does prepare the decls to become fully wired upon request, but if that request happens within finishPendingActions, it's just going to be added to PendingIncompleteDeclChains anyway.

The following is an example that I believe is correct:

Example 3: Exception Spec Propagation

     // We do this now rather than in finishPendingActions because we want to
     // be able to walk the complete redeclaration chains of the updated decls.

This one is outside of finishPendingActions, inside FinishedDeserializing, with NumCurrentElementsDeserializing == 0. As such, a call to getMostRecentDecl() here will actually perform the completion of redecl chains.

With these observations, I tried moving everything after the clearing loop from finishPendingActions to FinishedDeserializing such that the comments about completing redecl chains would become accurate and finishPendingActions would have the post-condition that all pending actions have been cleared. This however caused 2 test failures, both of which seemed to suffer from recursion issues. During name look-up, we have some deserialization work that calls FD->hasBody(Defn) which calls getMostRecentDecl(). Before this call to getMostRecentDecl added to the PendingIncompleteDeclChain and bailed. With the change, it actually ends up doing work which involves the name look-up of the same name we started with... and weirdness happens.

@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 Mar 6, 2025
@llvmbot
Copy link
Member

llvmbot commented Mar 6, 2025

@llvm/pr-subscribers-lldb

@llvm/pr-subscribers-clang-modules

Author: Michael Park (mpark)

Changes

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

4 Files Affected:

  • (modified) clang/lib/Sema/SemaType.cpp (+6)
  • (modified) clang/lib/Serialization/ASTReader.cpp (+12-13)
  • (added) clang/test/Modules/mpark.cpp (+107)
  • (added) clang/test/Modules/pr121245.cpp (+93)
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index 11943c0b53591..e2002bb410110 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -9180,6 +9180,12 @@ bool Sema::hasAcceptableDefinition(NamedDecl *D, NamedDecl **Suggested,
   if (!getLangOpts().Modules && !getLangOpts().ModulesLocalVisibility)
     return true;
 
+  // The external source may have additional definitions of this entity that are
+  // visible, so complete the redeclaration chain now.
+  if (auto *Source = Context.getExternalSource()) {
+    Source->CompleteRedeclChain(D);
+  }
+
   // If this definition was instantiated from a template, map back to the
   // pattern from which it was instantiated.
   if (isa<TagDecl>(D) && cast<TagDecl>(D)->isBeingDefined()) {
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index ca09c3d79d941..17d07f8535346 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -10178,12 +10178,12 @@ void ASTReader::visitTopLevelModuleMaps(
 }
 
 void ASTReader::finishPendingActions() {
-  while (
-      !PendingIdentifierInfos.empty() || !PendingDeducedFunctionTypes.empty() ||
-      !PendingDeducedVarTypes.empty() || !PendingIncompleteDeclChains.empty() ||
-      !PendingDeclChains.empty() || !PendingMacroIDs.empty() ||
-      !PendingDeclContextInfos.empty() || !PendingUpdateRecords.empty() ||
-      !PendingObjCExtensionIvarRedeclarations.empty()) {
+  while (!PendingIdentifierInfos.empty() ||
+         !PendingDeducedFunctionTypes.empty() ||
+         !PendingDeducedVarTypes.empty() || !PendingDeclChains.empty() ||
+         !PendingMacroIDs.empty() || !PendingDeclContextInfos.empty() ||
+         !PendingUpdateRecords.empty() ||
+         !PendingObjCExtensionIvarRedeclarations.empty()) {
     // If any identifiers with corresponding top-level declarations have
     // been loaded, load those declarations now.
     using TopLevelDeclsMap =
@@ -10231,13 +10231,6 @@ void ASTReader::finishPendingActions() {
     }
     PendingDeducedVarTypes.clear();
 
-    // For each decl chain that we wanted to complete while deserializing, mark
-    // it as "still needs to be completed".
-    for (unsigned I = 0; I != PendingIncompleteDeclChains.size(); ++I) {
-      markIncompleteDeclChain(PendingIncompleteDeclChains[I]);
-    }
-    PendingIncompleteDeclChains.clear();
-
     // Load pending declaration chains.
     for (unsigned I = 0; I != PendingDeclChains.size(); ++I)
       loadPendingDeclChain(PendingDeclChains[I].first,
@@ -10475,6 +10468,12 @@ void ASTReader::finishPendingActions() {
   for (auto *ND : PendingMergedDefinitionsToDeduplicate)
     getContext().deduplicateMergedDefinitonsFor(ND);
   PendingMergedDefinitionsToDeduplicate.clear();
+
+  // For each decl chain that we wanted to complete while deserializing, mark
+  // it as "still needs to be completed".
+  for (Decl *D : PendingIncompleteDeclChains)
+    markIncompleteDeclChain(D);
+  PendingIncompleteDeclChains.clear();
 }
 
 void ASTReader::diagnoseOdrViolations() {
diff --git a/clang/test/Modules/mpark.cpp b/clang/test/Modules/mpark.cpp
new file mode 100644
index 0000000000000..d3b45767526f2
--- /dev/null
+++ b/clang/test/Modules/mpark.cpp
@@ -0,0 +1,107 @@
+// If this test fails, it should be investigated under Debug builds.
+// Before the PR, this test was violating an assertion.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+
+// RUN: %clang_cc1 -std=c++17 -emit-obj -fmodules \
+// RUN:  -fmodule-map-file=%t/module.modulemap \
+// RUN:  -fmodules-cache-path=%t %t/a.cpp
+
+//--- module.modulemap
+module ebo {
+  header "ebo.h"
+}
+
+module fwd {
+  header "fwd.h"
+}
+
+module s {
+  header "s.h"
+  export *
+}
+
+module mod {
+  header "a.h"
+  header "b.h"
+}
+
+//--- ebo.h
+#pragma once
+
+namespace N { inline namespace __1 {
+
+template <typename T>
+struct EBO : T {
+  EBO() = default;
+};
+
+}}
+
+//--- fwd.h
+#pragma once
+
+namespace N { inline namespace __1 {
+
+template <typename T>
+struct Empty;
+
+template <typename T>
+struct BS;
+
+using S = BS<Empty<char>>;
+
+}}
+
+//--- s.h
+#pragma once
+
+#include "fwd.h"
+#include "ebo.h"
+
+namespace N { inline namespace __1 {
+
+template <typename T>
+struct Empty {};
+
+template <typename T>
+struct BS {
+    EBO<T> _;
+    void f();
+};
+
+extern template void BS<Empty<char>>::f();
+
+}}
+
+//--- b.h
+#pragma once
+
+#include "s.h"
+
+struct B {
+  void f() {
+    N::S{}.f();
+  }
+};
+
+//--- a.h
+#pragma once
+
+#include "s.h"
+
+struct A {
+  void f(int) {}
+  void f(const N::S &) {}
+
+  void g();
+};
+
+//--- a.cpp
+#include "a.h"
+
+void A::g() { f(0); }
+
+// expected-no-diagnostics
diff --git a/clang/test/Modules/pr121245.cpp b/clang/test/Modules/pr121245.cpp
new file mode 100644
index 0000000000000..0e276ad0e435d
--- /dev/null
+++ b/clang/test/Modules/pr121245.cpp
@@ -0,0 +1,93 @@
+// If this test fails, it should be investigated under Debug builds.
+// Before the PR, this test was encountering an `llvm_unreachable()`.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+// RUN: cd %t
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-01.h \
+// RUN:  -fcxx-exceptions -o %t/hu-01.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-02.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-02.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-03.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-03.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-04.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-04.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-05.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-03.pcm -fmodule-file=%t/hu-04.pcm \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-05.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-obj %t/main.cpp \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-02.pcm -fmodule-file=%t/hu-05.pcm \
+// RUN:  -fmodule-file=%t/hu-04.pcm -fmodule-file=%t/hu-03.pcm \
+// RUN:  -fmodule-file=%t/hu-01.pcm
+
+//--- hu-01.h
+template <typename T>
+struct A {
+  A() {}
+  ~A() {}
+};
+
+template <typename T>
+struct EBO : T {
+  EBO() = default;
+};
+
+template <typename T>
+struct HT : EBO<A<T>> {};
+
+//--- hu-02.h
+import "hu-01.h";
+
+inline void f() {
+  HT<int>();
+}
+
+//--- hu-03.h
+import "hu-01.h";
+
+struct C {
+  C();
+
+  HT<long> _;
+};
+
+//--- hu-04.h
+import "hu-01.h";
+
+void g(HT<long> = {});
+
+//--- hu-05.h
+import "hu-03.h";
+import "hu-04.h";
+import "hu-01.h";
+
+struct B {
+  virtual ~B() = default;
+
+  virtual void f() {
+    HT<long>();
+  }
+};
+
+//--- main.cpp
+import "hu-02.h";
+import "hu-05.h";
+import "hu-03.h";
+
+int main() {
+  f();
+  C();
+  B();
+}

@llvmbot
Copy link
Member

llvmbot commented Mar 6, 2025

@llvm/pr-subscribers-clang

Author: Michael Park (mpark)

Changes

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

4 Files Affected:

  • (modified) clang/lib/Sema/SemaType.cpp (+6)
  • (modified) clang/lib/Serialization/ASTReader.cpp (+12-13)
  • (added) clang/test/Modules/mpark.cpp (+107)
  • (added) clang/test/Modules/pr121245.cpp (+93)
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index 11943c0b53591..e2002bb410110 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -9180,6 +9180,12 @@ bool Sema::hasAcceptableDefinition(NamedDecl *D, NamedDecl **Suggested,
   if (!getLangOpts().Modules && !getLangOpts().ModulesLocalVisibility)
     return true;
 
+  // The external source may have additional definitions of this entity that are
+  // visible, so complete the redeclaration chain now.
+  if (auto *Source = Context.getExternalSource()) {
+    Source->CompleteRedeclChain(D);
+  }
+
   // If this definition was instantiated from a template, map back to the
   // pattern from which it was instantiated.
   if (isa<TagDecl>(D) && cast<TagDecl>(D)->isBeingDefined()) {
diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp
index ca09c3d79d941..17d07f8535346 100644
--- a/clang/lib/Serialization/ASTReader.cpp
+++ b/clang/lib/Serialization/ASTReader.cpp
@@ -10178,12 +10178,12 @@ void ASTReader::visitTopLevelModuleMaps(
 }
 
 void ASTReader::finishPendingActions() {
-  while (
-      !PendingIdentifierInfos.empty() || !PendingDeducedFunctionTypes.empty() ||
-      !PendingDeducedVarTypes.empty() || !PendingIncompleteDeclChains.empty() ||
-      !PendingDeclChains.empty() || !PendingMacroIDs.empty() ||
-      !PendingDeclContextInfos.empty() || !PendingUpdateRecords.empty() ||
-      !PendingObjCExtensionIvarRedeclarations.empty()) {
+  while (!PendingIdentifierInfos.empty() ||
+         !PendingDeducedFunctionTypes.empty() ||
+         !PendingDeducedVarTypes.empty() || !PendingDeclChains.empty() ||
+         !PendingMacroIDs.empty() || !PendingDeclContextInfos.empty() ||
+         !PendingUpdateRecords.empty() ||
+         !PendingObjCExtensionIvarRedeclarations.empty()) {
     // If any identifiers with corresponding top-level declarations have
     // been loaded, load those declarations now.
     using TopLevelDeclsMap =
@@ -10231,13 +10231,6 @@ void ASTReader::finishPendingActions() {
     }
     PendingDeducedVarTypes.clear();
 
-    // For each decl chain that we wanted to complete while deserializing, mark
-    // it as "still needs to be completed".
-    for (unsigned I = 0; I != PendingIncompleteDeclChains.size(); ++I) {
-      markIncompleteDeclChain(PendingIncompleteDeclChains[I]);
-    }
-    PendingIncompleteDeclChains.clear();
-
     // Load pending declaration chains.
     for (unsigned I = 0; I != PendingDeclChains.size(); ++I)
       loadPendingDeclChain(PendingDeclChains[I].first,
@@ -10475,6 +10468,12 @@ void ASTReader::finishPendingActions() {
   for (auto *ND : PendingMergedDefinitionsToDeduplicate)
     getContext().deduplicateMergedDefinitonsFor(ND);
   PendingMergedDefinitionsToDeduplicate.clear();
+
+  // For each decl chain that we wanted to complete while deserializing, mark
+  // it as "still needs to be completed".
+  for (Decl *D : PendingIncompleteDeclChains)
+    markIncompleteDeclChain(D);
+  PendingIncompleteDeclChains.clear();
 }
 
 void ASTReader::diagnoseOdrViolations() {
diff --git a/clang/test/Modules/mpark.cpp b/clang/test/Modules/mpark.cpp
new file mode 100644
index 0000000000000..d3b45767526f2
--- /dev/null
+++ b/clang/test/Modules/mpark.cpp
@@ -0,0 +1,107 @@
+// If this test fails, it should be investigated under Debug builds.
+// Before the PR, this test was violating an assertion.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+
+// RUN: %clang_cc1 -std=c++17 -emit-obj -fmodules \
+// RUN:  -fmodule-map-file=%t/module.modulemap \
+// RUN:  -fmodules-cache-path=%t %t/a.cpp
+
+//--- module.modulemap
+module ebo {
+  header "ebo.h"
+}
+
+module fwd {
+  header "fwd.h"
+}
+
+module s {
+  header "s.h"
+  export *
+}
+
+module mod {
+  header "a.h"
+  header "b.h"
+}
+
+//--- ebo.h
+#pragma once
+
+namespace N { inline namespace __1 {
+
+template <typename T>
+struct EBO : T {
+  EBO() = default;
+};
+
+}}
+
+//--- fwd.h
+#pragma once
+
+namespace N { inline namespace __1 {
+
+template <typename T>
+struct Empty;
+
+template <typename T>
+struct BS;
+
+using S = BS<Empty<char>>;
+
+}}
+
+//--- s.h
+#pragma once
+
+#include "fwd.h"
+#include "ebo.h"
+
+namespace N { inline namespace __1 {
+
+template <typename T>
+struct Empty {};
+
+template <typename T>
+struct BS {
+    EBO<T> _;
+    void f();
+};
+
+extern template void BS<Empty<char>>::f();
+
+}}
+
+//--- b.h
+#pragma once
+
+#include "s.h"
+
+struct B {
+  void f() {
+    N::S{}.f();
+  }
+};
+
+//--- a.h
+#pragma once
+
+#include "s.h"
+
+struct A {
+  void f(int) {}
+  void f(const N::S &) {}
+
+  void g();
+};
+
+//--- a.cpp
+#include "a.h"
+
+void A::g() { f(0); }
+
+// expected-no-diagnostics
diff --git a/clang/test/Modules/pr121245.cpp b/clang/test/Modules/pr121245.cpp
new file mode 100644
index 0000000000000..0e276ad0e435d
--- /dev/null
+++ b/clang/test/Modules/pr121245.cpp
@@ -0,0 +1,93 @@
+// If this test fails, it should be investigated under Debug builds.
+// Before the PR, this test was encountering an `llvm_unreachable()`.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+// RUN: cd %t
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-01.h \
+// RUN:  -fcxx-exceptions -o %t/hu-01.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-02.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-02.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-03.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-03.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-04.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-04.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-header-unit -xc++-user-header %t/hu-05.h \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-03.pcm -fmodule-file=%t/hu-04.pcm \
+// RUN:  -fmodule-file=%t/hu-01.pcm -o %t/hu-05.pcm
+
+// RUN: %clang_cc1 -std=c++20 -emit-obj %t/main.cpp \
+// RUN:  -Wno-experimental-header-units -fcxx-exceptions \
+// RUN:  -fmodule-file=%t/hu-02.pcm -fmodule-file=%t/hu-05.pcm \
+// RUN:  -fmodule-file=%t/hu-04.pcm -fmodule-file=%t/hu-03.pcm \
+// RUN:  -fmodule-file=%t/hu-01.pcm
+
+//--- hu-01.h
+template <typename T>
+struct A {
+  A() {}
+  ~A() {}
+};
+
+template <typename T>
+struct EBO : T {
+  EBO() = default;
+};
+
+template <typename T>
+struct HT : EBO<A<T>> {};
+
+//--- hu-02.h
+import "hu-01.h";
+
+inline void f() {
+  HT<int>();
+}
+
+//--- hu-03.h
+import "hu-01.h";
+
+struct C {
+  C();
+
+  HT<long> _;
+};
+
+//--- hu-04.h
+import "hu-01.h";
+
+void g(HT<long> = {});
+
+//--- hu-05.h
+import "hu-03.h";
+import "hu-04.h";
+import "hu-01.h";
+
+struct B {
+  virtual ~B() = default;
+
+  virtual void f() {
+    HT<long>();
+  }
+};
+
+//--- main.cpp
+import "hu-02.h";
+import "hu-05.h";
+import "hu-03.h";
+
+int main() {
+  f();
+  C();
+  B();
+}

@mpark mpark force-pushed the fix-incomplete-decl-chains branch from 4187847 to 44246b4 Compare March 6, 2025 04:00
@mpark mpark changed the title Fix incomplete decl chains [C++20][Modules][Serialization] Fix incomplete decl chains Mar 6, 2025
@mpark mpark changed the title [C++20][Modules][Serialization] Fix incomplete decl chains [C++20][Modules] Fix incomplete decl chains Mar 6, 2025
@mpark mpark force-pushed the fix-incomplete-decl-chains branch from 44246b4 to d6dbfd6 Compare March 6, 2025 04:21
Comment on lines 9183 to 9187
// The external source may have additional definitions of this entity that are
// visible, so complete the redeclaration chain now.
if (auto *Source = Context.getExternalSource()) {
Source->CompleteRedeclChain(D);
}
Copy link
Member

Choose a reason for hiding this comment

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

the fix here looks a little bit random to me. If we need to look at other declarations, we need to call redecls(). But redecls() should complete the redecl chain, right?

Copy link
Member Author

@mpark mpark Mar 6, 2025

Choose a reason for hiding this comment

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

Yes, calling redecls or getMostRecentDecl (similar to CXXRecordDecl::dataPtr) here would work as well. The reason I used this "pattern" here is because of this code at the end of the same function.

I'm very much open to refining the solution. I wanted to submit this PR now because the unit-test-level repro was something we were missing from #126973

Copy link
Member

Choose a reason for hiding this comment

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

To be honest, the completing redecl chain logic is more or less is in a mess. But let's try to do make things better if possible. I feel better to do this in redecls.

Copy link
Member Author

@mpark mpark Mar 6, 2025

Choose a reason for hiding this comment

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

Yeah, I agree. I'm kind of trying to figure out what the most recent pattern (😂) is for most recent decls.

Some options I've come across are:

  1. CompleteRedeclChain if getExternalSource() is present. e.g. SemaType.cpp (current state of the PR)
  // The external source may have additional definitions of this entity that are
  // visible, so complete the redeclaration chain now.
  if (auto *Source = Context.getExternalSource()) {
    Source->CompleteRedeclChain(D);
  }
  1. "Dummy" invocation of getMostRecentDecl() if getExternalSource() is present. e.g. DeclBase.cpp
  // If we have an external source, ensure that any later redeclarations of this
  // context have been loaded, since they may add names to the result of this
  // lookup (or add external visible storage).
  ExternalASTSource *Source = getParentASTContext().getExternalSource();
  if (Source)
    (void)cast<Decl>(this)->getMostRecentDecl();
  1. Unconditional "dummy" invocation of getMostRecentDecl(). e.g. CXXRecordDecl::dataPtr, ASTReader.cpp, RecordLayoutBuilder.cpp

I'm not quite sure I fully understood what you meant by:

I feel better to do this in redecls.

We don't need to iterate through the redecls() here, and while I see dummy invocations of getMostRecentDecl as a way to trigger redecl chain completion, I don't see examples of redecls being called to force that to happen.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe some gaps here. What I asked previously is:

  1. According to the comments, it looks like we're trying to load all other redeclarations of the current declaration, since we want to see if any redeclaration of the current declaration is acceptable.
  2. In Sema, ideally, when we want to the things above, we should call redecls().

then my question is, what is the problem?

  1. There is a path in Sema that meant to look at redeclarations but not called redecls().
  2. Redecls get called but didn't work as expceted (not work well).
  3. Or do I misunderstand the problem?

Copy link
Member Author

@mpark mpark Mar 7, 2025

Choose a reason for hiding this comment

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

On one thought, it seems like the decls inside DC should be notified of the fact that there have been new additions made to Lookups[DC].Table... but on the other hand, it's not clear to me whether this is an expected sequence of events in the first place.

If this is an expected sequence of events, I'm a bit surprised that this doesn't seem to be a bigger issue. If this is not an expected sequence of events, do we expect that the first call to CompleteRedeclChain should have the Lookups populated with everything already?

CC @ChuanqiXu9, maybe @Bigcheese can help here too?

Copy link
Member Author

Choose a reason for hiding this comment

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

CC @cor3ntin, @mizvekov, @dmpolukhin since they helped look at #121245 and might have some answers around invariants/expectations here.

Copy link
Member

Choose a reason for hiding this comment

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

@mpark thanks for the analysis. It is pretty clear. And I am wondering, why it was not a problem but it is after we delay pending the complete decl chain? Maybe this can help us to understand the problem better.

it seems like the decls inside DC should be notified of the fact that there have been new additions made to Lookups[DC].Table... but on the other hand, it's not clear to me whether this is an expected sequence of events in the first place.

I did similar experiments but it shows it has a pretty bad performance. And I don't feel it is wanted. We have similar (but not the same mechanism for hasNeedToReconcileExternalVisibleStorage())

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks @ChuanqiXu9 for following up!

why it was not a problem but it is after we delay pending the complete decl chain?

I'll look into this again in more detail, but previous investigations what I know is that the RD->addedMember(MD); adds more entries to PendingIncompleteDeclChains. Somewhat surprisingly, the entries that get added are the RD portion of the call: Empty<char>. When markIncompleteDeclChain happens prior to this (the current state, after the revert), we leave finishPendingActions with Empty<char> still in PendingIncompleteDeclChains, which is not actually marked as incomplete until the next time finishPendingActions is invoked. With #121245, the markIncompleteDeclChain happens after this, which marks Empty<char> incomplete and clears out PendingIncompleteDeclChain fully before leaving finishPendingActions.

That tends to change the order of how things get processed quite a bit. One place this effects for sure is this call to getMostRecentDecl(). In the current state (where Empty<char> is left in PendingIncompleteDeclChains), when we come across that getMostRecentDecl() call it does nothing. With #121245, Empty<char> has been marked incomplete, the getMostRecentDecl() call there actually does work.

I know these are rather specific, narrow-scoped observations. I'll need to dig a bit deeper to understand the bigger picture of what really changes.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. This is really in the dark corner.

@mpark mpark force-pushed the fix-incomplete-decl-chains branch from d6dbfd6 to 3134ad3 Compare March 6, 2025 20:42
@mpark mpark requested a review from JDevlieghere as a code owner March 6, 2025 20:42
@llvmbot llvmbot added the lldb label Mar 6, 2025
@mpark mpark changed the title [C++20][Modules] Fix incomplete decl chains [C++20][Modules] Do not update the generation number if the redecl chain completion was delayed. Mar 6, 2025
@mpark mpark changed the title [C++20][Modules] Do not update the generation number if the redecl chain completion was delayed. [C++20][Modules] Do not update the generation number if the redeclaration chain completion was delayed. Mar 6, 2025
@mpark mpark changed the title [C++20][Modules] Do not update the generation number if the redeclaration chain completion was delayed. [C++20][Modules] Do not update the declaration generation number if the redeclaration chain completion was delayed. Mar 6, 2025
@mpark mpark marked this pull request as draft March 7, 2025 00:54
@mpark mpark force-pushed the fix-incomplete-decl-chains branch from 3134ad3 to e38c598 Compare March 7, 2025 09:10
@cor3ntin
Copy link
Contributor

@iains @zygoloid Could you provide Micheal with some guidance? Thanks

@iains
Copy link
Contributor

iains commented Mar 10, 2025

@iains @zygoloid Could you provide Micheal with some guidance? Thanks

Having read through the discussion, I agree that the right strategy is to try and find out what the "bigger picture" / underlying reasons are. I've lost state somewhat on this so, sorry, do not have any details to add.

@mpark
Copy link
Member Author

mpark commented Mar 14, 2025

Alright folks, I've finally figured this out! I'll describe the high-level problem, what's happening specifically in the test case, and the solution.

High-Level Problem

ASTReader::FinishedDeserializing uses NumCurrentElementsDeserializing to keep track of nested Deserializing RAII actions. The FinishedDeserializing only performs actions if it is the top-level Deserializing layer. This works fine in general, but there is a problematic edge case.

If a call to redecls() in FinishedDeserializing performs deserialization, we re-enter FinishedDeserializing while in the middle of the previous FinishedDeserializing call.

It looks something like:

      +-- ... 2 ... --+
      |               |
   +--+       1       +--+     +-----+
   |  ^SD2         FD2^  |     |     |
|--+          0          +-----+-----+-------|
   ^SD1               FD1^     ^SD3  ^FD3 (FD1 still in progress)

The known problematic part of this is that this inner FinishedDeserializing (FD3 in the diagram) can go all the way to PassInterestingDeclsToConsumer, which operates on PotentiallyInterestingDecls data structure which contain decls that should be handled by the FD1 stage.

The other shared data structures are also somewhat concerning at a high-level in that the inner FinishedDeserializing would be handling pending actions that are not "within its scope", but this part is not known to be problematic.

Specifics

We perform a look-up on f for the f(0) call where f is an overloaded function. One of which is void f(const N::S &) {}, where S = BS<Empty<char>>. In finishPendingActions, we get to RD->addedMember(MD); where RD is the ClassTemplateSpecializationDecl of Empty<char> and MD is the destructor of Empty<char>.

Without #121245, the ClassTemplateSpecializationDecl of BS<Empty<char>> and Empty<char> are left in the PendingIncompleteDeclChains data structure as we exit finishPendingActions. We get through the rest of FinishedDeserializing, and gets to PassInterestingDeclsToConsumer. void f(const N::S &) (i.e. void f(const N::__1::BS<Empty<char>> &)) is identified as an interesting decl and gets name mangled. During name-mangling, we call redecls() on N, __1, BS, and Empty. The redecls() on BS<Empty<char>> specifically doesn't do anything, and instead gets marked incomplete later, and happens to be loaded.

With #121245, the ClassTemplateSpecializationDecl of BS<Empty<char>> and Empty<char> get marked incomplete as we exit finishPendingActions. In the second part of FinishedDeserializing, the call to Update.second->redecls() is made where Update.second is the ClassTemplateSpecializationDecl of Empty<char>. This time, since Empty<char> is marked incomplete, redecl chain completion logic is triggered.

Since we're in the NumCurrentElementsDeserializing == 0 part of FinishedDeserializing, we do not merely add to PendingIncompleteDeclChains, but rather get to the DC->lookup(Name) call in ASTReader::CompleteRedeclChain with DC = __1 and Name = "Empty".

The implementation of DC->lookup ensures that the DC is up-to-date by invoking getMostRecentDecl() on DC. This happens recursively for DC = __1, Name = "Empty", DC = N, Name = "__1" and DC = TranslationUnit, Name = "N". So now, FindExternalVisibleDeclsByName is called from here with DC = TranslationUnit and Name = "N".

Note that at this point, we haven't gotten to actually performing FindExternalVisibleDeclsByName with DC = N, Name = "__1" which will populate new declarations/definitions to __1.

There is an instance of Deserializing inside FindExternalVisibleDeclsByName here, end of which of course is an invocation to FinishedDeserializing. This call gets to PassInterestingDeclsToConsumer with void f(const N::S &) inside of it from before, and tries to mangle the name. During name-mangling, we call redecls() on N, __1, BS, and Empty, the same as without #121245. However, this time the redecls() on BS<Empty<char>> actually tries to do stuff since BS<Empty<char>> was marked incomplete coming out of finishPendingActions. What happens next is that we try to complete the redecl chain for BS<Empty<char>> at this point without having completed __1. So BS<Empty<char>> gets its lazy pointer generation updated with incorrect information.

The claim in this fix is that the real problem is the invocation of PassInterestingDeclsToConsumer deep within this recursion. The PotentiallyInterestingDecls data structure ends up being shared down the recursion and it ends up passing outer-level declarations that may not be prepared to be passed.

Solutions

As described in the High-level Problem section, the big picture problem is the recursive nature of FinishedDeserializing where we can perform further deserialization within FinishedDeserializing, and the inner FinishedDeserializing call operates on the shared data structures such as Pending* and PotentiallyInterestingDecls.

Proposed Solution

We already have a guard within PassInterestingDeclsToConsumer because we can end up with recursive deserialization within PassInterestingDeclsToConsumer. The proposed solution is to apply this guard to the portion of FinishedDeserializing that performs further deserialization as well. This ensures that recursive deserialization does not trigger PassInterestingDeclsToConsumer which may operate on entries that are not ready to be passed.

Alternative: Save and restore the PotentiallyInterestingDecls

An alternative approach would be to save the PotentiallyInterestingDecls on the side while recursive deserialization is happening, such that we do invoke PassInterestingDeclsToConsumer, but only with the decls that are within its scope. This approach works, and has been tested. The reason this approach is not taken is because this approach (in principle) should also save other shared data structures on the side as well, and the proposed solution fit in nicer with the existing mechanism.

Alternative: Guard recursive entering of the second part of FinishedDeserializing

This approach adds a new field bool FinishingDeserializing = false; to ASTReader, and guards the NumCurrentElementsDeserializing == 0 portion of the FinishedDeserializing like so:

  if (NumCurrentElementsDeserializing == 0) {
+   if (FinishingDeserializing)
+     return;

+   // Guard variable to avoid recursively redoing the process of passing
+   // decls to consumer.
+   SaveAndRestore GuardFinishingDeserializing(FinishingDeserializing, true);

    while (!PendingExceptionSpecUpdates.empty() ||
           !PendingDeducedTypeUpdates.empty() ||
           !PendingUndeducedFunctionDecls.empty()) {
      // ...
    }

    if (ReadTimer)
      ReadTimer->stopTimer();

    diagnoseOdrViolations();

    // We are not in recursive loading, so it's safe to pass the "interesting"
    // decls to the consumer.
    if (Consumer)
      PassInterestingDeclsToConsumer();
  }

This mirrors the current mechanism of the PassingDeclsToConsumer guard. This approach passes the test-suite. However, I don't believe it's quite correct.

Suppose we're at the diagnoseOdrViolations() part in the outer FinishedDeserializing where a redecls() is invoked. We could have a recursive deserialization at that point, and with this approach, the inner FinishedDeserializing would simply return rather than processing the Pending* data structures. We can't just rely on the outer FinishedDeserializing to finish the job.

@mpark mpark force-pushed the fix-incomplete-decl-chains branch from e38c598 to fb2de5d Compare March 14, 2025 21:34
Copy link

github-actions bot commented Mar 14, 2025

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

@mpark mpark changed the title [C++20][Modules] Do not update the declaration generation number if the redeclaration chain completion was delayed. [C++20][Modules] Prevent premature call to PassInterestingDeclsToConsumer() within FinishedDeserializing(). Mar 14, 2025
@mpark mpark force-pushed the fix-incomplete-decl-chains branch from fb2de5d to af2d121 Compare March 14, 2025 21:41
@mpark mpark marked this pull request as ready for review March 14, 2025 21:42
@mpark mpark requested review from ChuanqiXu9 and zygoloid March 14, 2025 21:43
Copy link
Collaborator

@zygoloid zygoloid left a comment

Choose a reason for hiding this comment

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

Thanks, the explanation and code change makes sense to me.

Should this bugfix have a release note?

@mpark mpark force-pushed the fix-incomplete-decl-chains branch from af2d121 to 20999a1 Compare March 14, 2025 23:02
@mpark
Copy link
Member Author

mpark commented Mar 14, 2025

Thanks, the explanation and code change makes sense to me.

❤️

Should this bugfix have a release note?

I've added a couple of bullet points in the release notes. Please let me know if this is along the lines of what's expected.

@mpark mpark self-assigned this Mar 14, 2025
@mpark mpark force-pushed the fix-incomplete-decl-chains branch 2 times, most recently from 3b29e91 to 5d21759 Compare March 14, 2025 23:07
@mpark mpark force-pushed the fix-incomplete-decl-chains branch from 5d21759 to d6ebb73 Compare March 14, 2025 23:26
@mpark
Copy link
Member Author

mpark commented Mar 14, 2025

@zygoloid as per previous request, I've also added a set of assertions at the end of finishPendingActions: d6ebb73

@zygoloid
Copy link
Collaborator

Thank you! I can imagine figuring out what was wrong here was not easy!

@mpark mpark changed the title [C++20][Modules] Prevent premature call to PassInterestingDeclsToConsumer() within FinishedDeserializing(). [C++20][Modules] Prevent premature calls to PassInterestingDeclsToConsumer() within FinishedDeserializing(). Mar 15, 2025
@mpark mpark merged commit 0689d23 into llvm:main Mar 16, 2025
12 checks passed
@mpark mpark deleted the fix-incomplete-decl-chains branch March 16, 2025 06:03
mpark added a commit that referenced this pull request Mar 19, 2025
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Mar 19, 2025
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 lldb
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants