https://github.com/Fznamznon created 
https://github.com/llvm/llvm-project/pull/236803

This adds a special class to to replace member initializers with memcpy when 
emitting assignment operator body. This is not entirely a cherry-pick but 
rather a port from the incubator because the current state of that class in the 
incubator is not buildable with LLVM main branch. This PR also ports relevant 
test cases from the incubator that were in 
clang/test/CIR/CodeGen/assign-operator.cpp in incubator's tree. However current 
LLVM main branch also has a test file with the same name but different test 
cases that are unaffected by AssignmentMemcpyizer, so I added ported cases to 
the new file assign-operator-memcpy.cpp . An additional test case `HasPadding` 
that originally motivated this work and that doesn't come from the incubator 
has been added to the new test file as well. There is no functional changes to 
the class, so a few TODOs are still present.

The relevant (non-refactoring) commits from the incubator that implement the 
class are:

https://github.com/llvm/clangir/commit/6cb05dcde4080a1bbe4c22e79015874e178f68fc 
, 
https://github.com/llvm/clangir/commit/b3dc28998f8f87ced528d4982bfe827fc9fd8dcc 
, 
https://github.com/llvm/clangir/commit/58b4687b64913be3762f5f0504634e0a75a87e61 

I used claude to port codestyle , port test cases and generate test checks.

>From 3e885303010148ac74bfee16ac7f1fa1d15bad13 Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 07:25:42 -0500
Subject: [PATCH 1/9] Bring FieldMemcpyizer

---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp | 162 ++++++++++++++++++++++++++
 1 file changed, 162 insertions(+)

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 738f5464672435..b931be6365f3c8 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -24,6 +24,168 @@
 using namespace clang;
 using namespace clang::CIRGen;
 
+namespace {
+/// TODO(cir): a lot of what we see under this namespace is a strong candidate
+/// to be shared between LLVM and CIR codegen.
+
+/// RAII object to indicate that codegen is copying the value representation
+/// instead of the object representation. Useful when copying a struct or
+/// class which has uninitialized members and we're only performing
+/// lvalue-to-rvalue conversion on the object but not its members.
+class CopyingValueRepresentation {
+public:
+  explicit CopyingValueRepresentation(CIRGenFunction &CGF)
+      : CGF(CGF), OldSanOpts(CGF.sanOpts) {
+    CGF.sanOpts.set(SanitizerKind::Bool, false);
+    CGF.sanOpts.set(SanitizerKind::Enum, false);
+  }
+  ~CopyingValueRepresentation() { CGF.sanOpts = OldSanOpts; }
+
+private:
+  CIRGenFunction &CGF;
+  SanitizerSet OldSanOpts;
+};
+
+class FieldMemcpyizer {
+public:
+  FieldMemcpyizer(CIRGenFunction &CGF, const CXXMethodDecl *MethodDecl,
+                  const VarDecl *SrcRec)
+      : CGF(CGF), MethodDecl(MethodDecl), ClassDecl(MethodDecl->getParent()),
+        SrcRec(SrcRec),
+        RecLayout(CGF.getContext().getASTRecordLayout(ClassDecl)),
+        FirstField(nullptr), LastField(nullptr), FirstFieldOffset(0),
+        LastFieldOffset(0), LastAddedFieldIndex(0) {}
+
+  bool isMemcpyableField(FieldDecl *F) const {
+    // Never memcpy fields when we are adding poised paddings.
+    if (CGF.getContext().getLangOpts().SanitizeAddressFieldPadding)
+      return false;
+    Qualifiers Qual = F->getType().getQualifiers();
+    if (Qual.hasVolatile() || Qual.hasObjCLifetime())
+      return false;
+    return true;
+  }
+
+  void addMemcpyableField(FieldDecl *F) {
+    //assert(!cir::MissingFeatures::isEmptyFieldForLayout());
+    if (F->isZeroSize(CGF.getContext()))
+      return;
+    if (!FirstField)
+      addInitialField(F);
+    else
+      addNextField(F);
+  }
+
+  CharUnits getMemcpySize(uint64_t FirstByteOffset) const {
+    ASTContext &astContext = CGF.getContext();
+    unsigned LastFieldSize =
+        LastField->isBitField()
+            ? LastField->getBitWidthValue()
+            : astContext.toBits(
+                  astContext.getTypeInfoDataSizeInChars(LastField->getType())
+                      .Width);
+    uint64_t MemcpySizeBits = LastFieldOffset + LastFieldSize -
+                              FirstByteOffset + astContext.getCharWidth() - 1;
+    CharUnits MemcpySize = astContext.toCharUnitsFromBits(MemcpySizeBits);
+    return MemcpySize;
+  }
+
+  void emitMemcpy() {
+    // Give the subclass a chance to bail out if it feels the memcpy isn't 
worth
+    // it (e.g. Hasn't aggregated enough data).
+    if (!FirstField) {
+      return;
+    }
+
+    uint64_t firstByteOffset;
+    if (FirstField->isBitField()) {
+      const CIRGenRecordLayout &rl =
+          CGF.getTypes().getCIRGenRecordLayout(FirstField->getParent());
+      const CIRGenBitFieldInfo &bfInfo = rl.getBitFieldInfo(FirstField);
+      // FirstFieldOffset is not appropriate for bitfields,
+      // we need to use the storage offset instead.
+      firstByteOffset = CGF.getContext().toBits(bfInfo.storageOffset);
+    } else {
+      firstByteOffset = FirstFieldOffset;
+    }
+
+    CharUnits memcpySize = getMemcpySize(firstByteOffset);
+    QualType recordTy = CGF.getContext().getTagType(
+        ElaboratedTypeKeyword::None, /*Qualifier=*/std::nullopt, ClassDecl,
+        /*OwnsTag=*/false);
+    Address thisPtr = CGF.loadCXXThisAddress();
+    LValue destLv = CGF.makeAddrLValue(thisPtr, recordTy);
+    LValue dest = CGF.emitLValueForFieldInitialization(destLv, FirstField,
+                                                       FirstField->getName());
+    cir::LoadOp srcPtr = CGF.getBuilder().createLoad(
+        CGF.getLoc(MethodDecl->getLocation()), CGF.getAddrOfLocalVar(SrcRec));
+    LValue srcLv = CGF.makeNaturalAlignAddrLValue(srcPtr, recordTy);
+    LValue src = CGF.emitLValueForFieldInitialization(srcLv, FirstField,
+                                                      FirstField->getName());
+
+    emitMemcpyIR(dest.isBitField() ? dest.getBitFieldAddress()
+                                   : dest.getAddress(),
+                 src.isBitField() ? src.getBitFieldAddress() : 
src.getAddress(),
+                 memcpySize);
+    reset();
+  }
+
+  void reset() { FirstField = nullptr; }
+
+protected:
+  CIRGenFunction &CGF;
+  const CXXMethodDecl *MethodDecl;
+  const CXXRecordDecl *ClassDecl;
+
+private:
+  void emitMemcpyIR(Address DestPtr, Address SrcPtr, CharUnits Size) {
+    mlir::Location loc = CGF.getLoc(MethodDecl->getLocation());
+    CIRGenBuilderTy &builder = CGF.getBuilder();
+    mlir::Value sizeOp =
+        builder.getConstInt(loc, CGF.sizeTy, Size.getQuantity());
+    builder.createMemCpy(loc, DestPtr.withElementType(builder, CGF.voidTy),
+                         SrcPtr.withElementType(builder, CGF.voidTy), sizeOp);
+  }
+
+  void addInitialField(FieldDecl *F) {
+    FirstField = F;
+    LastField = F;
+    FirstFieldOffset = RecLayout.getFieldOffset(F->getFieldIndex());
+    LastFieldOffset = FirstFieldOffset;
+    LastAddedFieldIndex = F->getFieldIndex();
+  }
+
+  void addNextField(FieldDecl *F) {
+    // For the most part, the following invariant will hold:
+    //   F->getFieldIndex() == LastAddedFieldIndex + 1
+    // The one exception is that Sema won't add a copy-initializer for an
+    // unnamed bitfield, which will show up here as a gap in the sequence.
+    assert(F->getFieldIndex() >= LastAddedFieldIndex + 1 &&
+           "Cannot aggregate fields out of order.");
+    LastAddedFieldIndex = F->getFieldIndex();
+
+    // The 'first' and 'last' fields are chosen by offset, rather than field
+    // index. This allows the code to support bitfields, as well as regular
+    // fields.
+    uint64_t FOffset = RecLayout.getFieldOffset(F->getFieldIndex());
+    if (FOffset < FirstFieldOffset) {
+      FirstField = F;
+      FirstFieldOffset = FOffset;
+    } else if (FOffset >= LastFieldOffset) {
+      LastField = F;
+      LastFieldOffset = FOffset;
+    }
+  }
+
+  const VarDecl *SrcRec;
+  const ASTRecordLayout &RecLayout;
+  FieldDecl *FirstField;
+  FieldDecl *LastField;
+  uint64_t FirstFieldOffset, LastFieldOffset;
+  unsigned LastAddedFieldIndex;
+};
+}
+
 /// Return the smallest possible amount of storage that might be allocated
 /// starting from the beginning of an object of a particular class.
 ///

>From 7f37051252d91b6185a0465f59b5ed6f0338ee79 Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 07:28:07 -0500
Subject: [PATCH 2/9] Bring assign memcpyizer

