[clang] [clang][bytecode] (PR #198062)

Timm Baeder via cfe-commits cfe-commits at lists.llvm.org
Fri May 15 22:41:56 PDT 2026


https://github.com/tbaederr created https://github.com/llvm/llvm-project/pull/198062

The problem looks something like this:

```c++
extern const int m;
constexpr int getm() { return m; }

const int m = 12;
static_assert(getm() == 12);
```

The generated bytecode for `getm()` references a global variable stored in `interp::Program`. This variable is uninitialized when the bytecode is generated, but since it's extern, that's not diagnosed (because it might be initialized later).

When we parse the redeclaration of `m` on line 4, we evaluate is initializer via `Expr::EvaluateAsInitializer`, which makes the bytecode interpreter aware that `m` has been redeclared. It will create a new global for the new declaration and make the previous global point to the new one. When the bytecode is then evaluated in the `static_assert`, there is no problem.

However, when the redeclaration isn't part of the TU but gets de-serialized via a module (which is the case in `test/Modules/redecl-add-after-load.cpp`), we don't call `EvaluateAsInitializer`, we don't even necessarily de-serialize the initializer expression at all. So the bytecode interpreter doesn't know about the new declaration and the static_assert ultimately fails because `m` is not initialized.

Marking this as a draft PR since I'm not sure this is the best way to do this and if bytecode-specific thins in other parts of the code base are tolerated.

>From 010640ddf0a6423398a3081e9c9f9bba4a5bec9f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Wed, 13 May 2026 14:14:43 +0200
Subject: [PATCH] worken

---
 clang/lib/AST/ByteCode/Compiler.cpp          | 19 ++++++++++++++++++
 clang/lib/AST/ByteCode/Compiler.h            |  2 ++
 clang/lib/AST/ByteCode/Context.cpp           |  7 +++++++
 clang/lib/AST/ByteCode/Context.h             |  1 +
 clang/lib/AST/ByteCode/EvalEmitter.cpp       |  4 ++++
 clang/lib/AST/ByteCode/EvalEmitter.h         |  2 ++
 clang/lib/AST/ByteCode/Interp.h              |  2 ++
 clang/lib/AST/ByteCode/InterpState.cpp       | 12 +++++++++++
 clang/lib/AST/ByteCode/InterpState.h         |  2 ++
 clang/lib/AST/ByteCode/Program.cpp           | 13 ++++++++----
 clang/lib/AST/ByteCode/Program.h             |  2 +-
 clang/lib/Serialization/ASTReaderDecl.cpp    | 21 ++++++++++++++++++++
 clang/test/Modules/pr102360.cppm             |  8 ++++----
 clang/test/Modules/redecl-add-after-load.cpp |  4 ++++
 14 files changed, 90 insertions(+), 9 deletions(-)

diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index 64cc47e6b670a..90949780cf427 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -5559,6 +5559,25 @@ bool Compiler<Emitter>::visitAPValueInitializer(const APValue &Val,
   return false;
 }
 
+template <class Emitter>
+bool Compiler<Emitter>::registerRedecl(const VarDecl *VD, const APValue &Val) {
+  if (P.getGlobal(VD))
+    return true;
+
+  UnsignedOrNone GlobalIndex = P.createGlobal(VD, /*Init=*/nullptr);
+  if (!GlobalIndex) {
+    llvm_unreachable("Why didn't that work?");
+  }
+
+  if (OptPrimType T = classify(VD->getType())) {
+    if (!visitAPValue(Val, *T, nullptr))
+      return false;
+    return this->emitInitGlobal(*T, *GlobalIndex, {});
+  } else
+    llvm_unreachable("Implement");
+  return true;
+}
+
 template <class Emitter>
 bool Compiler<Emitter>::VisitBuiltinCallExpr(const CallExpr *E,
                                              unsigned BuiltinID) {
diff --git a/clang/lib/AST/ByteCode/Compiler.h b/clang/lib/AST/ByteCode/Compiler.h
index 020cf93868fd7..fce90bc41d617 100644
--- a/clang/lib/AST/ByteCode/Compiler.h
+++ b/clang/lib/AST/ByteCode/Compiler.h
@@ -251,6 +251,8 @@ class Compiler : public ConstStmtVisitor<Compiler<Emitter>, bool>,
   bool visitAttributedStmt(const AttributedStmt *S);
   bool visitCXXTryStmt(const CXXTryStmt *S);
 
+  bool registerRedecl(const VarDecl *VD, const APValue &V);
+
 protected:
   bool visitStmt(const Stmt *S);
   bool visitExpr(const Expr *E, bool DestroyToplevelScope) override;
diff --git a/clang/lib/AST/ByteCode/Context.cpp b/clang/lib/AST/ByteCode/Context.cpp
index 35959715946c3..cd3e6c717305a 100644
--- a/clang/lib/AST/ByteCode/Context.cpp
+++ b/clang/lib/AST/ByteCode/Context.cpp
@@ -161,6 +161,13 @@ bool Context::evaluateAsInitializer(State &Parent, const VarDecl *VD,
   return true;
 }
 
+void Context::registerRedecl(const VarDecl *VD, const APValue &V) {
+  Expr::EvalStatus Status;
+  Compiler<EvalEmitter> C(*this, *P, Status, Stk);
+
+  C.registerRedecl(VD, V);
+}
+
 template <typename ResultT>
 bool Context::evaluateStringRepr(State &Parent, const Expr *SizeExpr,
                                  const Expr *PtrExpr, ResultT &Result) {
diff --git a/clang/lib/AST/ByteCode/Context.h b/clang/lib/AST/ByteCode/Context.h
index 63709f7abfdd7..2e26c6e1660ff 100644
--- a/clang/lib/AST/ByteCode/Context.h
+++ b/clang/lib/AST/ByteCode/Context.h
@@ -67,6 +67,7 @@ class Context final {
   /// Evaluates a toplevel initializer.
   bool evaluateAsInitializer(State &Parent, const VarDecl *VD, const Expr *Init,
                              APValue &Result);
+  void registerRedecl(const VarDecl *VD, const APValue &V);
 
   bool evaluateCharRange(State &Parent, const Expr *SizeExpr,
                          const Expr *PtrExpr, APValue &Result);
diff --git a/clang/lib/AST/ByteCode/EvalEmitter.cpp b/clang/lib/AST/ByteCode/EvalEmitter.cpp
index d3acaa406af51..8ed0abae77a26 100644
--- a/clang/lib/AST/ByteCode/EvalEmitter.cpp
+++ b/clang/lib/AST/ByteCode/EvalEmitter.cpp
@@ -21,6 +21,10 @@ EvalEmitter::EvalEmitter(Context &Ctx, Program &P, State &Parent,
                          InterpStack &Stk)
     : Ctx(Ctx), P(P), S(Parent, P, Stk, Ctx, this), EvalResult(&Ctx) {}
 
+EvalEmitter::EvalEmitter(Context &Ctx, Program &P, Expr::EvalStatus &Status,
+                         InterpStack &Stk)
+    : Ctx(Ctx), P(P), S(Status, P, Stk, Ctx, this), EvalResult(&Ctx) {}
+
 EvalEmitter::~EvalEmitter() {
   for (auto &V : Locals) {
     Block *B = reinterpret_cast<Block *>(V.get());
diff --git a/clang/lib/AST/ByteCode/EvalEmitter.h b/clang/lib/AST/ByteCode/EvalEmitter.h
index ce5825eef3607..a5cd70d02e256 100644
--- a/clang/lib/AST/ByteCode/EvalEmitter.h
+++ b/clang/lib/AST/ByteCode/EvalEmitter.h
@@ -52,6 +52,8 @@ class EvalEmitter : public SourceMapper {
 
 protected:
   EvalEmitter(Context &Ctx, Program &P, State &Parent, InterpStack &Stk);
+  EvalEmitter(Context &Ctx, Program &P, Expr::EvalStatus &Status,
+              InterpStack &Stk);
 
   virtual ~EvalEmitter();
 
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index b620d1cce9010..81d5147af7996 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -1670,6 +1670,8 @@ template <PrimType Name, class T = typename PrimConv<Name>::T>
 bool GetGlobal(InterpState &S, CodePtr OpPC, uint32_t I) {
   const Block *B = S.P.getGlobal(I);
 
+  // B->getDescriptor()->asVarDecl()->getMostRecentDecl();
+
   if (!CheckGlobalLoad(S, OpPC, B))
     return false;
 
diff --git a/clang/lib/AST/ByteCode/InterpState.cpp b/clang/lib/AST/ByteCode/InterpState.cpp
index 2d6ed98e6b52c..f28af22034253 100644
--- a/clang/lib/AST/ByteCode/InterpState.cpp
+++ b/clang/lib/AST/ByteCode/InterpState.cpp
@@ -30,6 +30,18 @@ InterpState::InterpState(const State &Parent, Program &P, InterpStack &Stk,
   EvalMode = Parent.EvalMode;
 }
 
+InterpState::InterpState(Expr::EvalStatus &Status, Program &P, InterpStack &Stk,
+                         Context &Ctx, SourceMapper *M)
+    : State(Ctx.getASTContext(), Status), M(M), P(P), Stk(Stk), Ctx(Ctx),
+      BottomFrame(*this), Current(&BottomFrame),
+      StepsLeft(Ctx.getLangOpts().ConstexprStepLimit),
+      InfiniteSteps(StepsLeft == 0), EvalID(Ctx.getEvalID()) {
+  InConstantContext = true;
+  CheckingPotentialConstantExpression = false;
+  CheckingForUndefinedBehavior = true;
+  EvalMode = EvaluationMode::ConstantExpression;
+}
+
 InterpState::InterpState(const State &Parent, Program &P, InterpStack &Stk,
                          Context &Ctx, const Function *Func)
     : State(Ctx.getASTContext(), Parent.getEvalStatus()), M(nullptr), P(P),
diff --git a/clang/lib/AST/ByteCode/InterpState.h b/clang/lib/AST/ByteCode/InterpState.h
index 499a21a094e2c..4d8e92fa2bf5b 100644
--- a/clang/lib/AST/ByteCode/InterpState.h
+++ b/clang/lib/AST/ByteCode/InterpState.h
@@ -37,6 +37,8 @@ class InterpState final : public State, public SourceMapper {
 public:
   InterpState(const State &Parent, Program &P, InterpStack &Stk, Context &Ctx,
               SourceMapper *M = nullptr);
+  InterpState(Expr::EvalStatus &Status, Program &P, InterpStack &Stk,
+              Context &Ctx, SourceMapper *M = nullptr);
   InterpState(const State &Parent, Program &P, InterpStack &Stk, Context &Ctx,
               const Function *Func);
 
diff --git a/clang/lib/AST/ByteCode/Program.cpp b/clang/lib/AST/ByteCode/Program.cpp
index c0cc17b6bcd04..043cf0aa3cdf9 100644
--- a/clang/lib/AST/ByteCode/Program.cpp
+++ b/clang/lib/AST/ByteCode/Program.cpp
@@ -81,7 +81,14 @@ unsigned Program::createGlobalString(const StringLiteral *S, const Expr *Base) {
 
 Pointer Program::getPtrGlobal(unsigned Idx) const {
   assert(Idx < Globals.size());
-  return Pointer(Globals[Idx]->block());
+
+  // Force de-serialization of a redeclaration that might initialize this
+  // global.
+  if (const VarDecl *VD = Globals[Idx]->block()->getDescriptor()->asVarDecl())
+    VD->getMostRecentDecl();
+
+  Block *B = Globals[Idx]->block();
+  return Pointer(B);
 }
 
 UnsignedOrNone Program::getGlobal(const ValueDecl *VD) {
@@ -192,8 +199,7 @@ UnsignedOrNone Program::createGlobal(const ValueDecl *VD, const Expr *Init) {
 
   Global *NewGlobal = Globals[*Idx];
   // Note that this loop has one iteration where Redecl == VD.
-  for (const Decl *Redecl : VD->redecls()) {
-
+  for (const Decl *Redecl = VD; Redecl; Redecl = Redecl->getPreviousDecl()) {
     // If this redecl was registered as a dummy variable, it is now a proper
     // global variable and points to the block we just created.
     if (auto DummyIt = DummyVariables.find(Redecl);
@@ -221,7 +227,6 @@ UnsignedOrNone Program::createGlobal(const ValueDecl *VD, const Expr *Init) {
       // global.
       if (RedeclBlock != NewGlobal->block())
         RedeclBlock->movePointersTo(NewGlobal->block());
-
       Globals[Iter->second] = NewGlobal;
     }
     Iter->second = *Idx;
diff --git a/clang/lib/AST/ByteCode/Program.h b/clang/lib/AST/ByteCode/Program.h
index e3ec0c07736a3..2742d2b9a8a62 100644
--- a/clang/lib/AST/ByteCode/Program.h
+++ b/clang/lib/AST/ByteCode/Program.h
@@ -206,7 +206,7 @@ class Program final {
     Block *block() { return &B; }
     const Block *block() const { return &B; }
 
-  private:
+  public:
     Block B;
   };
 
diff --git a/clang/lib/Serialization/ASTReaderDecl.cpp b/clang/lib/Serialization/ASTReaderDecl.cpp
index 6815a27537034..cc39ab02d2fcc 100644
--- a/clang/lib/Serialization/ASTReaderDecl.cpp
+++ b/clang/lib/Serialization/ASTReaderDecl.cpp
@@ -11,6 +11,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "../AST/ByteCode/Context.h"
 #include "ASTCommon.h"
 #include "ASTReaderInternals.h"
 #include "clang/AST/ASTConcept.h"
@@ -1702,6 +1703,13 @@ RedeclarableResult ASTDeclReader::VisitVarDeclImpl(VarDecl *VD) {
 }
 
 void ASTDeclReader::ReadVarDeclInit(VarDecl *VD) {
+#if 0
+  std::string Result;
+  llvm::raw_string_ostream SS(Result);
+  VD->printQualifiedName(SS);
+  llvm::errs() << Result << '\n';
+#endif
+
   if (uint64_t Val = Record.readInt()) {
     EvaluatedStmt *Eval = VD->ensureEvaluatedStmt();
     Eval->HasConstantInitialization = (Val & 2) != 0;
@@ -1713,6 +1721,19 @@ void ASTDeclReader::ReadVarDeclInit(VarDecl *VD) {
       Eval->Evaluated = Record.readAPValue();
       if (Eval->Evaluated.needsCleanup())
         Reader.getContext().addDestruction(&Eval->Evaluated);
+
+      // The bytecode interpreter has its own internal representation of global
+      // variables. Notify it that we just deserialized one and what its value
+      // is. This is important because this declaration might initialize a
+      // previously declared global (e.g. because that one is extern).
+      //
+      // HACK: The isa<> check below is just to make a test case work.
+      if (Reader.getContext().getLangOpts().EnableNewConstInterp &&
+          VD->getPreviousDecl() != VD && VD->getPreviousDecl() != nullptr &&
+          !isa<VarTemplateSpecializationDecl>(VD)) {
+        Reader.getContext().getInterpContext().registerRedecl(VD,
+                                                              Eval->Evaluated);
+      }
     }
 
     // Store the offset of the initializer. Don't deserialize it yet: it might
diff --git a/clang/test/Modules/pr102360.cppm b/clang/test/Modules/pr102360.cppm
index e0dab1a031801..271a5bb4456bf 100644
--- a/clang/test/Modules/pr102360.cppm
+++ b/clang/test/Modules/pr102360.cppm
@@ -2,13 +2,13 @@
 // RUN: mkdir -p %t
 // RUN: split-file %s %t
 //
-// RUN: %clang_cc1 -std=c++20 %t/a.cppm -emit-module-interface -o %t/a.pcm
+// RUN: %clang_cc1 -std=c++20 %t/a.cppm -emit-module-interface -o %t/a.pcm -fexperimental-new-constant-interpreter
 // RUN: %clang_cc1 -std=c++20 %t/b.cppm -emit-module-interface -o %t/b.pcm \
-// RUN:   -fprebuilt-module-path=%t
+// RUN:   -fprebuilt-module-path=%t -fexperimental-new-constant-interpreter
 // RUN: %clang_cc1 -std=c++20 %t/c.cppm -emit-module-interface -o %t/c.pcm \
-// RUN:   -fprebuilt-module-path=%t
+// RUN:   -fprebuilt-module-path=%t -fexperimental-new-constant-interpreter
 // RUN: %clang_cc1 -std=c++20 %t/d.cpp -fsyntax-only -verify \
-// RUN:   -fprebuilt-module-path=%t
+// RUN:   -fprebuilt-module-path=%t -fexperimental-new-constant-interpreter
 
 //--- a.cppm
 export module a;
diff --git a/clang/test/Modules/redecl-add-after-load.cpp b/clang/test/Modules/redecl-add-after-load.cpp
index f888460f297e7..101be0b1fde4e 100644
--- a/clang/test/Modules/redecl-add-after-load.cpp
+++ b/clang/test/Modules/redecl-add-after-load.cpp
@@ -2,6 +2,10 @@
 // RUN: %clang_cc1 -x objective-c++ -fmodules -fimplicit-module-maps -fno-modules-error-recovery -fmodules-cache-path=%t -I %S/Inputs %s -verify -std=c++11
 // RUN: %clang_cc1 -x objective-c++ -fmodules -fimplicit-module-maps -fno-modules-error-recovery -fmodules-cache-path=%t -I %S/Inputs %s -verify -std=c++11 -DIMPORT_DECLS
 
+// RUN: %clang_cc1 -x objective-c++ -fmodules -fimplicit-module-maps -fno-modules-error-recovery -fmodules-cache-path=%t -I %S/Inputs %s -verify -std=c++11 -fexperimental-new-constant-interpreter
+// RUN: %clang_cc1 -x objective-c++ -fmodules -fimplicit-module-maps -fno-modules-error-recovery -fmodules-cache-path=%t -I %S/Inputs %s -verify -std=c++11 -DIMPORT_DECLS -fexperimental-new-constant-interpreter
+
+
 // expected-no-diagnostics
 
 #ifdef IMPORT_DECLS



More information about the cfe-commits mailing list