[clang] [clang][bytecode] Use a special opcode for trivial defaulted copy/move operator calls (PR #222332)

via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 9 06:55:27 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang

Author: Timm Baeder (tbaederr)

<details>
<summary>Changes</summary>

The default code that clang synthesizes for them contains calls to `__builtin_memcpy`, e.g. in

```
CXXMethodDecl 0x7d471ee7cc90 <array.cpp:1983:10> col:10 implicit used constexpr operator= 'array &(const array &) noexcept' inline default trivial external-linkage
|-ParmVarDecl 0x7d471ee7cdd0 <col:10> col:10 used 'const array &'
`-CompoundStmt 0x7d471ee7e340 <col:10>
  |-CallExpr 0x7d471ee7e268 <col:10> 'void *'
  | |-ImplicitCastExpr 0x7d471ee7e248 <col:10> 'void *(*)(void *, const void *, __size_t) noexcept' <BuiltinFnToFnPtr>
  | | `-DeclRefExpr 0x7d471ee7e160 <col:10> '<builtin fn type>' Function 0x7d471ee7de88 '__builtin_memcpy' 'void *(void *, const void *, __size_t) noexcept'
  | |-ImplicitCastExpr 0x7d471ee7e2b0 <col:10> 'void *' <BitCast>
  | | `-UnaryOperator 0x7d471ee7dd18 <col:10> 'int (*)[4]' prefix '&' cannot overflow
  | |   `-MemberExpr 0x7d471ee7dca0 <col:10> 'int[4]' lvalue ->_M_elems 0x7d471ee52780
  | |     `-CXXThisExpr 0x7d471ee7dc88 <col:10> 'array *' this
  | |-ImplicitCastExpr 0x7d471ee7e2d0 <col:10> 'const void *' <BitCast>
  | | `-UnaryOperator 0x7d471ee7db38 <col:10> 'const int (*)[4]' prefix '&' cannot overflow
  | |   `-MemberExpr 0x7d471ee7dac0 <col:10> 'const int[4]' lvalue ._M_elems 0x7d471ee52780
  | |     `-DeclRefExpr 0x7d471ee7da98 <col:10> 'const array' lvalue ParmVar 0x7d471ee7cdd0 depth 0 index 0 'const array &'
  | `-IntegerLiteral 0x7d471ee7e1a8 <col:10> '__size_t':'unsigned long' 16
  `-ReturnStmt 0x7d471ee7e328 <col:10>
    `-UnaryOperator 0x7d471ee7e308 <col:10> 'array' lvalue prefix '*' cannot overflow
      `-CXXThisExpr 0x7d471ee7e2f0 <col:10> 'array *' this
```

However, this violates a contract of `__builtin_memcpy` if source and destination are the same pointer, as in `*__first = *__first`, which is happening in one of the attached test cases.  The check for this is correct though, so add a new opcode for this case. The functionality is similar to `__builtin_memcpy` but it doesn't check for overlapping regions (among others).


Fixes https://github.com/llvm/llvm-project/issues/221403

---

Patch is 21.05 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/222332.diff


12 Files Affected:

- (modified) clang/lib/AST/ByteCode/Compiler.cpp (+39-3) 
- (modified) clang/lib/AST/ByteCode/Interp.cpp (+66-8) 
- (modified) clang/lib/AST/ByteCode/Interp.h (+9-20) 
- (modified) clang/lib/AST/ByteCode/InterpBuiltin.cpp (+29-16) 
- (modified) clang/lib/AST/ByteCode/InterpHelpers.h (+6-1) 
- (modified) clang/lib/AST/ByteCode/Opcodes.td (+3) 
- (modified) clang/lib/AST/ByteCode/Pointer.h (+1) 
- (modified) clang/lib/AST/ExprConstShared.h (+3) 
- (modified) clang/lib/AST/ExprConstant.cpp (+3-3) 
- (modified) clang/test/AST/ByteCode/cxx14.cpp (+20) 
- (modified) clang/test/AST/ByteCode/cxx26.cpp (+56) 
- (modified) clang/test/AST/ByteCode/cxx2a.cpp (+17) 


``````````diff
diff --git a/clang/lib/AST/ByteCode/Compiler.cpp b/clang/lib/AST/ByteCode/Compiler.cpp
index daa5307c92298..e4f9c1d501fff 100644
--- a/clang/lib/AST/ByteCode/Compiler.cpp
+++ b/clang/lib/AST/ByteCode/Compiler.cpp
@@ -6142,6 +6142,15 @@ bool Compiler<Emitter>::VisitBuiltinCallExpr(const CallExpr *E,
   return true;
 }
 
+static bool isTrivialMemoryOperation(const CXXMethodDecl *MD) {
+  if (!MD || !MD->isDefaulted())
+    return false;
+  if (!MD->isCopyAssignmentOperator() && !MD->isMoveAssignmentOperator())
+    return false;
+  return MD->getParent()->isUnion() ||
+         (MD->isTrivial() && isReadByLvalueToRvalueConversion(MD->getParent()));
+}
+
 template <class Emitter>
 bool Compiler<Emitter>::VisitCallExpr(const CallExpr *E) {
   if (E->containsErrors())
@@ -6174,6 +6183,35 @@ bool Compiler<Emitter>::VisitCallExpr(const CallExpr *E) {
   }
 
   LocalScope<Emitter> CallScope(this, ScopeKind::Call);
+  ArrayRef<const Expr *> Args(E->getArgs(), E->getNumArgs());
+  bool ActivateLHS = false;
+
+  // Emit a special op for trivial copy/move operators.
+  if (isTrivialMemoryOperation(dyn_cast_if_present<CXXMethodDecl>(FuncDecl))) {
+    const Function *Func = getFunction(FuncDecl);
+    if (!Func)
+      return false;
+
+    if (const auto *OCE = dyn_cast<CXXOperatorCallExpr>(E);
+        OCE && OCE->isAssignmentOp()) {
+      const CXXRecordDecl *LHSRecord = Args[0]->getType()->getAsCXXRecordDecl();
+      ActivateLHS = LHSRecord && LHSRecord->hasTrivialDefaultConstructor();
+    }
+    if (const auto *MCE = dyn_cast<CXXMemberCallExpr>(E))
+      if (!this->visit(MCE->getImplicitObjectArgument()))
+        return false;
+
+    if (!this->visitCallArgs(Args, FuncDecl, /*ActivateLHS=*/ActivateLHS,
+                             isa<CXXOperatorCallExpr>(E)))
+      return false;
+
+    if (!this->emitTrivialCopy(ActivateLHS, Func, E))
+      return false;
+
+    if (!DiscardResult)
+      return CallScope.destroyLocals();
+    return this->emitPopPtr(E) && CallScope.destroyLocals();
+  }
 
   QualType ReturnType = E->getCallReturnType(Ctx.getASTContext());
   OptPrimType T = classify(ReturnType);
@@ -6202,11 +6240,8 @@ bool Compiler<Emitter>::VisitCallExpr(const CallExpr *E) {
     }
   }
 
-  ArrayRef<const Expr *> Args(E->getArgs(), E->getNumArgs());
   const Expr *ReversedArgs[2];
-
   bool IsAssignmentOperatorCall = false;
-  bool ActivateLHS = false;
   if (const auto *OCE = dyn_cast<CXXOperatorCallExpr>(E);
       OCE && OCE->isAssignmentOp()) {
     // Just like with regular assignments, we need to special-case assignment
@@ -6220,6 +6255,7 @@ bool Compiler<Emitter>::VisitCallExpr(const CallExpr *E) {
     ReversedArgs[1] = Args[0];
     Args = ReversedArgs;
   }
+
   // Calling a static operator will still
   // pass the instance, but we don't need it.
   // Discard it here.
diff --git a/clang/lib/AST/ByteCode/Interp.cpp b/clang/lib/AST/ByteCode/Interp.cpp
index a41cc1f5a564b..52ad35ed8826c 100644
--- a/clang/lib/AST/ByteCode/Interp.cpp
+++ b/clang/lib/AST/ByteCode/Interp.cpp
@@ -987,30 +987,30 @@ bool CheckFinalLoad(InterpState &S, CodePtr OpPC, const Pointer &Ptr) {
 }
 
 bool CheckStore(InterpState &S, CodePtr OpPC, const Pointer &Ptr,
-                bool WillBeActivated) {
+                AccessKinds AK, bool WillBeActivated) {
   if (!Ptr.isBlockPointer() || Ptr.isZero())
     return false;
 
   if (!Ptr.block()->isAccessible()) {
-    if (!CheckLive(S, OpPC, Ptr, AK_Assign))
+    if (!CheckLive(S, OpPC, Ptr, AK))
       return false;
     if (!CheckExtern(S, OpPC, Ptr))
       return false;
-    return CheckDummy(S, OpPC, Ptr.block(), AK_Assign);
+    return CheckDummy(S, OpPC, Ptr.block(), AK);
   }
-  if (!WillBeActivated && !CheckLifetime(S, OpPC, Ptr, AK_Assign))
+  if (!WillBeActivated && !CheckLifetime(S, OpPC, Ptr, AK))
     return false;
-  if (!CheckRange(S, OpPC, Ptr, AK_Assign))
+  if (!CheckRange(S, OpPC, Ptr, AK))
     return false;
-  if (!CheckActive(S, OpPC, Ptr, AK_Assign, WillBeActivated))
+  if (!CheckActive(S, OpPC, Ptr, AK, WillBeActivated))
     return false;
   if (!CheckGlobal(S, OpPC, Ptr))
     return false;
   if (!CheckConst(S, OpPC, Ptr))
     return false;
-  if (!CheckVolatile(S, OpPC, Ptr, AK_Assign))
+  if (!CheckVolatile(S, OpPC, Ptr, AK))
     return false;
-  if (!CheckMutable(S, OpPC, Ptr, AK_Assign))
+  if (!CheckMutable(S, OpPC, Ptr, AK))
     return false;
   if (isConstexprUnknown(Ptr))
     return false;
@@ -3404,6 +3404,64 @@ std::optional<Pointer> addSubOffsetOpaque(InterpState &S, CodePtr OpPC,
   return Pointer(OP.withPastEnd(true), NewOffset);
 }
 
+bool Memcpy(InterpState &S, CodePtr OpPC) {
+  const Pointer &Src = S.Stk.pop<Pointer>();
+  Pointer &Dest = S.Stk.peek<Pointer>();
+
+  if (Src.isDummy() || !Src.isBlockPointer())
+    return false;
+  if (!Dest.isBlockPointer())
+    return false;
+
+  if ((Src.getRecord() && Src.getRecord()->isUnion() &&
+       !Src.getRecord()->isAnonymousUnion()) ||
+      Src.inUnion()) {
+    if (!CheckLoad(S, OpPC, Src))
+      return false;
+  }
+
+  return DoMemcpy(S, OpPC, Src, Dest);
+}
+
+bool TrivialCopy(InterpState &S, CodePtr OpPC, bool Activate,
+                 const Function *Func) {
+  const Pointer &Src = S.Stk.pop<Pointer>();
+  Pointer &Dest = S.Stk.peek<Pointer>();
+
+  if (Src.isDummy() || Src.isConstexprUnknown() || !Src.isBlockPointer())
+    return false;
+  if (!Dest.isBlockPointer() || Dest.isDummy() || Dest.isConstexprUnknown())
+    return false;
+
+  if (!CheckStore(S, OpPC, Dest, AK_MemberCall,
+                  /*WillBeActivated=*/Activate))
+    return false;
+
+  if (S.checkingPotentialConstantExpression())
+    return false;
+
+  // NOTE: This is a fake function frame that doesn't do anything except show up
+  // in the "in call to" diagnostics. Since the copies we replace with this
+  // opcode are always defaulted/trivial, they don't add much there either
+  // though. Once we default to the bytecode interpreter, we shoud consider just
+  // removing it.
+  auto Memory = std::make_unique<char[]>(InterpFrame::allocSize(Func));
+  auto *NewFrame =
+      new (Memory.get()) InterpFrame(S, Func, S.PC, /*VarArgSize=*/0);
+  InterpFrame *FrameBefore = S.Current;
+  S.Current = NewFrame;
+
+  if (!CheckLoad(S, OpPC, Src, AK_Read)) {
+    S.Current = FrameBefore;
+    return false;
+  }
+
+  bool Result = DoMemcpy(S, OpPC, Src, Dest, Activate, /*Diagnose=*/true);
+  S.Current = FrameBefore;
+
+  return Result;
+}
+
 // FIXME: Would be nice to generate this instead of hardcoding it here.
 [[maybe_unused]] static constexpr bool OpReturns(Opcode Op) {
   return Op == OP_RetVoid || Op == OP_RetValue || Op == OP_NoRet ||
diff --git a/clang/lib/AST/ByteCode/Interp.h b/clang/lib/AST/ByteCode/Interp.h
index 61118d77b7ac2..cbd569fe81777 100644
--- a/clang/lib/AST/ByteCode/Interp.h
+++ b/clang/lib/AST/ByteCode/Interp.h
@@ -82,9 +82,6 @@ bool CheckFinalLoad(InterpState &S, CodePtr OpPC, const Pointer &Ptr);
 
 bool diagnoseUninitialized(InterpState &S, CodePtr OpPC, const Pointer &Ptr,
                            AccessKinds AK);
-bool diagnoseUninitialized(InterpState &S, CodePtr OpPC, bool Extern,
-                           const Block *B, Lifetime LT = Lifetime::Started,
-                           AccessKinds AK = AK_Read);
 
 /// Checks a direct load of a primitive value from a global or local variable.
 bool CheckGlobalLoad(InterpState &S, CodePtr OpPC, const Block *B);
@@ -92,7 +89,7 @@ bool CheckLocalLoad(InterpState &S, CodePtr OpPC, const Block *B);
 
 /// Checks if a value can be stored in a block.
 bool CheckStore(InterpState &S, CodePtr OpPC, const Pointer &Ptr,
-                bool WillBeActivated = false);
+                AccessKinds AK = AK_Assign, bool WillBeActivated = false);
 
 /// Checks if a value can be initialized.
 bool CheckInit(InterpState &S, CodePtr OpPC, const Pointer &Ptr);
@@ -2317,7 +2314,7 @@ bool StoreActivate(InterpState &S, CodePtr OpPC) {
   const T &Value = S.Stk.pop<T>();
   const Pointer &Ptr = S.Stk.peek<Pointer>();
 
-  if (!CheckStore(S, OpPC, Ptr, /*WillBeActivated=*/true))
+  if (!CheckStore(S, OpPC, Ptr, AK_Assign, /*WillBeActivated=*/true))
     return false;
   if (Ptr.canBeInitialized()) {
     Ptr.initialize();
@@ -2332,7 +2329,7 @@ bool StoreActivatePop(InterpState &S, CodePtr OpPC) {
   const T &Value = S.Stk.pop<T>();
   const Pointer &Ptr = S.Stk.pop<Pointer>();
 
-  if (!CheckStore(S, OpPC, Ptr, /*WillBeActivated=*/true))
+  if (!CheckStore(S, OpPC, Ptr, AK_Assign, /*WillBeActivated=*/true))
     return false;
   if (Ptr.canBeInitialized()) {
     Ptr.initialize();
@@ -2378,7 +2375,7 @@ bool StoreBitFieldActivate(InterpState &S, CodePtr OpPC) {
   const T &Value = S.Stk.pop<T>();
   const Pointer &Ptr = S.Stk.peek<Pointer>();
 
-  if (!CheckStore(S, OpPC, Ptr, /*WillBeActivated=*/true))
+  if (!CheckStore(S, OpPC, Ptr, AK_Assign, /*WillBeActivated=*/true))
     return false;
   if (Ptr.canBeInitialized()) {
     Ptr.initialize();
@@ -2396,7 +2393,7 @@ bool StoreBitFieldActivatePop(InterpState &S, CodePtr OpPC) {
   const T &Value = S.Stk.pop<T>();
   const Pointer &Ptr = S.Stk.pop<Pointer>();
 
-  if (!CheckStore(S, OpPC, Ptr, /*WillBeActivated=*/true))
+  if (!CheckStore(S, OpPC, Ptr, AK_Assign, /*WillBeActivated=*/true))
     return false;
   if (Ptr.canBeInitialized()) {
     Ptr.initialize();
@@ -2507,18 +2504,6 @@ bool InitElemPop(InterpState &S, CodePtr OpPC, uint32_t Idx) {
   return true;
 }
 
-inline bool Memcpy(InterpState &S, CodePtr OpPC) {
-  const Pointer &Src = S.Stk.pop<Pointer>();
-  Pointer &Dest = S.Stk.peek<Pointer>();
-
-  if (!Src.getRecord() || !Src.getRecord()->isAnonymousUnion()) {
-    if (!CheckLoad(S, OpPC, Src))
-      return false;
-  }
-
-  return DoMemcpy(S, OpPC, Src, Dest);
-}
-
 inline bool ToMemberPtr(InterpState &S) {
   const auto &Member = S.Stk.pop<MemberPointer>();
   const auto &Base = S.Stk.pop<Pointer>();
@@ -2537,6 +2522,10 @@ inline bool CastMemberPtrPtr(InterpState &S, CodePtr OpPC) {
   return Invalid(S, OpPC);
 }
 
+bool Memcpy(InterpState &S, CodePtr OpPC);
+bool TrivialCopy(InterpState &S, CodePtr OpPC, bool Activate,
+                 const Function *Func);
+
 //===----------------------------------------------------------------------===//
 // AddOffset, SubOffset
 //===----------------------------------------------------------------------===//
diff --git a/clang/lib/AST/ByteCode/InterpBuiltin.cpp b/clang/lib/AST/ByteCode/InterpBuiltin.cpp
index 0432308ef55f6..b9e96cf221c9a 100644
--- a/clang/lib/AST/ByteCode/InterpBuiltin.cpp
+++ b/clang/lib/AST/ByteCode/InterpBuiltin.cpp
@@ -6856,26 +6856,34 @@ static void zeroAll(PtrView Dest) {
 }
 
 static bool copyComposite(InterpState &S, CodePtr OpPC, PtrView Src,
-                          PtrView Dest, bool Activate);
+                          PtrView Dest, bool Activate, bool Diagnose);
 static bool copyRecord(InterpState &S, CodePtr OpPC, PtrView Src, PtrView Dest,
-                       bool Activate = false) {
+                       bool Activate = false, bool Diagnose = true) {
   [[maybe_unused]] const Descriptor *SrcDesc = Src.getFieldDesc();
   const Descriptor *DestDesc = Dest.getFieldDesc();
 
   auto copyField = [&](const Record::Field &F, bool Activate) -> bool {
     PtrView DestField = Dest.atField(F.Offset);
+    PtrView SrcField = Src.atField(F.Offset);
+
     if (OptPrimType FT = F.T) {
-      TYPE_SWITCH(*FT, {
-        DestField.deref<T>() = Src.atField(F.Offset).deref<T>();
-        if (Src.atField(F.Offset).isInitialized())
-          DestField.initialize();
-        if (Activate)
-          DestField.activate();
-      });
+      if (!SrcField.isInitialized()) {
+        if (Diagnose)
+          return diagnoseUninitialized(S, OpPC, false, SrcField.block(),
+                                       SrcField.getLifetime(), AK_Read);
+        // Just skip.
+        return true;
+      }
+
+      TYPE_SWITCH(*FT, DestField.deref<T>() = SrcField.deref<T>(););
+      if (DestField.canBeInitialized())
+        DestField.initialize();
+      if (Activate)
+        DestField.activate();
       return true;
     }
-    // Composite field.
-    return copyComposite(S, OpPC, Src.atField(F.Offset), DestField, Activate);
+
+    return copyComposite(S, OpPC, SrcField, DestField, Activate, Diagnose);
   };
 
   assert(SrcDesc->isRecord());
@@ -6904,16 +6912,20 @@ static bool copyRecord(InterpState &S, CodePtr OpPC, PtrView Src, PtrView Dest,
 
   for (const Record::Base &B : R->bases()) {
     PtrView DestBase = Dest.atField(B.Offset);
-    if (!copyRecord(S, OpPC, Src.atField(B.Offset), DestBase, Activate))
+    if (!copyRecord(S, OpPC, Src.atField(B.Offset), DestBase, Activate,
+                    Diagnose))
       return false;
   }
 
   Dest.initialize();
+  if (Activate)
+    Dest.activate();
   return true;
 }
 
 static bool copyComposite(InterpState &S, CodePtr OpPC, PtrView Src,
-                          PtrView Dest, bool Activate = false) {
+                          PtrView Dest, bool Activate = false,
+                          bool Diagnose = false) {
   assert(Src.isLive() && Dest.isLive());
 
   [[maybe_unused]] const Descriptor *SrcDesc = Src.getFieldDesc();
@@ -6961,18 +6973,19 @@ static bool copyComposite(InterpState &S, CodePtr OpPC, PtrView Src,
   if (DestDesc->isRecord()) {
     if (!SrcDesc->isRecord())
       return false;
-    return copyRecord(S, OpPC, Src, Dest, Activate);
+    return copyRecord(S, OpPC, Src, Dest, Activate, Diagnose);
   }
   return Invalid(S, OpPC);
 }
 
-bool DoMemcpy(InterpState &S, CodePtr OpPC, const Pointer &Src, Pointer &Dest) {
+bool DoMemcpy(InterpState &S, CodePtr OpPC, const Pointer &Src, Pointer &Dest,
+              bool Activate, bool Diagnose) {
   if (!Src.isBlockPointer() || Src.getFieldDesc()->isPrimitive())
     return false;
   if (!Dest.isBlockPointer() || Dest.getFieldDesc()->isPrimitive())
     return false;
 
-  return copyComposite(S, OpPC, Src.view(), Dest.view());
+  return copyComposite(S, OpPC, Src.view(), Dest.view(), Activate, Diagnose);
 }
 
 } // namespace interp
diff --git a/clang/lib/AST/ByteCode/InterpHelpers.h b/clang/lib/AST/ByteCode/InterpHelpers.h
index 4c60670ac5a0c..713e6d2a592b2 100644
--- a/clang/lib/AST/ByteCode/InterpHelpers.h
+++ b/clang/lib/AST/ByteCode/InterpHelpers.h
@@ -80,12 +80,17 @@ bool CheckNewDeleteForms(InterpState &S, CodePtr OpPC,
                          const Expr *NewExpr);
 
 /// Copy the contents of Src into Dest.
-bool DoMemcpy(InterpState &S, CodePtr OpPC, const Pointer &Src, Pointer &Dest);
+bool DoMemcpy(InterpState &S, CodePtr OpPC, const Pointer &Src, Pointer &Dest,
+              bool Activate = true, bool Diagnose = false);
 
 UnsignedOrNone evaluateBuiltinObjectSize(const ASTContext &ASTCtx,
                                          unsigned Kind, Pointer &Ptr,
                                          const Expr *E, bool IsDynamic = false);
 
+bool diagnoseUninitialized(InterpState &S, CodePtr OpPC, bool Extern,
+                           const Block *B, Lifetime LT = Lifetime::Started,
+                           AccessKinds AK = AK_Read);
+
 template <typename T>
 bool handleOverflow(InterpState &S, CodePtr OpPC, const T &SrcValue) {
   const Expr *E = S.Current->getExpr(OpPC);
diff --git a/clang/lib/AST/ByteCode/Opcodes.td b/clang/lib/AST/ByteCode/Opcodes.td
index 4be5495a7ed25..ee9b15d533c68 100644
--- a/clang/lib/AST/ByteCode/Opcodes.td
+++ b/clang/lib/AST/ByteCode/Opcodes.td
@@ -955,6 +955,9 @@ def CheckNonNullArg : Opcode {
 }
 
 def Memcpy : Opcode;
+def TrivialCopy : Opcode {
+  let Args = [ArgBool, ArgFunction];
+}
 
 def ToMemberPtr : SuccessOpcode;
 def CastMemberPtrPtr : Opcode;
diff --git a/clang/lib/AST/ByteCode/Pointer.h b/clang/lib/AST/ByteCode/Pointer.h
index fd09b4908f0b5..031acdee64d2a 100644
--- a/clang/lib/AST/ByteCode/Pointer.h
+++ b/clang/lib/AST/ByteCode/Pointer.h
@@ -53,6 +53,7 @@ struct PtrView {
   bool inUnion() const { return getInlineDesc()->InUnion; };
   bool inArray() const { return getFieldDesc()->IsArray; }
   bool inPrimitiveArray() const { return getFieldDesc()->isPrimitiveArray(); }
+  bool canBeInitialized() const { return Pointee && Base > 0; }
   const Block *block() const { return Pointee; }
 
   unsigned getEvalID() { return Pointee->getEvalID(); }
diff --git a/clang/lib/AST/ExprConstShared.h b/clang/lib/AST/ExprConstShared.h
index 0eee03dce57ab..2dec04b4d9932 100644
--- a/clang/lib/AST/ExprConstShared.h
+++ b/clang/lib/AST/ExprConstShared.h
@@ -112,4 +112,7 @@ EvalScalarMinMaxFp(const llvm::APFloat &A, const llvm::APFloat &B,
 
 const Expr *ignorePointerCastsAndParens(const Expr *E);
 
+bool isReadByLvalueToRvalueConversion(const CXXRecordDecl *RD);
+bool isReadByLvalueToRvalueConversion(QualType T);
+
 #endif
diff --git a/clang/lib/AST/ExprConstant.cpp b/clang/lib/AST/ExprConstant.cpp
index cc992309bc7a8..29beee30d6e1c 100644
--- a/clang/lib/AST/ExprConstant.cpp
+++ b/clang/lib/AST/ExprConstant.cpp
@@ -3697,12 +3697,12 @@ static void expandVector(APValue &Vec, unsigned NumElements) {
 /// is trivial. Note that this is never true for a union type with fields
 /// (because the copy always "reads" the active member) and always true for
 /// a non-class type.
-static bool isReadByLvalueToRvalueConversion(const CXXRecordDecl *RD);
-static bool isReadByLvalueToRvalueConversion(QualType T) {
+bool isReadByLvalueToRvalueConversion(const CXXRecordDecl *RD);
+bool isReadByLvalueToRvalueConversion(QualType T) {
   CXXRecordDecl *RD = T->getBaseElementTypeUnsafe()->getAsCXXRecordDecl();
   return !RD || isReadByLvalueToRvalueConversion(RD);
 }
-static bool isReadByLvalueToRvalueConversion(const CXXRecordDecl *RD) {
+bool isReadByLvalueToRvalueConversion(const CXXRecordDecl *RD) {
   // FIXME: A trivial copy of a union copies the object representation, even if
   // the union is empty.
   if (RD->isUnion())
diff --git a/clang/test/AST/ByteCode/cxx14.cpp b/clang/test/AST/ByteCode/cxx14.cpp
index 97d2c0ac4f711..7dcf385bab084 100644
--- a/clang/test/AST/ByteCode/cxx14.cpp
+++ b/clang/test/AST/ByteCode/cxx14.cpp
@@ -47,3 +47,23 @@ namespace InitListModify {
   constexpr Aggregate aggr2 = {};
   static_assert(aggr2.x == 1 && aggr2.y == 1, "");
 }
+
+namespace UnionTrivialCopy {
+  struct A {
+    constexpr A() {}
+    struct B {
+      union U {
+        constexpr U() : y(4) {}
+        int x;
+        int y;
+      } u;
+    } b;
+  };
+  constexpr int testA() {
+    A a, b;
+    a.b.u.y = 5;
+    b = a;
+    return b.b.u.y;
+  }
+  static_assert(testA() == 5, "");
+}
diff --git a/clang/test/AST/ByteCode/cxx26.cpp b/clang/test/AST/ByteCode/cxx26.cpp
index 9dc6270b9d551..68da8bae0e946 100644
--- a/clang/test/AST/ByteCode/cxx26.cpp
+++ b/clang/test/AST/ByteCode/cxx26.cpp
@@ -103,3 +103,59 @@ namespace UnknownSizeArrayString {
                              // ref-note {{initializer of 'foo' is unknown}} \
                              // both-error {{static assertion failed}}
 }
+
+namespace TrivialAssignment {
+  struct array {
+    int _M_elems[4];
+  };
+
+  struct item {
+    array words;
+    unsigned priority;
+  };
+
+  constexpr void __insertion_sort(item * __first,
+                                  item *__last) {
+    *__first = *__first;
+  }
+
+  consteval unsigned sorted_first() {
+    item items[]{{{}, 1}};
+    __insertion_sort(items, items + 1);
+
+    return items[0].priority;
+  }
+  static_assert(sorted_first());
+
+  constexpr void __insertion_sort2(item * __first,
+                                   item *__last) {
+    *__first = *__last; // both-note {{read of dereferenced one-past-the-end pointer}} \
+                        // both-note {{in call to}}
+  }
+
+  consteval unsigned sorted_first2() {
+    item items[]{{{}, 1}};
+    __insertion_sort2(items, items + 1); // both-note {{in call}}
+
+    return items[0].priority;
+  }
+  static_assert(sorted_first2()); // both-error {{not an integral constant expression}} \
+                                  // both-note {{in call}}
+
+
+
+  constexpr void __insertion_sort3(item * __first,
+                                   item *__last) {
+    *__last = *__first; // both-note {{member call on dereferenced one-past-the-end pointer}}
+  }
+
+  consteval unsigned sorted_first3() {
+    item items[]{{{}, 1}};
+    __insertion_sort3(items, items + 1); // both-note {{in call to '__insertion_sort3(&items[0], &items[1])'}}
+
+    return items[0].priority;
+  }
+  static_assert(sorted_first3()); // bo...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/222332


More information about the cfe-commits mailing list