https://github.com/adams381 updated 
https://github.com/llvm/llvm-project/pull/206579

>From eec608cd00de5e9ecaba8bfe362b5684978404e4 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 29 Jun 2026 13:14:15 -0700
Subject: [PATCH 1/5] [Clang][Sema] Synthesize a memcpy body for defaulted
 union assignment

A defaulted copy or move assignment operator for a union was left with an
empty body: the memberwise loop in DefineImplicitCopyAssignment /
DefineImplicitMoveAssignment skips union members, and the implied copy of the
object representation had no AST representation (the long-standing FIXME at the
union skip).  The operator therefore copied nothing.  Classic CodeGen hides
this at ordinary call sites by lowering a trivial assignment to a call-site
memcpy, but when the operator is genuinely called -- e.g. through a
pointer-to-member -- it silently copies nothing.  ClangIR, which calls the
operator at the call site, hits the empty body directly and miscompiles union
assignments.

Mirror the defaulted union copy constructor (CGClass.cpp's "union copy
constructor, we must emit a memcpy") in the AST: when the class is a union,
emit a single whole-object copy through buildMemcpyForAssignmentOp -- the same
helper already used for trivially-copyable array members -- instead of the
skipped per-member assignments.  The operator stays trivial (triviality is
fixed at declaration time, before the body is synthesized), so constant
evaluation, which copies trivial unions through its own semantic path and does
not execute the body, is unaffected.

The classic backend now emits the copy when the operator is odr-used, and
ClangIR's union assignment lowers to a real memcpy.
---
 clang/lib/Sema/SemaDeclCXX.cpp                | 40 +++++++++++++++--
 .../AST/ast-dump-union-copy-move-assign.cpp   | 26 +++++++++++
 .../CodeGen/union-copy-move-assignment.cpp    | 36 ++++++++++++++++
 .../CodeGenCXX/union-copy-move-assignment.cpp | 43 +++++++++++++++++++
 4 files changed, 141 insertions(+), 4 deletions(-)
 create mode 100644 clang/test/AST/ast-dump-union-copy-move-assign.cpp
 create mode 100644 clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
 create mode 100644 clang/test/CodeGenCXX/union-copy-move-assignment.cpp

diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index e5a5963db27c2..fbdd737abbf2c 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -15507,10 +15507,26 @@ void 
Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
     Statements.push_back(Copy.getAs<Expr>());
   }
 
