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

Reply via email to