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
