[clang] [clang][bytecode] Lazily run ctor func of local blocks (PR #233250)

Timm Baeder via cfe-commits cfe-commits at lists.llvm.org
Mon Oct 19 07:58:20 PDT 2026


Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>,
Timm =?utf-8?q?Bäder?= <tbaeder at redhat.com>
Message-ID:
In-Reply-To: <llvm.org/llvm/llvm-project/pull/233250 at github.com>


https://github.com/tbaederr updated https://github.com/llvm/llvm-project/pull/233250

>From b12030b794ee81e40aa60739a63fe8243bd71488 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Sun, 18 Oct 2026 09:28:17 +0200
Subject: [PATCH 1/3] local block lazy init

---
 clang/lib/AST/ByteCode/Compiler.cpp    | 20 --------------
 clang/lib/AST/ByteCode/Interp.h        |  5 ----
 clang/lib/AST/ByteCode/InterpFrame.cpp | 38 ++++++++------------------
 clang/lib/AST/ByteCode/InterpFrame.h   | 31 +++++++++++++++------
 clang/lib/AST/ByteCode/Opcodes.td      |  3 --
 clang/lib/AST/ByteCode/Pointer.cpp     | 12 --------
 6 files changed, 33 insertions(+), 76 deletions(-)

diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index 40b36b4e715a1..66234608666de 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -148,29 +148,12 @@ template <class Emitter> class LocalScope : public VariableScope<Emitter> {
     if (!Idx) {
       Idx = static_cast<unsigned>(this->Ctx->Descriptors.size());
       this->Ctx->Descriptors.emplace_back();
-
-      if constexpr (!std::is_same_v<Emitter, EvalEmitter>)
-        this->Ctx->emitInitScope(*Idx, {});
     }
 
     Local.EnabledByDefault = this->LocalsAlwaysEnabled;
     this->Ctx->Descriptors[*Idx].emplace_back(Local);
   }
 
-  /// Force-initialize this scope. Usually, scopes are lazily initialized when
-  /// the first local variable is created, but in scenarios with conditonal
-  /// operators, we need to ensure scope is initialized just in case one of the
-  /// arms will create a local and the other won't. In such a case, the
-  /// InitScope() op would be part of the arm that created the local.
-  void forceInit() override {
-    if (!Idx) {
-      Idx = static_cast<unsigned>(this->Ctx->Descriptors.size());
-      this->Ctx->Descriptors.emplace_back();
-      if constexpr (!std::is_same_v<Emitter, EvalEmitter>)
-        this->Ctx->emitInitScope(*Idx, {});
-    }
-  }
-
   bool emitDestructors(const Expr *E = nullptr) override {
     if (!Idx)
       return true;
@@ -3183,9 +3166,6 @@ bool Compiler<Emitter>::VisitAbstractConditionalOperator(
     return false;
   }
 
-  // Force-init the scope, which creates a InitScope op. This is necessary so
-  // the scope is not only initialized in one arm of the conditional operator.
-  this->VarScope->forceInit();
   // The TrueExpr and FalseExpr of a conditional operator do _not_ create a
   // scope, which means the local variables created within them unconditionally
   // always exist. However, we need to later differentiate which branch was
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index 73f2d415cf56e..f72063a310114 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -2854,11 +2854,6 @@ inline bool SubPtr(InterpState &S, CodePtr OpPC, uint32_t ElemSize) {
   return true;
 }
 
-inline bool InitScope(InterpState &S, uint32_t I) {
-  S.Current->initScope(I);
-  return true;
-}
-
 inline bool EnableLocal(InterpState &S, uint32_t I) {
   assert(!S.Current->isLocalEnabled(I));
   S.Current->enableLocal(I);
diff --git a/clang/lib/AST/ByteCode/InterpFrame.cpp b/clang/lib/AST/ByteCode/InterpFrame.cpp
index c04a70cc49f7c..e1a3bc96efdff 100644
--- a/clang/lib/AST/ByteCode/InterpFrame.cpp
+++ b/clang/lib/AST/ByteCode/InterpFrame.cpp
@@ -48,11 +48,10 @@ InterpFrame::InterpFrame(InterpState &S, const Function *Func,
 
   for (auto &Scope : Func->scopes()) {
     for (auto &Local : Scope.locals()) {
-      new (localBlock(Local.Offset))
-          Block(S.EvalID, Local.Desc, Block::InlineDescMD);
+      Block *B = localBlock(Local.Offset);
+      new (B) Block(S.EvalID, Local.Desc, Block::InlineDescMD);
       // Note that we are NOT calling invokeCtor() here, since that is done
-      // via the InitScope op.
-      new (localInlineDesc(Local.Offset)) InlineDescriptor(Local.Desc);
+      // lazily when accessing the local.
     }
   }
 }
@@ -81,22 +80,14 @@ InterpFrame::~InterpFrame() {
   // This happens when the execution is interruped midway-through.
   for (auto &Scope : Func->scopes()) {
     for (auto &Local : Scope.locals()) {
-      S.deallocate(localBlock(Local.Offset));
+      Block *B = localBlock(Local.Offset);
+      if (B->isInitialized())
+        S.deallocate(B);
     }
   }
 }
 
-void InterpFrame::initScope(unsigned Idx) {
-  if (!Func)
-    return;
-
-  for (auto &Local : Func->getScope(Idx).locals()) {
-    assert(!localBlock(Local.Offset)->isInitialized());
-    localBlock(Local.Offset)->invokeCtor();
-  }
-}
-
-void InterpFrame::enableLocal(unsigned Idx) {
+void InterpFrame::enableLocal(unsigned Offset) {
   assert(Func);
 
   // FIXME: This is a little dirty, but to avoid adding a flag to
@@ -104,12 +95,14 @@ void InterpFrame::enableLocal(unsigned Idx) {
   // variables, we reuse the IsActive flag for the enabled state. We should
   // probably use a different struct than InlineDescriptor for the block-level
   // inline descriptor of local varaibles.
-  localInlineDesc(Idx)->IsActive = true;
+  getLocalBlock(Offset)->getBlockDesc<InlineDescriptor>().IsActive = true;
 }
 
 void InterpFrame::destroy(unsigned Idx) {
   for (auto &Local : Func->getScope(Idx).locals_reverse()) {
-    S.deallocate(localBlock(Local.Offset));
+    Block *B = localBlock(Local.Offset);
+    if (B->isInitialized())
+      S.deallocate(B);
   }
 }
 
@@ -234,15 +227,6 @@ const FunctionDecl *InterpFrame::getCallee() const {
   return Func->getDecl();
 }
 
-Pointer InterpFrame::getLocalPointer(unsigned Offset) const {
-  assert(Offset < Func->getFrameSize() && "Invalid local offset.");
-  return Pointer(localBlock(Offset));
-}
-
-Block *InterpFrame::getLocalBlock(unsigned Offset) const {
-  return localBlock(Offset);
-}
-
 Pointer InterpFrame::getParamPointer(unsigned Index) {
   assert(!isBottomFrame());
 
diff --git a/clang/lib/AST/ByteCode/InterpFrame.h b/clang/lib/AST/ByteCode/InterpFrame.h
index acc9c337bfb1b..5e03d58790ed7 100644
--- a/clang/lib/AST/ByteCode/InterpFrame.h
+++ b/clang/lib/AST/ByteCode/InterpFrame.h
@@ -58,10 +58,9 @@ class InterpFrame final : public Frame {
 
   /// Invokes the destructors for a scope.
   void destroy(unsigned Idx);
-  void initScope(unsigned Idx);
-  void enableLocal(unsigned Idx);
-  bool isLocalEnabled(unsigned Idx) const {
-    return localInlineDesc(Idx)->IsActive;
+  void enableLocal(unsigned Offset);
+  bool isLocalEnabled(unsigned Offset) const {
+    return getLocalBlock(Offset)->getBlockDesc<InlineDescriptor>().IsActive;
   }
 
   /// Describes the frame with arguments for diagnostic purposes.
@@ -91,14 +90,27 @@ class InterpFrame final : public Frame {
 
   /// Mutates a local variable.
   template <typename T> void setLocal(unsigned Offset, const T &Value) {
-    localRef<T>(Offset) = Value;
-    localInlineDesc(Offset)->IsInitialized = true;
-    localInlineDesc(Offset)->LifeState = Lifetime::Started;
+    Block *B = getLocalBlock(Offset);
+
+    B->deref<T>() = Value;
+    auto &LID = B->getBlockDesc<InlineDescriptor>();
+    LID.IsInitialized = true;
+    LID.LifeState = Lifetime::Started;
   }
 
   /// Returns a pointer to a local variables.
-  Pointer getLocalPointer(unsigned Offset) const;
-  Block *getLocalBlock(unsigned Offset) const;
+  Pointer getLocalPointer(unsigned Offset) const {
+    assert(Offset < Func->getFrameSize() && "Invalid local offset.");
+    return Pointer(getLocalBlock(Offset));
+  }
+  Block *getLocalBlock(unsigned Offset) const {
+    Block *B = reinterpret_cast<Block *>(locals() + Offset - sizeof(Block));
+    if (LLVM_UNLIKELY(!B->isInitialized())) {
+      B->invokeCtor();
+      new (localInlineDesc(Offset)) InlineDescriptor(B->getDescriptor());
+    }
+    return B;
+  };
 
   /// Returns the value of an argument.
   template <typename T> const T &getParam(unsigned Index) const {
@@ -188,6 +200,7 @@ class InterpFrame final : public Frame {
   }
 
   /// Returns a pointer to a local's block.
+  /// Will NOT initialize the block.
   Block *localBlock(unsigned Offset) const {
     return reinterpret_cast<Block *>(locals() + Offset - sizeof(Block));
   }
diff --git a/clang/lib/AST/ByteCode/Opcodes.td b/clang/lib/AST/ByteCode/Opcodes.td
index 7cd2098514166..9e2e5e8552e8e 100644
--- a/clang/lib/AST/ByteCode/Opcodes.td
+++ b/clang/lib/AST/ByteCode/Opcodes.td
@@ -267,9 +267,6 @@ def Destroy : Opcode {
   let Args = [ArgUint32];
   let HasCustomEval = 1;
 }
-def InitScope : SuccessOpcode {
-  let Args = [ArgUint32];
-}
 
 def GetLocalEnabled : SuccessOpcode {
   let Args = [ArgUint32];
diff --git a/clang/lib/AST/ByteCode/Pointer.cpp b/clang/lib/AST/ByteCode/Pointer.cpp
index 839a0929d1852..cc7199236e8e6 100644
--- a/clang/lib/AST/ByteCode/Pointer.cpp
+++ b/clang/lib/AST/ByteCode/Pointer.cpp
@@ -807,12 +807,6 @@ void PtrView::setLifeState(Lifetime L) const {
 }
 
 void PtrView::initialize() const {
-  // FIXME: This happens when the control flow jumps right into a scope, e.g. in
-  // switch_into_init_stmt in constant-expression-cxx2a.cpp. I.e. we have never
-  // initialized the scope via an InitScope op.
-  if (LLVM_UNLIKELY(!Pointee->isInitialized()))
-    Pointee->invokeCtor();
-
   if (isRoot() && Base == sizeof(GlobalInlineDescriptor) && Offset == Base) {
     auto &GD = Pointee->getBlockDesc<GlobalInlineDescriptor>();
     GD.InitState = GlobalInitState::Initialized;
@@ -840,12 +834,6 @@ void PtrView::initializeElement(unsigned Index) const {
 
   assert(Index < getFieldDesc()->getNumElems());
 
-  // FIXME: This happens when the control flow jumps right into a scope, e.g. in
-  // switch_into_init_stmt in constant-expression-cxx2a.cpp. I.e. we have never
-  // initialized the scope via an InitScope op.
-  if (LLVM_UNLIKELY(!Pointee->isInitialized()))
-    Pointee->invokeCtor();
-
   InitMapPtr &IM = getInitMap();
   if (IM.allInitialized())
     return;

>From 62a090afc865d9522230155ff153de6aac137c84 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Sun, 18 Oct 2026 18:51:03 +0200
Subject: [PATCH 2/3] Remove forceInit and add test case

---
 clang/lib/AST/ByteCode/Compiler.cpp |  1 -
 clang/test/AST/ByteCode/cxx20.cpp   | 21 +++++++++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index 66234608666de..e20e54837e926 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -101,7 +101,6 @@ template <class Emitter> class VariableScope {
 
   virtual bool emitDestructors(const Expr *E = nullptr) { return true; }
   virtual bool destroyLocals(const Expr *E = nullptr) { return true; }
-  virtual void forceInit() {}
   VariableScope *getParent() const { return Parent; }
   ScopeKind getKind() const { return Kind; }
 
diff --git a/clang/test/AST/ByteCode/cxx20.cpp b/clang/test/AST/ByteCode/cxx20.cpp
index d9200d18898d1..5ef748e162a23 100644
--- a/clang/test/AST/ByteCode/cxx20.cpp
+++ b/clang/test/AST/ByteCode/cxx20.cpp
@@ -1577,3 +1577,24 @@ namespace DiscardedConstevalStruct {
     return (make(), 0); // both-warning {{left operand of comma operator has no effect}}
   }
 }
+
+namespace ConditionalOperatorInBORHS {
+  struct S { int x; };
+
+  struct a {
+    int c = 12;
+
+    constexpr int e() {
+
+      0 && (0 ? 0 : 0); // both-warning {{use of logical '&&' with constant operand}} \
+                        // both-note {{use '&' for a bitwise operation}} \
+                        // both-note {{remove constant to silence this warning}} \
+                        // both-warning {{expression result unused}}
+
+      int f = c;
+
+      return (f);
+    }
+  };
+  static_assert(a{}.e() == 12);
+}

>From 45a898c985ca355863e69177ee63bcf0ba3c95ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timm=20B=C3=A4der?= <tbaeder at redhat.com>
Date: Mon, 19 Oct 2026 16:58:01 +0200
Subject: [PATCH 3/3] code duplication

---
 clang/lib/AST/ByteCode/InterpFrame.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/lib/AST/ByteCode/InterpFrame.h b/clang/lib/AST/ByteCode/InterpFrame.h
index 5e03d58790ed7..1043fc20f4fed 100644
--- a/clang/lib/AST/ByteCode/InterpFrame.h
+++ b/clang/lib/AST/ByteCode/InterpFrame.h
@@ -104,7 +104,7 @@ class InterpFrame final : public Frame {
     return Pointer(getLocalBlock(Offset));
   }
   Block *getLocalBlock(unsigned Offset) const {
-    Block *B = reinterpret_cast<Block *>(locals() + Offset - sizeof(Block));
+    Block *B = localBlock(Offset);
     if (LLVM_UNLIKELY(!B->isInitialized())) {
       B->invokeCtor();
       new (localInlineDesc(Offset)) InlineDescriptor(B->getDescriptor());



More information about the cfe-commits mailing list