+  // A defaulted copy assignment operator for a union copies the object
+  // representation as if by a memcpy, the same way the defaulted union copy
+  // constructor does.  The memberwise loop below skips union members, so emit
+  // that whole-object copy here.
+  if (ClassDecl->isUnion()) {
+    ExprBuilder &To = ExplicitObject
+                          ? static_cast<ExprBuilder &>(*ExplicitObject)
+                          : static_cast<ExprBuilder &>(*DerefThis);
+    StmtResult Copy = buildMemcpyForAssignmentOp(
+        *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+    if (Copy.isInvalid()) {
+      CopyAssignOperator->setInvalidDecl();
+      return;
+    }
+    Statements.push_back(Copy.getAs<Stmt>());
+  }
+
   // Assign non-static members.
   for (auto *Field : ClassDecl->fields()) {
-    // FIXME: We should form some kind of AST representation for the implied
-    // memcpy in a union copy operation.
+    // Union members are copied by the whole-object memcpy emitted above.
     if (Field->isUnnamedBitField() || Field->getParent()->isUnion())
       continue;
 
@@ -15897,10 +15913,26 @@ void 
Sema::DefineImplicitMoveAssignment(SourceLocation CurrentLocation,
     Statements.push_back(Move.getAs<Expr>());
   }
 
+  // A defaulted move assignment operator for a union copies the object
+  // representation as if by a memcpy, the same way the defaulted union copy
+  // constructor does.  The memberwise loop below skips union members, so emit
+  // that whole-object copy here.
+  if (ClassDecl->isUnion()) {
+    ExprBuilder &To = ExplicitObject
+                          ? static_cast<ExprBuilder &>(*ExplicitObject)
+                          : static_cast<ExprBuilder &>(*DerefThis);
+    StmtResult Move = buildMemcpyForAssignmentOp(
+        *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+    if (Move.isInvalid()) {
+      MoveAssignOperator->setInvalidDecl();
+      return;
+    }
+    Statements.push_back(Move.getAs<Stmt>());
+  }
+
   // Assign non-static members.
   for (auto *Field : ClassDecl->fields()) {
-    // FIXME: We should form some kind of AST representation for the implied
-    // memcpy in a union copy operation.
+    // Union members are copied by the whole-object memcpy emitted above.
     if (Field->isUnnamedBitField() || Field->getParent()->isUnion())
       continue;
 
diff --git a/clang/test/AST/ast-dump-union-copy-move-assign.cpp 
b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
new file mode 100644
index 0000000000000..ab05bb9577916
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -0,0 +1,26 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -ast-dump %s | 
FileCheck %s
+
+union U {
+  int a;
+  float b;
+};
+
+void odr_use(U &x, const U &y, U &&z) {
+  x = y;
+  x = static_cast<U &&>(z);
+}
+
+// The implicitly-defined defaulted union assignment operators are synthesized
+// with a whole-object __builtin_memcpy body.
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:     ReturnStmt
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(U &&)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:     ReturnStmt
diff --git a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp 
b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
new file mode 100644
index 0000000000000..4f5ca5ef25a64
--- /dev/null
+++ b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
@@ -0,0 +1,36 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o 
%t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o 
%t-cir.ll
+// RUN: FileCheck --check-prefix=LLVMCIR --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+union U {
+  int a;
+  float b;
+};
+
+// Odr-use both defaulted assignment operators out of line so their bodies are
+// emitted under both backends.
+auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
+auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+
+// The defaulted union copy/move assignment operators copy the object
+// representation; CIR lowers that to a memcpy.  LLVM lowering uses a memcpy
+// libcall; the classic backend uses the llvm.memcpy intrinsic (the divergence
+// is the pre-existing builtin-memcpy lowering, not this feature).
+
+// CIR: cir.func{{.*}}@_ZN1UaSERKS_{{.*}}cxx_assign<!rec_U, copy, trivial true>
+// CIR:   cir.call @memcpy(
+// CIR: cir.func{{.*}}@_ZN1UaSEOS_{{.*}}cxx_assign<!rec_U, move, trivial true>
+// CIR:   cir.call @memcpy(
+
+// LLVMCIR: define{{.*}}ptr @_ZN1UaSERKS_
+// LLVMCIR:   call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+// LLVMCIR: define{{.*}}ptr @_ZN1UaSEOS_
+// LLVMCIR:   call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+
+// OGCG: define{{.*}}ptr @_ZN1UaSERKS_
+// OGCG:   call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 
false)
+// OGCG: define{{.*}}ptr @_ZN1UaSEOS_
+// OGCG:   call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 
false)
diff --git a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp 
b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
new file mode 100644
index 0000000000000..4cc90e9a249e1
--- /dev/null
+++ b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
@@ -0,0 +1,43 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -emit-llvm %s 
-o - | FileCheck %s
+
+union U {
+  int a;
+  float b;
+};
+
+// Odr-use both defaulted assignment operators out of line so their bodies are
+// emitted (a trivial assignment at a call site is otherwise memcpy'd 
directly).
+auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
+auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+
+// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSERKS_
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
4, i1 false)
+// CHECK:         ret ptr
+
+// CHECK-LABEL: define {{.*}} ptr @_ZN1UaSEOS_
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
4, i1 false)
+// CHECK:         ret ptr
+
+struct WithNamedUnion {
+  U u;
+  int x;
+};
+
+// A named union member is copied as part of the containing class's defaulted
+// assignment.
+void assign_named(WithNamedUnion *d, const WithNamedUnion *s) { *d = *s; }
+// CHECK-LABEL: define {{.*}} @_Z12assign_named
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
8, i1 false)
+
+struct WithAnonUnion {
+  union {
+    int a;
+    float b;
+  };
+  int x;
+};
+
+// An anonymous union member is likewise copied.
+void assign_anon(WithAnonUnion *d, const WithAnonUnion *s) { *d = *s; }
+// CHECK-LABEL: define {{.*}} @_Z11assign_anon
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
8, i1 false)

