https://github.com/adams381 updated https://github.com/llvm/llvm-project/pull/197068
>From 47f7a314d8d5f4eee43e2c8644cc1132b4d30894 Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Mon, 11 May 2026 16:37:47 -0700 Subject: [PATCH 1/2] [CIR] Lower C++23 static call/subscript operators correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C++23 static `operator()` (P1169R4) and static `operator[]` (P2589R1) still produce a CXXOperatorCallExpr in the AST, but the resolved CXXMethodDecl is static, so they should be lowered as ordinary static function calls — not routed through the implicit-object member call path. CIRGenFunction::emitCallExpr previously dispatched any CXXOperatorCallExpr whose callee was a CXXMethodDecl into emitCXXOperatorMemberCallExpr, which asserts md->isInstance(). Every C++23 static-operator call therefore hit: Assertion `md->isInstance() && "Trying to emit a member call expr on a static method!"' failed This dominates libcxx-with-CIR failures (~33% of fails in a partial `std/` run on the May 2026 ClangIR baseline) since recent libcxx threads `static constexpr operator()` through its functor types. Mirror classic CodeGen (CGExpr.cpp:6470-6477 and 7048-7080): - emitCallExpr only routes through emitCXXOperatorMemberCallExpr when the callee is an implicit-object member function. - emitCall, when given a CXXOperatorCallExpr whose CXXMethodDecl is static, emits the object expression for its side effects and drops it before walking the parameter arguments. New cxx2b-static-call-operator.cpp test (CIR/LLVM/OGCG) covering: - `static operator()`: object form `f(...)`, temporary `Functor{}(...)`, returned-by-value `make_functor()(...)` (verifies object-expression side effect is emitted before the static call), member-call syntax `f.operator()(...)`, and qualified syntax `Functor::operator()(...)`. - `static operator[]`: object form and temporary form. - `[](int, int) static {...}` lambda invocation. - Ordinary instance `operator()` continues to receive `this`. Out of scope: explicit-object member operators (`operator()(this Self&&)`) are correctly routed away from the member-call path by this change, but CIR currently hits a separate downstream assertion in MLIR `setArgAttrs` when the explicit-object parameter is materialised. Tracked for a follow-up PR. ninja check-clang-cir-codegen and ninja check-clang-cir remain green. Co-authored-by: Cursor <[email protected]> --- clang/lib/CIR/CodeGen/CIRGenExpr.cpp | 36 +++- .../CodeGen/cxx2b-static-call-operator.cpp | 187 ++++++++++++++++++ 2 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 clang/test/CIR/CodeGen/cxx2b-static-call-operator.cpp diff --git a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp index 34a7e4d655610..450523176b586 100644 --- a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp @@ -2252,7 +2252,26 @@ RValue CIRGenFunction::emitCall(clang::QualType calleeTy, CallArgList args; assert(!cir::MissingFeatures::opCallArgEvaluationOrder()); - emitCallArgs(args, dyn_cast<FunctionProtoType>(fnType), e->arguments(), + // C++23 static-member operators (`static operator()` / + // `static operator[]`) produce a CXXOperatorCallExpr whose first argument + // is the object expression even though the operator is static. Emit the + // object for its side effects and drop it before walking the parameter + // arguments. + bool staticOperator = false; + if (const auto *oce = dyn_cast<CXXOperatorCallExpr>(e)) { + if (const auto *md = + dyn_cast_if_present<CXXMethodDecl>(oce->getCalleeDecl()); + md && md->isStatic()) + staticOperator = true; + } + + auto arguments = e->arguments(); + if (staticOperator) { + emitIgnoredExpr(e->getArg(0)); + arguments = llvm::drop_begin(arguments, 1); + } + + emitCallArgs(args, dyn_cast<FunctionProtoType>(fnType), arguments, e->getDirectCallee()); const CIRGenFunctionInfo &funcInfo = @@ -2369,15 +2388,16 @@ RValue CIRGenFunction::emitCallExpr(const clang::CallExpr *e, if (const auto *cudaKernelCallExpr = dyn_cast<CUDAKernelCallExpr>(e)) return emitCUDAKernelCallExpr(cudaKernelCallExpr, returnValue); + // A CXXOperatorCallExpr is created even for explicit-object methods or + // static member operators (C++23 `static operator()` / `static + // operator[]`), but those should be treated like ordinary static function + // calls. Only route through the member-call path for ordinary instance + // operators. if (const auto *operatorCall = dyn_cast<CXXOperatorCallExpr>(e)) { - // If the callee decl is a CXXMethodDecl, we need to emit this as a C++ - // operator member call. - if (const CXXMethodDecl *md = - dyn_cast_or_null<CXXMethodDecl>(operatorCall->getCalleeDecl())) + if (const auto *md = + dyn_cast_if_present<CXXMethodDecl>(operatorCall->getCalleeDecl()); + md && md->isImplicitObjectMemberFunction()) return emitCXXOperatorMemberCallExpr(operatorCall, md, returnValue); - // A CXXOperatorCallExpr is created even for explicit object methods, but - // these should be treated like static function calls. Fall through to do - // that. } CIRGenCallee callee = emitCallee(e->getCallee()); diff --git a/clang/test/CIR/CodeGen/cxx2b-static-call-operator.cpp b/clang/test/CIR/CodeGen/cxx2b-static-call-operator.cpp new file mode 100644 index 0000000000000..6677a2aacbf57 --- /dev/null +++ b/clang/test/CIR/CodeGen/cxx2b-static-call-operator.cpp @@ -0,0 +1,187 @@ +// RUN: %clang_cc1 -std=c++23 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir +// RUN: FileCheck --input-file=%t.cir %s --check-prefix=CIR +// RUN: %clang_cc1 -std=c++23 -triple x86_64-unknown-linux-gnu -fclangir -emit-llvm %s -o %t-cir.ll +// RUN: FileCheck --input-file=%t-cir.ll %s --check-prefix=LLVM +// RUN: %clang_cc1 -std=c++23 -triple x86_64-unknown-linux-gnu -emit-llvm %s -o %t.ll +// RUN: FileCheck --input-file=%t.ll %s --check-prefix=OGCG + +// C++23 static call operator (P1169R4) and static subscript operator +// (P2589R1). Both produce CXXOperatorCallExpr nodes whose callee +// resolves to a static CXXMethodDecl. Such calls must be emitted as +// ordinary static-function calls (no `this` argument) rather than +// routed through the member-call path. + +struct CallFunctor { + static int operator()(int x, int y) { return x + y; } +}; + +struct SubscriptFunctor { + static int operator[](int x, int y) { return x + y; } +}; + +CallFunctor make_functor() { return {}; } + +// Object form: `f(...)` where `f` is a named CallFunctor. + +int call_static_call_operator() { + CallFunctor f; + return f(101, 102); +} + +// CIR-LABEL: cir.func{{.*}} @_Z25call_static_call_operatorv() +// CIR: %{{.+}} = cir.call @_ZN11CallFunctorclEii(%{{.+}}, %{{.+}}) +// CIR-NOT: cir.call @_ZN11CallFunctorclEii(%{{[^,]+}}, %{{[^,]+}}, %{{[^,]+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z25call_static_call_operatorv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}101, i32 {{.*}}102) + +// OGCG-LABEL: define {{.*}} i32 @_Z25call_static_call_operatorv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}101, i32 {{.*}}102) + +// Temporary form: `CallFunctor{}(...)` — exercises the emitIgnoredExpr +// path for the object expression (ctor of the temporary). + +int call_static_call_on_temporary() { + return CallFunctor{}(201, 202); +} + +// CIR-LABEL: cir.func{{.*}} @_Z29call_static_call_on_temporaryv() +// CIR: %{{.+}} = cir.call @_ZN11CallFunctorclEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z29call_static_call_on_temporaryv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}201, i32 {{.*}}202) + +// OGCG-LABEL: define {{.*}} i32 @_Z29call_static_call_on_temporaryv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}201, i32 {{.*}}202) + +// Returned-temporary form: `make_functor()(...)`. This is the +// common shape in libcxx where a projection / comparator is built +// and immediately applied. The object expression has a real side +// effect — emitIgnoredExpr must still call make_functor() before +// the static-operator call runs. + +int call_static_call_on_returned_temp() { + return make_functor()(301, 302); +} + +// CIR-LABEL: cir.func{{.*}} @_Z33call_static_call_on_returned_tempv() +// CIR: cir.call @_Z12make_functorv() +// CIR: %{{.+}} = cir.call @_ZN11CallFunctorclEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z33call_static_call_on_returned_tempv() +// LLVM: call {{.*}} @_Z12make_functorv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}301, i32 {{.*}}302) + +// OGCG-LABEL: define {{.*}} i32 @_Z33call_static_call_on_returned_tempv() +// OGCG: call {{.*}} @_Z12make_functorv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}301, i32 {{.*}}302) + +// Member-call syntax: `f.operator()(...)`. Goes through +// emitCXXMemberCallExpr (CXXMemberCallExpr, not CXXOperatorCallExpr), +// but should still emit a 2-arg static call — verifies that the +// member-call dispatcher also handles static call operators. + +int call_static_call_member_form() { + CallFunctor f; + return f.operator()(801, 802); +} + +// CIR-LABEL: cir.func{{.*}} @_Z28call_static_call_member_formv() +// CIR: %{{.+}} = cir.call @_ZN11CallFunctorclEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z28call_static_call_member_formv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}801, i32 {{.*}}802) + +// OGCG-LABEL: define {{.*}} i32 @_Z28call_static_call_member_formv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}801, i32 {{.*}}802) + +// Qualified form: `Functor::operator()(...)`. This is a plain +// CallExpr (no implicit object), so it falls through to emitCall +// without going through emitCallExpr's CXXOperatorCallExpr branch. + +int call_static_call_qualified_form() { + return CallFunctor::operator()(901, 902); +} + +// CIR-LABEL: cir.func{{.*}} @_Z31call_static_call_qualified_formv() +// CIR: %{{.+}} = cir.call @_ZN11CallFunctorclEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z31call_static_call_qualified_formv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}901, i32 {{.*}}902) + +// OGCG-LABEL: define {{.*}} i32 @_Z31call_static_call_qualified_formv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN11CallFunctorclEii(i32 {{.*}}901, i32 {{.*}}902) + +// Static subscript: object form. + +int call_static_subscript_operator() { + SubscriptFunctor f; + return f[401, 402]; +} + +// CIR-LABEL: cir.func{{.*}} @_Z30call_static_subscript_operatorv() +// CIR: %{{.+}} = cir.call @_ZN16SubscriptFunctorixEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z30call_static_subscript_operatorv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN16SubscriptFunctorixEii(i32 {{.*}}401, i32 {{.*}}402) + +// OGCG-LABEL: define {{.*}} i32 @_Z30call_static_subscript_operatorv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN16SubscriptFunctorixEii(i32 {{.*}}401, i32 {{.*}}402) + +// Static subscript: temporary form. + +int call_static_subscript_on_temporary() { + return SubscriptFunctor{}[501, 502]; +} + +// CIR-LABEL: cir.func{{.*}} @_Z34call_static_subscript_on_temporaryv() +// CIR: %{{.+}} = cir.call @_ZN16SubscriptFunctorixEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z34call_static_subscript_on_temporaryv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZN16SubscriptFunctorixEii(i32 {{.*}}501, i32 {{.*}}502) + +// OGCG-LABEL: define {{.*}} i32 @_Z34call_static_subscript_on_temporaryv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZN16SubscriptFunctorixEii(i32 {{.*}}501, i32 {{.*}}502) + +// Static lambda: `[](int, int) static {...}` produces a closure type +// with a static operator(); the call site is a CXXOperatorCallExpr +// that resolves to that static operator(). + +auto get_static_lambda() { return [](int x, int y) static { return x + y; }; } + +int call_static_lambda_operator() { + return get_static_lambda()(601, 602); +} + +// CIR-LABEL: cir.func{{.*}} @_Z27call_static_lambda_operatorv() +// CIR: cir.call @_Z17get_static_lambdav() +// CIR: cir.call @{{.*}}get_static_lambda{{.*}}clEii(%{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z27call_static_lambda_operatorv() +// LLVM: call {{.*}}@_Z17get_static_lambdav() +// LLVM: call {{.*}}get_static_lambda{{.*}}clEii{{.*}}601{{.*}}602 + +// OGCG-LABEL: define {{.*}} i32 @_Z27call_static_lambda_operatorv() +// OGCG: call {{.*}}@_Z17get_static_lambdav() +// OGCG: call {{.*}}get_static_lambda{{.*}}clEii{{.*}}601{{.*}}602 + +// Sanity: an instance call operator on the same TU still routes through +// the member-call path and gets a `this` pointer as the first argument. + +struct InstanceFunctor { + int operator()(int x, int y) const { return x - y; } +}; + +int call_instance_call_operator() { + InstanceFunctor f; + return f(701, 702); +} + +// CIR-LABEL: cir.func{{.*}} @_Z27call_instance_call_operatorv() +// CIR: %{{.+}} = cir.call @_ZNK15InstanceFunctorclEii(%{{.+}}, %{{.+}}, %{{.+}}) + +// LLVM-LABEL: define {{.*}} i32 @_Z27call_instance_call_operatorv() +// LLVM: %{{.+}} = call {{.*}}i32 @_ZNK15InstanceFunctorclEii(ptr {{.*}} %{{.+}}, i32 {{.*}}701, i32 {{.*}}702) + +// OGCG-LABEL: define {{.*}} i32 @_Z27call_instance_call_operatorv() +// OGCG: %{{.+}} = call {{.*}}i32 @_ZNK15InstanceFunctorclEii(ptr {{.*}} %{{.+}}, i32 {{.*}}701, i32 {{.*}}702) >From 27b5f47208f62f2dd44f81219467daca5a23940b Mon Sep 17 00:00:00 2001 From: Adam Smith <[email protected]> Date: Mon, 11 May 2026 17:26:32 -0700 Subject: [PATCH 2/2] [CIR] Address @erichkeane review on #197068: drop unused bool The `bool staticOperator` mirrored classic clang's emitCall, which uses the bool because the same enclosing block also handles `EvaluationOrder` for assignment / left-to-right operators (so the detection happens once and is consumed twice). CIR leaves `EvaluationOrder` as a MissingFeature, so the bool had only the immediately following `if (staticOperator)` as a consumer. Inline the `emitIgnoredExpr` + `drop_begin` directly into the existing nested `if` that detects the static operator. No behavior change; both check-clang-cir-codegen and check-clang-cir remain green. Co-authored-by: Cursor <[email protected]> --- clang/lib/CIR/CodeGen/CIRGenExpr.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp index 450523176b586..7c2729e07a0e4 100644 --- a/clang/lib/CIR/CodeGen/CIRGenExpr.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenExpr.cpp @@ -2257,18 +2257,14 @@ RValue CIRGenFunction::emitCall(clang::QualType calleeTy, // is the object expression even though the operator is static. Emit the // object for its side effects and drop it before walking the parameter // arguments. - bool staticOperator = false; + auto arguments = e->arguments(); if (const auto *oce = dyn_cast<CXXOperatorCallExpr>(e)) { if (const auto *md = dyn_cast_if_present<CXXMethodDecl>(oce->getCalleeDecl()); - md && md->isStatic()) - staticOperator = true; - } - - auto arguments = e->arguments(); - if (staticOperator) { - emitIgnoredExpr(e->getArg(0)); - arguments = llvm::drop_begin(arguments, 1); + md && md->isStatic()) { + emitIgnoredExpr(e->getArg(0)); + arguments = llvm::drop_begin(arguments, 1); + } } emitCallArgs(args, dyn_cast<FunctionProtoType>(fnType), arguments, _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