---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp | 107 +++++++++++++++++++++++++-
 1 file changed, 105 insertions(+), 2 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index b931be6365f3c8..2c8517815462ab 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -67,7 +67,7 @@ class FieldMemcpyizer {
   }
 
   void addMemcpyableField(FieldDecl *F) {
-    //assert(!cir::MissingFeatures::isEmptyFieldForLayout());
+    // assert(!cir::MissingFeatures::isEmptyFieldForLayout());
     if (F->isZeroSize(CGF.getContext()))
       return;
     if (!FirstField)
@@ -184,7 +184,110 @@ class FieldMemcpyizer {
   uint64_t FirstFieldOffset, LastFieldOffset;
   unsigned LastAddedFieldIndex;
 };
-}
+
+class AssignmentMemcpyizer : public FieldMemcpyizer {
+private:
+  // Returns the memcpyable field copied by the given statement, if one
+  // exists. Otherwise returns null.
+  FieldDecl *getMemcpyableField(Stmt *S) {
+    if (!AssignmentsMemcpyable)
+      return nullptr;
+    if (BinaryOperator *BO = dyn_cast<BinaryOperator>(S)) {
+      // Recognise trivial assignments.
+      if (BO->getOpcode() != BO_Assign)
+        return nullptr;
+      MemberExpr *ME = dyn_cast<MemberExpr>(BO->getLHS());
+      if (!ME)
+        return nullptr;
+      FieldDecl *Field = dyn_cast<FieldDecl>(ME->getMemberDecl());
+      if (!Field || !isMemcpyableField(Field))
+        return nullptr;
+      Stmt *RHS = BO->getRHS();
+      if (ImplicitCastExpr *EC = dyn_cast<ImplicitCastExpr>(RHS))
+        RHS = EC->getSubExpr();
+      if (!RHS)
+        return nullptr;
+      if (MemberExpr *ME2 = dyn_cast<MemberExpr>(RHS)) {
+        if (ME2->getMemberDecl() == Field)
+          return Field;
+      }
+      return nullptr;
+    } else if (isa<CXXMemberCallExpr>(S)) {
+      // We want to represent all calls explicitly for analysis purposes.
+      return nullptr;
+    } else if (CallExpr *CE = dyn_cast<CallExpr>(S)) {
+      // TODO(cir): https://github.com/llvm/clangir/issues/1177: This can 
result
+      // in memcpys instead of calls to trivial member functions.
+      FunctionDecl *FD = dyn_cast<FunctionDecl>(CE->getCalleeDecl());
+      if (!FD || FD->getBuiltinID() != Builtin::BI__builtin_memcpy)
+        return nullptr;
+      Expr *DstPtr = CE->getArg(0);
+      if (ImplicitCastExpr *DC = dyn_cast<ImplicitCastExpr>(DstPtr))
+        DstPtr = DC->getSubExpr();
+      UnaryOperator *DUO = dyn_cast<UnaryOperator>(DstPtr);
+      if (!DUO || DUO->getOpcode() != UO_AddrOf)
+        return nullptr;
+      MemberExpr *ME = dyn_cast<MemberExpr>(DUO->getSubExpr());
+      if (!ME)
+        return nullptr;
+      FieldDecl *Field = dyn_cast<FieldDecl>(ME->getMemberDecl());
+      if (!Field || !isMemcpyableField(Field))
+        return nullptr;
+      Expr *SrcPtr = CE->getArg(1);
+      if (ImplicitCastExpr *SC = dyn_cast<ImplicitCastExpr>(SrcPtr))
+        SrcPtr = SC->getSubExpr();
+      UnaryOperator *SUO = dyn_cast<UnaryOperator>(SrcPtr);
+      if (!SUO || SUO->getOpcode() != UO_AddrOf)
+        return nullptr;
+      MemberExpr *ME2 = dyn_cast<MemberExpr>(SUO->getSubExpr());
+      if (!ME2 || Field != dyn_cast<FieldDecl>(ME2->getMemberDecl()))
+        return nullptr;
+      return Field;
+    }
+
+    return nullptr;
+  }
+
+  bool AssignmentsMemcpyable;
+  SmallVector<Stmt *, 16> AggregatedStmts;
+
+public:
+  AssignmentMemcpyizer(CIRGenFunction &CGF, const CXXMethodDecl *AD,
+                       FunctionArgList &Args)
+      : FieldMemcpyizer(CGF, AD, Args[Args.size() - 1]),
+        AssignmentsMemcpyable(CGF.getLangOpts().getGC() == LangOptions::NonGC) 
{
+    assert(Args.size() == 2);
+  }
+
+  void emitAssignment(Stmt *S) {
+    FieldDecl *F = getMemcpyableField(S);
+    if (F) {
+      addMemcpyableField(F);
+      AggregatedStmts.push_back(S);
+    } else {
+      emitAggregatedStmts();
+      if (CGF.emitStmt(S, /*useCurrentScope=*/true).failed())
+        llvm_unreachable("Should not get here!");
+    }
+  }
+
+  void emitAggregatedStmts() {
+    if (AggregatedStmts.size() <= 1) {
+      if (!AggregatedStmts.empty()) {
+        CopyingValueRepresentation CVR(CGF);
+        if (CGF.emitStmt(AggregatedStmts[0], 
/*useCurrentScope=*/true).failed())
+          llvm_unreachable("Should not get here!");
+      }
+      reset();
+    }
+
+    emitMemcpy();
+    AggregatedStmts.clear();
+  }
+
+  void finish() { emitAggregatedStmts(); }
+};
+} // namespace
 
 /// Return the smallest possible amount of storage that might be allocated
 /// starting from the beginning of an object of a particular class.

>From 112b628791a7a469064a11f2cadbd90cd117f817 Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 07:44:23 -0500
Subject: [PATCH 3/9] Align codestyle, claude-assisted

