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

>From 55cbb3266302fe96bebb006bb0d997f0c827320d Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Wed, 20 May 2026 14:50:17 -0700
Subject: [PATCH 1/6] [CIR] Inline trivial copy/move assignment at call sites

Classic CodeGen does not call implicit trivial copy/move assignment
operators for unions and other memcpy-equivalent types; it emits an
aggregate copy at the assignment site.  CIR was calling the implicit
operator=, whose body is only "return *this", so LLVM at -O3 inferred
readonly on the destination pointer and deleted the store.  That broke
kimwitu++ kc (-fclangir -O3) and any bison-style *++yyvsp = yylval path.

Mirror CGExprCXX: evaluate the RHS first for operator=, then
emitAggregateAssign for trivial copy/move when the record may not insert
extra padding.  Add emitAggregateAssign on CIRGenFunction.

Regression test trivial-union-assign.cpp (CIR/LLVM/OGCG at -O3).
Update cxx-special-member-attr, lvalue-nttp, and device-stub checks for
the new emission shape.
---
 clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp       | 46 +++++++++++++------
 clang/lib/CIR/CodeGen/CIRGenFunction.h        |  6 +++
 .../CIR/CodeGen/cxx-special-member-attr.cpp   |  8 ++--
 .../test/CIR/CodeGen/trivial-union-assign.cpp | 36 +++++++++++++++
 clang/test/CIR/CodeGenCUDA/device-stub.cu     |  2 +-
 clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp     |  2 +-
 6 files changed, 79 insertions(+), 21 deletions(-)
 create mode 100644 clang/test/CIR/CodeGen/trivial-union-assign.cpp

diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp 
b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 1a565b077a2eb..94fca3f68aa40 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -169,24 +169,28 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
     }
   }
 
-  // Note on trivial assignment
-  // --------------------------
-  // Classic codegen avoids generating the trivial copy/move assignment 
operator
-  // when it isn't necessary, choosing instead to just produce IR with an
-  // equivalent effect. We have chosen not to do that in CIR, instead emitting
-  // trivial copy/move assignment operators and allowing later transformations
-  // to optimize them away if appropriate.
+  bool trivialForCodegen =
+      md->isTrivial() || (md->isDefaulted() && md->getParent()->isUnion());
+  bool trivialAssignment =
+      trivialForCodegen &&
+      (md->isCopyAssignmentOperator() || md->isMoveAssignmentOperator()) &&
+      !md->getParent()->mayInsertExtraPadding();
 
   // C++17 demands that we evaluate the RHS of a (possibly-compound) assignment
   // operator before the LHS.
   CallArgList rtlArgStorage;
   CallArgList *rtlArgs = nullptr;
+  LValue trivialAssignmentRhs;
   if (auto *oce = dyn_cast<CXXOperatorCallExpr>(ce)) {
     if (oce->isAssignmentOp()) {
-      rtlArgs = &rtlArgStorage;
-      emitCallArgs(*rtlArgs, md->getType()->castAs<FunctionProtoType>(),
-                   drop_begin(ce->arguments(), 1), ce->getDirectCallee(),
-                   /*ParamsToSkip*/ 0);
+      if (trivialAssignment)
+        trivialAssignmentRhs = emitLValue(oce->getArg(1));
+      else {
+        rtlArgs = &rtlArgStorage;
+        emitCallArgs(*rtlArgs, md->getType()->castAs<FunctionProtoType>(),
+                     drop_begin(ce->arguments(), 1), ce->getDirectCallee(),
+                     /*ParamsToSkip*/ 0);
+      }
     }
   }
 
@@ -195,7 +199,8 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
     LValueBaseInfo baseInfo;
     assert(!cir::MissingFeatures::opTBAA());
     Address thisValue = emitPointerWithAlignment(base, &baseInfo);
-    thisPtr = makeAddrLValue(thisValue, base->getType(), baseInfo);
+    thisPtr =
+        makeAddrLValue(thisValue, base->getType()->getPointeeType(), baseInfo);
   } else {
     thisPtr = emitLValue(base);
   }
@@ -206,9 +211,20 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
     return RValue::get(nullptr);
   }
 
-  if ((md->isTrivial() || (md->isDefaulted() && md->getParent()->isUnion())) &&
-      isa<CXXDestructorDecl>(md))
-    return RValue::get(nullptr);
+  if (trivialForCodegen) {
+    if (isa<CXXDestructorDecl>(md))
+      return RValue::get(nullptr);
+
+    if (trivialAssignment) {
+      LValue rhs = isa<CXXOperatorCallExpr>(ce) ? trivialAssignmentRhs
+                                                : emitLValue(*ce->arg_begin());
+      emitAggregateAssign(thisPtr, rhs, ce->getType());
+      return RValue::get(thisPtr.getPointer());
+    }
+
+    assert(md->getParent()->mayInsertExtraPadding() &&
+           "unknown trivial member function");
+  }
 
   // Compute the function type we're calling
   const CXXMethodDecl *calleeDecl =
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h 
b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index 9f2facd12f417..6053bedba81af 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -1512,6 +1512,12 @@ class CIRGenFunction : public CIRGenTypeCache {
                          AggValueSlot::Overlap_t mayOverlap,
                          bool isVolatile = false);
 
+  /// Emit an aggregate assignment.
+  void emitAggregateAssign(LValue dest, LValue src, QualType eltTy) {
+    emitAggregateCopy(dest, src, eltTy, AggValueSlot::MayOverlap,
+                      hasVolatileMember(eltTy));
+  }
+
   /// Emit code to compute the specified expression which can have any type. 
The
   /// result is returned as an RValue struct. If this is an aggregate
   /// expression, the aggloc/agglocvolatile arguments indicate where the result
diff --git a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp 
b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
index bfca59db44fdf..3c529c9426bc2 100644
--- a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
+++ b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
@@ -30,10 +30,6 @@ struct Foo {
   ~Foo();
 };
 
