[clang] [clang-repl] Keep earlier declarations alive when an input fails (PR #218149)

Vipul Cariappa via cfe-commits cfe-commits at lists.llvm.org
Mon Aug 24 05:08:11 PDT 2026


https://github.com/Vipul-Cariappa updated https://github.com/llvm/llvm-project/pull/218149

>From a3386428ad025c57212b4c3c7fc0cf96372dc0bc Mon Sep 17 00:00:00 2001
From: Vipul Cariappa <vipulcariappa at gmail.com>
Date: Fri, 21 Aug 2026 23:56:37 +0530
Subject: [PATCH 1/2] [clang-repl] Keep earlier declarations alive when an
 input fails

Fixes llvm/llvm-project#201844.
---
 clang/include/clang/AST/DeclCXX.h             |   1 +
 clang/lib/Interpreter/IncrementalAction.cpp   |   2 +
 clang/lib/Interpreter/IncrementalAction.h     |   4 +
 clang/lib/Interpreter/IncrementalParser.cpp   | 118 +++++++++++++++++-
 clang/lib/Interpreter/IncrementalParser.h     |   9 ++
 .../failed-input-keeps-redecls.cpp            | 101 +++++++++++++++
 6 files changed, 232 insertions(+), 3 deletions(-)
 create mode 100644 clang/test/Interpreter/failed-input-keeps-redecls.cpp

diff --git a/clang/include/clang/AST/DeclCXX.h b/clang/include/clang/AST/DeclCXX.h
index a42884be71d68..11d3db5d99293 100644
--- a/clang/include/clang/AST/DeclCXX.h
+++ b/clang/include/clang/AST/DeclCXX.h
@@ -264,6 +264,7 @@ class CXXRecordDecl : public RecordDecl {
   friend class ASTRecordWriter;
   friend class ASTWriter;
   friend class DeclContext;
+  friend class IncrementalParser;
   friend class LambdaExpr;
   friend class ODRDiagsEmitter;
 
diff --git a/clang/lib/Interpreter/IncrementalAction.cpp b/clang/lib/Interpreter/IncrementalAction.cpp
index d22031c8fa893..10ca3d3a753c5 100644
--- a/clang/lib/Interpreter/IncrementalAction.cpp
+++ b/clang/lib/Interpreter/IncrementalAction.cpp
@@ -120,6 +120,8 @@ std::unique_ptr<llvm::Module> IncrementalAction::GenModule() {
   return nullptr;
 }
 
+void IncrementalAction::discardModule() { GenModule(); }
+
 CodeGenerator *IncrementalAction::getCodeGen() const {
   FrontendAction *WrappedAct = getWrapped();
   if (!WrappedAct || !WrappedAct->hasIRSupport())
diff --git a/clang/lib/Interpreter/IncrementalAction.h b/clang/lib/Interpreter/IncrementalAction.h
index 725cdd0c27cf4..31547cd785967 100644
--- a/clang/lib/Interpreter/IncrementalAction.h
+++ b/clang/lib/Interpreter/IncrementalAction.h
@@ -74,6 +74,10 @@ class IncrementalAction : public WrapperFrontendAction {
 
   /// Generate an LLVM module for the most recent parsed input.
   std::unique_ptr<llvm::Module> GenModule();
+
+  /// Throw away what CodeGen emitted for an input that failed to parse and
+  /// start a fresh module.
+  void discardModule();
 };
 
 class InProcessPrintingASTConsumer final : public MultiplexConsumer {
diff --git a/clang/lib/Interpreter/IncrementalParser.cpp b/clang/lib/Interpreter/IncrementalParser.cpp
index 59018907056b8..a30c720873abb 100644
--- a/clang/lib/Interpreter/IncrementalParser.cpp
+++ b/clang/lib/Interpreter/IncrementalParser.cpp
@@ -15,7 +15,9 @@
 
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
 #include "clang/AST/DeclContextInternals.h"
+#include "clang/AST/DeclTemplate.h"
 #include "clang/Frontend/CompilerInstance.h"
 #include "clang/Interpreter/PartialTranslationUnit.h"
 #include "clang/Parse/Parser.h"
@@ -87,6 +89,12 @@ IncrementalParser::ParseOrWrapTopLevelDecl() {
   if (Diags.hasErrorOccurred()) {
     CleanUpPTU(C.getTranslationUnitDecl());
 
+    // Whatever got through before the error was already handed to CodeGen. It
+    // belongs to no PTU and will never be executed, but it stays in the module
+    // the next input emits into and collides with the definitions that input
+    // provides. Drop it and start a fresh module.
+    Act->discardModule();
+
     Diags.Reset(/*soft=*/true);
     Diags.getClient()->clear();
     return llvm::make_error<llvm::StringError>("Parsing failed.",
@@ -191,30 +199,134 @@ void IncrementalParser::withdrawMostRecentTU(
   C.TUDecl = Prev;
 }
 
+/// The newest declaration of whatever D redeclares that still lives outside
+/// DiscardedTU
+static NamedDecl *findSurvivingPrevDecl(NamedDecl *D,
+                                        TranslationUnitDecl *DiscardedTU) {
+  for (Decl *Prev = D->getPreviousDecl(); Prev; Prev = Prev->getPreviousDecl())
+    if (Prev->getTranslationUnitDecl() != DiscardedTU)
+      return dyn_cast<NamedDecl>(Prev);
+  return nullptr;
+}
+
+/// Unlink everything a discarded re-opening of a namespace put into it. Members
+/// are made visible in the namespace's primary context, which outlives the
+/// discarded PTU, so leaving them behind would keep half-parsed declarations
+/// reachable
+static void dropContainingMembers(NamespaceDecl *ND) {
+  llvm::SmallVector<Decl *, 8> Members(ND->decls());
+  for (Decl *M : Members)
+    ND->removeDecl(M);
+}
+
+bool IncrementalParser::withdrawRedecl(NamedDecl *D, NamedDecl *Prev,
+                                       TranslationUnitDecl *DiscardedTU) {
+  ASTContext &C = S.getASTContext();
+
+  auto Unlink = [&C](auto *Latest, NamedDecl *SurvivorND) {
+    using T = std::remove_pointer_t<decltype(Latest)>;
+    auto *Survivor = cast<T>(SurvivorND);
+
+    // Rebuild First -> ... -> Survivor -> ... -> Latest as
+    // First -> ... -> Survivor.
+    Latest->getFirstDecl()->RedeclLink.setLatest(Survivor);
+
+    // The chain is circular: a withdrawn declaration still linked into it can
+    // never walk back around to itself, so redecls() on one would not
+    // terminate. Give each a chain of its own.
+    for (T *Dead = Latest; Dead != Survivor;) {
+      T *Next = Dead->getPreviousDecl();
+      Dead->First = Dead;
+      Dead->RedeclLink = Redeclarable<T>::LatestDeclLink(C);
+      Dead = Next;
+    }
+  };
+
+  if (auto *TD = dyn_cast<TagDecl>(D)) {
+    Unlink(TD, Prev);
+    // A class keeps its definition outside the redeclaration chain, in the
+    // DefinitionData that startDefinition() hands to every redeclaration.
+    // Unlinking leaves the survivors pointing at the discarded definition, so
+    // drop it; a later definition allocates a fresh one for the whole chain.
+    if (auto *RD = dyn_cast<CXXRecordDecl>(Prev))
+      if (CXXRecordDecl *Def = RD->getDefinition();
+          Def && Def->getTranslationUnitDecl() == DiscardedTU)
+        for (auto *R : RD->redecls())
+          cast<CXXRecordDecl>(R)->DefinitionData = nullptr;
+    return true;
+  }
+  if (auto *FD = dyn_cast<FunctionDecl>(D)) {
+    Unlink(FD, Prev);
+    return true;
+  }
+  if (auto *VD = dyn_cast<VarDecl>(D)) {
+    Unlink(VD, Prev);
+    return true;
+  }
+  if (auto *ND = dyn_cast<NamespaceDecl>(D)) {
+    dropContainingMembers(ND);
+    Unlink(ND, Prev);
+    return true;
+  }
+  if (auto *TND = dyn_cast<TypedefNameDecl>(D)) {
+    Unlink(TND, Prev);
+    return true;
+  }
+  if (auto *RTD = dyn_cast<RedeclarableTemplateDecl>(D)) {
+    auto *PrevRTD = cast<RedeclarableTemplateDecl>(Prev);
+    Unlink(RTD, Prev);
+    // A template declares keeps a redeclaration chain of its own,
+    // running alongside the template's.
+    return withdrawRedecl(RTD->getTemplatedDecl(), PrevRTD->getTemplatedDecl(),
+                          DiscardedTU);
+  }
+  return false;
+}
+
 void IncrementalParser::CleanUpPTU(TranslationUnitDecl *MostRecentTU) {
   if (StoredDeclsMap *Map = MostRecentTU->getPrimaryContext()->getLookupPtr()) {
     // Collect the keys to erase: erasing during iteration invalidates the map
     // iterator under backward-shift deletion.
     llvm::SmallVector<DeclarationName, 16> KeysToErase;
+    // Declarations an earlier input made and this one only redeclared: the
+    // name goes back to what it meant before rather than disappearing.
+    llvm::SmallVector<std::pair<DeclarationName, NamedDecl *>, 4>
+        DeclsToRestore;
     for (auto &&[Key, List] : *Map) {
       DeclContextLookupResult R = List.getLookupResult();
       std::vector<NamedDecl *> NamedDeclsToRemove;
       bool RemoveAll = true;
       for (NamedDecl *D : R) {
-        if (D->getTranslationUnitDecl() == MostRecentTU)
-          NamedDeclsToRemove.push_back(D);
-        else
+        if (D->getTranslationUnitDecl() != MostRecentTU) {
           RemoveAll = false;
+          continue;
+        }
+        NamedDeclsToRemove.push_back(D);
       }
+      // Dropping the lookup entries is not enough: what survives still
+      // redeclares the discarded declarations and would reach them by walking
+      // the chain.
+      llvm::SmallVector<NamedDecl *, 4> Survivors;
+      for (NamedDecl *D : NamedDeclsToRemove)
+        if (NamedDecl *Prev = findSurvivingPrevDecl(D, MostRecentTU))
+          if (withdrawRedecl(D, Prev, MostRecentTU))
+            Survivors.push_back(Prev);
+
       if (LLVM_LIKELY(RemoveAll)) {
         KeysToErase.push_back(Key);
+        for (NamedDecl *Prev : Survivors)
+          DeclsToRestore.emplace_back(Key, Prev);
       } else {
+        // Other declarations of the name remain visible, so there is nothing
+        // to put back.
         for (NamedDecl *D : NamedDeclsToRemove)
           List.remove(D);
       }
     }
     for (DeclarationName Key : KeysToErase)
       Map->erase(Key);
+    for (auto &[Key, Prev] : DeclsToRestore)
+      (*Map)[Key].addOrReplaceDecl(Prev);
   }
 
   ExternCContextDecl *ECCD = S.getASTContext().getExternCContextDecl();
diff --git a/clang/lib/Interpreter/IncrementalParser.h b/clang/lib/Interpreter/IncrementalParser.h
index b626cebaafcd7..a07ab784f4a59 100644
--- a/clang/lib/Interpreter/IncrementalParser.h
+++ b/clang/lib/Interpreter/IncrementalParser.h
@@ -25,6 +25,7 @@ class ASTConsumer;
 class CompilerInstance;
 class Parser;
 class Sema;
+class NamedDecl;
 class TranslationUnitDecl;
 class IncrementalAction;
 struct PartialTranslationUnit;
@@ -73,6 +74,14 @@ class IncrementalParser {
   /// Rebuild the translation unit redeclaration chain without \p MostRecentTU,
   /// making its predecessor the current unit again.
   void withdrawMostRecentTU(TranslationUnitDecl *MostRecentTU);
+
+  /// Rebuild \p D's redeclaration chain without the
+  /// declarations \p DiscardedTU contributed, making \p Prev current again.
+  /// \returns false if \p D is of a kind we cannot unlink, in which case the
+  /// caller must drop the name rather than leave \p Prev pointing into
+  /// \p DiscardedTU.
+  bool withdrawRedecl(NamedDecl *D, NamedDecl *Prev,
+                      TranslationUnitDecl *DiscardedTU);
 };
 } // end namespace clang
 
diff --git a/clang/test/Interpreter/failed-input-keeps-redecls.cpp b/clang/test/Interpreter/failed-input-keeps-redecls.cpp
new file mode 100644
index 0000000000000..0fec65683584f
--- /dev/null
+++ b/clang/test/Interpreter/failed-input-keeps-redecls.cpp
@@ -0,0 +1,101 @@
+// REQUIRES: host-supports-jit
+// RUN: cat %s | clang-repl 2>&1 | FileCheck %s
+// RUN: cat %s | clang-repl 2>&1 | FileCheck %s --check-prefix=NEG
+
+// A failed input must not take earlier declarations down with it, and must not
+// leave anything of its own behind for a later input to trip over.
+
+extern "C" int printf(const char *, ...);
+
+namespace N { struct S { int v; }; void foo() { printf("foo\n"); } }
+
+namespace N { void bar() { printf("bar\n" } }
+// CHECK-DAG: error: expected ')'
+
+// Everything N held before the failed input is still reachable.
+N::foo();
+// CHECK-DAG: foo
+N::S s; s.v = 7; printf("s.v = %d\n", s.v);
+// CHECK-DAG: s.v = 7
+
+// N is still open for business, and bar is free to be defined properly.
+namespace N { void bar() { printf("bar\n"); } }
+N::bar();
+// CHECK-DAG: bar
+// NEG-NOT: error: call to 'bar' is ambiguous
+
+namespace N { void baz() { printf("baz\n"); } }
+N::baz();
+// CHECK-DAG: baz
+
+// A name that only ever existed in a failed input stays gone.
+namespace M { int m = undeclared_thing; }
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+int probe = M::m;
+// CHECK-DAG: error: use of undeclared identifier 'M'
+
+// A class survives a failed redefinition, and the failed definition does not
+// become the one everybody sees.
+struct T;
+struct T { int a; }; int e1 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+T *tp = nullptr; printf("T reachable %d\n", tp == nullptr);
+// CHECK-DAG: T reachable 1
+struct T { int a; int b; };
+printf("sizeof(T) = %d\n", (int)sizeof(T));
+// CHECK-DAG: sizeof(T) = 
+
+enum E : int;
+enum E : int { A = 1 }; int e2 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+enum E : int { A = 1, B = 2 };
+printf("B = %d\n", (int)B);
+// CHECK-DAG: B = 2
+
+// The body a failed input got as far as emitting must not be what gets called.
+void f();
+void f() { printf("f discarded\n"); } int e3 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+void f() { printf("f kept\n"); }
+f();
+// CHECK-DAG: f kept
+// NEG-NOT: {{^}}f discarded
+
+// Same for one overload of a set whose other overloads survive.
+void g(int) { printf("g int\n"); }
+void g(double) { printf("g discarded\n"); } int e4 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+g(1);
+// CHECK-DAG: g int
+void g(double) { printf("g kept\n"); }
+g(1.5);
+// CHECK-DAG: g kept
+// NEG-NOT: {{^}}g discarded
+g(1);
+// CHECK-DAG: g int
+
+// A template's pattern has a redeclaration chain of its own.
+template <class X> struct Box;
+template <class X> struct Box { X v; }; int e5 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+template <class X> struct Box { X v; int tag; };
+Box<int> b; b.v = 5; b.tag = 6; printf("box %d %d\n", b.v, b.tag);
+// CHECK-DAG: box 5 6
+
+template <class X> X twice(X x, int y) { return x * y; }
+template <class X> X twice(X x) { return x + x; } int e6 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+printf("twice = %d\n", twice(21, 2));
+// CHECK-DAG: twice = 42
+template <class X> X twice(X x) { return x * 2; }
+printf("twice = %d\n", twice(21));
+// CHECK-DAG: twice = 42
+
+extern int gv;
+int gv = 1; int e7 = undeclared_thing;
+// CHECK-DAG: error: use of undeclared identifier 'undeclared_thing'
+int gv = 9;
+printf("gv = %d\n", gv);
+// CHECK-DAG: gv = 9
+
+%quit

>From d0f04192ba160d53c45bae4a116bbda7fcd6b410 Mon Sep 17 00:00:00 2001
From: Vipul Cariappa <vipulcariappa at gmail.com>
Date: Mon, 24 Aug 2026 17:29:36 +0530
Subject: [PATCH 2/2] remove duplicated code

---
 clang/lib/Interpreter/IncrementalParser.cpp | 41 +++++++++------------
 1 file changed, 18 insertions(+), 23 deletions(-)

diff --git a/clang/lib/Interpreter/IncrementalParser.cpp b/clang/lib/Interpreter/IncrementalParser.cpp
index a30c720873abb..de72ea99abfd8 100644
--- a/clang/lib/Interpreter/IncrementalParser.cpp
+++ b/clang/lib/Interpreter/IncrementalParser.cpp
@@ -223,7 +223,10 @@ bool IncrementalParser::withdrawRedecl(NamedDecl *D, NamedDecl *Prev,
                                        TranslationUnitDecl *DiscardedTU) {
   ASTContext &C = S.getASTContext();
 
-  auto Unlink = [&C](auto *Latest, NamedDecl *SurvivorND) {
+  auto Unlink = [&C](auto *Latest, NamedDecl *SurvivorND) -> bool {
+    if (!Latest)
+      return false;
+
     using T = std::remove_pointer_t<decltype(Latest)>;
     auto *Survivor = cast<T>(SurvivorND);
 
@@ -240,10 +243,14 @@ bool IncrementalParser::withdrawRedecl(NamedDecl *D, NamedDecl *Prev,
       Dead->RedeclLink = Redeclarable<T>::LatestDeclLink(C);
       Dead = Next;
     }
+    return true;
   };
 
-  if (auto *TD = dyn_cast<TagDecl>(D)) {
-    Unlink(TD, Prev);
+  if (Unlink(dyn_cast<TagDecl>(D), Prev) ||
+      Unlink(dyn_cast<FunctionDecl>(D), Prev) ||
+      Unlink(dyn_cast<VarDecl>(D), Prev) ||
+      Unlink(dyn_cast<TypedefNameDecl>(D), Prev) ||
+      Unlink(dyn_cast<RedeclarableTemplateDecl>(D), Prev)) {
     // A class keeps its definition outside the redeclaration chain, in the
     // DefinitionData that startDefinition() hands to every redeclaration.
     // Unlinking leaves the survivors pointing at the discarded definition, so
@@ -253,14 +260,14 @@ bool IncrementalParser::withdrawRedecl(NamedDecl *D, NamedDecl *Prev,
           Def && Def->getTranslationUnitDecl() == DiscardedTU)
         for (auto *R : RD->redecls())
           cast<CXXRecordDecl>(R)->DefinitionData = nullptr;
-    return true;
-  }
-  if (auto *FD = dyn_cast<FunctionDecl>(D)) {
-    Unlink(FD, Prev);
-    return true;
-  }
-  if (auto *VD = dyn_cast<VarDecl>(D)) {
-    Unlink(VD, Prev);
+
+    // A template declares keeps a redeclaration chain of its own, running
+    // alongside the template's.
+    if (auto *RTD = dyn_cast<RedeclarableTemplateDecl>(D)) {
+      auto *PrevRTD = cast<RedeclarableTemplateDecl>(Prev);
+      return withdrawRedecl(RTD->getTemplatedDecl(),
+                            PrevRTD->getTemplatedDecl(), DiscardedTU);
+    }
     return true;
   }
   if (auto *ND = dyn_cast<NamespaceDecl>(D)) {
@@ -268,18 +275,6 @@ bool IncrementalParser::withdrawRedecl(NamedDecl *D, NamedDecl *Prev,
     Unlink(ND, Prev);
     return true;
   }
-  if (auto *TND = dyn_cast<TypedefNameDecl>(D)) {
-    Unlink(TND, Prev);
-    return true;
-  }
-  if (auto *RTD = dyn_cast<RedeclarableTemplateDecl>(D)) {
-    auto *PrevRTD = cast<RedeclarableTemplateDecl>(Prev);
-    Unlink(RTD, Prev);
-    // A template declares keeps a redeclaration chain of its own,
-    // running alongside the template's.
-    return withdrawRedecl(RTD->getTemplatedDecl(), PrevRTD->getTemplatedDecl(),
-                          DiscardedTU);
-  }
   return false;
 }
 



More information about the cfe-commits mailing list