[clang] [clang-repl] fix vtable symbol duplication error (closes #141039) (PR #185648)

Emery Conrad via cfe-commits cfe-commits at lists.llvm.org
Thu Mar 12 07:52:57 PDT 2026


https://github.com/conrade-ctc updated https://github.com/llvm/llvm-project/pull/185648

>From 577ec1ada50a24adc99d0197b92d8919f97ba501 Mon Sep 17 00:00:00 2001
From: Emery Conrad <emery.conrad at chicagotrading.com>
Date: Tue, 10 Mar 2026 07:22:18 -0500
Subject: [PATCH] [clang-repl] fix vtable double-emission via DefinedVTables
 tracking

clang/test/Interpreter/virtualdef-outside.cpp exposes bug, which
has the following root cause: moveLazyEmissionStates() carries
DeferredVTables across PTU boundaries but not the ItaniumCXXABI::VTables
DenseMap, which is the per-module cache that guards against re-emission
via hasInitializer(). Each new PTU's fresh ItaniumCXXABI doesn't know a
previous PTU already defined the vtable, so EmitDeferredVTables()
re-emits it.

Fix: add CodeGenModule::DefinedVTables (SmallPtrSet) to track classes
whose vtable was defined with ExternalLinkage in the current module.
moveLazyEmissionStates() transfers the set to the next PTU's module.
EmitDeferredVTables() skips any class already in DefinedVTables, leaving
its GlobalVariable as an external declaration that the JIT resolves from
the defining PTU's already-loaded code.

Only ExternalLinkage vtables are tracked: the JIT reliably exports strong
symbols cross-module. WeakODR/LinkOnceODR vtables (inline key functions,
template instantiations) are not reliably cross-module resolvable in the
JIT, so later PTUs must re-emit their own copy via linkonce deduplication.

closes #141039
---
 clang/lib/CodeGen/CGVTables.cpp               |  8 +++-
 clang/lib/CodeGen/CodeGenModule.cpp           |  4 ++
 clang/lib/CodeGen/CodeGenModule.h             |  7 ++++
 clang/lib/CodeGen/ItaniumCXXABI.cpp           |  9 +++++
 clang/test/Interpreter/virtualdef-outside.cpp | 40 +++++++++++++++++++
 5 files changed, 67 insertions(+), 1 deletion(-)
 create mode 100644 clang/test/Interpreter/virtualdef-outside.cpp

diff --git a/clang/lib/CodeGen/CGVTables.cpp b/clang/lib/CodeGen/CGVTables.cpp
index 3891697a986e4..86aa56e0400f1 100644
--- a/clang/lib/CodeGen/CGVTables.cpp
+++ b/clang/lib/CodeGen/CGVTables.cpp
@@ -1300,11 +1300,17 @@ void CodeGenModule::EmitDeferredVTables() {
   size_t savedSize = DeferredVTables.size();
 #endif
 
-  for (const CXXRecordDecl *RD : DeferredVTables)
+  for (const CXXRecordDecl *RD : DeferredVTables) {
+    // In incremental compilation, the vtable may have been defined in a
+    // previous PTU's module.  The GlobalVariable in this module is just an
+    // external declaration; the JIT resolves it from the earlier module.
+    if (EmittedVTables.count(RD))
+      continue;
     if (shouldEmitVTableAtEndOfTranslationUnit(*this, RD))
       VTables.GenerateClassData(RD);
     else if (shouldOpportunisticallyEmitVTables())
       OpportunisticVTables.push_back(RD);
+  }
 
   assert(savedSize == DeferredVTables.size() &&
          "deferred extra vtables during vtable emission?");
diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp
index 3b64be7a477d6..ffa8377f03e45 100644
--- a/clang/lib/CodeGen/CodeGenModule.cpp
+++ b/clang/lib/CodeGen/CodeGenModule.cpp
@@ -8506,6 +8506,10 @@ void CodeGenModule::moveLazyEmissionStates(CodeGenModule *NewBuilder) {
          "Newly created module should not have deferred vtables");
   NewBuilder->DeferredVTables = std::move(DeferredVTables);
 
+  assert(NewBuilder->EmittedVTables.empty() &&
+         "Newly created module should not have defined vtables");
+  NewBuilder->EmittedVTables = std::move(EmittedVTables);
+
   assert(NewBuilder->MangledDeclNames.empty() &&
          "Newly created module should not have mangled decl names");
   assert(NewBuilder->Manglings.empty() &&
diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h
index 0081bf5c4cf5f..ed023548c7334 100644
--- a/clang/lib/CodeGen/CodeGenModule.h
+++ b/clang/lib/CodeGen/CodeGenModule.h
@@ -456,6 +456,11 @@ class CodeGenModule : public CodeGenTypeCache {
   /// A queue of (optional) vtables to consider emitting.
   std::vector<const CXXRecordDecl*> DeferredVTables;
 
+  /// In incremental compilation, the set of vtable classes whose vtable
+  /// definitions were emitted into a previous PTU's module. Carried forward
+  /// by moveLazyEmissionStates() so later PTUs skip re-defining them.
+  llvm::SmallPtrSet<const CXXRecordDecl *, 8> EmittedVTables;
+
   /// A queue of (optional) vtables that may be emitted opportunistically.
   std::vector<const CXXRecordDecl *> OpportunisticVTables;
 
@@ -1574,6 +1579,8 @@ class CodeGenModule : public CodeGenTypeCache {
     DeferredVTables.push_back(RD);
   }
 
+  void markVTableEmitted(const CXXRecordDecl *RD) { EmittedVTables.insert(RD); }
+
   /// Emit code for a single global function or var decl. Forward declarations
   /// are emitted lazily.
   void EmitGlobal(GlobalDecl D);
diff --git a/clang/lib/CodeGen/ItaniumCXXABI.cpp b/clang/lib/CodeGen/ItaniumCXXABI.cpp
index 8a06051a1c730..9be49da6caed3 100644
--- a/clang/lib/CodeGen/ItaniumCXXABI.cpp
+++ b/clang/lib/CodeGen/ItaniumCXXABI.cpp
@@ -2097,6 +2097,15 @@ void ItaniumCXXABI::emitVTableDefinitions(CodeGenVTables &CGVT,
   // Set the correct linkage.
   VTable->setLinkage(Linkage);
 
+  // Track ExternalLinkage vtable definitions so moveLazyEmissionStates() can
+  // tell the next PTU not to re-emit them.  We only do this for ExternalLinkage
+  // because the JIT reliably exports strong symbols cross-module.
+  // WeakODR/LinkOnceODR vtables (e.g. inline key functions, template
+  // instantiations) are not reliably cross-module resolvable in the JIT, so
+  // later PTUs must re-emit their own copy.
+  if (Linkage == llvm::GlobalValue::ExternalLinkage)
+    CGM.markVTableEmitted(RD);
+
   if (CGM.supportsCOMDAT() && VTable->isWeakForLinker())
     VTable->setComdat(CGM.getModule().getOrInsertComdat(VTable->getName()));
 
diff --git a/clang/test/Interpreter/virtualdef-outside.cpp b/clang/test/Interpreter/virtualdef-outside.cpp
new file mode 100644
index 0000000000000..8c2fc4891182b
--- /dev/null
+++ b/clang/test/Interpreter/virtualdef-outside.cpp
@@ -0,0 +1,40 @@
+// REQUIRES: host-supports-jit
+// RUN: cat %s | clang-repl | FileCheck %s
+// virtual functions defined outside of class had duplicate symbols:
+//     duplicate definition of symbol '__ZTV3Two' (i.e., vtable for Two)
+// see https://github.com/llvm/llvm-project/issues/141039.
+// fixed in PR #185648
+
+extern "C" int printf(const char *, ...);
+
+struct X1 { virtual void vi() { printf("1vi\n"); } };
+X1().vi();
+// CHECK: 1vi
+
+struct X2 { virtual void vo(); };
+void X2::vo() { printf("2vo\n"); }
+X2().vo();
+// CHECK: 2vo
+
+struct X3 { \
+  void ni() { printf("3ni\n"); } \
+  void no(); \
+  virtual void vi() { printf("3vi\n"); } \
+  virtual void vo(); \
+  virtual ~X3() { printf("3d\n"); } \
+};
+void X3::no() { printf("3no\n"); }
+void X3::vo() { printf("3vo\n"); }
+auto x3 = new X3;
+x3->ni();
+// CHECK: 3ni
+x3->no();
+// CHECK: 3no
+x3->vi();
+// CHECK: 3vi
+x3->vo();
+// CHECK: 3vo
+delete x3;
+// CHECK: 3d
+
+%quit



More information about the cfe-commits mailing list