-// Trivial copy/move assignment operator definitions appear at module level.
-// CIR: @_ZN4FlubaSERKS_(%arg0: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Flub>{{.*}}) special_member<#cir.cxx_assign<!rec_Flub, copy, 
trivial true>>
-// CIR: @_ZN4FlubaSEOS_(%arg0: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Flub>{{.*}}) special_member<#cir.cxx_assign<!rec_Flub, move, 
trivial true>>
-
 void trivial_func() {
   Flub f1{};
 
@@ -45,7 +41,11 @@ void trivial_func() {
   // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
 
   f2 = f1;
+  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+  // CIR-NOT: cir.call{{.*}}@_ZN4FlubaSERKS_
   f1 = static_cast<Flub&&>(f3);
+  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+  // CIR-NOT: cir.call{{.*}}@_ZN4FlubaSEOS_
 }
 
 void non_trivial_func() {
diff --git a/clang/test/CIR/CodeGen/trivial-union-assign.cpp 
b/clang/test/CIR/CodeGen/trivial-union-assign.cpp
new file mode 100644
index 0000000000000..1d02a4634806e
--- /dev/null
+++ b/clang/test/CIR/CodeGen/trivial-union-assign.cpp
@@ -0,0 +1,36 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -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 -O3 -fclangir -emit-llvm 
%s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
+
+union YYSTYPE {
+  void *yt_casestring;
+  void *yt_ID;
+};
+
+extern YYSTYPE yylval;
+
+static int consume(YYSTYPE v) { return v.yt_casestring != nullptr; }
+
+int test_shift(YYSTYPE *yyvsp) {
+  yylval.yt_casestring = reinterpret_cast<void *>(0x42);
+  *++yyvsp = yylval;
+  return consume(yyvsp[0]);
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE
+// CIR-NOT: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
+// CIR: cir.copy
+
+// LLVM-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
+// LLVM: store{{.*}}@yylval
+// LLVM: store i64 66
+// LLVM-NOT: readonly
+// LLVM: ret i32 1
+
+// OGCG-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
+// OGCG: store{{.*}}@yylval
+// OGCG: store i64 66
+// OGCG: ret i32 1
diff --git a/clang/test/CIR/CodeGenCUDA/device-stub.cu 
b/clang/test/CIR/CodeGenCUDA/device-stub.cu
index ca5e185add5fc..3ab60009b6045 100644
--- a/clang/test/CIR/CodeGenCUDA/device-stub.cu
+++ b/clang/test/CIR/CodeGenCUDA/device-stub.cu
@@ -255,7 +255,7 @@ __device__ _BitInt(36) c;
 // HIP-CIR-SAME: #cir.int<1> : !s32i,
 // HIP-CIR-SAME: #cir.global_view<@__hip_fatbin_str> : !cir.ptr<!void>,
 // HIP-CIR-SAME: #cir.ptr<null> : !cir.ptr<!void>
-// HIP-CIR-SAME: }> : !rec_anon_struct {section = ".hipFatBinSegment"}
+// HIP-CIR-SAME: }> : !rec_anon_struct{{.*}} {section = ".hipFatBinSegment"}
 
 // HIP-CIR: cir.global "private" internal @__hip_gpubin_handle = 
#cir.ptr<null> : !cir.ptr<!cir.ptr<!void>>
 // HIP-CIR: cir.func private @__hipRegisterFatBinary(!cir.ptr<!void>) -> 
!cir.ptr<!cir.ptr<!void>>
diff --git a/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp 
b/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp
index 961430a1a3fb9..7261f61d462a3 100644
--- a/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp
+++ b/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp
@@ -34,7 +34,7 @@ template void templ<s>();
 // CIR-NEXT: %[[ONE:.*]] = cir.const #cir.int<1>
 // CIR-NEXT: cir.call @_ZN6StructC1Ei(%[[TEMP]], %[[ONE]])
 // CIR-NEXT: %[[GLOB:.*]] = cir.get_global @s : !cir.ptr<!rec_Struct>
-// CIR-NEXT: cir.call @_ZN6StructaSEOS_(%[[GLOB]], %[[TEMP]])
+// CIR-NEXT: cir.return
 
 // LLVM: define{{.*}}@_Z5templITnRDaL_Z1sEEvv()
 // LLVM: %[[TEMP:.*]] = alloca %struct.Struct

>From f82b7274301e7337d052a4c2c0ade705440fca6e Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Thu, 21 May 2026 11:06:22 -0700
Subject: [PATCH 2/6] [CIR] Fold trivial-union-assign test into
 cxx-special-member-attr

Move the -O3 yacc-stack repro into cxx-special-member-attr.cpp via
split-file and drop trivial-union-assign.cpp per review.  Use one LLVM
check prefix for CIR-lowered and classic output.
---
 .../CIR/CodeGen/cxx-special-member-attr.cpp   | 42 ++++++++++++++++++-
 .../test/CIR/CodeGen/trivial-union-assign.cpp | 36 ----------------
 2 files changed, 40 insertions(+), 38 deletions(-)
 delete mode 100644 clang/test/CIR/CodeGen/trivial-union-assign.cpp

diff --git a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp 
b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
index 3c529c9426bc2..b92e7b5eb6549 100644
--- a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
+++ b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
@@ -1,5 +1,9 @@
-// RUN: %clang_cc1 -std=c++11 -triple aarch64-none-linux-android21 -fclangir 
-emit-cir %s -o %t.cir
-// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: split-file %s %t
+
+//--- special_member_attr.cpp
+
+// RUN: %clang_cc1 -std=c++11 -triple aarch64-none-linux-android21 -fclangir 
-emit-cir %t/special_member_attr.cpp -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir 
%t/special_member_attr.cpp
 
 struct Flub {
   int a = 123;
@@ -65,3 +69,37 @@ void non_trivial_func() {
   // CIR: @_ZN3FooaSEOS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Foo>{{.*}}) special_member<#cir.cxx_assign<!rec_Foo, move>>
   // CIR: @_ZN3FooD1Ev(!cir.ptr<!rec_Foo> {{.*}}) 
special_member<#cir.cxx_dtor<!rec_Foo>>
 }
+
+//--- trivial_union_assign.cpp
+
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -fclangir -emit-cir 
%t/trivial_union_assign.cpp -o %t-union.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t-union.cir 
%t/trivial_union_assign.cpp
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -fclangir -emit-llvm 
%t/trivial_union_assign.cpp -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll 
%t/trivial_union_assign.cpp
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -emit-llvm 
%t/trivial_union_assign.cpp -o %t.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t.ll 
%t/trivial_union_assign.cpp
+
+union YYSTYPE {
+  void *yt_casestring;
+  void *yt_ID;
+};
+
+extern YYSTYPE yylval;
+
+static int consume(YYSTYPE v) { return v.yt_casestring != nullptr; }
+
+int test_shift(YYSTYPE *yyvsp) {
+  yylval.yt_casestring = reinterpret_cast<void *>(0x42);
+  *++yyvsp = yylval;
+  return consume(yyvsp[0]);
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE
+// CIR-NOT: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
+// CIR: cir.copy
+
+// LLVM-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
+// LLVM: store{{.*}}@yylval
+// LLVM: store i64 66
+// LLVM-NOT: readonly
+// LLVM: ret i32 1
diff --git a/clang/test/CIR/CodeGen/trivial-union-assign.cpp 
b/clang/test/CIR/CodeGen/trivial-union-assign.cpp
deleted file mode 100644
index 1d02a4634806e..0000000000000
--- a/clang/test/CIR/CodeGen/trivial-union-assign.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -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 -O3 -fclangir -emit-llvm 
%s -o %t-cir.ll
-// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -emit-llvm %s -o %t.ll
-// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
-
-union YYSTYPE {
-  void *yt_casestring;
-  void *yt_ID;
-};
-
-extern YYSTYPE yylval;
-
-static int consume(YYSTYPE v) { return v.yt_casestring != nullptr; }
-
-int test_shift(YYSTYPE *yyvsp) {
-  yylval.yt_casestring = reinterpret_cast<void *>(0x42);
-  *++yyvsp = yylval;
-  return consume(yyvsp[0]);
-}
-
-// CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE
-// CIR-NOT: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
-// CIR: cir.copy
-
-// LLVM-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
-// LLVM: store{{.*}}@yylval
-// LLVM: store i64 66
-// LLVM-NOT: readonly
-// LLVM: ret i32 1
-
-// OGCG-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
-// OGCG: store{{.*}}@yylval
-// OGCG: store i64 66
-// OGCG: ret i32 1

>From bbcf200da0629b97c54d5aa3de6cfa6bbd6b79d9 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Wed, 27 May 2026 15:48:37 -0700
Subject: [PATCH 3/6] [CIR] NFC: comment on trivialAssignment inlining path

Mirror the rationale from classic CodeGen's
EmitCXXMemberOrOperatorMemberCallExpr: we inline trivial copy/move
assignment rather than calling the operator to preserve TBAA information
from the RHS.  emitLValue must be used here instead of emitting call
arguments for the same reason.
---
 clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp 
b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 94fca3f68aa40..93526c7791170 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -216,6 +216,11 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
       return RValue::get(nullptr);
 
     if (trivialAssignment) {
+      // We don't like to generate the trivial copy/move assignment operator
+      // when it isn't necessary; just produce the proper effect here.  It's
+      // important that we use the result of emitLValue here rather than
+      // emitting call arguments, in order to preserve TBAA information from
+      // the RHS.
       LValue rhs = isa<CXXOperatorCallExpr>(ce) ? trivialAssignmentRhs
                                                 : emitLValue(*ce->arg_begin());
       emitAggregateAssign(thisPtr, rhs, ce->getType());

>From 339c135f201fedcc3fcd69dd73beb33eec729a21 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Wed, 10 Jun 2026 15:25:13 -0700
Subject: [PATCH 4/6] [CIR] Copy the object in trivial assignment bodies

A defaulted trivial copy/move assignment operator was emitted with a body
that performs no copy: for a union the implicit body is just `return *this`,
and CIRGen does not yet run classic CodeGen's AssignmentMemcpyizer.  The
operator was therefore a no-op, and at -O3 LLVM deleted the assignment's store
entirely -- seen in kimwitu++ `kc`, where `*++yyvsp = yylval` was dropped.

An earlier version of this change worked around that by eliding the operator=
call at the call site, the way classic CodeGen does.  That discards the
high-level assignment that CIR's own passes want to reason about and lowers
prematurely, so fix the body instead.  When the operator is a
memcpy-equivalent special member, copy the whole object from the source
reference into `this`, then emit the trailing `return *this`.  The call is
kept; replacing such calls is an optimization that can live in a later CIR
pass.

Non-trivial assignment operators keep their member-wise body emission, and the
general field-coalescing memcpyizer remains not-yet-implemented.
---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp         | 27 ++++++
 clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp       | 51 ++++-------
 .../CIR/CodeGen/cxx-special-member-attr.cpp   | 90 +++++++++++--------
 clang/test/CIR/CodeGenCUDA/device-stub.cu     |  2 +-
 clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp     |  2 +-
 5 files changed, 99 insertions(+), 73 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index eb6490973da75..b6f02204a72bd 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -901,6 +901,33 @@ void 
CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
   assert(!cir::MissingFeatures::incrementProfileCounter());
   assert(!cir::MissingFeatures::runCleanupsScope());
 
+  // For a memcpy-equivalent special member (a union, or a trivially-copyable
+  // record) the synthesized body either copies nothing -- a union body is just
+  // `return *this` -- or relies on a memcpy the AST does not spell out as
+  // field assignments.  Mirror classic CodeGen's AssignmentMemcpyizer: copy
+  // the whole object once, then fall through to emit the trailing
+  // `return *this`.  Emitting the copy but skipping the return would leave the
+  // result reference uninitialized.
+  if (assignOp->isMemcpyEquivalentSpecialMember(getContext())) {
+    CanQualType recordTy =
+        getContext().getCanonicalTagType(assignOp->getParent());
+    LValue dest = makeNaturalAlignAddrLValue(loadCXXThis(), recordTy);
+    // The source is the trailing reference parameter; load it to get the
+    // referent's address before copying (mirrors the copy-constructor path).
+    mlir::Value srcPtr = builder.createLoad(getLoc(assignOp->getLocation()),
+                                            getAddrOfLocalVar(args.back()));
+    LValue src = makeNaturalAlignAddrLValue(srcPtr, recordTy);
+    emitAggregateAssign(dest, src, recordTy);
+
+    for (Stmt *s : rootCS->body())
+      if (isa<ReturnStmt>(s))
+        if (emitStmt(s, /*useCurrentScope=*/true).failed())
+          cgm.errorNYI(s->getSourceRange(),
+                       std::string("emitImplicitAssignmentOperatorBody: ") +
+                           s->getStmtClassName());
+    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/CIR/CodeGen/CIRGenExprCXX.cpp 
b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
index 93526c7791170..1a565b077a2eb 100644
--- a/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
@@ -169,28 +169,24 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
     }
   }
 
-  bool trivialForCodegen =
-      md->isTrivial() || (md->isDefaulted() && md->getParent()->isUnion());
-  bool trivialAssignment =
-      trivialForCodegen &&
-      (md->isCopyAssignmentOperator() || md->isMoveAssignmentOperator()) &&
-      !md->getParent()->mayInsertExtraPadding();
+  // Note on trivial assignment
+  // --------------------------
+  // Classic codegen avoids generating the trivial copy/move assignment 
operator
+  // when it isn't necessary, choosing instead to just produce IR with an
+  // equivalent effect. We have chosen not to do that in CIR, instead emitting
+  // trivial copy/move assignment operators and allowing later transformations
+  // to optimize them away if appropriate.
 
   // C++17 demands that we evaluate the RHS of a (possibly-compound) assignment
   // operator before the LHS.
   CallArgList rtlArgStorage;
   CallArgList *rtlArgs = nullptr;
-  LValue trivialAssignmentRhs;
   if (auto *oce = dyn_cast<CXXOperatorCallExpr>(ce)) {
     if (oce->isAssignmentOp()) {
-      if (trivialAssignment)
-        trivialAssignmentRhs = emitLValue(oce->getArg(1));
-      else {
-        rtlArgs = &rtlArgStorage;
-        emitCallArgs(*rtlArgs, md->getType()->castAs<FunctionProtoType>(),
-                     drop_begin(ce->arguments(), 1), ce->getDirectCallee(),
-                     /*ParamsToSkip*/ 0);
-      }
+      rtlArgs = &rtlArgStorage;
+      emitCallArgs(*rtlArgs, md->getType()->castAs<FunctionProtoType>(),
+                   drop_begin(ce->arguments(), 1), ce->getDirectCallee(),
+                   /*ParamsToSkip*/ 0);
     }
   }
 
@@ -199,8 +195,7 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
     LValueBaseInfo baseInfo;
     assert(!cir::MissingFeatures::opTBAA());
     Address thisValue = emitPointerWithAlignment(base, &baseInfo);
-    thisPtr =
-        makeAddrLValue(thisValue, base->getType()->getPointeeType(), baseInfo);
+    thisPtr = makeAddrLValue(thisValue, base->getType(), baseInfo);
   } else {
     thisPtr = emitLValue(base);
   }
@@ -211,25 +206,9 @@ RValue 
CIRGenFunction::emitCXXMemberOrOperatorMemberCallExpr(
     return RValue::get(nullptr);
   }
 
-  if (trivialForCodegen) {
-    if (isa<CXXDestructorDecl>(md))
-      return RValue::get(nullptr);
-
-    if (trivialAssignment) {
-      // We don't like to generate the trivial copy/move assignment operator
-      // when it isn't necessary; just produce the proper effect here.  It's
-      // important that we use the result of emitLValue here rather than
-      // emitting call arguments, in order to preserve TBAA information from
-      // the RHS.
-      LValue rhs = isa<CXXOperatorCallExpr>(ce) ? trivialAssignmentRhs
-                                                : emitLValue(*ce->arg_begin());
-      emitAggregateAssign(thisPtr, rhs, ce->getType());
-      return RValue::get(thisPtr.getPointer());
-    }
-
-    assert(md->getParent()->mayInsertExtraPadding() &&
-           "unknown trivial member function");
-  }
+  if ((md->isTrivial() || (md->isDefaulted() && md->getParent()->isUnion())) &&
+      isa<CXXDestructorDecl>(md))
+    return RValue::get(nullptr);
 
   // Compute the function type we're calling
   const CXXMethodDecl *calleeDecl =
diff --git a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp 
b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
index b92e7b5eb6549..70ef5018b81a7 100644
--- a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
+++ b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
@@ -34,50 +34,59 @@ struct Foo {
   ~Foo();
 };
 
+// The trivial copy/move assignment operators are emitted at module scope with
+// the special_member attribute, and their bodies perform a whole-object copy.
+// CIR-LABEL: cir.func{{.*}} @_ZN4FlubaSERKS_(
+// CIR-SAME: special_member<#cir.cxx_assign<!rec_Flub, copy, trivial true>>
+// CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+// CIR-LABEL: cir.func{{.*}} @_ZN4FlubaSEOS_(
+// CIR-SAME: special_member<#cir.cxx_assign<!rec_Flub, move, trivial true>>
+// CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+
 void trivial_func() {
   Flub f1{};
 
   Flub f2 = f1;
-  // Trivial copy/move constructors are inlined as cir.copy
-  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
-
-  Flub f3 = static_cast<Flub&&>(f1);
-  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+  Flub f3 = static_cast<Flub &&>(f1);
 
   f2 = f1;
-  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
-  // CIR-NOT: cir.call{{.*}}@_ZN4FlubaSERKS_
-  f1 = static_cast<Flub&&>(f3);
-  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
-  // CIR-NOT: cir.call{{.*}}@_ZN4FlubaSEOS_
+  f1 = static_cast<Flub &&>(f3);
 }
 
+// Trivial assignment keeps the operator= call; its body (above) does the copy.
+// CIR-LABEL: cir.func{{.*}} @_Z12trivial_funcv(
+// CIR: cir.call{{.*}}@_ZN4FlubaSERKS_
+// CIR: cir.call{{.*}}@_ZN4FlubaSEOS_
+
 void non_trivial_func() {
   Foo f1{};
-  // CIR: @_ZN3FooC2Ev(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) 
special_member<#cir.cxx_ctor<!rec_Foo, default>>
-
   Foo f2 = f1;
-  // CIR: @_ZN3FooC2ERKS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) 
special_member<#cir.cxx_ctor<!rec_Foo, copy>>
-
-  Foo f3 = static_cast<Foo&&>(f1);
-  // CIR: @_ZN3FooC2EOS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) 
special_member<#cir.cxx_ctor<!rec_Foo, move>>
-
+  Foo f3 = static_cast<Foo &&>(f1);
   f2 = f1;
-  // CIR: @_ZN3FooaSERKS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Foo>{{.*}}) special_member<#cir.cxx_assign<!rec_Foo, copy>>
-
-  f1 = static_cast<Foo&&>(f3);
-  // CIR: @_ZN3FooaSEOS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Foo>{{.*}}) special_member<#cir.cxx_assign<!rec_Foo, move>>
-  // CIR: @_ZN3FooD1Ev(!cir.ptr<!rec_Foo> {{.*}}) 
special_member<#cir.cxx_dtor<!rec_Foo>>
+  f1 = static_cast<Foo &&>(f3);
 }
 
+// CIR-LABEL: cir.func{{.*}} @_ZN3FooC2Ev(
+// CIR-SAME: special_member<#cir.cxx_ctor<!rec_Foo, default>>
+// CIR-LABEL: cir.func{{.*}} @_ZN3FooC2ERKS_(
+// CIR-SAME: special_member<#cir.cxx_ctor<!rec_Foo, copy>>
+// CIR-LABEL: cir.func{{.*}} @_ZN3FooC2EOS_(
+// CIR-SAME: special_member<#cir.cxx_ctor<!rec_Foo, move>>
+// CIR-LABEL: cir.func{{.*}} @_ZN3FooaSERKS_(
+// CIR-SAME: special_member<#cir.cxx_assign<!rec_Foo, copy>>
+// CIR-LABEL: cir.func{{.*}} @_ZN3FooaSEOS_(
+// CIR-SAME: special_member<#cir.cxx_assign<!rec_Foo, move>>
+// CIR-LABEL: cir.func{{.*}} @_ZN3FooD1Ev(
+// CIR-SAME: special_member<#cir.cxx_dtor<!rec_Foo>>
+
 //--- trivial_union_assign.cpp
 
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -fclangir -emit-cir 
%t/trivial_union_assign.cpp -o %t-union.cir
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir 
%t/trivial_union_assign.cpp -o %t-union.cir
 // RUN: FileCheck --check-prefix=CIR --input-file=%t-union.cir 
%t/trivial_union_assign.cpp
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -fclangir -emit-llvm 
%t/trivial_union_assign.cpp -o %t-cir.ll
-// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll 
%t/trivial_union_assign.cpp
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O3 -emit-llvm 
%t/trivial_union_assign.cpp -o %t.ll
-// RUN: FileCheck --check-prefix=LLVM --input-file=%t.ll 
%t/trivial_union_assign.cpp
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm 
%t/trivial_union_assign.cpp -o %t-union-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-union-cir.ll 
%t/trivial_union_assign.cpp
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm 
%t/trivial_union_assign.cpp -o %t-union.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t-union.ll 
%t/trivial_union_assign.cpp
 
 union YYSTYPE {
   void *yt_casestring;
@@ -94,12 +103,23 @@ int test_shift(YYSTYPE *yyvsp) {
   return consume(yyvsp[0]);
 }
 
-// CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE
-// CIR-NOT: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
-// CIR: cir.copy
-
+// The defaulted union copy-assignment operator copies the whole object in its
+// body -- previously the body was a no-op that LLVM deleted, dropping the
+// store at -O3 -- and the call is kept at the assignment site.
+// CIR-LABEL: cir.func{{.*}} @_ZN7YYSTYPEaSERKS_(
+// CIR-SAME: special_member<#cir.cxx_assign<!rec_YYSTYPE, copy, trivial true>>
+// CIR: cir.copy {{.*}} : !cir.ptr<!rec_YYSTYPE>
+// CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE(
+// CIR: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
+
+// LLVM: define{{.*}} @_ZN7YYSTYPEaSERKS_(
+// LLVM: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 
false)
 // LLVM-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
-// LLVM: store{{.*}}@yylval
-// LLVM: store i64 66
-// LLVM-NOT: readonly
-// LLVM: ret i32 1
+// LLVM: call{{.*}}@_ZN7YYSTYPEaSERKS_
+
+// Classic CodeGen inlines the trivial union assignment at the call site and
+// emits no operator= function; the store is performed directly.
+// OGCG-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
+// OGCG: store ptr inttoptr (i64 66 to ptr), ptr @yylval
+// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}@yylval, i64 8, i1 false)
+// OGCG-NOT: @_ZN7YYSTYPEaSERKS_
diff --git a/clang/test/CIR/CodeGenCUDA/device-stub.cu 
b/clang/test/CIR/CodeGenCUDA/device-stub.cu
index 3ab60009b6045..ca5e185add5fc 100644
--- a/clang/test/CIR/CodeGenCUDA/device-stub.cu
+++ b/clang/test/CIR/CodeGenCUDA/device-stub.cu
@@ -255,7 +255,7 @@ __device__ _BitInt(36) c;
 // HIP-CIR-SAME: #cir.int<1> : !s32i,
 // HIP-CIR-SAME: #cir.global_view<@__hip_fatbin_str> : !cir.ptr<!void>,
 // HIP-CIR-SAME: #cir.ptr<null> : !cir.ptr<!void>
-// HIP-CIR-SAME: }> : !rec_anon_struct{{.*}} {section = ".hipFatBinSegment"}
+// HIP-CIR-SAME: }> : !rec_anon_struct {section = ".hipFatBinSegment"}
 
 // HIP-CIR: cir.global "private" internal @__hip_gpubin_handle = 
#cir.ptr<null> : !cir.ptr<!cir.ptr<!void>>
 // HIP-CIR: cir.func private @__hipRegisterFatBinary(!cir.ptr<!void>) -> 
!cir.ptr<!cir.ptr<!void>>
diff --git a/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp 
b/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp
index 7261f61d462a3..961430a1a3fb9 100644
--- a/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp
+++ b/clang/test/CIR/CodeGenCXX/lvalue-nttp.cpp
@@ -34,7 +34,7 @@ template void templ<s>();
 // CIR-NEXT: %[[ONE:.*]] = cir.const #cir.int<1>
 // CIR-NEXT: cir.call @_ZN6StructC1Ei(%[[TEMP]], %[[ONE]])
 // CIR-NEXT: %[[GLOB:.*]] = cir.get_global @s : !cir.ptr<!rec_Struct>
-// CIR-NEXT: cir.return
+// CIR-NEXT: cir.call @_ZN6StructaSEOS_(%[[GLOB]], %[[TEMP]])
 
 // LLVM: define{{.*}}@_Z5templITnRDaL_Z1sEEvv()
 // LLVM: %[[TEMP:.*]] = alloca %struct.Struct

>From b4d9db65aad402736cb71b7e16e1dc5aabc61c21 Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Mon, 15 Jun 2026 12:54:07 -0700
Subject: [PATCH 5/6] [CIR] Tighten memcpy-equivalent assignment body emission

Address review feedback on the trivial copy/move assignment body emission:
assert that every non-return statement is an expression -- the memberwise
field copies, including the builtin memcpy a struct with array members
generates -- rather than silently dropping unexpected statements, and report
a should-never-happen failure to emit the return with cgm.error instead of
errorNYI.

Consolidate the union codegen test into cxx-special-member-attr.cpp as a
single x86_64 module (dropping split-file) and add a struct-with-array-member
case so the body-shape assert is exercised.
---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp         | 31 +++++++++--------
 .../CIR/CodeGen/cxx-special-member-attr.cpp   | 34 +++++++++++--------
 2 files changed, 36 insertions(+), 29 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index b6f02204a72bd..462b8cad5d3d4 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -901,30 +901,33 @@ void 
CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
   assert(!cir::MissingFeatures::incrementProfileCounter());
   assert(!cir::MissingFeatures::runCleanupsScope());
 
-  // For a memcpy-equivalent special member (a union, or a trivially-copyable
-  // record) the synthesized body either copies nothing -- a union body is just
-  // `return *this` -- or relies on a memcpy the AST does not spell out as
-  // field assignments.  Mirror classic CodeGen's AssignmentMemcpyizer: copy
-  // the whole object once, then fall through to emit the trailing
-  // `return *this`.  Emitting the copy but skipping the return would leave the
-  // result reference uninitialized.
+  // For a memcpy-equivalent assignment operator, copy the whole object, then
+  // fall through to emit the trailing `return *this`.
   if (assignOp->isMemcpyEquivalentSpecialMember(getContext())) {
     CanQualType recordTy =
         getContext().getCanonicalTagType(assignOp->getParent());
     LValue dest = makeNaturalAlignAddrLValue(loadCXXThis(), recordTy);
-    // The source is the trailing reference parameter; load it to get the
-    // referent's address before copying (mirrors the copy-constructor path).
     mlir::Value srcPtr = builder.createLoad(getLoc(assignOp->getLocation()),
                                             getAddrOfLocalVar(args.back()));
     LValue src = makeNaturalAlignAddrLValue(srcPtr, recordTy);
     emitAggregateAssign(dest, src, recordTy);
 
-    for (Stmt *s : rootCS->body())
-      if (isa<ReturnStmt>(s))
+    // The whole-object copy above subsumes the implicit memberwise field
+    // copies -- per-field assignment expressions, or a builtin memcpy call for
+    // array members -- so only the trailing `return *this` still needs to be
+    // emitted.  Those copies are all expressions; assert that nothing else
+    // appears rather than silently dropping an unexpected statement.
+    for (Stmt *s : rootCS->body()) {
+      if (isa<ReturnStmt>(s)) {
         if (emitStmt(s, /*useCurrentScope=*/true).failed())
-          cgm.errorNYI(s->getSourceRange(),
-                       std::string("emitImplicitAssignmentOperatorBody: ") +
-                           s->getStmtClassName());
+          cgm.error(s->getBeginLoc(),
+                    "failed to emit return in implicit assignment operator");
+        continue;
+      }
+      assert(isa<Expr>(s) &&
+             "unexpected statement in memcpy-equivalent assignment operator "
+             "body");
+    }
     return;
   }
 
diff --git a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp 
b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
index 70ef5018b81a7..0d4526fdc347d 100644
--- a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
+++ b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
@@ -1,9 +1,9 @@
-// RUN: split-file %s %t
-
-//--- special_member_attr.cpp
-
-// RUN: %clang_cc1 -std=c++11 -triple aarch64-none-linux-android21 -fclangir 
-emit-cir %t/special_member_attr.cpp -o %t.cir
-// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir 
%t/special_member_attr.cpp
+// RUN: %clang_cc1 -std=c++11 -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 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir 
-emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -emit-llvm %s 
-o %t.ll
+// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
 
 struct Flub {
   int a = 123;
@@ -79,15 +79,6 @@ void non_trivial_func() {
 // CIR-LABEL: cir.func{{.*}} @_ZN3FooD1Ev(
 // CIR-SAME: special_member<#cir.cxx_dtor<!rec_Foo>>
 
-//--- trivial_union_assign.cpp
-
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir 
%t/trivial_union_assign.cpp -o %t-union.cir
-// RUN: FileCheck --check-prefix=CIR --input-file=%t-union.cir 
%t/trivial_union_assign.cpp
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm 
%t/trivial_union_assign.cpp -o %t-union-cir.ll
-// RUN: FileCheck --check-prefix=LLVM --input-file=%t-union-cir.ll 
%t/trivial_union_assign.cpp
-// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -emit-llvm 
%t/trivial_union_assign.cpp -o %t-union.ll
-// RUN: FileCheck --check-prefix=OGCG --input-file=%t-union.ll 
%t/trivial_union_assign.cpp
-
 union YYSTYPE {
   void *yt_casestring;
   void *yt_ID;
@@ -103,6 +94,15 @@ int test_shift(YYSTYPE *yyvsp) {
   return consume(yyvsp[0]);
 }
 
+struct WithArray {
+  int arr[4];
+};
+
+// An array member makes the implicit copy-assignment body a builtin memcpy
+// call rather than per-field assignments; it is still memcpy-equivalent and
+// the whole-object copy subsumes it.
+void array_member_func(WithArray &a, WithArray &b) { a = b; }
+
 // The defaulted union copy-assignment operator copies the whole object in its
 // body -- previously the body was a no-op that LLVM deleted, dropping the
 // store at -O3 -- and the call is kept at the assignment site.
@@ -112,6 +112,10 @@ int test_shift(YYSTYPE *yyvsp) {
 // CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE(
 // CIR: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
 
+// CIR-LABEL: cir.func{{.*}} @_ZN9WithArrayaSERKS_(
+// CIR-SAME: special_member<#cir.cxx_assign<!rec_WithArray, copy, trivial 
true>>
+// CIR: cir.copy {{.*}} : !cir.ptr<!rec_WithArray>
+
 // LLVM: define{{.*}} @_ZN7YYSTYPEaSERKS_(
 // LLVM: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 
false)
 // LLVM-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(

>From fd90c9077df838f1dfa2c4acfa465cd11aa9da2c Mon Sep 17 00:00:00 2001
From: Adam Smith <[email protected]>
Date: Tue, 23 Jun 2026 12:49:35 -0700
Subject: [PATCH 6/6] [CIR] Report NYI for defaulted union copy/move assignment

A defaulted union copy/move assignment operator 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 copy.  CIRGen emitted that empty body and silently
dropped the copy, miscompiling by-value union assignment such as
MultiSource kimwitu++/kc's `*++yyvsp = yylval`.

Report NYI for that case rather than emitting a body that does nothing.
Struct and array memcpy-equivalent assignments are unaffected: their
synthesized bodies carry the implicit memberwise copies (per-field
assignment expressions, or a builtin memcpy call for array members) and
lower correctly through the existing body-emission loop.

Emitting the copy properly means implementing the SemaDeclCXX FIXME so the
synthesized union body carries an AST node for the implied memcpy.  That is
a front-end change that also affects classic CodeGen and will be a separate
PR; this one stops the silent miscompile in the meantime.
---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp         |  38 ++-----
 clang/lib/CIR/CodeGen/CIRGenFunction.h        |   6 --
 .../CIR/CodeGen/cxx-special-member-attr.cpp   | 102 ++++--------------
 .../CIR/CodeGen/trivial-union-assign-nyi.cpp  |  15 +++
 4 files changed, 46 insertions(+), 115 deletions(-)
 create 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 462b8cad5d3d4..97fcc0b78b3c5 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -901,33 +901,17 @@ void 
CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
   assert(!cir::MissingFeatures::incrementProfileCounter());
   assert(!cir::MissingFeatures::runCleanupsScope());
 
-  // For a memcpy-equivalent assignment operator, copy the whole object, then
-  // fall through to emit the trailing `return *this`.
-  if (assignOp->isMemcpyEquivalentSpecialMember(getContext())) {
-    CanQualType recordTy =
-        getContext().getCanonicalTagType(assignOp->getParent());
-    LValue dest = makeNaturalAlignAddrLValue(loadCXXThis(), recordTy);
-    mlir::Value srcPtr = builder.createLoad(getLoc(assignOp->getLocation()),
-                                            getAddrOfLocalVar(args.back()));
-    LValue src = makeNaturalAlignAddrLValue(srcPtr, recordTy);
-    emitAggregateAssign(dest, src, recordTy);
-
-    // The whole-object copy above subsumes the implicit memberwise field
-    // copies -- per-field assignment expressions, or a builtin memcpy call for
-    // array members -- so only the trailing `return *this` still needs to be
-    // emitted.  Those copies are all expressions; assert that nothing else
-    // appears rather than silently dropping an unexpected statement.
-    for (Stmt *s : rootCS->body()) {
-      if (isa<ReturnStmt>(s)) {
-        if (emitStmt(s, /*useCurrentScope=*/true).failed())
-          cgm.error(s->getBeginLoc(),
-                    "failed to emit return in implicit assignment operator");
-        continue;
-      }
-      assert(isa<Expr>(s) &&
-             "unexpected statement in memcpy-equivalent assignment operator "
-             "body");
-    }
+  // 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;
   }
 
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h 
b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index 6053bedba81af..9f2facd12f417 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -1512,12 +1512,6 @@ class CIRGenFunction : public CIRGenTypeCache {
                          AggValueSlot::Overlap_t mayOverlap,
                          bool isVolatile = false);
 
-  /// Emit an aggregate assignment.
-  void emitAggregateAssign(LValue dest, LValue src, QualType eltTy) {
-    emitAggregateCopy(dest, src, eltTy, AggValueSlot::MayOverlap,
-                      hasVolatileMember(eltTy));
-  }
-
   /// Emit code to compute the specified expression which can have any type. 
The
   /// result is returned as an RValue struct. If this is an aggregate
   /// expression, the aggloc/agglocvolatile arguments indicate where the result
diff --git a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp 
b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
index 0d4526fdc347d..bfca59db44fdf 100644
--- a/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
+++ b/clang/test/CIR/CodeGen/cxx-special-member-attr.cpp
@@ -1,9 +1,5 @@
-// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir 
-emit-cir %s -o %t.cir
+// RUN: %clang_cc1 -std=c++11 -triple aarch64-none-linux-android21 -fclangir 
-emit-cir %s -o %t.cir
 // RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
-// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir 
-emit-llvm %s -o %t-cir.ll
-// RUN: FileCheck --check-prefix=LLVM --input-file=%t-cir.ll %s
-// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -emit-llvm %s 
-o %t.ll
-// RUN: FileCheck --check-prefix=OGCG --input-file=%t.ll %s
 
 struct Flub {
   int a = 123;
@@ -34,96 +30,38 @@ struct Foo {
   ~Foo();
 };
 
-// The trivial copy/move assignment operators are emitted at module scope with
-// the special_member attribute, and their bodies perform a whole-object copy.
-// CIR-LABEL: cir.func{{.*}} @_ZN4FlubaSERKS_(
-// CIR-SAME: special_member<#cir.cxx_assign<!rec_Flub, copy, trivial true>>
-// CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
-// CIR-LABEL: cir.func{{.*}} @_ZN4FlubaSEOS_(
-// CIR-SAME: special_member<#cir.cxx_assign<!rec_Flub, move, trivial true>>
-// CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+// Trivial copy/move assignment operator definitions appear at module level.
+// CIR: @_ZN4FlubaSERKS_(%arg0: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Flub>{{.*}}) special_member<#cir.cxx_assign<!rec_Flub, copy, 
trivial true>>
+// CIR: @_ZN4FlubaSEOS_(%arg0: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Flub> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Flub>{{.*}}) special_member<#cir.cxx_assign<!rec_Flub, move, 
trivial true>>
 
 void trivial_func() {
   Flub f1{};
 
   Flub f2 = f1;
-  Flub f3 = static_cast<Flub &&>(f1);
+  // Trivial copy/move constructors are inlined as cir.copy
+  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
+
+  Flub f3 = static_cast<Flub&&>(f1);
+  // CIR: cir.copy {{.*}} : !cir.ptr<!rec_Flub>
 
   f2 = f1;
-  f1 = static_cast<Flub &&>(f3);
+  f1 = static_cast<Flub&&>(f3);
 }
 
-// Trivial assignment keeps the operator= call; its body (above) does the copy.
-// CIR-LABEL: cir.func{{.*}} @_Z12trivial_funcv(
-// CIR: cir.call{{.*}}@_ZN4FlubaSERKS_
-// CIR: cir.call{{.*}}@_ZN4FlubaSEOS_
-
 void non_trivial_func() {
   Foo f1{};
-  Foo f2 = f1;
-  Foo f3 = static_cast<Foo &&>(f1);
-  f2 = f1;
-  f1 = static_cast<Foo &&>(f3);
-}
+  // CIR: @_ZN3FooC2Ev(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) 
special_member<#cir.cxx_ctor<!rec_Foo, default>>
 
-// CIR-LABEL: cir.func{{.*}} @_ZN3FooC2Ev(
-// CIR-SAME: special_member<#cir.cxx_ctor<!rec_Foo, default>>
-// CIR-LABEL: cir.func{{.*}} @_ZN3FooC2ERKS_(
-// CIR-SAME: special_member<#cir.cxx_ctor<!rec_Foo, copy>>
-// CIR-LABEL: cir.func{{.*}} @_ZN3FooC2EOS_(
-// CIR-SAME: special_member<#cir.cxx_ctor<!rec_Foo, move>>
-// CIR-LABEL: cir.func{{.*}} @_ZN3FooaSERKS_(
-// CIR-SAME: special_member<#cir.cxx_assign<!rec_Foo, copy>>
-// CIR-LABEL: cir.func{{.*}} @_ZN3FooaSEOS_(
-// CIR-SAME: special_member<#cir.cxx_assign<!rec_Foo, move>>
-// CIR-LABEL: cir.func{{.*}} @_ZN3FooD1Ev(
-// CIR-SAME: special_member<#cir.cxx_dtor<!rec_Foo>>
-
-union YYSTYPE {
-  void *yt_casestring;
-  void *yt_ID;
-};
+  Foo f2 = f1;
+  // CIR: @_ZN3FooC2ERKS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) 
special_member<#cir.cxx_ctor<!rec_Foo, copy>>
 
-extern YYSTYPE yylval;
+  Foo f3 = static_cast<Foo&&>(f1);
+  // CIR: @_ZN3FooC2EOS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) 
special_member<#cir.cxx_ctor<!rec_Foo, move>>
 
-static int consume(YYSTYPE v) { return v.yt_casestring != nullptr; }
+  f2 = f1;
+  // CIR: @_ZN3FooaSERKS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Foo>{{.*}}) special_member<#cir.cxx_assign<!rec_Foo, copy>>
 
-int test_shift(YYSTYPE *yyvsp) {
-  yylval.yt_casestring = reinterpret_cast<void *>(0x42);
-  *++yyvsp = yylval;
-  return consume(yyvsp[0]);
+  f1 = static_cast<Foo&&>(f3);
+  // CIR: @_ZN3FooaSEOS_(%arg0: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} 
loc({{.*}}), %arg1: !cir.ptr<!rec_Foo> {{[{][^}]*[}]}} loc({{.*}})) -> 
(!cir.ptr<!rec_Foo>{{.*}}) special_member<#cir.cxx_assign<!rec_Foo, move>>
+  // CIR: @_ZN3FooD1Ev(!cir.ptr<!rec_Foo> {{.*}}) 
special_member<#cir.cxx_dtor<!rec_Foo>>
 }
-
-struct WithArray {
-  int arr[4];
-};
-
-// An array member makes the implicit copy-assignment body a builtin memcpy
-// call rather than per-field assignments; it is still memcpy-equivalent and
-// the whole-object copy subsumes it.
-void array_member_func(WithArray &a, WithArray &b) { a = b; }
-
-// The defaulted union copy-assignment operator copies the whole object in its
-// body -- previously the body was a no-op that LLVM deleted, dropping the
-// store at -O3 -- and the call is kept at the assignment site.
-// CIR-LABEL: cir.func{{.*}} @_ZN7YYSTYPEaSERKS_(
-// CIR-SAME: special_member<#cir.cxx_assign<!rec_YYSTYPE, copy, trivial true>>
-// CIR: cir.copy {{.*}} : !cir.ptr<!rec_YYSTYPE>
-// CIR-LABEL: cir.func{{.*}} @_Z10test_shiftP7YYSTYPE(
-// CIR: cir.call{{.*}}@_ZN7YYSTYPEaSERKS_
-
-// CIR-LABEL: cir.func{{.*}} @_ZN9WithArrayaSERKS_(
-// CIR-SAME: special_member<#cir.cxx_assign<!rec_WithArray, copy, trivial 
true>>
-// CIR: cir.copy {{.*}} : !cir.ptr<!rec_WithArray>
-
-// LLVM: define{{.*}} @_ZN7YYSTYPEaSERKS_(
-// LLVM: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}, ptr {{.*}}, i64 8, i1 
false)
-// LLVM-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
-// LLVM: call{{.*}}@_ZN7YYSTYPEaSERKS_
-
-// Classic CodeGen inlines the trivial union assignment at the call site and
-// emits no operator= function; the store is performed directly.
-// OGCG-LABEL: define{{.*}} @_Z10test_shiftP7YYSTYPE(
-// OGCG: store ptr inttoptr (i64 66 to ptr), ptr @yylval
-// OGCG: call void @llvm.memcpy.p0.p0.i64(ptr {{.*}}@yylval, i64 8, i1 false)
-// OGCG-NOT: @_ZN7YYSTYPEaSERKS_
diff --git a/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp 
b/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
new file mode 100644
index 0000000000000..e457dca4fd23d
--- /dev/null
+++ b/clang/test/CIR/CodeGen/trivial-union-assign-nyi.cpp
@@ -0,0 +1,15 @@
+// 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