>From ab1f557d28690b4459a5d8eeea918dc6c81f6a75 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 29 Jun 2026 15:41:29 -0700
Subject: [PATCH 2/5] [Clang][Sema] Factor union assignment memcpy into a
 shared helper

The defaulted copy and move assignment paths emitted identical whole-object
union memcpy blocks, differing only in which assignment operator they marked
invalid.  Extract buildUnionAssignmentCopy so both call sites share it; no
functional change.
---
 clang/lib/Sema/SemaDeclCXX.cpp | 70 ++++++++++++++++++----------------
 1 file changed, 38 insertions(+), 32 deletions(-)

diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index fbdd737abbf2c..ead4c06c13c53 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -15383,6 +15383,30 @@ static void diagnoseDeprecatedCopyOperation(Sema &S, 
CXXMethodDecl *CopyOp) {
   }
 }
 
+// A defaulted copy or move assignment operator for a union copies the object
+// representation as if by a memcpy, the same way the defaulted union copy
+// constructor does.  The memberwise loops in DefineImplicitCopyAssignment and
+// DefineImplicitMoveAssignment skip union members, so the whole-object copy is
+// emitted here instead.  Marks AssignOp invalid and returns false on failure.
+static bool buildUnionAssignmentCopy(Sema &S, SourceLocation Loc,
+                                     CXXRecordDecl *ClassDecl,
+                                     std::optional<RefBuilder> &ExplicitObject,
+                                     std::optional<DerefBuilder> &DerefThis,
+                                     const ExprBuilder &From,
+                                     CXXMethodDecl *AssignOp,
+                                     SmallVectorImpl<Stmt *> &Statements) {
+  ExprBuilder &To = ExplicitObject ? static_cast<ExprBuilder 
&>(*ExplicitObject)
+                                   : static_cast<ExprBuilder &>(*DerefThis);
+  StmtResult Copy = buildMemcpyForAssignmentOp(
+      S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From);
+  if (Copy.isInvalid()) {
+    AssignOp->setInvalidDecl();
+    return false;
+  }
+  Statements.push_back(Copy.getAs<Stmt>());
+  return true;
+}
+
 void Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
                                         CXXMethodDecl *CopyAssignOperator) {
   DefaultedFunctionFPFeaturesRAII RestoreFP(*this, CopyAssignOperator);
@@ -15507,22 +15531,13 @@ void 
Sema::DefineImplicitCopyAssignment(SourceLocation CurrentLocation,
     Statements.push_back(Copy.getAs<Expr>());
   }
 
-  // A defaulted copy assignment operator for a union copies the object
-  // representation as if by a memcpy, the same way the defaulted union copy
-  // constructor does.  The memberwise loop below skips union members, so emit
-  // that whole-object copy here.
-  if (ClassDecl->isUnion()) {
-    ExprBuilder &To = ExplicitObject
-                          ? static_cast<ExprBuilder &>(*ExplicitObject)
-                          : static_cast<ExprBuilder &>(*DerefThis);
-    StmtResult Copy = buildMemcpyForAssignmentOp(
-        *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
-    if (Copy.isInvalid()) {
-      CopyAssignOperator->setInvalidDecl();
-      return;
-    }
-    Statements.push_back(Copy.getAs<Stmt>());
-  }
+  // A union's defaulted copy assignment copies the whole object; see
+  // buildUnionAssignmentCopy.
+  if (ClassDecl->isUnion() &&
+      !buildUnionAssignmentCopy(*this, Loc, ClassDecl, ExplicitObject,
+                                DerefThis, OtherRef, CopyAssignOperator,
+                                Statements))
+    return;
 
   // Assign non-static members.
   for (auto *Field : ClassDecl->fields()) {
@@ -15913,22 +15928,13 @@ void 
Sema::DefineImplicitMoveAssignment(SourceLocation CurrentLocation,
     Statements.push_back(Move.getAs<Expr>());
   }
 
-  // A defaulted move assignment operator for a union copies the object
-  // representation as if by a memcpy, the same way the defaulted union copy
-  // constructor does.  The memberwise loop below skips union members, so emit
-  // that whole-object copy here.
-  if (ClassDecl->isUnion()) {
-    ExprBuilder &To = ExplicitObject
-                          ? static_cast<ExprBuilder &>(*ExplicitObject)
-                          : static_cast<ExprBuilder &>(*DerefThis);
-    StmtResult Move = buildMemcpyForAssignmentOp(
-        *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
-    if (Move.isInvalid()) {
-      MoveAssignOperator->setInvalidDecl();
-      return;
-    }
-    Statements.push_back(Move.getAs<Stmt>());
-  }
+  // A union's defaulted move assignment copies the whole object; see
+  // buildUnionAssignmentCopy.
+  if (ClassDecl->isUnion() &&
+      !buildUnionAssignmentCopy(*this, Loc, ClassDecl, ExplicitObject,
+                                DerefThis, OtherRef, MoveAssignOperator,
+                                Statements))
+    return;
 
   // Assign non-static members.
   for (auto *Field : ClassDecl->fields()) {

>From 8f80389fc1fd0b0f3af6f41029b7984f5855b4ae Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Tue, 30 Jun 2026 15:09:14 -0700
Subject: [PATCH 3/5] [Clang][Sema] Cast synthesized union assignment memcpy to
 void*

The whole-object memcpy synthesized for a defaulted union copy/move assignment
copies the union by its object representation, which is correct even when the
union is not trivially copyable -- the defaulted union copy constructor does the
same.  But because the copy is now a real __builtin_memcpy call expression in
the AST, CheckMemaccessArguments runs on it and reports -Wnontrivial-memcall
whenever the union has a non-trivially-copyable member (for example a member
with a non-trivial destructor, which leaves the assignment trivial but the union
not trivially copyable).  libc++'s variant union hits exactly this, so the
synthesized body broke any -Werror build that includes <variant>.

Cast the source and destination addresses to void* in
buildMemcpyForAssignmentOp when copying the whole union, which is the (void*)
silencing that -Wnontrivial-memcall itself recommends.  The cast is an explicit
CStyleCastExpr, so it survives IgnoreParenImpCasts and CheckMemaccessArguments
sees a void pointee and skips the warning; the existing array-element callers 
are
unchanged.  A direct user memcpy of a non-trivially-copyable union still warns.

Add a SemaCXX regression test under -Wnontrivial-memcall (the synthesized copy
stays quiet, a user memcpy of the same union still warns) and pin the void*
casts in the AST dump.
---
 clang/lib/Sema/SemaDeclCXX.cpp                | 29 +++++++++++++++++--
 .../AST/ast-dump-union-copy-move-assign.cpp   |  7 ++++-
 .../union-assign-memcpy-nontrivial.cpp        | 26 +++++++++++++++++
 3 files changed, 59 insertions(+), 3 deletions(-)
 create mode 100644 clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp

diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index ead4c06c13c53..82fd2dc50e736 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -14960,9 +14960,17 @@ class SubscriptBuilder: public ExprBuilder {
 /// should be copied with __builtin_memcpy rather than via explicit 
assignments,
 /// do so. This optimization only applies for arrays of scalars, and for arrays
 /// of class type where the selected copy/move-assignment operator is trivial.
+///
+/// \param SuppressMemaccessWarning casts the source and destination addresses
+/// to void pointers so that CheckMemaccessArguments does not warn.  A union's
+/// defaulted assignment copies the whole object representation by memcpy (like
+/// the defaulted union copy constructor), which is correct even when the union
+/// is not trivially copyable; the cast marks the copy as intentional, matching
+/// the documented (void*) silencing of -Wnontrivial-memcall.
 static StmtResult
 buildMemcpyForAssignmentOp(Sema &S, SourceLocation Loc, QualType T,
-                           const ExprBuilder &ToB, const ExprBuilder &FromB) {
+                           const ExprBuilder &ToB, const ExprBuilder &FromB,
+                           bool SuppressMemaccessWarning = false) {
   // Compute the size of the memory buffer to be copied.
   QualType SizeType = S.Context.getSizeType();
   llvm::APInt Size(S.Context.getTypeSize(SizeType),
@@ -14980,6 +14988,22 @@ buildMemcpyForAssignmentOp(Sema &S, SourceLocation 
Loc, QualType T,
       S.Context, To, UO_AddrOf, S.Context.getPointerType(To->getType()),
       VK_PRValue, OK_Ordinary, Loc, false, S.CurFPFeatureOverrides());
 
+  if (SuppressMemaccessWarning) {
+    // An explicit cast to void* survives IgnoreParenImpCasts, so the memaccess
+    // check sees a void pointee and skips the non-trivial-copy warning.
+    QualType ConstVoidPtr =
+        S.Context.getPointerType(S.Context.VoidTy.withConst());
+    ExprResult FromCast = S.BuildCStyleCastExpr(
+        Loc, S.Context.getTrivialTypeSourceInfo(ConstVoidPtr, Loc), Loc, From);
+    ExprResult ToCast = S.BuildCStyleCastExpr(
+        Loc, S.Context.getTrivialTypeSourceInfo(S.Context.VoidPtrTy, Loc), Loc,
+        To);
+    assert(FromCast.isUsable() && ToCast.isUsable() &&
+           "cast to void* cannot fail");
+    From = FromCast.get();
+    To = ToCast.get();
+  }
+
   bool NeedsCollectableMemCpy = false;
   if (auto *RD = T->getBaseElementTypeUnsafe()->getAsRecordDecl())
     NeedsCollectableMemCpy = RD->hasObjectMember();
@@ -15398,7 +15422,8 @@ static bool buildUnionAssignmentCopy(Sema &S, 
SourceLocation Loc,
   ExprBuilder &To = ExplicitObject ? static_cast<ExprBuilder 
&>(*ExplicitObject)
                                    : static_cast<ExprBuilder &>(*DerefThis);
   StmtResult Copy = buildMemcpyForAssignmentOp(
-      S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From);
+      S, Loc, S.Context.getCanonicalTagType(ClassDecl), To, From,
+      /*SuppressMemaccessWarning=*/true);
   if (Copy.isInvalid()) {
     AssignOp->setInvalidDecl();
     return false;
diff --git a/clang/test/AST/ast-dump-union-copy-move-assign.cpp 
b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
index ab05bb9577916..12a1a6b30fccb 100644
--- a/clang/test/AST/ast-dump-union-copy-move-assign.cpp
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -11,16 +11,21 @@ void odr_use(U &x, const U &y, U &&z) {
 }
 
 // The implicitly-defined defaulted union assignment operators are synthesized
-// with a whole-object __builtin_memcpy body.
+// with a whole-object __builtin_memcpy body whose pointer arguments are cast 
to
+// void* so the copy is not flagged by -Wnontrivial-memcall.
 
 // CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
 // CHECK:   CompoundStmt
 // CHECK:     CallExpr
 // CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       CStyleCastExpr {{.*}} 'void *'
+// CHECK:       CStyleCastExpr {{.*}} 'const void *'
 // CHECK:     ReturnStmt
 
 // CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(U &&)
 // CHECK:   CompoundStmt
 // CHECK:     CallExpr
 // CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       CStyleCastExpr {{.*}} 'void *'
+// CHECK:       CStyleCastExpr {{.*}} 'const void *'
 // CHECK:     ReturnStmt
diff --git a/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp 
b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
new file mode 100644
index 0000000000000..9865b117c2e7c
--- /dev/null
+++ b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
@@ -0,0 +1,26 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only \
+// RUN:   -Wnontrivial-memcall -verify %s
+
+// A union with a member that is not trivially copyable (here, a non-trivial
+// destructor) is itself not trivially copyable, yet its defaulted assignment
+// operators stay trivial and non-deleted.  Their synthesized whole-object
+// memcpy body must not trip -Wnontrivial-memcall.
+
+struct NonTrivialDtor {
+  ~NonTrivialDtor();
+};
+
+union U {
+  NonTrivialDtor n;
+  int i;
+};
+
+// Odr-use both defaulted assignment operators so their bodies are synthesized.
+// The synthesized memcpy must not warn.
+auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
+auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
+
+// A user-written memcpy of the same union is not suppressed and still warns.
+void user_memcpy(U *d, const U *s) {
+  __builtin_memcpy(d, s, sizeof(U)); // expected-warning {{first argument in 
call to '__builtin_memcpy' is a pointer to non-trivially copyable type 'U'}} 
expected-note {{explicitly cast the pointer to silence this warning}}
+}

>From 43e35484e5df9f57b8b1dd4dd406aad03efd54f4 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Thu, 23 Jul 2026 08:03:29 -0700
Subject: [PATCH 4/5] [CIR] Expand defaulted union-assignment test coverage

Add tests exercising paths the union-assignment memcpy synthesis already
handled but left uncovered: a C++23 explicit-object defaulted operator
(AST dump), constant evaluation of a defaulted union assignment, and a
tail-padded union whose whole-object copy spans the padding.  Add
CHECK-NOT guards so each synthesized body keeps exactly one memcpy.
---
 .../ast-dump-union-assign-explicit-object.cpp | 33 +++++++++++++++++++
 .../CodeGen/union-copy-move-assignment.cpp    |  2 ++
 .../CodeGenCXX/union-copy-move-assignment.cpp | 16 +++++++++
 clang/test/SemaCXX/union-assign-constexpr.cpp | 28 ++++++++++++++++
 4 files changed, 79 insertions(+)
 create mode 100644 clang/test/AST/ast-dump-union-assign-explicit-object.cpp
 create mode 100644 clang/test/SemaCXX/union-assign-constexpr.cpp

diff --git a/clang/test/AST/ast-dump-union-assign-explicit-object.cpp 
b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
new file mode 100644
index 0000000000000..e3692a4f11fb6
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
@@ -0,0 +1,33 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++2b -ast-dump %s | 
FileCheck %s
+
+union U {
+  int a;
+  float b;
+  U &operator=(this U &self, const U &) = default;
+  U &operator=(this U &self, U &&) = default;
+};
+
+void odr_use(U &x, const U &y, U &&z) {
+  x = y;
+  x = static_cast<U &&>(z);
+}
+
+// A defaulted union assignment operator written with a C++23 explicit object
+// parameter is synthesized with a whole-object __builtin_memcpy body whose
+// pointer arguments are cast to void* so -Wnontrivial-memcall stays quiet.
+
+// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, const U &)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       CStyleCastExpr {{.*}} 'void *'
+// CHECK:       CStyleCastExpr {{.*}} 'const void *'
+// CHECK:     ReturnStmt
+
+// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, U &&)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       CStyleCastExpr {{.*}} 'void *'
+// CHECK:       CStyleCastExpr {{.*}} 'const void *'
+// CHECK:     ReturnStmt
diff --git a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp 
b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
index 4f5ca5ef25a64..635fb4e5b08e4 100644
--- a/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
+++ b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
@@ -27,10 +27,12 @@ auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
 
 // LLVMCIR: define{{.*}}ptr @_ZN1UaSERKS_
 // LLVMCIR:   call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
+// LLVMCIR-NOT: @memcpy
 // LLVMCIR: define{{.*}}ptr @_ZN1UaSEOS_
 // LLVMCIR:   call ptr @memcpy(ptr {{.*}}, ptr {{.*}}, i64 noundef 4)
 
 // OGCG: define{{.*}}ptr @_ZN1UaSERKS_
 // OGCG:   call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 
false)
+// OGCG-NOT: @llvm.memcpy
 // OGCG: define{{.*}}ptr @_ZN1UaSEOS_
 // OGCG:   call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 4, i1 
false)
diff --git a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp 
b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
index 4cc90e9a249e1..3bb19def61774 100644
--- a/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
+++ b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
@@ -10,12 +10,28 @@ union U {
 auto get_copy = static_cast<U &(U::*)(const U &)>(&U::operator=);
 auto get_move = static_cast<U &(U::*)(U &&)>(&U::operator=);
 
+// Exactly one whole-object memcpy per assignment body.
 // CHECK-LABEL: define {{.*}} ptr @_ZN1UaSERKS_
 // CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
4, i1 false)
+// CHECK-NOT:     memcpy
 // CHECK:         ret ptr
 
 // CHECK-LABEL: define {{.*}} ptr @_ZN1UaSEOS_
 // CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
4, i1 false)
+// CHECK-NOT:     memcpy
+// CHECK:         ret ptr
+
+union Padded {
+  int a;
+  char b[5];
+};
+
+// sizeof(Padded) == 8, so the whole-object copy includes the tail padding.
+auto get_copy_padded = static_cast<Padded &(Padded::*)(const Padded 
&)>(&Padded::operator=);
+
+// CHECK-LABEL: define {{.*}} ptr @_ZN6PaddedaSERKS_
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 
8, i1 false)
+// CHECK-NOT:     memcpy
 // CHECK:         ret ptr
 
 struct WithNamedUnion {
diff --git a/clang/test/SemaCXX/union-assign-constexpr.cpp 
b/clang/test/SemaCXX/union-assign-constexpr.cpp
new file mode 100644
index 0000000000000..3a86442820c7f
--- /dev/null
+++ b/clang/test/SemaCXX/union-assign-constexpr.cpp
@@ -0,0 +1,28 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only 
-verify %s
+// expected-no-diagnostics
+
+// The memcpy body must not block constant evaluation of a union assignment.
+
+union U {
+  int a;
+  float b;
+};
+
+constexpr int copy_active() {
+  U x{};
+  x.a = 7;
+  U y{};
+  y = x;
+  return y.a;
+}
+
+constexpr int move_active() {
+  U x{};
+  x.a = 9;
+  U y{};
+  y = static_cast<U &&>(x);
+  return y.a;
+}
+
+static_assert(copy_active() == 7);
+static_assert(move_active() == 9);

>From 6a31b7a37b3c46358a093a34b55e49a7ba3ccf4d Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Thu, 23 Jul 2026 08:49:03 -0700
Subject: [PATCH 5/5] [CIR] Lower defaulted union copy/move assignment

A defaulted union copy/move assignment previously hit an errorNYI guard
in emitImplicitAssignmentOperatorBody: the synthesized body was empty
because Sema skipped union fields, so lowering it would have silently
dropped the whole-object copy.

With Sema now synthesizing a whole-object memcpy body for these
operators earlier in this PR, the guard is obsolete and blocks the
valid body from lowering.  Remove it so the body lowers to a
cir.call @memcpy, matching classic CodeGen.  Delete
trivial-union-assign-nyi.cpp; its behavior is now covered by
union-copy-move-assignment.cpp, which checks the emitted memcpy.
---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp             | 14 --------------
 .../test/CIR/CodeGen/trivial-union-assign-nyi.cpp | 15 ---------------
 2 files changed, 29 deletions(-)
 delete mode 100644 clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 2e50956e1c2ee..96ece6703a512 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -901,20 +901,6 @@ void 
CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
   assert(!cir::MissingFeatures::incrementProfileCounter());
   assert(!cir::MissingFeatures::runCleanupsScope());
 
-  // A defaulted union copy/move assignment has an empty synthesized body:
-  // Sema skips union fields (the FIXME in SemaDeclCXX::buildSingleCopyAssign),
-  // so there is no AST expression for the implied whole-object memcpy.
-  // Emitting that body would silently drop the copy, so report NYI instead.
-  // Struct/array memcpy-equivalent assignments carry the implicit memberwise
-  // copies in the AST (per-field assignment expressions, or a builtin memcpy
-  // call for array members) and lower correctly through the loop below.
-  if (assignOp->isMemcpyEquivalentSpecialMember(getContext()) &&
-      assignOp->getParent()->isUnion()) {
-    cgm.errorNYI(assignOp->getSourceRange(),
-                 "defaulted union copy/move assignment operator");
-    return;
-  }
-
   // Classic codegen uses a special class to attempt to replace member
   // initializers with memcpy. We could possibly defer that to the
   // lowering or optimization phases to keep the memory accesses more
diff --git a/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp 
b/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
deleted file mode 100644
index e457dca4fd23d..0000000000000
--- a/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
+++ /dev/null
@@ -1,15 +0,0 @@
-// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir 
-emit-cir -verify %s
-
-// The defaulted copy/move assignment operator of a union has an empty
-// synthesized body -- Sema skips union fields, leaving no AST expression for
-// the implied whole-object copy.  Emitting that body would silently drop the
-// copy, so CIRGen reports NYI instead.
-
-// expected-error@+1 2 {{ClangIR code gen Not Yet Implemented: defaulted union 
copy/move assignment operator}}
-union U {
-  void *p;
-  int i;
-};
-
-void copy_assign(U &a, U &b) { a = b; }
-void move_assign(U &a, U &b) { a = static_cast<U &&>(b); }

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to