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