---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp | 293 +++++++++++++-------------
 1 file changed, 147 insertions(+), 146 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index 2c8517815462ab..ce985cd77665fa 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -25,8 +25,8 @@ using namespace clang;
 using namespace clang::CIRGen;
 
 namespace {
-/// TODO(cir): a lot of what we see under this namespace is a strong candidate
-/// to be shared between LLVM and CIR codegen.
+// TODO(cir): a lot of what we see under this namespace is a strong candidate
+// to be shared between LLVM and CIR codegen.
 
 /// RAII object to indicate that codegen is copying the value representation
 /// instead of the object representation. Useful when copying a struct or
@@ -34,94 +34,93 @@ namespace {
 /// lvalue-to-rvalue conversion on the object but not its members.
 class CopyingValueRepresentation {
 public:
-  explicit CopyingValueRepresentation(CIRGenFunction &CGF)
-      : CGF(CGF), OldSanOpts(CGF.sanOpts) {
-    CGF.sanOpts.set(SanitizerKind::Bool, false);
-    CGF.sanOpts.set(SanitizerKind::Enum, false);
+  explicit CopyingValueRepresentation(CIRGenFunction &cgf)
+      : cgf(cgf), oldSanOpts(cgf.sanOpts) {
+    cgf.sanOpts.set(SanitizerKind::Bool, false);
+    cgf.sanOpts.set(SanitizerKind::Enum, false);
   }
-  ~CopyingValueRepresentation() { CGF.sanOpts = OldSanOpts; }
+  ~CopyingValueRepresentation() { cgf.sanOpts = oldSanOpts; }
 
 private:
-  CIRGenFunction &CGF;
-  SanitizerSet OldSanOpts;
+  CIRGenFunction &cgf;
+  SanitizerSet oldSanOpts;
 };
 
 class FieldMemcpyizer {
 public:
-  FieldMemcpyizer(CIRGenFunction &CGF, const CXXMethodDecl *MethodDecl,
-                  const VarDecl *SrcRec)
-      : CGF(CGF), MethodDecl(MethodDecl), ClassDecl(MethodDecl->getParent()),
-        SrcRec(SrcRec),
-        RecLayout(CGF.getContext().getASTRecordLayout(ClassDecl)),
-        FirstField(nullptr), LastField(nullptr), FirstFieldOffset(0),
-        LastFieldOffset(0), LastAddedFieldIndex(0) {}
-
-  bool isMemcpyableField(FieldDecl *F) const {
+  FieldMemcpyizer(CIRGenFunction &cgf, const CXXMethodDecl *methodDecl,
+                  const VarDecl *srcRec)
+      : cgf(cgf), methodDecl(methodDecl), classDecl(methodDecl->getParent()),
+        srcRec(srcRec),
+        recLayout(cgf.getContext().getASTRecordLayout(classDecl)),
+        firstField(nullptr), lastField(nullptr), firstFieldOffset(0),
+        lastFieldOffset(0), lastAddedFieldIndex(0) {}
+
+  bool isMemcpyableField(FieldDecl *f) const {
     // Never memcpy fields when we are adding poised paddings.
-    if (CGF.getContext().getLangOpts().SanitizeAddressFieldPadding)
+    if (cgf.getContext().getLangOpts().SanitizeAddressFieldPadding)
       return false;
-    Qualifiers Qual = F->getType().getQualifiers();
-    if (Qual.hasVolatile() || Qual.hasObjCLifetime())
+    Qualifiers qual = f->getType().getQualifiers();
+    if (qual.hasVolatile() || qual.hasObjCLifetime())
       return false;
     return true;
   }
 
-  void addMemcpyableField(FieldDecl *F) {
-    // assert(!cir::MissingFeatures::isEmptyFieldForLayout());
-    if (F->isZeroSize(CGF.getContext()))
+  void addMemcpyableField(FieldDecl *f) {
+    // TODO(cir): isEmptyFieldForLayout is not handled here yet.
+    // Incubator has assert(!cir::MissingFeatures::isEmptyFieldForLayout());
+    if (f->isZeroSize(cgf.getContext()))
       return;
-    if (!FirstField)
-      addInitialField(F);
+    if (!firstField)
+      addInitialField(f);
     else
-      addNextField(F);
+      addNextField(f);
   }
 
-  CharUnits getMemcpySize(uint64_t FirstByteOffset) const {
-    ASTContext &astContext = CGF.getContext();
-    unsigned LastFieldSize =
-        LastField->isBitField()
-            ? LastField->getBitWidthValue()
+  CharUnits getMemcpySize(uint64_t firstByteOffset) const {
+    ASTContext &astContext = cgf.getContext();
+    unsigned lastFieldSize =
+        lastField->isBitField()
+            ? lastField->getBitWidthValue()
             : astContext.toBits(
-                  astContext.getTypeInfoDataSizeInChars(LastField->getType())
+                  astContext.getTypeInfoDataSizeInChars(lastField->getType())
                       .Width);
-    uint64_t MemcpySizeBits = LastFieldOffset + LastFieldSize -
-                              FirstByteOffset + astContext.getCharWidth() - 1;
-    CharUnits MemcpySize = astContext.toCharUnitsFromBits(MemcpySizeBits);
-    return MemcpySize;
+    uint64_t memcpySizeBits = lastFieldOffset + lastFieldSize -
+                              firstByteOffset + astContext.getCharWidth() - 1;
+    return astContext.toCharUnitsFromBits(memcpySizeBits);
   }
 
   void emitMemcpy() {
     // Give the subclass a chance to bail out if it feels the memcpy isn't 
worth
     // it (e.g. Hasn't aggregated enough data).
-    if (!FirstField) {
+    if (!firstField)
       return;
-    }
 
     uint64_t firstByteOffset;
-    if (FirstField->isBitField()) {
+    if (firstField->isBitField()) {
       const CIRGenRecordLayout &rl =
-          CGF.getTypes().getCIRGenRecordLayout(FirstField->getParent());
-      const CIRGenBitFieldInfo &bfInfo = rl.getBitFieldInfo(FirstField);
-      // FirstFieldOffset is not appropriate for bitfields,
+          cgf.getTypes().getCIRGenRecordLayout(firstField->getParent());
+      const CIRGenBitFieldInfo &bfInfo = rl.getBitFieldInfo(firstField);
+      // firstFieldOffset is not appropriate for bitfields,
       // we need to use the storage offset instead.
-      firstByteOffset = CGF.getContext().toBits(bfInfo.storageOffset);
+      firstByteOffset = cgf.getContext().toBits(bfInfo.storageOffset);
     } else {
-      firstByteOffset = FirstFieldOffset;
+      firstByteOffset = firstFieldOffset;
     }
 
     CharUnits memcpySize = getMemcpySize(firstByteOffset);
-    QualType recordTy = CGF.getContext().getTagType(
-        ElaboratedTypeKeyword::None, /*Qualifier=*/std::nullopt, ClassDecl,
+    QualType recordTy = cgf.getContext().getTagType(
+        ElaboratedTypeKeyword::None, /*Qualifier=*/std::nullopt, classDecl,
         /*OwnsTag=*/false);
-    Address thisPtr = CGF.loadCXXThisAddress();
-    LValue destLv = CGF.makeAddrLValue(thisPtr, recordTy);
-    LValue dest = CGF.emitLValueForFieldInitialization(destLv, FirstField,
-                                                       FirstField->getName());
-    cir::LoadOp srcPtr = CGF.getBuilder().createLoad(
-        CGF.getLoc(MethodDecl->getLocation()), CGF.getAddrOfLocalVar(SrcRec));
-    LValue srcLv = CGF.makeNaturalAlignAddrLValue(srcPtr, recordTy);
-    LValue src = CGF.emitLValueForFieldInitialization(srcLv, FirstField,
-                                                      FirstField->getName());
+    Address thisPtr = cgf.loadCXXThisAddress();
+    LValue destLv = cgf.makeAddrLValue(thisPtr, recordTy);
+    LValue dest = cgf.emitLValueForFieldInitialization(destLv, firstField,
+                                                       firstField->getName());
+    cir::LoadOp srcPtr = cgf.getBuilder().createLoad(
+        cgf.getLoc(methodDecl->getLocation()), cgf.getAddrOfLocalVar(srcRec));
+    LValue srcLv = cgf.makeNaturalAlignAddrLValue(srcPtr, recordTy);
+    LValue src = cgf.emitLValueForFieldInitialization(srcLv, firstField,
+                                                      firstField->getName());
 
     emitMemcpyIR(dest.isBitField() ? dest.getBitFieldAddress()
                                    : dest.getAddress(),
@@ -130,159 +129,161 @@ class FieldMemcpyizer {
     reset();
   }
 
-  void reset() { FirstField = nullptr; }
+  void reset() { firstField = nullptr; }
 
 protected:
-  CIRGenFunction &CGF;
-  const CXXMethodDecl *MethodDecl;
-  const CXXRecordDecl *ClassDecl;
+  CIRGenFunction &cgf;
+  const CXXMethodDecl *methodDecl;
+  const CXXRecordDecl *classDecl;
 
 private:
-  void emitMemcpyIR(Address DestPtr, Address SrcPtr, CharUnits Size) {
-    mlir::Location loc = CGF.getLoc(MethodDecl->getLocation());
-    CIRGenBuilderTy &builder = CGF.getBuilder();
+  void emitMemcpyIR(Address destPtr, Address srcPtr, CharUnits size) {
+    mlir::Location loc = cgf.getLoc(methodDecl->getLocation());
+    CIRGenBuilderTy &builder = cgf.getBuilder();
     mlir::Value sizeOp =
-        builder.getConstInt(loc, CGF.sizeTy, Size.getQuantity());
-    builder.createMemCpy(loc, DestPtr.withElementType(builder, CGF.voidTy),
-                         SrcPtr.withElementType(builder, CGF.voidTy), sizeOp);
+        builder.getConstInt(loc, cgf.sizeTy, size.getQuantity());
+    builder.createMemCpy(loc, destPtr.withElementType(builder, cgf.voidTy),
+                         srcPtr.withElementType(builder, cgf.voidTy), sizeOp);
   }
 
-  void addInitialField(FieldDecl *F) {
-    FirstField = F;
-    LastField = F;
-    FirstFieldOffset = RecLayout.getFieldOffset(F->getFieldIndex());
-    LastFieldOffset = FirstFieldOffset;
-    LastAddedFieldIndex = F->getFieldIndex();
+  void addInitialField(FieldDecl *f) {
+    firstField = f;
+    lastField = f;
+    firstFieldOffset = recLayout.getFieldOffset(f->getFieldIndex());
+    lastFieldOffset = firstFieldOffset;
+    lastAddedFieldIndex = f->getFieldIndex();
   }
 
-  void addNextField(FieldDecl *F) {
+  void addNextField(FieldDecl *f) {
     // For the most part, the following invariant will hold:
-    //   F->getFieldIndex() == LastAddedFieldIndex + 1
+    //   f->getFieldIndex() == lastAddedFieldIndex + 1
     // The one exception is that Sema won't add a copy-initializer for an
     // unnamed bitfield, which will show up here as a gap in the sequence.
-    assert(F->getFieldIndex() >= LastAddedFieldIndex + 1 &&
+    assert(f->getFieldIndex() >= lastAddedFieldIndex + 1 &&
            "Cannot aggregate fields out of order.");
-    LastAddedFieldIndex = F->getFieldIndex();
+    lastAddedFieldIndex = f->getFieldIndex();
 
     // The 'first' and 'last' fields are chosen by offset, rather than field
     // index. This allows the code to support bitfields, as well as regular
     // fields.
-    uint64_t FOffset = RecLayout.getFieldOffset(F->getFieldIndex());
-    if (FOffset < FirstFieldOffset) {
-      FirstField = F;
-      FirstFieldOffset = FOffset;
-    } else if (FOffset >= LastFieldOffset) {
-      LastField = F;
-      LastFieldOffset = FOffset;
+    uint64_t fOffset = recLayout.getFieldOffset(f->getFieldIndex());
+    if (fOffset < firstFieldOffset) {
+      firstField = f;
+      firstFieldOffset = fOffset;
+    } else if (fOffset >= lastFieldOffset) {
+      lastField = f;
+      lastFieldOffset = fOffset;
     }
   }
 
-  const VarDecl *SrcRec;
-  const ASTRecordLayout &RecLayout;
-  FieldDecl *FirstField;
-  FieldDecl *LastField;
-  uint64_t FirstFieldOffset, LastFieldOffset;
-  unsigned LastAddedFieldIndex;
+  const VarDecl *srcRec;
+  const ASTRecordLayout &recLayout;
+  FieldDecl *firstField;
+  FieldDecl *lastField;
+  uint64_t firstFieldOffset, lastFieldOffset;
+  unsigned lastAddedFieldIndex;
 };
 
 class AssignmentMemcpyizer : public FieldMemcpyizer {
-private:
   // Returns the memcpyable field copied by the given statement, if one
   // exists. Otherwise returns null.
-  FieldDecl *getMemcpyableField(Stmt *S) {
-    if (!AssignmentsMemcpyable)
+  FieldDecl *getMemcpyableField(Stmt *s) {
+    if (!assignmentsMemcpyable)
       return nullptr;
-    if (BinaryOperator *BO = dyn_cast<BinaryOperator>(S)) {
+    if (auto *bo = dyn_cast<BinaryOperator>(s)) {
       // Recognise trivial assignments.
-      if (BO->getOpcode() != BO_Assign)
+      if (bo->getOpcode() != BO_Assign)
         return nullptr;
-      MemberExpr *ME = dyn_cast<MemberExpr>(BO->getLHS());
-      if (!ME)
+      auto *me = dyn_cast<MemberExpr>(bo->getLHS());
+      if (!me)
         return nullptr;
-      FieldDecl *Field = dyn_cast<FieldDecl>(ME->getMemberDecl());
-      if (!Field || !isMemcpyableField(Field))
+      auto *field = dyn_cast<FieldDecl>(me->getMemberDecl());
+      if (!field || !isMemcpyableField(field))
         return nullptr;
-      Stmt *RHS = BO->getRHS();
-      if (ImplicitCastExpr *EC = dyn_cast<ImplicitCastExpr>(RHS))
-        RHS = EC->getSubExpr();
-      if (!RHS)
+      Stmt *rhs = bo->getRHS();
+      if (auto *ec = dyn_cast<ImplicitCastExpr>(rhs))
+        rhs = ec->getSubExpr();
+      if (!rhs)
         return nullptr;
-      if (MemberExpr *ME2 = dyn_cast<MemberExpr>(RHS)) {
-        if (ME2->getMemberDecl() == Field)
-          return Field;
+      if (auto *me2 = dyn_cast<MemberExpr>(rhs)) {
+        if (me2->getMemberDecl() == field)
+          return field;
       }
       return nullptr;
-    } else if (isa<CXXMemberCallExpr>(S)) {
+    } else if (isa<CXXMemberCallExpr>(s)) {
       // We want to represent all calls explicitly for analysis purposes.
       return nullptr;
-    } else if (CallExpr *CE = dyn_cast<CallExpr>(S)) {
+    } else if (auto *ce = dyn_cast<CallExpr>(s)) {
       // TODO(cir): https://github.com/llvm/clangir/issues/1177: This can 
result
       // in memcpys instead of calls to trivial member functions.
-      FunctionDecl *FD = dyn_cast<FunctionDecl>(CE->getCalleeDecl());
-      if (!FD || FD->getBuiltinID() != Builtin::BI__builtin_memcpy)
+      auto *fd = dyn_cast<FunctionDecl>(ce->getCalleeDecl());
+      if (!fd || fd->getBuiltinID() != Builtin::BI__builtin_memcpy)
         return nullptr;
-      Expr *DstPtr = CE->getArg(0);
-      if (ImplicitCastExpr *DC = dyn_cast<ImplicitCastExpr>(DstPtr))
-        DstPtr = DC->getSubExpr();
-      UnaryOperator *DUO = dyn_cast<UnaryOperator>(DstPtr);
-      if (!DUO || DUO->getOpcode() != UO_AddrOf)
+      Expr *dstPtr = ce->getArg(0);
+      if (auto *dc = dyn_cast<ImplicitCastExpr>(dstPtr))
+        dstPtr = dc->getSubExpr();
+      auto *duo = dyn_cast<UnaryOperator>(dstPtr);
+      if (!duo || duo->getOpcode() != UO_AddrOf)
         return nullptr;
-      MemberExpr *ME = dyn_cast<MemberExpr>(DUO->getSubExpr());
-      if (!ME)
+      auto *me = dyn_cast<MemberExpr>(duo->getSubExpr());
+      if (!me)
         return nullptr;
-      FieldDecl *Field = dyn_cast<FieldDecl>(ME->getMemberDecl());
-      if (!Field || !isMemcpyableField(Field))
+      auto *field = dyn_cast<FieldDecl>(me->getMemberDecl());
+      if (!field || !isMemcpyableField(field))
         return nullptr;
-      Expr *SrcPtr = CE->getArg(1);
-      if (ImplicitCastExpr *SC = dyn_cast<ImplicitCastExpr>(SrcPtr))
-        SrcPtr = SC->getSubExpr();
-      UnaryOperator *SUO = dyn_cast<UnaryOperator>(SrcPtr);
-      if (!SUO || SUO->getOpcode() != UO_AddrOf)
+      Expr *srcPtr = ce->getArg(1);
+      if (auto *sc = dyn_cast<ImplicitCastExpr>(srcPtr))
+        srcPtr = sc->getSubExpr();
+      auto *suo = dyn_cast<UnaryOperator>(srcPtr);
+      if (!suo || suo->getOpcode() != UO_AddrOf)
         return nullptr;
-      MemberExpr *ME2 = dyn_cast<MemberExpr>(SUO->getSubExpr());
-      if (!ME2 || Field != dyn_cast<FieldDecl>(ME2->getMemberDecl()))
+      auto *me2 = dyn_cast<MemberExpr>(suo->getSubExpr());
+      if (!me2 || field != dyn_cast<FieldDecl>(me2->getMemberDecl()))
         return nullptr;
-      return Field;
+      return field;
     }
 
     return nullptr;
   }
 
-  bool AssignmentsMemcpyable;
-  SmallVector<Stmt *, 16> AggregatedStmts;
+  bool assignmentsMemcpyable;
+  SmallVector<Stmt *, 16> aggregatedStmts;
 
 public:
-  AssignmentMemcpyizer(CIRGenFunction &CGF, const CXXMethodDecl *AD,
-                       FunctionArgList &Args)
-      : FieldMemcpyizer(CGF, AD, Args[Args.size() - 1]),
-        AssignmentsMemcpyable(CGF.getLangOpts().getGC() == LangOptions::NonGC) 
{
-    assert(Args.size() == 2);
+  AssignmentMemcpyizer(CIRGenFunction &cgf, const CXXMethodDecl *ad,
+                       FunctionArgList &args)
+      : FieldMemcpyizer(cgf, ad, args[args.size() - 1]),
+        assignmentsMemcpyable(cgf.getLangOpts().getGC() == LangOptions::NonGC) 
{
+    assert(args.size() == 2);
   }
 
-  void emitAssignment(Stmt *S) {
-    FieldDecl *F = getMemcpyableField(S);
-    if (F) {
-      addMemcpyableField(F);
-      AggregatedStmts.push_back(S);
+  void emitAssignment(Stmt *s) {
+    FieldDecl *f = getMemcpyableField(s);
+    if (f) {
+      addMemcpyableField(f);
+      aggregatedStmts.push_back(s);
     } else {
       emitAggregatedStmts();
-      if (CGF.emitStmt(S, /*useCurrentScope=*/true).failed())
-        llvm_unreachable("Should not get here!");
+      if (cgf.emitStmt(s, /*useCurrentScope=*/true).failed())
+        cgf.cgm.errorNYI(s->getSourceRange(), std::string("emitAssignment: ") +
+                                                  s->getStmtClassName());
     }
   }
 
   void emitAggregatedStmts() {
-    if (AggregatedStmts.size() <= 1) {
-      if (!AggregatedStmts.empty()) {
-        CopyingValueRepresentation CVR(CGF);
-        if (CGF.emitStmt(AggregatedStmts[0], 
/*useCurrentScope=*/true).failed())
-          llvm_unreachable("Should not get here!");
+    if (aggregatedStmts.size() <= 1) {
+      if (!aggregatedStmts.empty()) {
+        CopyingValueRepresentation cvr(cgf);
+        if (cgf.emitStmt(aggregatedStmts[0], 
/*useCurrentScope=*/true).failed())
+          cgf.cgm.errorNYI(aggregatedStmts[0]->getSourceRange(),
+                           std::string("emitAggregatedStmts: ") +
+                               aggregatedStmts[0]->getStmtClassName());
       }
       reset();
     }
 
     emitMemcpy();
-    AggregatedStmts.clear();
+    aggregatedStmts.clear();
   }
 
   void finish() { emitAggregatedStmts(); }

>From cdb364aecaa82e5c9201328dec57d014c396b39f Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 09:08:28 -0500
Subject: [PATCH 4/9] Use the new class

---
 clang/include/clang/CIR/MissingFeatures.h |  1 -
 clang/lib/CIR/CodeGen/CIRGenClass.cpp     | 10 ++++------
 2 files changed, 4 insertions(+), 7 deletions(-)

diff --git a/clang/include/clang/CIR/MissingFeatures.h 
b/clang/include/clang/CIR/MissingFeatures.h
index 1348206f1738ad..01cec9d3f31ece 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -189,7 +189,6 @@ struct MissingFeatures {
   static bool asmMemoryEffects() { return false; }
   static bool asmUnwindClobber() { return false; }
   static bool asmVectorType() { return false; }
-  static bool assignMemcpyizer() { return false; }
   static bool astVarDeclInterface() { return false; }
   static bool attributeNoBuiltin() { return false; }
   static bool branchProtection() { return false; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index ce985cd77665fa..e19158126e0f25 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -1144,12 +1144,10 @@ void 
CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
   // lowering or optimization phases to keep the memory accesses more
   // explicit. For now, we don't insert memcpy at all, though in some
   // cases the AST contains a call to memcpy.
-  assert(!cir::MissingFeatures::assignMemcpyizer());
-  for (Stmt *s : rootCS->body())
-    if (emitStmt(s, /*useCurrentScope=*/true).failed())
-      cgm.errorNYI(s->getSourceRange(),
-                   std::string("emitImplicitAssignmentOperatorBody: ") +
-                       s->getStmtClassName());
+  AssignmentMemcpyizer AM(*this, assignOp, args);
+  for (auto *i : rootCS->body())
+    AM.emitAssignment(i);
+  AM.finish();
 }
 
 void CIRGenFunction::emitForwardingCallToLambda(

>From b39f1bc3e5957224685750c772512f0428a352bf Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 09:10:05 -0500
Subject: [PATCH 5/9] Add ported test, claude assisted

---
 .../CIR/CodeGen/assign-operator-memcpy.cpp    | 83 +++++++++++++++++++
 1 file changed, 83 insertions(+)
 create mode 100644 clang/test/CIR/CodeGen/assign-operator-memcpy.cpp

diff --git a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp 
b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
new file mode 100644
index 00000000000000..b1b57898238206
--- /dev/null
+++ b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
@@ -0,0 +1,83 @@
+// RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir 
-emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s
+
+// Runs of adjacent trivially copyable fields in an implicitly defined copy
+// assignment operator are merged into a single memcpy, as classic codegen 
does.
+
+struct HasNonTrivialAssignOp {
+  HasNonTrivialAssignOp &operator=(const HasNonTrivialAssignOp &);
+};
+
+struct ContainsNonTrivial {
+  HasNonTrivialAssignOp start;
+  int i;
+  int *j;
+  HasNonTrivialAssignOp middle;
+  int k : 4;
+  int l : 4;
+  int m : 4;
+  HasNonTrivialAssignOp end;
+  ContainsNonTrivial &operator=(const ContainsNonTrivial &);
+};
+
+// Fields i and j are merged into one memcpy, k, l and m (bit-fields) into
+// another. The non-trivial members are assigned with explicit calls in 
between.
+// CHECK-LABEL: cir.func {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// CHECK:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
+// CHECK:         %[[SIZE_IJ:.*]] = cir.const #cir.int<12> : !u64i
+// CHECK:         cir.libc.memcpy %[[SIZE_IJ]] bytes from
+// CHECK:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
+// CHECK:         %[[SIZE_KLM:.*]] = cir.const #cir.int<2> : !u64i
+// CHECK:         cir.libc.memcpy %[[SIZE_KLM]] bytes from
+// CHECK:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
+// CHECK:         cir.return
+ContainsNonTrivial &
+ContainsNonTrivial::operator=(const ContainsNonTrivial &) = default;
+
+struct Trivial {
+  int i;
+  int *j;
+  double k;
+  int l[3];
+};
+
+// Trivial assignment operators are still called explicitly.
+// CHECK-LABEL: cir.func {{.*}} @_Z11copyTrivialR7TrivialS0_(
+// CHECK:         cir.call @_ZN7TrivialaSERKS_(
+void copyTrivial(Trivial &a, Trivial &b) { a = b; }
+
+// All fields are merged into one memcpy. Note that tail padding bytes are not
+// included: sizeof(Trivial) is 40.
+// CHECK-LABEL: cir.func {{.*}} @_ZN7TrivialaSERKS_(
+// CHECK:         %[[SIZE:.*]] = cir.const #cir.int<36> : !u64i
+// CHECK-NEXT:    cir.cast bitcast
+// CHECK-NEXT:    cir.cast bitcast
+// CHECK-NEXT:    cir.libc.memcpy %[[SIZE]] bytes from
+// CHECK:         cir.return
+
+struct ContainsTrivial {
+  Trivial t1;
+  Trivial t2;
+  ContainsTrivial &operator=(const ContainsTrivial &);
+};
+
+// Fields of trivial class type are assigned by calling their operator=.
+// CHECK-LABEL: cir.func {{.*}} @_ZN15ContainsTrivialaSERKS_(
+// CHECK:         cir.call @_ZN7TrivialaSERKS_(
+// CHECK:         cir.call @_ZN7TrivialaSERKS_(
+// CHECK-NOT:     cir.libc.memcpy
+// CHECK:         cir.return
+ContainsTrivial &ContainsTrivial::operator=(const ContainsTrivial &) = default;
+
+struct ContainsTrivialArray {
+  Trivial arr[2];
+  ContainsTrivialArray &operator=(const ContainsTrivialArray &);
+};
+
+// An array of trivial class types is copied with a memcpy.
+// CHECK-LABEL: cir.func {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
+// CHECK:         %[[SIZE_ARR:.*]] = cir.const #cir.int<80> : !u64i
+// CHECK:         cir.libc.memcpy %[[SIZE_ARR]] bytes from
+// CHECK:         cir.return
+ContainsTrivialArray &
+ContainsTrivialArray::operator=(const ContainsTrivialArray &) = default;

>From 5e9c3459a085c99a574ef49064f2da79522d9873 Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 09:18:18 -0500
Subject: [PATCH 6/9] Add LLVM IR checks, claude assisted

---
 .../CIR/CodeGen/assign-operator-memcpy.cpp    | 100 +++++++++++++-----
 1 file changed, 73 insertions(+), 27 deletions(-)

diff --git a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp 
b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
index b1b57898238206..448aa4752fe013 100644
--- a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
+++ b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
@@ -1,5 +1,9 @@
 // RUN: %clang_cc1 -std=c++11 -triple x86_64-unknown-linux-gnu -fclangir 
-emit-cir %s -o %t.cir
-// RUN: FileCheck --input-file=%t.cir %s
+// 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
 
 // Runs of adjacent trivially copyable fields in an implicitly defined copy
 // assignment operator are merged into a single memcpy, as classic codegen 
does.
@@ -22,18 +26,32 @@ struct ContainsNonTrivial {
 
 // Fields i and j are merged into one memcpy, k, l and m (bit-fields) into
 // another. The non-trivial members are assigned with explicit calls in 
between.
-// CHECK-LABEL: cir.func {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
-// CHECK:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
-// CHECK:         %[[SIZE_IJ:.*]] = cir.const #cir.int<12> : !u64i
-// CHECK:         cir.libc.memcpy %[[SIZE_IJ]] bytes from
-// CHECK:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
-// CHECK:         %[[SIZE_KLM:.*]] = cir.const #cir.int<2> : !u64i
-// CHECK:         cir.libc.memcpy %[[SIZE_KLM]] bytes from
-// CHECK:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
-// CHECK:         cir.return
+// CIR-LABEL: cir.func {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// CIR:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
+// CIR:         %[[SIZE_IJ:.*]] = cir.const #cir.int<12> : !u64i
+// CIR:         cir.libc.memcpy %[[SIZE_IJ]] bytes from
+// CIR:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
+// CIR:         %[[SIZE_KLM:.*]] = cir.const #cir.int<2> : !u64i
+// CIR:         cir.libc.memcpy %[[SIZE_KLM]] bytes from
+// CIR:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
+// CIR:         cir.return
 ContainsNonTrivial &
 ContainsNonTrivial::operator=(const ContainsNonTrivial &) = default;
 
+// LLVM-LABEL: define {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// LLVM:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 12, i1 false)
+// LLVM:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 2, i1 false)
+// LLVM:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+
+// OGCG-LABEL: define {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 12, i1 false)
+// OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 2, i1 false)
+// OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+
 struct Trivial {
   int i;
   int *j;
@@ -42,18 +60,30 @@ struct Trivial {
 };
 
 // Trivial assignment operators are still called explicitly.
-// CHECK-LABEL: cir.func {{.*}} @_Z11copyTrivialR7TrivialS0_(
-// CHECK:         cir.call @_ZN7TrivialaSERKS_(
+// CIR-LABEL: cir.func {{.*}} @_Z11copyTrivialR7TrivialS0_(
+// CIR:         cir.call @_ZN7TrivialaSERKS_(
 void copyTrivial(Trivial &a, Trivial &b) { a = b; }
 
+// CIR emits the call; the operator body copies only up to the end of the last
+// field (36 bytes). Classic codegen never emits the trivial operator and 
copies
+// the whole object (40 bytes) inline.
+// LLVM-LABEL: define {{.*}} @_Z11copyTrivialR7TrivialS0_(
+// LLVM:         call {{.*}} @_ZN7TrivialaSERKS_(
+// LLVM-LABEL: define {{.*}} @_ZN7TrivialaSERKS_(
+// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 36, i1 false)
+
+// OGCG-LABEL: define {{.*}} @_Z11copyTrivialR7TrivialS0_(
+// OGCG-NOT:     call {{.*}} @_ZN7TrivialaSERKS_(
+// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 40, i1 false)
+
 // All fields are merged into one memcpy. Note that tail padding bytes are not
 // included: sizeof(Trivial) is 40.
-// CHECK-LABEL: cir.func {{.*}} @_ZN7TrivialaSERKS_(
-// CHECK:         %[[SIZE:.*]] = cir.const #cir.int<36> : !u64i
-// CHECK-NEXT:    cir.cast bitcast
-// CHECK-NEXT:    cir.cast bitcast
-// CHECK-NEXT:    cir.libc.memcpy %[[SIZE]] bytes from
-// CHECK:         cir.return
+// CIR-LABEL: cir.func {{.*}} @_ZN7TrivialaSERKS_(
+// CIR:         %[[SIZE:.*]] = cir.const #cir.int<36> : !u64i
+// CIR-NEXT:    cir.cast bitcast
+// CIR-NEXT:    cir.cast bitcast
+// CIR-NEXT:    cir.libc.memcpy %[[SIZE]] bytes from
+// CIR:         cir.return
 
 struct ContainsTrivial {
   Trivial t1;
@@ -62,22 +92,38 @@ struct ContainsTrivial {
 };
 
 // Fields of trivial class type are assigned by calling their operator=.
-// CHECK-LABEL: cir.func {{.*}} @_ZN15ContainsTrivialaSERKS_(
-// CHECK:         cir.call @_ZN7TrivialaSERKS_(
-// CHECK:         cir.call @_ZN7TrivialaSERKS_(
-// CHECK-NOT:     cir.libc.memcpy
-// CHECK:         cir.return
+// CIR-LABEL: cir.func {{.*}} @_ZN15ContainsTrivialaSERKS_(
+// CIR:         cir.call @_ZN7TrivialaSERKS_(
+// CIR:         cir.call @_ZN7TrivialaSERKS_(
+// CIR-NOT:     cir.libc.memcpy
+// CIR:         cir.return
 ContainsTrivial &ContainsTrivial::operator=(const ContainsTrivial &) = default;
 
+// LLVM-LABEL: define {{.*}} @_ZN15ContainsTrivialaSERKS_(
+// LLVM:         call {{.*}} @_ZN7TrivialaSERKS_(
+// LLVM:         call {{.*}} @_ZN7TrivialaSERKS_(
+// LLVM-NOT:     call void @llvm.memcpy
+
+// Classic codegen merges both fields into a single memcpy of the whole object.
+// OGCG-LABEL: define {{.*}} @_ZN15ContainsTrivialaSERKS_(
+// OGCG-NOT:     call {{.*}} @_ZN7TrivialaSERKS_(
+// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)
+
 struct ContainsTrivialArray {
   Trivial arr[2];
   ContainsTrivialArray &operator=(const ContainsTrivialArray &);
 };
 
 // An array of trivial class types is copied with a memcpy.
-// CHECK-LABEL: cir.func {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
-// CHECK:         %[[SIZE_ARR:.*]] = cir.const #cir.int<80> : !u64i
-// CHECK:         cir.libc.memcpy %[[SIZE_ARR]] bytes from
-// CHECK:         cir.return
+// CIR-LABEL: cir.func {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
+// CIR:         %[[SIZE_ARR:.*]] = cir.const #cir.int<80> : !u64i
+// CIR:         cir.libc.memcpy %[[SIZE_ARR]] bytes from
+// CIR:         cir.return
 ContainsTrivialArray &
 ContainsTrivialArray::operator=(const ContainsTrivialArray &) = default;
+
+// LLVM-LABEL: define {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
+// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)
+
+// OGCG-LABEL: define {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
+// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)

>From 1e13dccc629c5ba756ab46a3ac9af55d943b5e1e Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 09:29:11 -0500
Subject: [PATCH 7/9] Add additional test case, claude assisted

---
 .../CIR/CodeGen/assign-operator-memcpy.cpp    | 31 +++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp 
b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
index 448aa4752fe013..1f72a11e050fdd 100644
--- a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
+++ b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
@@ -127,3 +127,34 @@ ContainsTrivialArray::operator=(const ContainsTrivialArray 
&) = default;
 
 // OGCG-LABEL: define {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
 // OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)
+
+struct HasPadding {
+  int a;
+  long b[32];
+  int c;
+};
+// FIXME: CIR zeroes the temporary member by member (leaving its padding
+// uninitialized) and the assignment operator copies 268 bytes (up to the end 
of
+// the last field), not the 272 of the object, so the tail padding is not 
copied
+// either. Update the CIR and LLVM checks when this is fixed.
+
+// CIR-LABEL: cir.func {{.*}} @_Z11resetPaddedP10HasPadding(
+// CIR-NOT:     cir.libc.memset %{{.*}} bytes
+// CIR:         cir.get_member %{{.*}}[0] {name = "a"}
+// CIR:         cir.libc.memset %{{.*}} bytes at
+// CIR:         cir.get_member %{{.*}}[2] {name = "c"}
+// CIR:         cir.call @_ZN10HasPaddingaSEOS_(
+// CIR-LABEL: cir.func {{.*}} @_ZN10HasPaddingaSEOS_(
+// CIR:         %[[SIZE_PADDED:.*]] = cir.const #cir.int<268> : !u64i
+// CIR:         cir.libc.memcpy %[[SIZE_PADDED]] bytes from
+
+// LLVM-LABEL: define {{.*}} @_Z11resetPaddedP10HasPadding(
+// LLVM:         call void @llvm.memset.p0.i64({{.*}}, i64 256, i1 false)
+// LLVM:         call {{.*}} @_ZN10HasPaddingaSEOS_(
+// LLVM-LABEL: define {{.*}} @_ZN10HasPaddingaSEOS_(
+// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 268, i1 false)
+
+// OGCG-LABEL: define {{.*}} @_Z11resetPaddedP10HasPadding(
+// OGCG:         call void @llvm.memset.p0.i64({{.*}}, i64 272, i1 false)
+// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 272, i1 false)
+void resetPadded(HasPadding *p) { *p = {}; }

>From 3a02b9363021f2f37f87876db1fe4c89386e06a8 Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 09:46:38 -0500
Subject: [PATCH 8/9] Tighten the checks

---
 .../CIR/CodeGen/assign-operator-memcpy.cpp    | 222 ++++++++++++------
 1 file changed, 156 insertions(+), 66 deletions(-)

diff --git a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp 
b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
index 1f72a11e050fdd..c5ddcfffbd0d05 100644
--- a/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
+++ b/clang/test/CIR/CodeGen/assign-operator-memcpy.cpp
@@ -5,9 +5,6 @@
 // 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
 
-// Runs of adjacent trivially copyable fields in an implicitly defined copy
-// assignment operator are merged into a single memcpy, as classic codegen 
does.
-
 struct HasNonTrivialAssignOp {
   HasNonTrivialAssignOp &operator=(const HasNonTrivialAssignOp &);
 };
@@ -24,33 +21,59 @@ struct ContainsNonTrivial {
   ContainsNonTrivial &operator=(const ContainsNonTrivial &);
 };
 
-// Fields i and j are merged into one memcpy, k, l and m (bit-fields) into
-// another. The non-trivial members are assigned with explicit calls in 
between.
 // CIR-LABEL: cir.func {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// CIR:         %[[NT_THIS:.*]] = cir.load deref %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsNonTrivial>>
 // CIR:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
-// CIR:         %[[SIZE_IJ:.*]] = cir.const #cir.int<12> : !u64i
-// CIR:         cir.libc.memcpy %[[SIZE_IJ]] bytes from
+// CIR-NEXT:    %[[NT_I_DST:.*]] = cir.get_member %[[NT_THIS]][2] {name = "i"}
+// CIR-NEXT:    %[[NT_OTHER_I:.*]] = cir.load align(8) %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsNonTrivial>>
+// CIR-NEXT:    %[[NT_I_SRC:.*]] = cir.get_member %[[NT_OTHER_I]][2] {name = 
"i"}
+// CIR-NEXT:    %[[NT_SIZE_IJ:.*]] = cir.const #cir.int<12> : !u64i
+// CIR-NEXT:    %[[NT_I_SRC_VOID:.*]] = cir.cast bitcast %[[NT_I_SRC]] : 
!cir.ptr<!s32i> -> !cir.ptr<!void>
+// CIR-NEXT:    %[[NT_I_DST_VOID:.*]] = cir.cast bitcast %[[NT_I_DST]] : 
!cir.ptr<!s32i> -> !cir.ptr<!void>
+// CIR-NEXT:    cir.libc.memcpy %[[NT_SIZE_IJ]] bytes from %[[NT_I_SRC_VOID]] 
align(4) to %[[NT_I_DST_VOID]] align(4)
 // CIR:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
-// CIR:         %[[SIZE_KLM:.*]] = cir.const #cir.int<2> : !u64i
-// CIR:         cir.libc.memcpy %[[SIZE_KLM]] bytes from
+// CIR-NEXT:    %[[NT_K_DST:.*]] = cir.get_member %[[NT_THIS]][5] {name = "k"}
+// CIR-NEXT:    %[[NT_OTHER_K:.*]] = cir.load align(8) %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsNonTrivial>>
+// CIR-NEXT:    %[[NT_K_SRC:.*]] = cir.get_member %[[NT_OTHER_K]][5] {name = 
"k"}
+// CIR-NEXT:    %[[NT_SIZE_KLM:.*]] = cir.const #cir.int<2> : !u64i
+// CIR-NEXT:    %[[NT_K_SRC_VOID:.*]] = cir.cast bitcast %[[NT_K_SRC]] : 
!cir.ptr<!u16i> -> !cir.ptr<!void>
+// CIR-NEXT:    %[[NT_K_DST_VOID:.*]] = cir.cast bitcast %[[NT_K_DST]] : 
!cir.ptr<!u16i> -> !cir.ptr<!void>
+// CIR-NEXT:    cir.libc.memcpy %[[NT_SIZE_KLM]] bytes from %[[NT_K_SRC_VOID]] 
align(1) to %[[NT_K_DST_VOID]] align(1)
 // CIR:         cir.call @_ZN21HasNonTrivialAssignOpaSERKS_(
 // CIR:         cir.return
-ContainsNonTrivial &
-ContainsNonTrivial::operator=(const ContainsNonTrivial &) = default;
 
 // LLVM-LABEL: define {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// LLVM:         [[NT_THIS:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[NT_OTHER0:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(ptr {{.*}} 
[[NT_THIS]], ptr {{.*}} [[NT_OTHER0]])
+// LLVM-NEXT:    [[NT_I_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_THIS]], i32 0, i32 2
+// LLVM-NEXT:    [[NT_OTHER_I:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[NT_I_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_OTHER_I]], i32 0, i32 2
+// LLVM-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 4 [[NT_I_DST]], 
ptr align 4 [[NT_I_SRC]], i64 12, i1 false)
 // LLVM:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
-// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 12, i1 false)
-// LLVM:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
-// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 2, i1 false)
+// LLVM-NEXT:    [[NT_K_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_THIS]], i32 0, i32 5
+// LLVM-NEXT:    [[NT_OTHER_K:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[NT_K_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_OTHER_K]], i32 0, i32 5
+// LLVM-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 1 [[NT_K_DST]], 
ptr align 1 [[NT_K_SRC]], i64 2, i1 false)
 // LLVM:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+// LLVM:         ret ptr
 
 // OGCG-LABEL: define {{.*}} @_ZN18ContainsNonTrivialaSERKS_(
+// OGCG:         [[NT_THIS:%.*]] = load ptr, ptr %this.addr
+// OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(ptr {{.*}} 
[[NT_THIS]],
+// OGCG-NEXT:    [[NT_I_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_THIS]], i32 0, i32 1
+// OGCG-NEXT:    [[NT_OTHER_I:%.*]] = load ptr, ptr %.addr
+// OGCG-NEXT:    [[NT_I_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_OTHER_I]], i32 0, i32 1
+// OGCG-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 4 [[NT_I_DST]], 
ptr align 4 [[NT_I_SRC]], i64 12, i1 false)
 // OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
-// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 12, i1 false)
-// OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
-// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 2, i1 false)
+// OGCG-NEXT:    [[NT_K_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_THIS]], i32 0, i32 4
+// OGCG-NEXT:    [[NT_OTHER_K:%.*]] = load ptr, ptr %.addr
+// OGCG-NEXT:    [[NT_K_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsNonTrivial, ptr [[NT_OTHER_K]], i32 0, i32 4
+// OGCG-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 1 [[NT_K_DST]], 
ptr align 1 [[NT_K_SRC]], i64 2, i1 false)
 // OGCG:         call {{.*}} @_ZN21HasNonTrivialAssignOpaSERKS_(
+// OGCG:         ret ptr [[NT_THIS]]
+ContainsNonTrivial &
+ContainsNonTrivial::operator=(const ContainsNonTrivial &) = default;
 
 struct Trivial {
   int i;
@@ -59,102 +82,169 @@ struct Trivial {
   int l[3];
 };
 
-// Trivial assignment operators are still called explicitly.
+// Trivial assignment operators are still called explicitly by CIR. Classic
+// codegen never emits the trivial operator and copies the whole object (40
+// bytes) inline.
+
 // CIR-LABEL: cir.func {{.*}} @_Z11copyTrivialR7TrivialS0_(
-// CIR:         cir.call @_ZN7TrivialaSERKS_(
-void copyTrivial(Trivial &a, Trivial &b) { a = b; }
+// CIR:         %[[CT_B:.*]] = cir.load %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_Trivial>>, !cir.ptr<!rec_Trivial>
+// CIR-NEXT:    %[[CT_A:.*]] = cir.load %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_Trivial>>, !cir.ptr<!rec_Trivial>
+// CIR-NEXT:    cir.call @_ZN7TrivialaSERKS_(%[[CT_A]], %[[CT_B]])
+// CIR-NEXT:    cir.return
 
-// CIR emits the call; the operator body copies only up to the end of the last
-// field (36 bytes). Classic codegen never emits the trivial operator and 
copies
-// the whole object (40 bytes) inline.
 // LLVM-LABEL: define {{.*}} @_Z11copyTrivialR7TrivialS0_(
-// LLVM:         call {{.*}} @_ZN7TrivialaSERKS_(
-// LLVM-LABEL: define {{.*}} @_ZN7TrivialaSERKS_(
-// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 36, i1 false)
+// LLVM:         [[CT_B:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[CT_A:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    call {{.*}} @_ZN7TrivialaSERKS_(ptr {{.*}} [[CT_A]], ptr 
{{.*}} [[CT_B]])
+// LLVM-NEXT:    ret void
 
 // OGCG-LABEL: define {{.*}} @_Z11copyTrivialR7TrivialS0_(
-// OGCG-NOT:     call {{.*}} @_ZN7TrivialaSERKS_(
-// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 40, i1 false)
+// OGCG:         [[CT_B:%.*]] = load ptr, ptr %{{.*}}
+// OGCG-NEXT:    [[CT_A:%.*]] = load ptr, ptr %{{.*}}
+// OGCG-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[CT_A]], ptr 
align 8 [[CT_B]], i64 40, i1 false)
+// OGCG-NEXT:    ret void
+void copyTrivial(Trivial &a, Trivial &b) { a = b; }
+
+// The operator that CIR calls copies all fields in a single memcpy. Note that
+// tail padding bytes are not included: sizeof(Trivial) is 40, the memcpy is 
36.
 
-// All fields are merged into one memcpy. Note that tail padding bytes are not
-// included: sizeof(Trivial) is 40.
 // CIR-LABEL: cir.func {{.*}} @_ZN7TrivialaSERKS_(
-// CIR:         %[[SIZE:.*]] = cir.const #cir.int<36> : !u64i
-// CIR-NEXT:    cir.cast bitcast
-// CIR-NEXT:    cir.cast bitcast
-// CIR-NEXT:    cir.libc.memcpy %[[SIZE]] bytes from
+// CIR:         %[[TR_THIS:.*]] = cir.load deref %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_Trivial>>
+// CIR-NEXT:    %[[TR_DST:.*]] = cir.get_member %[[TR_THIS]][0] {name = "i"}
+// CIR-NEXT:    %[[TR_OTHER:.*]] = cir.load align(8) %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_Trivial>>
+// CIR-NEXT:    %[[TR_SRC:.*]] = cir.get_member %[[TR_OTHER]][0] {name = "i"}
+// CIR-NEXT:    %[[TR_SIZE:.*]] = cir.const #cir.int<36> : !u64i
+// CIR-NEXT:    %[[TR_SRC_VOID:.*]] = cir.cast bitcast %[[TR_SRC]] : 
!cir.ptr<!s32i> -> !cir.ptr<!void>
+// CIR-NEXT:    %[[TR_DST_VOID:.*]] = cir.cast bitcast %[[TR_DST]] : 
!cir.ptr<!s32i> -> !cir.ptr<!void>
+// CIR-NEXT:    cir.libc.memcpy %[[TR_SIZE]] bytes from %[[TR_SRC_VOID]] 
align(8) to %[[TR_DST_VOID]] align(8)
+// CIR-NEXT:    cir.store %[[TR_THIS]], %{{.*}}
 // CIR:         cir.return
 
+// LLVM-LABEL: define {{.*}} @_ZN7TrivialaSERKS_(
+// LLVM:         [[TR_THIS:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[TR_DST:%.*]] = getelementptr inbounds nuw %struct.Trivial, 
ptr [[TR_THIS]], i32 0, i32 0
+// LLVM-NEXT:    [[TR_OTHER:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[TR_SRC:%.*]] = getelementptr inbounds nuw %struct.Trivial, 
ptr [[TR_OTHER]], i32 0, i32 0
+// LLVM-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[TR_DST]], ptr 
align 8 [[TR_SRC]], i64 36, i1 false)
+// LLVM:         ret ptr
+
 struct ContainsTrivial {
   Trivial t1;
   Trivial t2;
   ContainsTrivial &operator=(const ContainsTrivial &);
 };
 
-// Fields of trivial class type are assigned by calling their operator=.
+// CIR assigns fields of trivial class type by calling their operator=. Classic
+// codegen merges both fields into a single memcpy of the whole object.
+
 // CIR-LABEL: cir.func {{.*}} @_ZN15ContainsTrivialaSERKS_(
-// CIR:         cir.call @_ZN7TrivialaSERKS_(
-// CIR:         cir.call @_ZN7TrivialaSERKS_(
+// CIR:         %[[CTR_THIS:.*]] = cir.load deref %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsTrivial>>
+// CIR-NEXT:    %[[CTR_T1_DST:.*]] = cir.get_member %[[CTR_THIS]][0] {name = 
"t1"}
+// CIR-NEXT:    %[[CTR_OTHER1:.*]] = cir.load %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsTrivial>>
+// CIR-NEXT:    %[[CTR_T1_SRC:.*]] = cir.get_member %[[CTR_OTHER1]][0] {name = 
"t1"}
+// CIR-NEXT:    cir.call @_ZN7TrivialaSERKS_(%[[CTR_T1_DST]], %[[CTR_T1_SRC]])
+// CIR-NEXT:    %[[CTR_T2_DST:.*]] = cir.get_member %[[CTR_THIS]][1] {name = 
"t2"}
+// CIR-NEXT:    %[[CTR_OTHER2:.*]] = cir.load %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsTrivial>>
+// CIR-NEXT:    %[[CTR_T2_SRC:.*]] = cir.get_member %[[CTR_OTHER2]][1] {name = 
"t2"}
+// CIR-NEXT:    cir.call @_ZN7TrivialaSERKS_(%[[CTR_T2_DST]], %[[CTR_T2_SRC]])
 // CIR-NOT:     cir.libc.memcpy
 // CIR:         cir.return
-ContainsTrivial &ContainsTrivial::operator=(const ContainsTrivial &) = default;
 
 // LLVM-LABEL: define {{.*}} @_ZN15ContainsTrivialaSERKS_(
-// LLVM:         call {{.*}} @_ZN7TrivialaSERKS_(
-// LLVM:         call {{.*}} @_ZN7TrivialaSERKS_(
+// LLVM:         [[CTR_THIS:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[CTR_T1_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivial, ptr [[CTR_THIS]], i32 0, i32 0
+// LLVM-NEXT:    [[CTR_OTHER1:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[CTR_T1_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivial, ptr [[CTR_OTHER1]], i32 0, i32 0
+// LLVM-NEXT:    call {{.*}} @_ZN7TrivialaSERKS_(ptr {{.*}} [[CTR_T1_DST]], 
ptr {{.*}} [[CTR_T1_SRC]])
+// LLVM-NEXT:    [[CTR_T2_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivial, ptr [[CTR_THIS]], i32 0, i32 1
+// LLVM-NEXT:    [[CTR_OTHER2:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[CTR_T2_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivial, ptr [[CTR_OTHER2]], i32 0, i32 1
+// LLVM-NEXT:    call {{.*}} @_ZN7TrivialaSERKS_(ptr {{.*}} [[CTR_T2_DST]], 
ptr {{.*}} [[CTR_T2_SRC]])
 // LLVM-NOT:     call void @llvm.memcpy
+// LLVM:         ret ptr
 
-// Classic codegen merges both fields into a single memcpy of the whole object.
 // OGCG-LABEL: define {{.*}} @_ZN15ContainsTrivialaSERKS_(
-// OGCG-NOT:     call {{.*}} @_ZN7TrivialaSERKS_(
-// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)
+// OGCG:         [[CTR_THIS:%.*]] = load ptr, ptr %this.addr
+// OGCG-NEXT:    [[CTR_T1_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivial, ptr [[CTR_THIS]], i32 0, i32 0
+// OGCG-NEXT:    [[CTR_OTHER:%.*]] = load ptr, ptr %.addr
+// OGCG-NEXT:    [[CTR_T1_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivial, ptr [[CTR_OTHER]], i32 0, i32 0
+// OGCG-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[CTR_T1_DST]], 
ptr align 8 [[CTR_T1_SRC]], i64 80, i1 false)
+// OGCG-NEXT:    ret ptr [[CTR_THIS]]
+ContainsTrivial &ContainsTrivial::operator=(const ContainsTrivial &) = default;
 
 struct ContainsTrivialArray {
   Trivial arr[2];
   ContainsTrivialArray &operator=(const ContainsTrivialArray &);
 };
 
-// An array of trivial class types is copied with a memcpy.
 // CIR-LABEL: cir.func {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
-// CIR:         %[[SIZE_ARR:.*]] = cir.const #cir.int<80> : !u64i
-// CIR:         cir.libc.memcpy %[[SIZE_ARR]] bytes from
+// CIR:         %[[ARR_THIS:.*]] = cir.load deref %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsTrivialArray>>
+// CIR-NEXT:    %[[ARR_DST:.*]] = cir.get_member %[[ARR_THIS]][0] {name = 
"arr"}
+// CIR-NEXT:    %[[ARR_DST_VOID:.*]] = cir.cast bitcast %[[ARR_DST]] : {{.*}} 
-> !cir.ptr<!void>
+// CIR-NEXT:    %[[ARR_OTHER:.*]] = cir.load %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_ContainsTrivialArray>>
+// CIR-NEXT:    %[[ARR_SRC:.*]] = cir.get_member %[[ARR_OTHER]][0] {name = 
"arr"}
+// CIR-NEXT:    %[[ARR_SRC_VOID:.*]] = cir.cast bitcast %[[ARR_SRC]] : {{.*}} 
-> !cir.ptr<!void>
+// CIR-NEXT:    %[[ARR_SIZE:.*]] = cir.const #cir.int<80> : !u64i
+// CIR-NEXT:    cir.libc.memcpy %[[ARR_SIZE]] bytes from %[[ARR_SRC_VOID]] 
align(8) to %[[ARR_DST_VOID]] align(8)
 // CIR:         cir.return
-ContainsTrivialArray &
-ContainsTrivialArray::operator=(const ContainsTrivialArray &) = default;
 
 // LLVM-LABEL: define {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
-// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)
+// LLVM:         [[ARR_THIS:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[ARR_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivialArray, ptr [[ARR_THIS]], i32 0, i32 0
+// LLVM-NEXT:    [[ARR_OTHER:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[ARR_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivialArray, ptr [[ARR_OTHER]], i32 0, i32 0
+// LLVM-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[ARR_DST]], ptr 
align 8 [[ARR_SRC]], i64 80, i1 false)
+// LLVM:         ret ptr
 
 // OGCG-LABEL: define {{.*}} @_ZN20ContainsTrivialArrayaSERKS_(
-// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 80, i1 false)
+// OGCG:         [[ARR_THIS:%.*]] = load ptr, ptr %this.addr
+// OGCG-NEXT:    [[ARR_DST:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivialArray, ptr [[ARR_THIS]], i32 0, i32 0
+// OGCG-NEXT:    [[ARR_OTHER:%.*]] = load ptr, ptr %.addr
+// OGCG-NEXT:    [[ARR_SRC:%.*]] = getelementptr inbounds nuw 
%struct.ContainsTrivialArray, ptr [[ARR_OTHER]], i32 0, i32 0
+// OGCG-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[ARR_DST]], ptr 
align 8 [[ARR_SRC]], i64 80, i1 false)
+// OGCG-NEXT:    ret ptr [[ARR_THIS]]
+ContainsTrivialArray &
+ContainsTrivialArray::operator=(const ContainsTrivialArray &) = default;
 
+// FIXME: CIR zeroes the temporary member by member (leaving its padding
+// uninitialized) whereas OGCG memsets
 struct HasPadding {
   int a;
   long b[32];
   int c;
 };
-// FIXME: CIR zeroes the temporary member by member (leaving its padding
-// uninitialized) and the assignment operator copies 268 bytes (up to the end 
of
-// the last field), not the 272 of the object, so the tail padding is not 
copied
-// either. Update the CIR and LLVM checks when this is fixed.
 
 // CIR-LABEL: cir.func {{.*}} @_Z11resetPaddedP10HasPadding(
-// CIR-NOT:     cir.libc.memset %{{.*}} bytes
-// CIR:         cir.get_member %{{.*}}[0] {name = "a"}
-// CIR:         cir.libc.memset %{{.*}} bytes at
-// CIR:         cir.get_member %{{.*}}[2] {name = "c"}
-// CIR:         cir.call @_ZN10HasPaddingaSEOS_(
+// CIR:    cir.call @_ZN10HasPaddingaSEOS_(
+
 // CIR-LABEL: cir.func {{.*}} @_ZN10HasPaddingaSEOS_(
-// CIR:         %[[SIZE_PADDED:.*]] = cir.const #cir.int<268> : !u64i
-// CIR:         cir.libc.memcpy %[[SIZE_PADDED]] bytes from
+// CIR:         %[[HP_THIS:.*]] = cir.load deref %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_HasPadding>>
+// CIR-NEXT:    %[[HP_DST:.*]] = cir.get_member %[[HP_THIS]][0] {name = "a"}
+// CIR-NEXT:    %[[HP_OTHER:.*]] = cir.load align(8) %{{.*}} : 
!cir.ptr<!cir.ptr<!rec_HasPadding>>
+// CIR-NEXT:    %[[HP_SRC:.*]] = cir.get_member %[[HP_OTHER]][0] {name = "a"}
+// CIR-NEXT:    %[[HP_SIZE:.*]] = cir.const #cir.int<268> : !u64i
+// CIR-NEXT:    %[[HP_SRC_VOID:.*]] = cir.cast bitcast %[[HP_SRC]] : 
!cir.ptr<!s32i> -> !cir.ptr<!void>
+// CIR-NEXT:    %[[HP_DST_VOID:.*]] = cir.cast bitcast %[[HP_DST]] : 
!cir.ptr<!s32i> -> !cir.ptr<!void>
+// CIR-NEXT:    cir.libc.memcpy %[[HP_SIZE]] bytes from %[[HP_SRC_VOID]] 
align(8) to %[[HP_DST_VOID]] align(8)
+// CIR-NEXT:    cir.store %[[HP_THIS]], %{{.*}}
+// CIR:         cir.return
 
 // LLVM-LABEL: define {{.*}} @_Z11resetPaddedP10HasPadding(
-// LLVM:         call void @llvm.memset.p0.i64({{.*}}, i64 256, i1 false)
-// LLVM:         call {{.*}} @_ZN10HasPaddingaSEOS_(
+// LLVM:    call {{.*}} @_ZN10HasPaddingaSEOS_(ptr {{.*}} [[RP_P]], ptr {{.*}} 
[[RP_TMP]])
+
 // LLVM-LABEL: define {{.*}} @_ZN10HasPaddingaSEOS_(
-// LLVM:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 268, i1 false)
+// LLVM:         [[HP_THIS:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[HP_DST:%.*]] = getelementptr inbounds nuw 
%struct.HasPadding, ptr [[HP_THIS]], i32 0, i32 0
+// LLVM-NEXT:    [[HP_OTHER:%.*]] = load ptr, ptr {{.*}}
+// LLVM-NEXT:    [[HP_SRC:%.*]] = getelementptr inbounds nuw 
%struct.HasPadding, ptr [[HP_OTHER]], i32 0, i32 0
+// LLVM-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[HP_DST]], ptr 
align 8 [[HP_SRC]], i64 268, i1 false)
+// LLVM:         ret ptr
 
 // OGCG-LABEL: define {{.*}} @_Z11resetPaddedP10HasPadding(
-// OGCG:         call void @llvm.memset.p0.i64({{.*}}, i64 272, i1 false)
-// OGCG:         call void @llvm.memcpy.p0.p0.i64({{.*}}, i64 272, i1 false)
+// OGCG:         [[RP_TMP:%.*]] = alloca %struct.HasPadding, align 8
+// OGCG:         call void @llvm.memset.p0.i64(ptr align 8 [[RP_TMP]], i8 0, 
i64 272, i1 false)
+// OGCG:         [[RP_P:%.*]] = load ptr, ptr %p.addr
+// OGCG-NEXT:    call void @llvm.memcpy.p0.p0.i64(ptr align 8 [[RP_P]], ptr 
align 8 [[RP_TMP]], i64 272, i1 false)
+// OGCG-NEXT:    ret void
+// OGCG-NOT:   call {{.*}} @_ZN10HasPaddingaSEOS_(
 void resetPadded(HasPadding *p) { *p = {}; }

>From a1cd493d098d9fd0d9081e5fe6512a984556337c Mon Sep 17 00:00:00 2001
From: Mariya Podchishchaeva <[email protected]>
Date: Thu, 29 Oct 2026 09:50:26 -0500
Subject: [PATCH 9/9] Remove the comment

---
 clang/lib/CIR/CodeGen/CIRGenClass.cpp | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenClass.cpp 
b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
index e19158126e0f25..d818f90791ad45 100644
--- a/clang/lib/CIR/CodeGen/CIRGenClass.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenClass.cpp
@@ -1139,11 +1139,6 @@ void 
CIRGenFunction::emitImplicitAssignmentOperatorBody(FunctionArgList &args) {
   assert(!cir::MissingFeatures::incrementProfileCounter());
   assert(!cir::MissingFeatures::runCleanupsScope());
 
-  // 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
-  // explicit. For now, we don't insert memcpy at all, though in some
-  // cases the AST contains a call to memcpy.
   AssignmentMemcpyizer AM(*this, assignOp, args);
   for (auto *i : rootCS->body())
     AM.emitAssignment(i);

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

Reply via email to