Skip to content

Conversation

@ChuanqiXu9
Copy link
Member

@ChuanqiXu9 ChuanqiXu9 commented Dec 16, 2025

Close #172241

This wasn't meant to be a fix. Just during my debugging, I want to see the effect/reason/motivation of the action, then I simply remove the loop to demote var definition as declaration, and all tests suddenly passed. I've also tested our internal workloads and it looks well too.

So I am wondering if this can be a real fix. The comment says “We should keep at most one definition on the chain.” But I am not sure if this is a strong restriction. To my mind model, I think it is fine as long as the definitions along the chain are equal.

@zygoloid would you like to take a look? Why do we have this and do we have a counter example?

@mpark @alexfh maybe you're interested to test this against your internal workloads to avoid break your internal code later.

@ChuanqiXu9 ChuanqiXu9 added the clang:modules C++20 modules and Clang Header Modules label Dec 16, 2025
@llvmbot llvmbot added the clang Clang issues not falling into any other category label Dec 16, 2025
@llvmbot
Copy link
Member

llvmbot commented Dec 16, 2025

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-modules

Author: Chuanqi Xu (ChuanqiXu9)

Changes

Close #172241

This was meant to be a fix. Just during my debugging, I want to see the effect/reason/motivation of the action, then I simply remove the loop to demote var definition as declaration, and all tests suddenly passed. I've also tested our internal workloads and it looks well too.

So I am wondering if this can be a real fix. The comment says “We should keep at most one definition on the chain.” But I am not sure if this is a strong restriction. To my mind model, I think it is fine as long as the definitions along the chain are equal.

@zygoloid would you like to take a look? Why do we have this and do we have a counter example?

@mpark @alexfh maybe you're interested to test this against your internal workloads to avoid break your internal code later.


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

2 Files Affected:

  • (modified) clang/lib/Serialization/ASTReaderDecl.cpp (-13)
  • (added) clang/test/Modules/pr172241.cppm (+47)
diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp
index d245a111088d5..f1b7a3c937551 100644
--- a/clang/lib/Serialization/ASTReaderDecl.cpp
+++ b/clang/lib/Serialization/ASTReaderDecl.cpp
@@ -3645,19 +3645,6 @@ void ASTDeclReader::attachPreviousDeclImpl(ASTReader &Reader,
   auto *PrevVD = cast<VarDecl>(Previous);
   D->RedeclLink.setPrevious(PrevVD);
   D->First = PrevVD->First;
-
-  // We should keep at most one definition on the chain.
-  // FIXME: Cache the definition once we've found it. Building a chain with
-  // N definitions currently takes O(N^2) time here.
-  if (VD->isThisDeclarationADefinition() == VarDecl::Definition) {
-    for (VarDecl *CurD = PrevVD; CurD; CurD = CurD->getPreviousDecl()) {
-      if (CurD->isThisDeclarationADefinition() == VarDecl::Definition) {
-        Reader.mergeDefinitionVisibility(CurD, VD);
-        VD->demoteThisDefinitionToDeclaration();
-        break;
-      }
-    }
-  }
 }
 
 static bool isUndeducedReturnType(QualType T) {
diff --git a/clang/test/Modules/pr172241.cppm b/clang/test/Modules/pr172241.cppm
new file mode 100644
index 0000000000000..3eb885e8b2d9f
--- /dev/null
+++ b/clang/test/Modules/pr172241.cppm
@@ -0,0 +1,47 @@
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+// RUN: split-file %s %t
+//
+// RUN: %clang_cc1 -std=c++20 -triple %itanium_abi_triple %t/m.cppm -emit-module-interface -o %t/m.pcm
+// RUN: %clang_cc1 -std=c++20 -triple %itanium_abi_triple %t/use.cpp -fmodule-file=m=%t/m.pcm -emit-llvm -o - | FileCheck %t/use.cpp
+//
+// RUN: %clang_cc1 -std=c++20 -triple %itanium_abi_triple %t/m.cppm -emit-reduced-module-interface -o %t/m.pcm
+// RUN: %clang_cc1 -std=c++20 -triple %itanium_abi_triple %t/use.cpp -fmodule-file=m=%t/m.pcm -emit-llvm -o - | FileCheck %t/use.cpp
+
+//--- header.h
+#pragma once
+
+template <unsigned T>
+class Templ {
+public:
+    void lock() { __set_locked_bit(); }
+
+private:
+    static constexpr auto __set_locked_bit = [](){};
+};
+
+class JT {
+public:
+    ~JT() {
+        Templ<4> state;
+        state.lock();
+    }
+};
+
+//--- m.cppm
+module;
+#include "header.h"
+export module m;
+export struct M {
+    JT jt;
+};
+//--- use.cpp
+#include "header.h"
+import m;
+
+int main() {
+    M m;
+    return 0;
+}
+
+// CHECK: @_ZN5TemplILj4EE16__set_locked_bitE = {{.*}}linkonce_odr

@github-actions
Copy link

github-actions bot commented Dec 16, 2025

🐧 Linux x64 Test Results

  • 112081 tests passed
  • 4516 tests skipped

✅ The build succeeded and all tests passed.

@github-actions
Copy link

⚠️ C/C++ code formatter, clang-format found issues in your code. ⚠️

You can test this locally with the following command:
git-clang-format --diff origin/main HEAD --extensions cppm,cpp -- clang/test/Modules/pr172241.cppm clang/lib/Serialization/ASTReaderDecl.cpp --diff_from_common_commit

⚠️
The reproduction instructions above might return results for more than one PR
in a stack if you are using a stacked PR workflow. You can limit the results by
changing origin/main to the base branch/commit you want to compare against.
⚠️

View the diff from clang-format here.
diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp
index 71b86c90d..d119dc459 100644
--- a/clang/lib/Serialization/ASTReaderDecl.cpp
+++ b/clang/lib/Serialization/ASTReaderDecl.cpp
@@ -3637,7 +3637,7 @@ void ASTDeclReader::attachPreviousDeclImpl(ASTReader &Reader,
 
 namespace clang {
 
-template<>
+template <>
 void ASTDeclReader::attachPreviousDeclImpl(ASTReader &Reader,
                                            Redeclarable<VarDecl> *D,
                                            Decl *Previous, Decl *Canon) {

@zygoloid
Copy link
Collaborator

The purpose of this mechanism was to maintain an AST invariant that other parts of Clang may reasonably be relying on -- specifically that there is at most one definition in a redeclaration chain for a variable.

Ultimately we have a choice: either we make the modules code attempt to build an AST that "looks like" a non-modules AST, for example with at most one definition per variable, function, or class, and the rest of Clang gets to assume that normal traditional C++ rules are in effect, or we allow the AST to directly represent things like a variable that has multiple definitions (or a class with multiple definitions or an inline function with multiple definitions) and the complexity associated with dealing with those things gets distributed across all parts of Clang and tools that consume its ASTs.

So far we'e largely picked the first option. In part that's because Clang's modules system was originally a language extension, so keeping its impact contained made sense, and in part that's because the intent of the modules system has historically mostly been around getting to the same state that a non-modules compilation would reach, but faster. Maybe it's time to change that, given that modules is now a standard language feature. But I think this is probably something the Clang Area Team should consider and make a conscious decision on.

(If we choose to directly represent the post-modules state in the AST -- for example, including the possibility of there being multiple definitions of entities in different TUs in the same AST -- I'd encourage that you also model the multiple TUs themselves explicitly, by creating multiple distinct TranslationUnitDecls, one per module.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

C++20 modules and std::jthread: link failed with clang & libc++ v21.1.x

3 participants