Author: Adam Smith
Date: 2026-07-31T15:49:40-05:00
New Revision: ec2bf56676ae4a46f47e3fbc62d5208b729792a1

URL: 
https://github.com/llvm/llvm-project/commit/ec2bf56676ae4a46f47e3fbc62d5208b729792a1
DIFF: 
https://github.com/llvm/llvm-project/commit/ec2bf56676ae4a46f47e3fbc62d5208b729792a1.diff

LOG: [Clang][Sema] Synthesize a memcpy body for defaulted union assignment 
(#206579)

A defaulted copy or move assignment operator for a union is synthesized
with an empty body. `DefineImplicitCopyAssignment` /
`DefineImplicitMoveAssignment` skip union members in the memberwise
loop, and the implied copy of the object representation has no AST
representation, a FIXME that has sat at that skip for a long time. The
operator ends up copying nothing.

Classic CodeGen hides this at ordinary call sites by lowering a trivial
assignment to a memcpy at the call site, so `u1 = u2` works even though
the operator body is a no-op. But when the operator is genuinely called,
through a pointer-to-member for instance, it silently copies nothing.
ClangIR calls the assignment operator at the call site rather than
eliding it, so it hits the empty body directly and drops every union
assignment. The no-op is then deleted at `-O3`. That is the MultiSource
`kc` miscompile, where a `YYSTYPE` union assignment (`*++yyvsp =
yylval`) becomes a no-op.

This implements the FIXME by mirroring the defaulted union copy
constructor (`CGClass.cpp`: "union copy constructor, we must emit a
memcpy") in the AST: when the class is a union,
`DefineImplicit{Copy,Move}Assignment` emits a single whole-object copy
through `buildMemcpyForAssignmentOp`, the 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 never executes the body, is unchanged.

Copying the object representation is correct even when a union member is
not trivially copyable, but the synthesized memcpy would then trip
`-Wnontrivial-memcall`. A `void*` cast would drop pointee qualifiers
such as the address space and inject an explicit cast node the source
never wrote. Instead the call keeps its typed union-pointer arguments,
and the union branch in `DefineImplicit{Copy,Move}Assignment` wraps the
build in `IgnoreAllWarningDiagRAII`, a save-and-restore around the
engine's existing ignore-all-warnings state. `CheckMemaccessArguments`
defers the warning and its note through `DiagRuntimeBehavior`, and that
queue flushes after the RAII has restored the previous ignore state, so
`DiagIfReachable` samples that state as it enqueues and drops what is
not error-class. Under `-w` the same queue is discarded before it
flushes, so nothing outside the scoped region changes.

Classic CodeGen now emits the copy when the operator is odr-used, and
ClangIR's union assignment lowers to a real memcpy. On the Sema side,
tests cover the synthesized AST for the implicit-`this` and C++23
explicit-object spellings, the `-Wnontrivial-memcall` suppression, the
preserved address space, the clean `-ast-print` output, and constexpr
active-member copy. The classic and ClangIR lowering is checked across
named, anonymous, and tail-padded unions.

#198918 added a defaulted-union `errorNYI` in ClangIR and deferred this
AST fix to a separate change. This PR makes that fix and drops the
guard, so the now-non-empty body lowers to a `cir.call @memcpy` instead
of erroring. Its own `union-copy-move-assignment.cpp` CIR test, which
expects that call, is the regression check.

Added: 
    clang/test/AST/ast-dump-union-assign-address-space.clcpp
    clang/test/AST/ast-dump-union-assign-explicit-object.cpp
    clang/test/AST/ast-dump-union-copy-move-assign.cpp
    clang/test/AST/ast-print-union-assign.cpp
    clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
    clang/test/CodeGenCXX/union-copy-move-assignment.cpp
    clang/test/SemaCXX/union-assign-constexpr.cpp
    clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp

Modified: 
    clang/docs/ReleaseNotes.md
    clang/include/clang/Basic/Diagnostic.h
    clang/lib/CIR/CodeGen/CIRGenClass.cpp
    clang/lib/Sema/SemaDeclCXX.cpp
    clang/lib/Sema/SemaExpr.cpp

Removed: 
    clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp


################################################################################
diff  --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index ca0cdf5e32a10..a38b99ff8e075 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -418,6 +418,12 @@ features cannot lower the translation-unit ABI level;
   libstdc++15 has been extended to support preprocessed input. Previously, 
splitting the preprocessing and
   compilation step would result in the fix not being applied. (#GH160314)
 
+- A defaulted copy or move assignment operator for a union was left with an
+  empty body and copied nothing when the operator was actually called, for
+  example through a pointer to member. Clang now synthesizes a whole-object
+  copy so the union's object representation is copied, matching the defaulted
+  union copy constructor.
+
 #### Bug Fixes to AST Handling
 
 - Fixed a non-deterministic ordering of unused local typedefs that made

diff  --git a/clang/include/clang/Basic/Diagnostic.h 
b/clang/include/clang/Basic/Diagnostic.h
index c4325ba410655..834f026aff62d 100644
--- a/clang/include/clang/Basic/Diagnostic.h
+++ b/clang/include/clang/Basic/Diagnostic.h
@@ -1124,6 +1124,22 @@ class DiagnosticErrorTrap {
   }
 };
 
+/// RAII class that temporarily sets the "ignore all warnings" state on a
+/// DiagnosticsEngine and restores the previous state on destruction.  Use it 
to
+/// silence warnings around a self-contained region of diagnostics, such as a
+/// compiler-synthesized call whose arguments are known to be correct.
+class IgnoreAllWarningDiagRAII {
+  DiagnosticsEngine &Diag;
+  bool OldValue;
+
+public:
+  explicit IgnoreAllWarningDiagRAII(DiagnosticsEngine &Diag)
+      : Diag(Diag), OldValue(Diag.getIgnoreAllWarnings()) {
+    Diag.setIgnoreAllWarnings(true);
+  }
+  ~IgnoreAllWarningDiagRAII() { Diag.setIgnoreAllWarnings(OldValue); }
+};
+
 /// The streaming interface shared between DiagnosticBuilder and
 /// PartialDiagnostic. This class is not intended to be constructed directly
 /// but only as base class of DiagnosticBuilder and PartialDiagnostic builder.

diff  --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 31e09320a43d6..8ead754d7cf3c 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/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index e5a5963db27c2..47b01b913b428 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -15507,10 +15507,30 @@ 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.
+  if (ClassDecl->isUnion()) {
+    ExprBuilder &To = ExplicitObject
+                          ? static_cast<ExprBuilder &>(*ExplicitObject)
+                          : static_cast<ExprBuilder &>(*DerefThis);
+    // Copying the object representation is correct even for a union that is
+    // not trivially copyable, so -Wnontrivial-memcall is a false positive
+    // here.  Ignoring warnings rather than casting the arguments to void*
+    // keeps them typed, which preserves their address space.
+    IgnoreAllWarningDiagRAII IgnoreWarnings(Diags);
+    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 +15917,30 @@ 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.
+  if (ClassDecl->isUnion()) {
+    ExprBuilder &To = ExplicitObject
+                          ? static_cast<ExprBuilder &>(*ExplicitObject)
+                          : static_cast<ExprBuilder &>(*DerefThis);
+    // Copying the object representation is correct even for a union that is
+    // not trivially copyable, so -Wnontrivial-memcall is a false positive
+    // here.  Ignoring warnings rather than casting the arguments to void*
+    // keeps them typed, which preserves their address space.
+    IgnoreAllWarningDiagRAII IgnoreWarnings(Diags);
+    StmtResult Copy = buildMemcpyForAssignmentOp(
+        *this, Loc, Context.getCanonicalTagType(ClassDecl), To, OtherRef);
+    if (Copy.isInvalid()) {
+      MoveAssignOperator->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;
 

diff  --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index ed3d27b5adc27..59b8c9b60663c 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -21114,6 +21114,14 @@ bool Sema::DiagIfReachable(SourceLocation Loc, 
ArrayRef<const Stmt *> Stmts,
   }
 
   if (getCurFunction()) {
+    // This queue flushes after the function is analyzed, by which time an
+    // ignore-all-warnings region live here is gone, so sample it now.  A note
+    // is not error-class either, so this also drops the notes that accompany a
+    // skipped warning.  They arrive on their own call, out of reach of the
+    // engine's rule that drops a note whose warning was ignored.
+    if (Diags.getIgnoreAllWarnings() &&
+        Diags.getDiagnosticIDs()->isWarningOrExtension(PD.getDiagID()))
+      return false;
     FunctionScopes.back()->PossiblyUnreachableDiags.push_back(
         sema::PossiblyUnreachableDiag(PD, Loc, Stmts));
     return true;

diff  --git a/clang/test/AST/ast-dump-union-assign-address-space.clcpp 
b/clang/test/AST/ast-dump-union-assign-address-space.clcpp
new file mode 100644
index 0000000000000..069d375afa046
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-assign-address-space.clcpp
@@ -0,0 +1,23 @@
+// RUN: %clang_cc1 -triple spir-unknown-unknown -cl-std=clc++2021 -x cl \
+// RUN:   -ast-dump %s | FileCheck %s
+
+union U {
+  int a;
+  float b;
+};
+
+__kernel void k(__global U *g, __global const U *s) {
+  *g = *s;
+}
+
+// Converting the typed union pointers preserves the address space, which a
+// compiler-written cast to plain void * would drop.
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= '__generic U &(const 
__generic U &)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       ImplicitCastExpr {{.*}} '__generic void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} '__generic U *' prefix '&'
+// CHECK:       ImplicitCastExpr {{.*}} 'const __generic void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'const __generic U *' prefix '&'

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..f37d3ee32103a
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-assign-explicit-object.cpp
@@ -0,0 +1,35 @@
+// 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);
+}
+
+// C++23 explicit-object form uses the same typed-pointer memcpy.
+
+// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, const U &)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK:       ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'const U *' prefix '&'
+// CHECK:     ReturnStmt
+
+// CHECK: CXXMethodDecl {{.*}} operator= 'U &(U &, U &&)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK:       ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK:     ReturnStmt

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..ed393420eaf3b
--- /dev/null
+++ b/clang/test/AST/ast-dump-union-copy-move-assign.cpp
@@ -0,0 +1,34 @@
+// 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 memcpy operands are typed union pointers, converted to void * only by
+// the builtin's parameter.
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(const U &)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK:       ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'const U *' prefix '&'
+// CHECK:     ReturnStmt
+
+// CHECK: CXXMethodDecl {{.*}} implicit {{.*}}operator= 'U &(U &&)
+// CHECK:   CompoundStmt
+// CHECK:     CallExpr
+// CHECK:       DeclRefExpr {{.*}} '__builtin_memcpy'
+// CHECK:       ImplicitCastExpr {{.*}} 'void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK:       ImplicitCastExpr {{.*}} 'const void *' <BitCast>
+// CHECK:         UnaryOperator {{.*}} 'U *' prefix '&'
+// CHECK:     ReturnStmt

diff  --git a/clang/test/AST/ast-print-union-assign.cpp 
b/clang/test/AST/ast-print-union-assign.cpp
new file mode 100644
index 0000000000000..ba158804e8802
--- /dev/null
+++ b/clang/test/AST/ast-print-union-assign.cpp
@@ -0,0 +1,21 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -ast-print %s | 
FileCheck %s
+
+union U {
+  int a;
+  float b;
+  U &operator=(const U &) = default;
+  U &operator=(U &&) = default;
+};
+
+void odr_use(U &x, const U &y, U &&z) {
+  x = y;
+  x = static_cast<U &&>(z);
+}
+
+// The synthesized memcpy body must not leak into -ast-print.
+
+// CHECK: union U {
+// CHECK: U &operator=(const U &) noexcept = default;
+// CHECK: U &operator=(U &&) noexcept = default;
+// CHECK-NOT: __builtin_memcpy
+// CHECK-NOT: (void *)

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); }

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..aeb3461990e5c
--- /dev/null
+++ b/clang/test/CIR/CodeGen/union-copy-move-assignment.cpp
@@ -0,0 +1,33 @@
+// 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-prefixes=LLVM,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-prefixes=LLVM,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=);
+
+// 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(
+
+// The CIR backend calls the memcpy libcall where the classic backend emits the
+// llvm.memcpy intrinsic.
+
+// LLVM: define linkonce_odr noundef nonnull align 4 dereferenceable(4) ptr 
@_ZN1UaSERKS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr 
noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// LLVMCIR:     call ptr @memcpy(ptr noundef %{{.+}}, ptr noundef %{{.+}}, i64 
noundef 4)
+// LLVMCIR-NOT: call ptr @memcpy
+// OGCG:        call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, i64 4, i1 false)
+// OGCG-NOT:    call void @llvm.memcpy
+// LLVM: define linkonce_odr noundef nonnull align 4 dereferenceable(4) ptr 
@_ZN1UaSEOS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr 
noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// LLVMCIR:     call ptr @memcpy(ptr noundef %{{.+}}, ptr noundef %{{.+}}, i64 
noundef 4)
+// OGCG:        call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, 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..406285cd3d353
--- /dev/null
+++ b/clang/test/CodeGenCXX/union-copy-move-assignment.cpp
@@ -0,0 +1,59 @@
+// 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=);
+
+// Exactly one whole-object memcpy per assignment body.
+// CHECK-LABEL: define linkonce_odr noundef nonnull align 4 dereferenceable(4) 
ptr @_ZN1UaSERKS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr 
noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, i64 4, i1 false)
+// CHECK-NOT:     memcpy
+// CHECK:         ret ptr
+
+// CHECK-LABEL: define linkonce_odr noundef nonnull align 4 dereferenceable(4) 
ptr @_ZN1UaSEOS_(ptr noundef nonnull align 4 dereferenceable(4) %{{.+}}, ptr 
noundef nonnull align 4 dereferenceable(4) %{{.+}})
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, 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 linkonce_odr noundef nonnull align 4 dereferenceable(8) 
ptr @_ZN6PaddedaSERKS_(ptr noundef nonnull align 4 dereferenceable(8) %{{.+}}, 
ptr noundef nonnull align 4 dereferenceable(8) %{{.+}})
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, i64 8, i1 false)
+// CHECK-NOT:     memcpy
+// 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 dso_local void 
@_Z12assign_namedP14WithNamedUnionPKS_(ptr noundef %{{.+}}, ptr noundef %{{.+}})
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, 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 dso_local void @_Z11assign_anonP13WithAnonUnionPKS_(ptr 
noundef %{{.+}}, ptr noundef %{{.+}})
+// CHECK:         call void @llvm.memcpy.p0.p0.i64(ptr align 4 %{{.+}}, ptr 
align 4 %{{.+}}, i64 8, i1 false)

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);

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..53dec05e077b4
--- /dev/null
+++ b/clang/test/SemaCXX/union-assign-memcpy-nontrivial.cpp
@@ -0,0 +1,33 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++17 -fsyntax-only \
+// RUN:   -Wnontrivial-memcall -Wdeprecated-copy-with-user-provided-dtor 
-verify %s
+
+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}}
+}
+
+// The memcpy suppression is scoped to the synthesized call, so an unrelated
+// warning for the same union still fires: a user-provided destructor 
deprecates
+// the implicit copy assignment.
+union V {
+  int i;
+  ~V() {} // expected-warning {{definition of implicit copy assignment 
operator for 'V' is deprecated because it has a user-provided destructor}}
+};
+
+void use_deprecated_copy(V &a, const V &b) {
+  a = b; // expected-note {{in implicit copy assignment operator for 'V' first 
required here}}
+}


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

Reply via email to