[Mlir-commits] [mlir] [mlir][emitc]: support method calls in CallOpaqueOp (PR #200057)

Jeremy Kun llvmlistbot at llvm.org
Tue Jun 2 07:13:08 PDT 2026


https://github.com/j2kun updated https://github.com/llvm/llvm-project/pull/200057

>From fecadd4be5238f03563c5439b7fbfa606428d736 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Wed, 27 May 2026 14:10:04 -0700
Subject: [PATCH 1/8] [mlir][emitc]: support method calls in CallOpaqueOp

---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td | 22 +++++++++++++-
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp         |  3 ++
 mlir/lib/Target/Cpp/TranslateToCpp.cpp      | 33 ++++++++++++++++++---
 mlir/test/Dialect/EmitC/invalid_ops.mlir    |  8 +++++
 mlir/test/Target/Cpp/call.mlir              | 24 +++++++++++++++
 5 files changed, 85 insertions(+), 5 deletions(-)

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index e1ccf29ada660..dc83b85993a99 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -324,6 +324,19 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
     - integer value of index type refers to an operand;
     - attribute which will get lowered to constant value in call;
 
+    If `is_member_call` is true, the operation represents a member call. The
+    first operand is treated as the receiver and emitted before the member
+    access operator (`.` or `->`).
+
+    The `args` attribute can be used to override the default operand order and
+    to mix operands with attributes. If `args` is present, it specifies the
+    arguments passed to the call in parentheses.
+
+    If `args` is not present:
+    - If `is_member_call` is true, the first operand is the receiver, and the
+      remaining operands are passed as arguments.
+    - If `is_member_call` is false, all operands are passed as arguments.
+
     Example:
 
     ```mlir
@@ -332,12 +345,19 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
 
     // Generic form of the same operation.
     %0 = "emitc.call_opaque"() {callee = "foo"} : () -> i32
+
+    // Member call
+    %1 = emitc.call_opaque "method" (%arg0, %arg1) {is_member_call = true} : (!emitc.opaque<"MyClass">, i32) -> i32
+
+    // Member call with args attribute
+    %2 = emitc.call_opaque "method" (%arg0, %arg1) {is_member_call = true, args = [1 : index]} : (!emitc.opaque<"MyClass">, i32) -> i32
     ```
   }];
   let arguments = (ins
     Arg<StrAttr, "the C++ function to call">:$callee,
     Arg<OptionalAttr<ArrayAttr>, "the order of operands and further attributes">:$args,
     Arg<OptionalAttr<ArrayAttr>, "template arguments">:$template_args,
+    DefaultValuedAttr<BoolAttr, "false">:$is_member_call,
     Variadic<EmitCType>:$operands
   );
   let results = (outs Variadic<EmitCType>);
@@ -349,7 +369,7 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
       CArg<"::mlir::ArrayAttr", "{}">:$args,
       CArg<"::mlir::ArrayAttr", "{}">:$template_args), [{
         build($_builder, $_state, resultTypes, callee, args, template_args,
-            operands);
+            false, operands);
       }]
     >
   ];
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index 36394e67008da..cd27a97941e10 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -345,6 +345,9 @@ LogicalResult emitc::CallOpaqueOp::verify() {
   if (getCallee().empty())
     return emitOpError("callee must not be empty");
 
+  if (getIsMemberCall() && getNumOperands() == 0)
+    return emitOpError("member call requires at least one operand (receiver)");
+
   if (std::optional<ArrayAttr> argsAttr = getArgs()) {
     for (Attribute arg : *argsAttr) {
       auto intAttr = llvm::dyn_cast<IntegerAttr>(arg);
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index a670fb152a1e5..71ed42b7d5aa2 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -916,6 +916,23 @@ static LogicalResult printOperation(CppEmitter &emitter,
 
   if (failed(emitter.emitAssignPrefix(op)))
     return failure();
+
+  bool isMemberCall = callOpaqueOp.getIsMemberCall();
+
+  if (isMemberCall) {
+    Value receiver = op.getOperand(0);
+    bool isPtr = llvm::isa<emitc::PointerType>(receiver.getType());
+
+    if (failed(emitter.emitOperand(receiver)))
+      return failure();
+
+    if (isPtr) {
+      os << "->";
+    } else {
+      os << ".";
+    }
+  }
+
   os << callOpaqueOp.getCallee();
 
   // Template arguments can't refer to SSA values and as such the template
@@ -951,10 +968,18 @@ static LogicalResult printOperation(CppEmitter &emitter,
 
   os << "(";
 
-  LogicalResult emittedArgs =
-      callOpaqueOp.getArgs()
-          ? interleaveCommaWithError(*callOpaqueOp.getArgs(), os, emitArgs)
-          : emitter.emitOperands(op);
+  LogicalResult emittedArgs = success();
+  if (callOpaqueOp.getArgs()) {
+    emittedArgs =
+        interleaveCommaWithError(*callOpaqueOp.getArgs(), os, emitArgs);
+  } else if (isMemberCall) {
+    emittedArgs = interleaveCommaWithError(
+        llvm::seq<size_t>(1, op.getNumOperands()), os,
+        [&](size_t i) { return emitter.emitOperand(op.getOperand(i)); });
+  } else {
+    emittedArgs = emitter.emitOperands(op);
+  }
+
   if (failed(emittedArgs))
     return failure();
   os << ")";
diff --git a/mlir/test/Dialect/EmitC/invalid_ops.mlir b/mlir/test/Dialect/EmitC/invalid_ops.mlir
index 0d878e90cdf0c..79f2c0b210be9 100644
--- a/mlir/test/Dialect/EmitC/invalid_ops.mlir
+++ b/mlir/test/Dialect/EmitC/invalid_ops.mlir
@@ -48,6 +48,14 @@ func.func @index_args_out_of_range_2(%arg : i32) {
 
 // -----
 
+func.func @member_call_no_operands() {
+    // expected-error @+1 {{'emitc.call_opaque' op member call requires at least one operand (receiver)}}
+    emitc.call_opaque "method" () {is_member_call = true} : () -> ()
+    return
+}
+
+// -----
+
 func.func @empty_callee() {
     // expected-error @+1 {{'emitc.call_opaque' op callee must not be empty}}
     emitc.call_opaque "" () : () -> ()
diff --git a/mlir/test/Target/Cpp/call.mlir b/mlir/test/Target/Cpp/call.mlir
index e3ac392f30b62..c447b149f84c3 100644
--- a/mlir/test/Target/Cpp/call.mlir
+++ b/mlir/test/Target/Cpp/call.mlir
@@ -34,3 +34,27 @@ func.func @emitc_call_opaque_two_results() {
 // CPP-DECLTOP-NEXT: int32_t [[V3:[^ ]*]];
 // CPP-DECLTOP-NEXT: [[V1]] = 0;
 // CPP-DECLTOP-NEXT: std::tie([[V2]], [[V3]]) = two_results();
+
+func.func @emitc_call_opaque_member(%arg0 : !emitc.opaque<"MyClass">, %arg1 : !emitc.ptr<!emitc.opaque<"MyClass">>) {
+  %0 = emitc.call_opaque "method" (%arg0) {is_member_call = true} : (!emitc.opaque<"MyClass">) -> i32
+  %1 = emitc.call_opaque "ptr_method" (%arg1) {is_member_call = true} : (!emitc.ptr<!emitc.opaque<"MyClass">>) -> i32
+  return
+}
+// CPP-DEFAULT: void emitc_call_opaque_member(MyClass [[V0:[^ ]*]], MyClass* [[V1:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: int32_t [[V2:[^ ]*]] = [[V0]].method();
+// CPP-DEFAULT-NEXT: int32_t [[V3:[^ ]*]] = [[V1]]->ptr_method();
+
+func.func @emitc_call_opaque_member_args(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
+  %0 = emitc.call_opaque "method" (%arg0, %arg1, %arg2) {is_member_call = true} : (!emitc.opaque<"MyClass">, i32, i32) -> i32
+  %1 = emitc.call_opaque "method_with_args" (%arg0, %arg1, %arg2) {is_member_call = true, args = [1 : index, 2 : index]} : (!emitc.opaque<"MyClass">, i32, i32) -> i32
+  return
+}
+// CPP-DEFAULT: void emitc_call_opaque_member_args(MyClass [[V0:[^ ]*]], int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: int32_t [[V3:[^ ]*]] = [[V0]].method([[V1]], [[V2]]);
+// CPP-DEFAULT-NEXT: int32_t [[V4:[^ ]*]] = [[V0]].method_with_args([[V1]], [[V2]]);
+
+// CPP-DECLTOP: void emitc_call_opaque_member_args(MyClass [[V0:[^ ]*]], int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
+// CPP-DECLTOP-NEXT: int32_t [[V3:[^ ]*]];
+// CPP-DECLTOP-NEXT: int32_t [[V4:[^ ]*]];
+// CPP-DECLTOP-NEXT: [[V3]] = [[V0]].method([[V1]], [[V2]]);
+// CPP-DECLTOP-NEXT: [[V4]] = [[V0]].method_with_args([[V1]], [[V2]]);

>From de71b7b2705e0a06cfe453f43cf67a226e62d629 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Thu, 28 May 2026 10:50:52 -0700
Subject: [PATCH 2/8] Revert "[mlir][emitc]: support method calls in
 CallOpaqueOp"

This reverts commit 54c78bae731103d2719514ae893ab5e72596ec04.
---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td | 22 +-------------
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp         |  3 --
 mlir/lib/Target/Cpp/TranslateToCpp.cpp      | 33 +++------------------
 mlir/test/Dialect/EmitC/invalid_ops.mlir    |  8 -----
 mlir/test/Target/Cpp/call.mlir              | 24 ---------------
 5 files changed, 5 insertions(+), 85 deletions(-)

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index dc83b85993a99..e1ccf29ada660 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -324,19 +324,6 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
     - integer value of index type refers to an operand;
     - attribute which will get lowered to constant value in call;
 
-    If `is_member_call` is true, the operation represents a member call. The
-    first operand is treated as the receiver and emitted before the member
-    access operator (`.` or `->`).
-
-    The `args` attribute can be used to override the default operand order and
-    to mix operands with attributes. If `args` is present, it specifies the
-    arguments passed to the call in parentheses.
-
-    If `args` is not present:
-    - If `is_member_call` is true, the first operand is the receiver, and the
-      remaining operands are passed as arguments.
-    - If `is_member_call` is false, all operands are passed as arguments.
-
     Example:
 
     ```mlir
@@ -345,19 +332,12 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
 
     // Generic form of the same operation.
     %0 = "emitc.call_opaque"() {callee = "foo"} : () -> i32
-
-    // Member call
-    %1 = emitc.call_opaque "method" (%arg0, %arg1) {is_member_call = true} : (!emitc.opaque<"MyClass">, i32) -> i32
-
-    // Member call with args attribute
-    %2 = emitc.call_opaque "method" (%arg0, %arg1) {is_member_call = true, args = [1 : index]} : (!emitc.opaque<"MyClass">, i32) -> i32
     ```
   }];
   let arguments = (ins
     Arg<StrAttr, "the C++ function to call">:$callee,
     Arg<OptionalAttr<ArrayAttr>, "the order of operands and further attributes">:$args,
     Arg<OptionalAttr<ArrayAttr>, "template arguments">:$template_args,
-    DefaultValuedAttr<BoolAttr, "false">:$is_member_call,
     Variadic<EmitCType>:$operands
   );
   let results = (outs Variadic<EmitCType>);
@@ -369,7 +349,7 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
       CArg<"::mlir::ArrayAttr", "{}">:$args,
       CArg<"::mlir::ArrayAttr", "{}">:$template_args), [{
         build($_builder, $_state, resultTypes, callee, args, template_args,
-            false, operands);
+            operands);
       }]
     >
   ];
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index cd27a97941e10..36394e67008da 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -345,9 +345,6 @@ LogicalResult emitc::CallOpaqueOp::verify() {
   if (getCallee().empty())
     return emitOpError("callee must not be empty");
 
-  if (getIsMemberCall() && getNumOperands() == 0)
-    return emitOpError("member call requires at least one operand (receiver)");
-
   if (std::optional<ArrayAttr> argsAttr = getArgs()) {
     for (Attribute arg : *argsAttr) {
       auto intAttr = llvm::dyn_cast<IntegerAttr>(arg);
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index 71ed42b7d5aa2..a670fb152a1e5 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -916,23 +916,6 @@ static LogicalResult printOperation(CppEmitter &emitter,
 
   if (failed(emitter.emitAssignPrefix(op)))
     return failure();
-
-  bool isMemberCall = callOpaqueOp.getIsMemberCall();
-
-  if (isMemberCall) {
-    Value receiver = op.getOperand(0);
-    bool isPtr = llvm::isa<emitc::PointerType>(receiver.getType());
-
-    if (failed(emitter.emitOperand(receiver)))
-      return failure();
-
-    if (isPtr) {
-      os << "->";
-    } else {
-      os << ".";
-    }
-  }
-
   os << callOpaqueOp.getCallee();
 
   // Template arguments can't refer to SSA values and as such the template
@@ -968,18 +951,10 @@ static LogicalResult printOperation(CppEmitter &emitter,
 
   os << "(";
 
-  LogicalResult emittedArgs = success();
-  if (callOpaqueOp.getArgs()) {
-    emittedArgs =
-        interleaveCommaWithError(*callOpaqueOp.getArgs(), os, emitArgs);
-  } else if (isMemberCall) {
-    emittedArgs = interleaveCommaWithError(
-        llvm::seq<size_t>(1, op.getNumOperands()), os,
-        [&](size_t i) { return emitter.emitOperand(op.getOperand(i)); });
-  } else {
-    emittedArgs = emitter.emitOperands(op);
-  }
-
+  LogicalResult emittedArgs =
+      callOpaqueOp.getArgs()
+          ? interleaveCommaWithError(*callOpaqueOp.getArgs(), os, emitArgs)
+          : emitter.emitOperands(op);
   if (failed(emittedArgs))
     return failure();
   os << ")";
diff --git a/mlir/test/Dialect/EmitC/invalid_ops.mlir b/mlir/test/Dialect/EmitC/invalid_ops.mlir
index 79f2c0b210be9..0d878e90cdf0c 100644
--- a/mlir/test/Dialect/EmitC/invalid_ops.mlir
+++ b/mlir/test/Dialect/EmitC/invalid_ops.mlir
@@ -48,14 +48,6 @@ func.func @index_args_out_of_range_2(%arg : i32) {
 
 // -----
 
-func.func @member_call_no_operands() {
-    // expected-error @+1 {{'emitc.call_opaque' op member call requires at least one operand (receiver)}}
-    emitc.call_opaque "method" () {is_member_call = true} : () -> ()
-    return
-}
-
-// -----
-
 func.func @empty_callee() {
     // expected-error @+1 {{'emitc.call_opaque' op callee must not be empty}}
     emitc.call_opaque "" () : () -> ()
diff --git a/mlir/test/Target/Cpp/call.mlir b/mlir/test/Target/Cpp/call.mlir
index c447b149f84c3..e3ac392f30b62 100644
--- a/mlir/test/Target/Cpp/call.mlir
+++ b/mlir/test/Target/Cpp/call.mlir
@@ -34,27 +34,3 @@ func.func @emitc_call_opaque_two_results() {
 // CPP-DECLTOP-NEXT: int32_t [[V3:[^ ]*]];
 // CPP-DECLTOP-NEXT: [[V1]] = 0;
 // CPP-DECLTOP-NEXT: std::tie([[V2]], [[V3]]) = two_results();
-
-func.func @emitc_call_opaque_member(%arg0 : !emitc.opaque<"MyClass">, %arg1 : !emitc.ptr<!emitc.opaque<"MyClass">>) {
-  %0 = emitc.call_opaque "method" (%arg0) {is_member_call = true} : (!emitc.opaque<"MyClass">) -> i32
-  %1 = emitc.call_opaque "ptr_method" (%arg1) {is_member_call = true} : (!emitc.ptr<!emitc.opaque<"MyClass">>) -> i32
-  return
-}
-// CPP-DEFAULT: void emitc_call_opaque_member(MyClass [[V0:[^ ]*]], MyClass* [[V1:[^ ]*]]) {
-// CPP-DEFAULT-NEXT: int32_t [[V2:[^ ]*]] = [[V0]].method();
-// CPP-DEFAULT-NEXT: int32_t [[V3:[^ ]*]] = [[V1]]->ptr_method();
-
-func.func @emitc_call_opaque_member_args(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
-  %0 = emitc.call_opaque "method" (%arg0, %arg1, %arg2) {is_member_call = true} : (!emitc.opaque<"MyClass">, i32, i32) -> i32
-  %1 = emitc.call_opaque "method_with_args" (%arg0, %arg1, %arg2) {is_member_call = true, args = [1 : index, 2 : index]} : (!emitc.opaque<"MyClass">, i32, i32) -> i32
-  return
-}
-// CPP-DEFAULT: void emitc_call_opaque_member_args(MyClass [[V0:[^ ]*]], int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
-// CPP-DEFAULT-NEXT: int32_t [[V3:[^ ]*]] = [[V0]].method([[V1]], [[V2]]);
-// CPP-DEFAULT-NEXT: int32_t [[V4:[^ ]*]] = [[V0]].method_with_args([[V1]], [[V2]]);
-
-// CPP-DECLTOP: void emitc_call_opaque_member_args(MyClass [[V0:[^ ]*]], int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
-// CPP-DECLTOP-NEXT: int32_t [[V3:[^ ]*]];
-// CPP-DECLTOP-NEXT: int32_t [[V4:[^ ]*]];
-// CPP-DECLTOP-NEXT: [[V3]] = [[V0]].method([[V1]], [[V2]]);
-// CPP-DECLTOP-NEXT: [[V4]] = [[V0]].method_with_args([[V1]], [[V2]]);

>From 4c92de38ef839e276c32c2d848833496b8c37228 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Thu, 28 May 2026 12:26:50 -0700
Subject: [PATCH 3/8] [mlir][emitc]: add MemberCallOpaque op

---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td   | 33 +++++++++
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp           | 36 +++++++++
 mlir/lib/Target/Cpp/TranslateToCpp.cpp        | 74 ++++++++++++++++++-
 mlir/test/Dialect/EmitC/invalid_ops.mlir      | 40 ++++++++++
 .../Dialect/EmitC/member_call_opaque.mlir     | 29 ++++++++
 mlir/test/Target/Cpp/call.mlir                | 30 ++++++++
 6 files changed, 238 insertions(+), 4 deletions(-)
 create mode 100644 mlir/test/Dialect/EmitC/member_call_opaque.mlir

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index e1ccf29ada660..c0a9b4523256d 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -360,6 +360,39 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
   let hasVerifier = 1;
 }
 
+def EmitC_MemberCallOpaqueOp : EmitC_Op<"member_call_opaque", [CExpressionInterface]> {
+  let summary = "Opaque member call operation";
+  let description = [{
+    The `emitc.member_call_opaque` operation represents a C++ member function
+    call. It takes a receiver operand, a callee string attribute (the method
+    name), and variadic operands for arguments.
+
+    The call allows specifying order of operands and attributes in the call as
+    follows:
+    - integer value of index type refers to an argument operand;
+    - attribute which will get lowered to constant value in call;
+
+    Example:
+
+    ```mlir
+    %0 = emitc.member_call_opaque %receiver "method" (%arg0, %arg1) : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+    ```
+  }];
+  let arguments = (ins
+    EmitCType:$receiver,
+    StrAttr:$callee,
+    OptionalAttr<ArrayAttr>:$args,
+    OptionalAttr<ArrayAttr>:$template_args,
+    Variadic<EmitCType>:$args_operands
+  );
+  let results = (outs Variadic<EmitCType>);
+
+  let assemblyFormat = [{
+    $receiver $callee `(` $args_operands `)` attr-dict `:` type($receiver) `,` functional-type($args_operands, results)
+  }];
+  let hasVerifier = 1;
+}
+
 def EmitC_CastOp : EmitC_Op<"cast",
     [CExpressionInterface,
      DeclareOpInterfaceMethods<CastOpInterface>]> {
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index 36394e67008da..80f856f5a1234 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -378,6 +378,42 @@ LogicalResult emitc::CallOpaqueOp::verify() {
   return success();
 }
 
+LogicalResult emitc::MemberCallOpaqueOp::verify() {
+  // Callee must not be empty.
+  if (getCallee().empty())
+    return emitOpError("callee must not be empty");
+
+  if (std::optional<ArrayAttr> argsAttr = getArgs()) {
+    for (Attribute arg : *argsAttr) {
+      auto intAttr = llvm::dyn_cast<IntegerAttr>(arg);
+      if (intAttr && llvm::isa<IndexType>(intAttr.getType())) {
+        int64_t index = intAttr.getInt();
+        // Args with elements of type index must be in range
+        // [0..args_operands.size).
+        if ((index < 0) ||
+            (index >= static_cast<int64_t>(getArgsOperands().size())))
+          return emitOpError("index argument is out of range");
+
+      } else if (llvm::isa<ArrayAttr>(arg)) {
+        return emitOpError("array argument has no type");
+      }
+    }
+  }
+
+  if (std::optional<ArrayAttr> templateArgsAttr = getTemplateArgs()) {
+    for (Attribute tArg : *templateArgsAttr) {
+      if (!llvm::isa<TypeAttr, IntegerAttr, FloatAttr, emitc::OpaqueAttr>(tArg))
+        return emitOpError("template argument has invalid type");
+    }
+  }
+
+  if (llvm::any_of(getResultTypes(), llvm::IsaPred<ArrayType>)) {
+    return emitOpError() << "cannot return array type";
+  }
+
+  return success();
+}
+
 //===----------------------------------------------------------------------===//
 // ConstantOp
 //===----------------------------------------------------------------------===//
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index a670fb152a1e5..7ab1062bf0820 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -961,6 +961,71 @@ static LogicalResult printOperation(CppEmitter &emitter,
   return success();
 }
 
+static LogicalResult
+printOperation(CppEmitter &emitter,
+               emitc::MemberCallOpaqueOp memberCallOpaqueOp) {
+  raw_ostream &os = emitter.ostream();
+  Operation &op = *memberCallOpaqueOp.getOperation();
+
+  if (failed(emitter.emitAssignPrefix(op)))
+    return failure();
+
+  Value receiver = memberCallOpaqueOp.getReceiver();
+  if (failed(emitter.emitOperand(receiver)))
+    return failure();
+
+  if (llvm::isa<emitc::PointerType>(receiver.getType()))
+    os << "->";
+  else
+    os << ".";
+
+  os << memberCallOpaqueOp.getCallee();
+
+  auto emitTemplateArgs = [&](Attribute attr) -> LogicalResult {
+    return emitter.emitAttribute(op.getLoc(), attr);
+  };
+
+  if (memberCallOpaqueOp.getTemplateArgs()) {
+    os << "<";
+    if (failed(interleaveCommaWithError(*memberCallOpaqueOp.getTemplateArgs(),
+                                        os, emitTemplateArgs)))
+      return failure();
+    os << ">";
+  }
+
+  auto emitArgs = [&](Attribute attr) -> LogicalResult {
+    if (auto t = dyn_cast<IntegerAttr>(attr)) {
+      if (t.getType().isIndex()) {
+        int64_t idx = t.getInt();
+        Value operand = op.getOperand(idx + 1);
+        return emitter.emitOperand(operand, /*isInBrackets=*/true);
+      }
+    }
+    if (failed(emitter.emitAttribute(op.getLoc(), attr)))
+      return failure();
+
+    return success();
+  };
+
+  os << "(";
+
+  LogicalResult emittedArgs = success();
+  if (memberCallOpaqueOp.getArgs()) {
+    emittedArgs =
+        interleaveCommaWithError(*memberCallOpaqueOp.getArgs(), os, emitArgs);
+  } else {
+    auto operands = op.getOperands().drop_front(1);
+    emittedArgs = interleaveCommaWithError(operands, os, [&](Value operand) {
+      return emitter.emitOperand(operand, /*isInBrackets=*/true);
+    });
+  }
+
+  if (failed(emittedArgs))
+    return failure();
+  os << ")";
+  return success();
+}
+
 static LogicalResult printOperation(CppEmitter &emitter,
                                     emitc::ApplyOp applyOp) {
   raw_ostream &os = emitter.ostream();
@@ -1875,10 +1940,11 @@ LogicalResult CppEmitter::emitOperation(Operation &op, bool trailingSemicolon) {
                 emitc::GetGlobalOp, emitc::GlobalOp, emitc::IfOp,
                 emitc::IncludeOp, emitc::LiteralOp, emitc::LoadOp,
                 emitc::LogicalAndOp, emitc::LogicalNotOp, emitc::LogicalOrOp,
-                emitc::MemberOfPtrOp, emitc::MemberOp, emitc::MulOp,
-                emitc::RemOp, emitc::ReturnOp, emitc::SubscriptOp, emitc::SubOp,
-                emitc::SwitchOp, emitc::UnaryMinusOp, emitc::UnaryPlusOp,
-                emitc::VariableOp, emitc::VerbatimOp>(
+                emitc::MemberCallOpaqueOp, emitc::MemberOfPtrOp,
+                emitc::MemberOp, emitc::MulOp, emitc::RemOp, emitc::ReturnOp,
+                emitc::SubscriptOp, emitc::SubOp, emitc::SwitchOp,
+                emitc::UnaryMinusOp, emitc::UnaryPlusOp, emitc::VariableOp,
+                emitc::VerbatimOp>(
 
               [&](auto op) { return printOperation(*this, op); })
           // Func ops.
diff --git a/mlir/test/Dialect/EmitC/invalid_ops.mlir b/mlir/test/Dialect/EmitC/invalid_ops.mlir
index 0d878e90cdf0c..1c15af89bdbcd 100644
--- a/mlir/test/Dialect/EmitC/invalid_ops.mlir
+++ b/mlir/test/Dialect/EmitC/invalid_ops.mlir
@@ -88,6 +88,46 @@ func.func @array_result() {
 
 // -----
 
+func.func @member_call_empty_callee(%arg0 : !emitc.opaque<"MyClass">) {
+    // expected-error @+1 {{'emitc.member_call_opaque' op callee must not be empty}}
+    emitc.member_call_opaque %arg0 "" () : !emitc.opaque<"MyClass">, () -> ()
+    return
+}
+
+// -----
+
+func.func @member_call_index_out_of_range(%arg0 : !emitc.opaque<"MyClass">) {
+    // expected-error @+1 {{'emitc.member_call_opaque' op index argument is out of range}}
+    emitc.member_call_opaque %arg0 "test" () {args = [1 : index]} : !emitc.opaque<"MyClass">, () -> ()
+    return
+}
+
+// -----
+
+func.func @member_call_array_result(%arg0 : !emitc.opaque<"MyClass">) {
+    // expected-error @+1 {{'emitc.member_call_opaque' op cannot return array type}}
+    emitc.member_call_opaque %arg0 "array_result"() : !emitc.opaque<"MyClass">, () -> !emitc.array<4xi32>
+    return
+}
+
+// -----
+
+func.func @member_call_nonetype_template_arg(%arg0 : !emitc.opaque<"MyClass">) {
+    // expected-error @+1 {{'emitc.member_call_opaque' op template argument has invalid type}}
+    emitc.member_call_opaque %arg0 "nonetype_template_arg"() {template_args = [[0, 1, 2]]} : !emitc.opaque<"MyClass">, () -> ()
+    return
+}
+
+// -----
+
+func.func @member_call_dense_template_argument(%arg0 : !emitc.opaque<"MyClass">) {
+    // expected-error @+1 {{'emitc.member_call_opaque' op template argument has invalid type}}
+    emitc.member_call_opaque %arg0 "dense_template_argument"() {template_args = [dense<[1.0, 1.0]> : tensor<2xf32>]} : !emitc.opaque<"MyClass">, () -> ()
+    return
+}
+
+// -----
+
 func.func @empty_operator() {
     %0 = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<i32>
     // expected-error @+1 {{'emitc.apply' op applicable operator must not be empty}}
diff --git a/mlir/test/Dialect/EmitC/member_call_opaque.mlir b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
new file mode 100644
index 0000000000000..e7a0a0be5eef6
--- /dev/null
+++ b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
@@ -0,0 +1,29 @@
+// RUN: mlir-opt %s | mlir-opt | FileCheck %s
+
+func.func @member_call(%arg0 : !emitc.opaque<"MyClass">) {
+  %0 = emitc.member_call_opaque %arg0 "method" () : !emitc.opaque<"MyClass">, () -> i32
+  return
+}
+// CHECK-LABEL: func @member_call
+// CHECK: emitc.member_call_opaque %arg0 "method"() : !emitc.opaque<"MyClass">, () -> i32
+
+func.func @member_call_args(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32) {
+  %0 = emitc.member_call_opaque %arg0 "method" (%arg1) : !emitc.opaque<"MyClass">, (i32) -> i32
+  return
+}
+// CHECK-LABEL: func @member_call_args
+// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1) : !emitc.opaque<"MyClass">, (i32) -> i32
+
+func.func @member_call_template_args(%arg0 : !emitc.opaque<"MyClass">) {
+  %0 = emitc.member_call_opaque %arg0 "method" () {template_args = [i32]} : !emitc.opaque<"MyClass">, () -> i32
+  return
+}
+// CHECK-LABEL: func @member_call_template_args
+// CHECK: emitc.member_call_opaque %arg0 "method"() {template_args = [i32]} : !emitc.opaque<"MyClass">, () -> i32
+
+func.func @member_call_reorder(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
+  %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) {args = [0 : index, 2 : index, 1 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+  return
+}
+// CHECK-LABEL: func @member_call_reorder
+// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) {args = [0 : index, 2 : index, 1 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
diff --git a/mlir/test/Target/Cpp/call.mlir b/mlir/test/Target/Cpp/call.mlir
index e3ac392f30b62..de279e667cac0 100644
--- a/mlir/test/Target/Cpp/call.mlir
+++ b/mlir/test/Target/Cpp/call.mlir
@@ -34,3 +34,33 @@ func.func @emitc_call_opaque_two_results() {
 // CPP-DECLTOP-NEXT: int32_t [[V3:[^ ]*]];
 // CPP-DECLTOP-NEXT: [[V1]] = 0;
 // CPP-DECLTOP-NEXT: std::tie([[V2]], [[V3]]) = two_results();
+
+func.func @emitc_member_call(%arg0 : !emitc.opaque<"MyClass">, %arg1 : !emitc.ptr<!emitc.opaque<"MyClass">> ) {
+  %0 = emitc.member_call_opaque %arg0 "method" () : !emitc.opaque<"MyClass">, () -> i32
+  %1 = emitc.member_call_opaque %arg1 "ptr_method" () : !emitc.ptr<!emitc.opaque<"MyClass">>, () -> i32
+  return
+}
+// CPP-DEFAULT: void emitc_member_call(MyClass [[V0:[^ ]*]], MyClass* [[V1:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: int32_t [[V2:[^ ]*]] = [[V0]].method();
+// CPP-DEFAULT-NEXT: int32_t [[V3:[^ ]*]] = [[V1]]->ptr_method();
+
+func.func @emitc_member_call_args(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32) {
+  %0 = emitc.member_call_opaque %arg0 "method" (%arg1) : !emitc.opaque<"MyClass">, (i32) -> i32
+  return
+}
+// CPP-DEFAULT: void emitc_member_call_args(MyClass [[V0:[^ ]*]], int32_t [[V1:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: int32_t [[V2:[^ ]*]] = [[V0]].method([[V1]]);
+
+func.func @emitc_member_call_args_reorder(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
+  %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) {args = [1 : index, 0 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+  return
+}
+// CPP-DEFAULT: void emitc_member_call_args_reorder(MyClass [[V0:[^ ]*]], int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: int32_t [[V3:[^ ]*]] = [[V0]].method([[V2]], [[V1]]);
+
+func.func @emitc_member_call_template_args(%arg0 : !emitc.opaque<"MyClass">) {
+  %0 = emitc.member_call_opaque %arg0 "method" () {template_args = [i32]} : !emitc.opaque<"MyClass">, () -> i32
+  return
+}
+// CPP-DEFAULT: void emitc_member_call_template_args(MyClass [[V0:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: int32_t [[V1:[^ ]*]] = [[V0]].method<int32_t>();

>From d332e89d3ae73655f90e1c96e6be85e4dd44c903 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Thu, 28 May 2026 13:45:00 -0700
Subject: [PATCH 4/8] refactor common logic among verifier and emitter

---
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp           |  81 +++++-------
 mlir/lib/Target/Cpp/TranslateToCpp.cpp        | 120 +++++++-----------
 .../Dialect/EmitC/member_call_opaque.mlir     |   4 +-
 3 files changed, 78 insertions(+), 127 deletions(-)

diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index 80f856f5a1234..d57bf5e2faf55 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -340,78 +340,55 @@ bool CastOp::areCastCompatible(TypeRange inputs, TypeRange outputs) {
 // CallOpaqueOp
 //===----------------------------------------------------------------------===//
 
-LogicalResult emitc::CallOpaqueOp::verify() {
+static LogicalResult
+verifyOpaqueCallCommon(Operation *op, StringRef callee,
+                       std::optional<ArrayAttr> args,
+                       std::optional<ArrayAttr> templateArgs,
+                       TypeRange resultTypes, size_t numArgsOperands) {
   // Callee must not be empty.
-  if (getCallee().empty())
-    return emitOpError("callee must not be empty");
+  if (callee.empty())
+    return op->emitOpError("callee must not be empty");
 
-  if (std::optional<ArrayAttr> argsAttr = getArgs()) {
-    for (Attribute arg : *argsAttr) {
+  if (args) {
+    for (Attribute arg : *args) {
       auto intAttr = llvm::dyn_cast<IntegerAttr>(arg);
       if (intAttr && llvm::isa<IndexType>(intAttr.getType())) {
         int64_t index = intAttr.getInt();
         // Args with elements of type index must be in range
-        // [0..operands.size).
-        if ((index < 0) || (index >= static_cast<int64_t>(getNumOperands())))
-          return emitOpError("index argument is out of range");
-
-        // Args with elements of type ArrayAttr must have a type.
-      } else if (llvm::isa<ArrayAttr>(
-                     arg) /*&& llvm::isa<NoneType>(arg.getType())*/) {
-        // FIXME: Array attributes never have types
-        return emitOpError("array argument has no type");
+        // [0..numArgsOperands).
+        if ((index < 0) || (index >= static_cast<int64_t>(numArgsOperands)))
+          return op->emitOpError("index argument is out of range");
+
+      } else if (llvm::isa<ArrayAttr>(arg)) {
+        return op->emitOpError("array argument has no type");
       }
     }
   }
 
-  if (std::optional<ArrayAttr> templateArgsAttr = getTemplateArgs()) {
-    for (Attribute tArg : *templateArgsAttr) {
+  if (templateArgs) {
+    for (Attribute tArg : *templateArgs) {
       if (!llvm::isa<TypeAttr, IntegerAttr, FloatAttr, emitc::OpaqueAttr>(tArg))
-        return emitOpError("template argument has invalid type");
+        return op->emitOpError("template argument has invalid type");
     }
   }
 
-  if (llvm::any_of(getResultTypes(), llvm::IsaPred<ArrayType>)) {
-    return emitOpError() << "cannot return array type";
+  if (llvm::any_of(resultTypes, llvm::IsaPred<ArrayType>)) {
+    return op->emitOpError() << "cannot return array type";
   }
 
   return success();
 }
 
-LogicalResult emitc::MemberCallOpaqueOp::verify() {
-  // Callee must not be empty.
-  if (getCallee().empty())
-    return emitOpError("callee must not be empty");
-
-  if (std::optional<ArrayAttr> argsAttr = getArgs()) {
-    for (Attribute arg : *argsAttr) {
-      auto intAttr = llvm::dyn_cast<IntegerAttr>(arg);
-      if (intAttr && llvm::isa<IndexType>(intAttr.getType())) {
-        int64_t index = intAttr.getInt();
-        // Args with elements of type index must be in range
-        // [0..args_operands.size).
-        if ((index < 0) ||
-            (index >= static_cast<int64_t>(getArgsOperands().size())))
-          return emitOpError("index argument is out of range");
-
-      } else if (llvm::isa<ArrayAttr>(arg)) {
-        return emitOpError("array argument has no type");
-      }
-    }
-  }
-
-  if (std::optional<ArrayAttr> templateArgsAttr = getTemplateArgs()) {
-    for (Attribute tArg : *templateArgsAttr) {
-      if (!llvm::isa<TypeAttr, IntegerAttr, FloatAttr, emitc::OpaqueAttr>(tArg))
-        return emitOpError("template argument has invalid type");
-    }
-  }
-
-  if (llvm::any_of(getResultTypes(), llvm::IsaPred<ArrayType>)) {
-    return emitOpError() << "cannot return array type";
-  }
+LogicalResult emitc::CallOpaqueOp::verify() {
+  return verifyOpaqueCallCommon(getOperation(), getCallee(), getArgs(),
+                                getTemplateArgs(), getResultTypes(),
+                                getNumOperands());
+}
 
-  return success();
+LogicalResult emitc::MemberCallOpaqueOp::verify() {
+  return verifyOpaqueCallCommon(getOperation(), getCallee(), getArgs(),
+                                getTemplateArgs(), getResultTypes(),
+                                getArgsOperands().size());
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index 7ab1062bf0820..595ff07f99455 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -909,86 +909,40 @@ static LogicalResult printOperation(CppEmitter &emitter, emitc::CallOp callOp) {
   return printCallOperation(emitter, operation, callee);
 }
 
-static LogicalResult printOperation(CppEmitter &emitter,
-                                    emitc::CallOpaqueOp callOpaqueOp) {
-  raw_ostream &os = emitter.ostream();
-  Operation &op = *callOpaqueOp.getOperation();
-
-  if (failed(emitter.emitAssignPrefix(op)))
-    return failure();
-  os << callOpaqueOp.getCallee();
-
-  // Template arguments can't refer to SSA values and as such the template
-  // arguments which are supplied in form of attributes can be emitted as is. We
-  // don't need to handle integer attributes specially like we do for arguments
-  // - see below.
-  auto emitTemplateArgs = [&](Attribute attr) -> LogicalResult {
-    return emitter.emitAttribute(op.getLoc(), attr);
-  };
-
-  if (callOpaqueOp.getTemplateArgs()) {
-    os << "<";
-    if (failed(interleaveCommaWithError(*callOpaqueOp.getTemplateArgs(), os,
-                                        emitTemplateArgs)))
-      return failure();
-    os << ">";
-  }
-
-  auto emitArgs = [&](Attribute attr) -> LogicalResult {
-    if (auto t = dyn_cast<IntegerAttr>(attr)) {
-      // Index attributes are treated specially as operand index.
-      if (t.getType().isIndex()) {
-        int64_t idx = t.getInt();
-        Value operand = op.getOperand(idx);
-        return emitter.emitOperand(operand);
-      }
-    }
-    if (failed(emitter.emitAttribute(op.getLoc(), attr)))
-      return failure();
-
-    return success();
-  };
-
-  os << "(";
-
-  LogicalResult emittedArgs =
-      callOpaqueOp.getArgs()
-          ? interleaveCommaWithError(*callOpaqueOp.getArgs(), os, emitArgs)
-          : emitter.emitOperands(op);
-  if (failed(emittedArgs))
-    return failure();
-  os << ")";
-  return success();
-}
-
 static LogicalResult
-printOperation(CppEmitter &emitter,
-               emitc::MemberCallOpaqueOp memberCallOpaqueOp) {
+printOpaqueCallCommon(CppEmitter &emitter, Operation &op, StringRef callee,
+                      std::optional<ArrayAttr> templateArgs,
+                      std::optional<ArrayAttr> args, bool isMemberCall,
+                      Value receiver = nullptr) {
   raw_ostream &os = emitter.ostream();
-  Operation &op = *memberCallOpaqueOp.getOperation();
 
   if (failed(emitter.emitAssignPrefix(op)))
     return failure();
 
-  Value receiver = memberCallOpaqueOp.getReceiver();
-  if (failed(emitter.emitOperand(receiver)))
-    return failure();
+  if (isMemberCall) {
+    assert(receiver && "Expected receiver for member call");
+    if (failed(emitter.emitOperand(receiver)))
+      return failure();
 
-  if (llvm::isa<emitc::PointerType>(receiver.getType()))
-    os << "->";
-  else
-    os << ".";
+    if (llvm::isa<emitc::PointerType>(receiver.getType()))
+      os << "->";
+    else
+      os << ".";
+  }
 
-  os << memberCallOpaqueOp.getCallee();
+  os << callee;
 
+  // Template arguments can't refer to SSA values and as such the template
+  // arguments which are supplied in form of attributes can be emitted as is. We
+  // don't need to handle integer attributes specially like we do for arguments
+  // - see below.
   auto emitTemplateArgs = [&](Attribute attr) -> LogicalResult {
     return emitter.emitAttribute(op.getLoc(), attr);
   };
 
-  if (memberCallOpaqueOp.getTemplateArgs()) {
+  if (templateArgs) {
     os << "<";
-    if (failed(interleaveCommaWithError(*memberCallOpaqueOp.getTemplateArgs(),
-                                        os, emitTemplateArgs)))
+    if (failed(interleaveCommaWithError(*templateArgs, os, emitTemplateArgs)))
       return failure();
     os << ">";
   }
@@ -997,8 +951,9 @@ printOperation(CppEmitter &emitter,
     if (auto t = dyn_cast<IntegerAttr>(attr)) {
       if (t.getType().isIndex()) {
         int64_t idx = t.getInt();
-        Value operand = op.getOperand(idx + 1);
-        return emitter.emitOperand(operand, /*isInBrackets=*/true);
+        // Shift index by 1 for member calls to skip the receiver operand.
+        Value operand = op.getOperand(isMemberCall ? idx + 1 : idx);
+        return emitter.emitOperand(operand, /*isInBrackets=*/false);
       }
     }
     if (failed(emitter.emitAttribute(op.getLoc(), attr)))
@@ -1010,22 +965,41 @@ printOperation(CppEmitter &emitter,
   os << "(";
 
   LogicalResult emittedArgs = success();
-  if (memberCallOpaqueOp.getArgs()) {
-    emittedArgs =
-        interleaveCommaWithError(*memberCallOpaqueOp.getArgs(), os, emitArgs);
+  if (args) {
+    emittedArgs = interleaveCommaWithError(*args, os, emitArgs);
   } else {
-    auto operands = op.getOperands().drop_front(1);
+    auto operands = op.getOperands();
+    if (isMemberCall)
+      operands = operands.drop_front(1);
+
     emittedArgs = interleaveCommaWithError(operands, os, [&](Value operand) {
       return emitter.emitOperand(operand, /*isInBrackets=*/true);
     });
   }
-
   if (failed(emittedArgs))
     return failure();
   os << ")";
   return success();
 }
 
+static LogicalResult printOperation(CppEmitter &emitter,
+                                    emitc::CallOpaqueOp callOpaqueOp) {
+  return printOpaqueCallCommon(
+      emitter, *callOpaqueOp.getOperation(), callOpaqueOp.getCallee(),
+      callOpaqueOp.getTemplateArgs(), callOpaqueOp.getArgs(),
+      /*isMemberCall=*/false);
+}
+
+static LogicalResult
+printOperation(CppEmitter &emitter,
+               emitc::MemberCallOpaqueOp memberCallOpaqueOp) {
+  return printOpaqueCallCommon(
+      emitter, *memberCallOpaqueOp.getOperation(),
+      memberCallOpaqueOp.getCallee(), memberCallOpaqueOp.getTemplateArgs(),
+      memberCallOpaqueOp.getArgs(),
+      /*isMemberCall=*/true, memberCallOpaqueOp.getReceiver());
+}
+
 static LogicalResult printOperation(CppEmitter &emitter,
                                     emitc::ApplyOp applyOp) {
   raw_ostream &os = emitter.ostream();
diff --git a/mlir/test/Dialect/EmitC/member_call_opaque.mlir b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
index e7a0a0be5eef6..2413da6f740d2 100644
--- a/mlir/test/Dialect/EmitC/member_call_opaque.mlir
+++ b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
@@ -22,8 +22,8 @@ func.func @member_call_template_args(%arg0 : !emitc.opaque<"MyClass">) {
 // CHECK: emitc.member_call_opaque %arg0 "method"() {template_args = [i32]} : !emitc.opaque<"MyClass">, () -> i32
 
 func.func @member_call_reorder(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
-  %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) {args = [0 : index, 2 : index, 1 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+  %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) {args = [1 : index, 0 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
   return
 }
 // CHECK-LABEL: func @member_call_reorder
-// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) {args = [0 : index, 2 : index, 1 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) {args = [1 : index, 0 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32

>From ae0430003090932020d785bd7ce9185f22f81152 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Thu, 28 May 2026 20:31:00 -0700
Subject: [PATCH 5/8] use prop-dict attr-dict on both member_call_opaque and
 call_opaque

---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td     | 4 ++--
 mlir/test/Dialect/EmitC/member_call_opaque.mlir | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index c0a9b4523256d..c15e8b415f83d 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -355,7 +355,7 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
   ];
 
   let assemblyFormat = [{
-    $callee `(` $operands `)` attr-dict `:` functional-type($operands, results)
+    $callee `(` $operands `)` prop-dict attr-dict `:` functional-type($operands, results)
   }];
   let hasVerifier = 1;
 }
@@ -388,7 +388,7 @@ def EmitC_MemberCallOpaqueOp : EmitC_Op<"member_call_opaque", [CExpressionInterf
   let results = (outs Variadic<EmitCType>);
 
   let assemblyFormat = [{
-    $receiver $callee `(` $args_operands `)` attr-dict `:` type($receiver) `,` functional-type($args_operands, results)
+    $receiver $callee `(` $args_operands `)` prop-dict attr-dict `:` type($receiver) `,` functional-type($args_operands, results)
   }];
   let hasVerifier = 1;
 }
diff --git a/mlir/test/Dialect/EmitC/member_call_opaque.mlir b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
index 2413da6f740d2..6e64ac1ee3c7b 100644
--- a/mlir/test/Dialect/EmitC/member_call_opaque.mlir
+++ b/mlir/test/Dialect/EmitC/member_call_opaque.mlir
@@ -15,15 +15,15 @@ func.func @member_call_args(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32) {
 // CHECK: emitc.member_call_opaque %arg0 "method"(%arg1) : !emitc.opaque<"MyClass">, (i32) -> i32
 
 func.func @member_call_template_args(%arg0 : !emitc.opaque<"MyClass">) {
-  %0 = emitc.member_call_opaque %arg0 "method" () {template_args = [i32]} : !emitc.opaque<"MyClass">, () -> i32
+  %0 = emitc.member_call_opaque %arg0 "method" () <{template_args = [i32]}> : !emitc.opaque<"MyClass">, () -> i32
   return
 }
 // CHECK-LABEL: func @member_call_template_args
-// CHECK: emitc.member_call_opaque %arg0 "method"() {template_args = [i32]} : !emitc.opaque<"MyClass">, () -> i32
+// CHECK: emitc.member_call_opaque %arg0 "method"() <{template_args = [i32]}> : !emitc.opaque<"MyClass">, () -> i32
 
 func.func @member_call_reorder(%arg0 : !emitc.opaque<"MyClass">, %arg1 : i32, %arg2 : i32) {
-  %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) {args = [1 : index, 0 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+  %0 = emitc.member_call_opaque %arg0 "method" (%arg1, %arg2) <{args = [1 : index, 0 : index]}> : !emitc.opaque<"MyClass">, (i32, i32) -> i32
   return
 }
 // CHECK-LABEL: func @member_call_reorder
-// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) {args = [1 : index, 0 : index]} : !emitc.opaque<"MyClass">, (i32, i32) -> i32
+// CHECK: emitc.member_call_opaque %arg0 "method"(%arg1, %arg2) <{args = [1 : index, 0 : index]}> : !emitc.opaque<"MyClass">, (i32, i32) -> i32

>From 1c5859961ea05b253be29be5080c1890fd994f0e Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Thu, 28 May 2026 20:38:34 -0700
Subject: [PATCH 6/8] update tests to match new property syntax

---
 mlir/test/Dialect/EmitC/attrs.mlir       |  4 ++--
 mlir/test/Dialect/EmitC/invalid_ops.mlir | 16 ++++++++--------
 mlir/test/Dialect/EmitC/ops.mlir         |  4 ++--
 mlir/test/Target/Cpp/expressions.mlir    |  2 +-
 4 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/mlir/test/Dialect/EmitC/attrs.mlir b/mlir/test/Dialect/EmitC/attrs.mlir
index 5a219c462678e..cc01673c3803c 100644
--- a/mlir/test/Dialect/EmitC/attrs.mlir
+++ b/mlir/test/Dialect/EmitC/attrs.mlir
@@ -5,8 +5,8 @@
 // CHECK-LABEL: func @opaque_attrs() {
 func.func @opaque_attrs() {
   // CHECK-NEXT: #emitc.opaque<"attr">
-  emitc.call_opaque "f"() {args = [#emitc.opaque<"attr">]} : () -> ()
+  emitc.call_opaque "f"() <{args = [#emitc.opaque<"attr">]}> : () -> ()
   // CHECK-NEXT: #emitc.opaque<"\22quoted_attr\22">
-  emitc.call_opaque "f"() {args = [#emitc.opaque<"\"quoted_attr\"">]} : () -> ()
+  emitc.call_opaque "f"() <{args = [#emitc.opaque<"\"quoted_attr\"">]}> : () -> ()
   return
 }
diff --git a/mlir/test/Dialect/EmitC/invalid_ops.mlir b/mlir/test/Dialect/EmitC/invalid_ops.mlir
index 1c15af89bdbcd..e9b9987c9d73c 100644
--- a/mlir/test/Dialect/EmitC/invalid_ops.mlir
+++ b/mlir/test/Dialect/EmitC/invalid_ops.mlir
@@ -34,7 +34,7 @@ func.func @empty_constant() {
 
 func.func @index_args_out_of_range_1() {
     // expected-error @+1 {{'emitc.call_opaque' op index argument is out of range}}
-    emitc.call_opaque "test" () {args = [0 : index]} : () -> ()
+    emitc.call_opaque "test" () <{args = [0 : index]}> : () -> ()
     return
 }
 
@@ -42,7 +42,7 @@ func.func @index_args_out_of_range_1() {
 
 func.func @index_args_out_of_range_2(%arg : i32) {
     // expected-error @+1 {{'emitc.call_opaque' op index argument is out of range}}
-    emitc.call_opaque "test" (%arg, %arg) {args = [2 : index]} : (i32, i32) -> ()
+    emitc.call_opaque "test" (%arg, %arg) <{args = [2 : index]}> : (i32, i32) -> ()
     return
 }
 
@@ -58,7 +58,7 @@ func.func @empty_callee() {
 
 func.func @nonetype_arg(%arg : i32) {
     // expected-error @+1 {{'emitc.call_opaque' op array argument has no type}}
-    emitc.call_opaque "nonetype_arg"(%arg) {args = [0 : index, [0, 1, 2]]} : (i32) -> i32
+    emitc.call_opaque "nonetype_arg"(%arg) <{args = [0 : index, [0, 1, 2]]}> : (i32) -> i32
     return
 }
 
@@ -66,7 +66,7 @@ func.func @nonetype_arg(%arg : i32) {
 
 func.func @array_template_arg(%arg : i32) {
     // expected-error @+1 {{'emitc.call_opaque' op template argument has invalid type}}
-    emitc.call_opaque "nonetype_template_arg"(%arg) {template_args = [[0, 1, 2]]} : (i32) -> i32
+    emitc.call_opaque "nonetype_template_arg"(%arg) <{template_args = [[0, 1, 2]]}> : (i32) -> i32
     return
 }
 
@@ -74,7 +74,7 @@ func.func @array_template_arg(%arg : i32) {
 
 func.func @dense_template_argument(%arg : i32) {
     // expected-error @+1 {{'emitc.call_opaque' op template argument has invalid type}}
-    emitc.call_opaque "dense_template_argument"(%arg) {template_args = [dense<[1.0, 1.0]> : tensor<2xf32>]} : (i32) -> i32
+    emitc.call_opaque "dense_template_argument"(%arg) <{template_args = [dense<[1.0, 1.0]> : tensor<2xf32>]}> : (i32) -> i32
     return
 }
 
@@ -98,7 +98,7 @@ func.func @member_call_empty_callee(%arg0 : !emitc.opaque<"MyClass">) {
 
 func.func @member_call_index_out_of_range(%arg0 : !emitc.opaque<"MyClass">) {
     // expected-error @+1 {{'emitc.member_call_opaque' op index argument is out of range}}
-    emitc.member_call_opaque %arg0 "test" () {args = [1 : index]} : !emitc.opaque<"MyClass">, () -> ()
+    emitc.member_call_opaque %arg0 "test" () <{args = [1 : index]}> : !emitc.opaque<"MyClass">, () -> ()
     return
 }
 
@@ -114,7 +114,7 @@ func.func @member_call_array_result(%arg0 : !emitc.opaque<"MyClass">) {
 
 func.func @member_call_nonetype_template_arg(%arg0 : !emitc.opaque<"MyClass">) {
     // expected-error @+1 {{'emitc.member_call_opaque' op template argument has invalid type}}
-    emitc.member_call_opaque %arg0 "nonetype_template_arg"() {template_args = [[0, 1, 2]]} : !emitc.opaque<"MyClass">, () -> ()
+    emitc.member_call_opaque %arg0 "nonetype_template_arg"() <{template_args = [[0, 1, 2]]}> : !emitc.opaque<"MyClass">, () -> ()
     return
 }
 
@@ -122,7 +122,7 @@ func.func @member_call_nonetype_template_arg(%arg0 : !emitc.opaque<"MyClass">) {
 
 func.func @member_call_dense_template_argument(%arg0 : !emitc.opaque<"MyClass">) {
     // expected-error @+1 {{'emitc.member_call_opaque' op template argument has invalid type}}
-    emitc.member_call_opaque %arg0 "dense_template_argument"() {template_args = [dense<[1.0, 1.0]> : tensor<2xf32>]} : !emitc.opaque<"MyClass">, () -> ()
+    emitc.member_call_opaque %arg0 "dense_template_argument"() <{template_args = [dense<[1.0, 1.0]> : tensor<2xf32>]}> : !emitc.opaque<"MyClass">, () -> ()
     return
 }
 
diff --git a/mlir/test/Dialect/EmitC/ops.mlir b/mlir/test/Dialect/EmitC/ops.mlir
index 2f7544b5db096..82e8aa8d140c0 100644
--- a/mlir/test/Dialect/EmitC/ops.mlir
+++ b/mlir/test/Dialect/EmitC/ops.mlir
@@ -9,9 +9,9 @@ emitc.include "test.h"
 // CHECK-LABEL: func @f(%{{.*}}: i32, %{{.*}}: !emitc.opaque<"int32_t">) {
 func.func @f(%arg0: i32, %f: !emitc.opaque<"int32_t">) {
   %1 = "emitc.call_opaque"() {callee = "blah"} : () -> i64
-  emitc.call_opaque "foo" (%1) {args = [
+  emitc.call_opaque "foo" (%1) <{args = [
     0 : index, dense<[0, 1]> : tensor<2xi32>, 0 : index
-  ]} : (i64) -> ()
+  ]}> : (i64) -> ()
   return
 }
 
diff --git a/mlir/test/Target/Cpp/expressions.mlir b/mlir/test/Target/Cpp/expressions.mlir
index 7280377990cfc..7ea7affc86669 100644
--- a/mlir/test/Target/Cpp/expressions.mlir
+++ b/mlir/test/Target/Cpp/expressions.mlir
@@ -486,7 +486,7 @@ emitc.func @expression_with_load_and_call(%arg0: !emitc.ptr<i32>) -> i1 {
 emitc.func @expression_with_call_opaque_with_args_array(%0 : i32, %1 : i32) {
   %2 = expression %0, %1 : (i32, i32) -> i1 {
     %3 = cmp lt, %0, %1 : (i32, i32) -> i1
-    %4 = emitc.call_opaque "f"(%3) {"args" = [0: index]} : (i1) -> i1
+    %4 = emitc.call_opaque "f"(%3) {args = [0: index]} : (i1) -> i1
     yield %4 : i1
   }
   return

>From f6e7d317baa0fc6a4e1a8de744694ba383dc0a58 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Thu, 28 May 2026 21:07:34 -0700
Subject: [PATCH 7/8] more syntax updates for MemRefToEmitC tests

---
 .../MemRefToEmitC/memref-to-emitc-alloc-copy.mlir    |  8 ++++----
 .../MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir | 12 ++++++------
 .../memref-to-emitc-alloc-load-store.mlir            |  4 ++--
 .../MemRefToEmitC/memref-to-emitc-copy.mlir          |  2 +-
 4 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir
index 19e1c7ae4263e..be0b9baf502bc 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-copy.mlir
@@ -18,7 +18,7 @@ func.func @alloc_copy(%arg0: memref<999xi32>) {
 // CHECK-LABEL:   func.func @alloc_copy(
 // CHECK-SAME:      %[[ARG0:.*]]: memref<999xi32>) {
 // CHECK:           %[[UNREALIZED_CONVERSION_CAST_0:.*]] = builtin.unrealized_conversion_cast %[[ARG0]] : memref<999xi32> to !emitc.array<999xi32>
-// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
 // CHECK:           %[[VAL_0:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_0:.*]] = emitc.mul %[[CALL_OPAQUE_0]], %[[VAL_0]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           %[[CALL_OPAQUE_1:.*]] = emitc.call_opaque "malloc"(%[[MUL_0]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -30,11 +30,11 @@ func.func @alloc_copy(%arg0: memref<999xi32>) {
 // CHECK:           %[[VAL_2:.*]] = "emitc.constant"() <{value = 0 : index}> : () -> index
 // CHECK:           %[[SUBSCRIPT_1:.*]] = emitc.subscript %[[UNREALIZED_CONVERSION_CAST_1]]{{\[}}%[[VAL_2]]] : (!emitc.array<999xi32>, index) -> !emitc.lvalue<i32>
 // CHECK:           %[[ADDRESS_OF_1:.*]] = emitc.address_of %[[SUBSCRIPT_1]] : !emitc.lvalue<i32>
-// CHECK:           %[[CALL_OPAQUE_2:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_2:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
 // CHECK:           %[[VAL_3:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_1:.*]] = emitc.mul %[[CALL_OPAQUE_2]], %[[VAL_3]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           emitc.call_opaque "memcpy"(%[[ADDRESS_OF_1]], %[[ADDRESS_OF_0]], %[[MUL_1]]) : (!emitc.ptr<i32>, !emitc.ptr<i32>, !emitc.size_t) -> ()
-// CHECK:           %[[CALL_OPAQUE_3:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_3:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
 // CHECK:           %[[VAL_4:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_2:.*]] = emitc.mul %[[CALL_OPAQUE_3]], %[[VAL_4]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           %[[CALL_OPAQUE_4:.*]] = emitc.call_opaque "malloc"(%[[MUL_2]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -46,7 +46,7 @@ func.func @alloc_copy(%arg0: memref<999xi32>) {
 // CHECK:           %[[VAL_6:.*]] = "emitc.constant"() <{value = 0 : index}> : () -> index
 // CHECK:           %[[SUBSCRIPT_3:.*]] = emitc.subscript %[[UNREALIZED_CONVERSION_CAST_2]]{{\[}}%[[VAL_6]]] : (!emitc.array<999xi32>, index) -> !emitc.lvalue<i32>
 // CHECK:           %[[ADDRESS_OF_3:.*]] = emitc.address_of %[[SUBSCRIPT_3]] : !emitc.lvalue<i32>
-// CHECK:           %[[CALL_OPAQUE_5:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_5:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t
 // CHECK:           %[[VAL_7:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CHECK:           %[[MUL_3:.*]] = emitc.mul %[[CALL_OPAQUE_5]], %[[VAL_7]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           emitc.call_opaque "memcpy"(%[[ADDRESS_OF_3]], %[[ADDRESS_OF_2]], %[[MUL_3]]) : (!emitc.ptr<i32>, !emitc.ptr<i32>, !emitc.size_t) -> ()
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir
index e391a893bc44a..ca7f3fbe20eff 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-dealloc.mlir
@@ -9,7 +9,7 @@ func.func @alloc() {
 // CPP:      module {
 // CPP-NEXT:   emitc.include <"cstdlib">
 // CPP-LABEL:  alloc()
-// CPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t 
+// CPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t 
 // CPP-NEXT:   %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CPP-NEXT:   %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT:   %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -19,7 +19,7 @@ func.func @alloc() {
 // NOCPP:      module {
 // NOCPP-NEXT:   emitc.include <"stdlib.h">
 // NOCPP-LABEL: alloc()
-// NOCPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t 
+// NOCPP-NEXT:   %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t 
 // NOCPP-NEXT:   %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // NOCPP-NEXT:   %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT:   %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
@@ -32,7 +32,7 @@ func.func @alloc_aligned() {
 }
 
 // CPP-LABEL: alloc_aligned
-// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() {args = [f32]} : () -> !emitc.size_t 
+// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t 
 // CPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // CPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT: %[[ALIGNMENT:.*]] = "emitc.constant"() <{value = 64 : index}> : () -> !emitc.size_t 
@@ -41,7 +41,7 @@ func.func @alloc_aligned() {
 // CPP-NEXT: return
 
 // NOCPP-LABEL: alloc_aligned
-// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() {args = [f32]} : () -> !emitc.size_t 
+// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t 
 // NOCPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 999 : index}> : () -> index
 // NOCPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT: %[[ALIGNMENT:.*]] = "emitc.constant"() <{value = 64 : index}> : () -> !emitc.size_t 
@@ -55,7 +55,7 @@ func.func @allocating_multi() {
 }
 
 // CPP-LABEL: allocating_multi
-// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t 
+// CPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t 
 // CPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 6993 : index}> : () -> index
 // CPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // CPP-NEXT: %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">
@@ -63,7 +63,7 @@ func.func @allocating_multi() {
 // CPP-NEXT: return 
 
 // NOCPP-LABEL: allocating_multi
-// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() {args = [i32]} : () -> !emitc.size_t 
+// NOCPP-NEXT: %[[ALLOC:.*]] = emitc.call_opaque "sizeof"() <{args = [i32]}> : () -> !emitc.size_t 
 // NOCPP-NEXT: %[[ALLOC_SIZE:.*]] = "emitc.constant"() <{value = 6993 : index}> : () -> index
 // NOCPP-NEXT: %[[ALLOC_TOTAL_SIZE:.*]] = emitc.mul %[[ALLOC]], %[[ALLOC_SIZE]] : (!emitc.size_t, index) -> !emitc.size_t
 // NOCPP-NEXT: %[[ALLOC_PTR:.*]] = emitc.call_opaque "malloc"(%[[ALLOC_TOTAL_SIZE]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir
index 4b396005a7da3..07cad3b0c4dc2 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-alloc-load-store.mlir
@@ -21,7 +21,7 @@
 // CHECK-SAME:  %[[ARG_J:.*]]: !emitc.size_t)
 func.func private @memref_alloc_store(%v : f32, %i: index, %j: index) {
   /// Allocation size  computation
-  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() {args = [f32]} : () -> !emitc.size_t
+  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
   // CHECK:     %[[NUM_ELEMS:.*]] = "emitc.constant"() <{value = 32 : index}> : () -> index
   // CHECK:     %[[TOTAL_BYTES:.*]] = mul %[[SIZEOF_F32]], %[[NUM_ELEMS]] : (!emitc.size_t, index) -> !emitc.size_t
   /// Alloc
@@ -42,7 +42,7 @@ func.func private @memref_alloc_store(%v : f32, %i: index, %j: index) {
 // CHECK-SAME:  %[[ARG_I:.*]]: !emitc.size_t,
 // CHECK-SAME:  %[[ARG_J:.*]]: !emitc.size_t) -> f32
 func.func private @memref_alloc_load(%i: index, %j: index) -> f32 {
-  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() {args = [f32]} : () -> !emitc.size_t
+  // CHECK:     %[[SIZEOF_F32:.*]] = call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
   // CHECK:     %[[NUM_ELEMS:.*]] = "emitc.constant"() <{value = 32 : index}> : () -> index
   // CHECK:     %[[TOTAL_BYTES:.*]] = mul %[[SIZEOF_F32]], %[[NUM_ELEMS]] : (!emitc.size_t, index) -> !emitc.size_t
   // CHECK:     %[[MALLOC_PTR:.*]] = call_opaque "malloc"(%[[TOTAL_BYTES]]) : (!emitc.size_t) -> !emitc.ptr<!emitc.opaque<"void">>
diff --git a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir
index 3de2d25f2b0d4..04e6edd5b6981 100644
--- a/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir
+++ b/mlir/test/Conversion/MemRefToEmitC/memref-to-emitc-copy.mlir
@@ -21,7 +21,7 @@ func.func @copying(%arg0 : memref<9x4x5x7xf32>, %arg1 : memref<9x4x5x7xf32>) {
 // CHECK:           %[[VAL_1:.*]] = "emitc.constant"() <{value = 0 : index}> : () -> index
 // CHECK:           %[[SUBSCRIPT_1:.*]] = emitc.subscript %[[UNREALIZED_CONVERSION_CAST_0]]{{\[}}%[[VAL_1]], %[[VAL_1]], %[[VAL_1]], %[[VAL_1]]] : (!emitc.array<9x4x5x7xf32>, index, index, index, index) -> !emitc.lvalue<f32>
 // CHECK:           %[[ADDRESS_OF_1:.*]] = emitc.address_of %[[SUBSCRIPT_1]] : !emitc.lvalue<f32>
-// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() {args = [f32]} : () -> !emitc.size_t
+// CHECK:           %[[CALL_OPAQUE_0:.*]] = emitc.call_opaque "sizeof"() <{args = [f32]}> : () -> !emitc.size_t
 // CHECK:           %[[VAL_2:.*]] = "emitc.constant"() <{value = 1260 : index}> : () -> index
 // CHECK:           %[[MUL_0:.*]] = emitc.mul %[[CALL_OPAQUE_0]], %[[VAL_2]] : (!emitc.size_t, index) -> !emitc.size_t
 // CHECK:           emitc.call_opaque "memcpy"(%[[ADDRESS_OF_1]], %[[ADDRESS_OF_0]], %[[MUL_0]]) : (!emitc.ptr<f32>, !emitc.ptr<f32>, !emitc.size_t) -> ()

>From 4da46098bdbf609fa8f4660604d687e70477f053 Mon Sep 17 00:00:00 2001
From: Jeremy Kun <jkun at google.com>
Date: Tue, 2 Jun 2026 07:07:46 -0700
Subject: [PATCH 8/8] rename as arg_operands for more unified implementation

---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td | 44 ++++++++++-----------
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp         |  2 +-
 mlir/lib/Target/Cpp/TranslateToCpp.cpp      | 32 +++++++--------
 3 files changed, 37 insertions(+), 41 deletions(-)

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index c15e8b415f83d..65361a987a08e 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -338,24 +338,24 @@ def EmitC_CallOpaqueOp : EmitC_Op<"call_opaque", [CExpressionInterface]> {
     Arg<StrAttr, "the C++ function to call">:$callee,
     Arg<OptionalAttr<ArrayAttr>, "the order of operands and further attributes">:$args,
     Arg<OptionalAttr<ArrayAttr>, "template arguments">:$template_args,
-    Variadic<EmitCType>:$operands
+    Variadic<EmitCType>:$arg_operands
   );
   let results = (outs Variadic<EmitCType>);
   let builders = [
     OpBuilder<(ins
       "::mlir::TypeRange":$resultTypes,
       "::llvm::StringRef":$callee,
-      "::mlir::ValueRange":$operands,
+      "::mlir::ValueRange":$arg_operands,
       CArg<"::mlir::ArrayAttr", "{}">:$args,
       CArg<"::mlir::ArrayAttr", "{}">:$template_args), [{
         build($_builder, $_state, resultTypes, callee, args, template_args,
-            operands);
+            arg_operands);
       }]
     >
   ];
 
   let assemblyFormat = [{
-    $callee `(` $operands `)` prop-dict attr-dict `:` functional-type($operands, results)
+    $callee `(` $arg_operands `)` prop-dict attr-dict `:` functional-type($arg_operands, results)
   }];
   let hasVerifier = 1;
 }
@@ -383,12 +383,12 @@ def EmitC_MemberCallOpaqueOp : EmitC_Op<"member_call_opaque", [CExpressionInterf
     StrAttr:$callee,
     OptionalAttr<ArrayAttr>:$args,
     OptionalAttr<ArrayAttr>:$template_args,
-    Variadic<EmitCType>:$args_operands
+    Variadic<EmitCType>:$arg_operands
   );
   let results = (outs Variadic<EmitCType>);
 
   let assemblyFormat = [{
-    $receiver $callee `(` $args_operands `)` prop-dict attr-dict `:` type($receiver) `,` functional-type($args_operands, results)
+    $receiver $callee `(` $arg_operands `)` prop-dict attr-dict `:` type($receiver) `,` functional-type($arg_operands, results)
   }];
   let hasVerifier = 1;
 }
@@ -428,7 +428,7 @@ def EmitC_CastOp : EmitC_Op<"cast",
 def EmitC_CmpOp : EmitC_BinaryOp<"cmp", []> {
   let summary = "Comparison operation";
   let description = [{
-    With the `emitc.cmp` operation the comparison operators ==, !=, <, <=, >, >=, <=> 
+    With the `emitc.cmp` operation the comparison operators ==, !=, <, <=, >, >=, <=>
     can be applied.
 
     Its first argument is an attribute that defines the comparison operator:
@@ -445,7 +445,7 @@ def EmitC_CmpOp : EmitC_BinaryOp<"cmp", []> {
     ```mlir
     // Custom form of the cmp operation.
     %0 = emitc.cmp eq, %arg0, %arg1 : (i32, i32) -> i1
-    %1 = emitc.cmp lt, %arg2, %arg3 : 
+    %1 = emitc.cmp lt, %arg2, %arg3 :
         (
           !emitc.opaque<"std::valarray<float>">,
           !emitc.opaque<"std::valarray<float>">
@@ -570,7 +570,7 @@ def EmitC_ExpressionOp
   let summary = "Expression operation";
   let description = [{
     The `emitc.expression` operation returns a single SSA value which is yielded by
-    its single-basic-block region. The operation takes zero or more input operands 
+    its single-basic-block region. The operation takes zero or more input operands
     that are passed as block arguments to the region.
 
     As the operation is to be emitted as a C expression, the operations within
@@ -725,7 +725,7 @@ def EmitC_CallOp : EmitC_Op<"call",
     %2 = emitc.call @my_add(%0, %1) : (f32, f32) -> f32
     ```
   }];
-  let arguments = (ins 
+  let arguments = (ins
     FlatSymbolRefAttr:$callee,
     Variadic<EmitCType>:$operands,
     OptionalAttr<DictArrayAttr>:$arg_attrs,
@@ -1075,8 +1075,8 @@ def EmitC_LoadOp : EmitC_Op<"load", [CExpressionInterface,
 ]> {
   let summary = "Load an lvalue into an SSA value.";
   let description = [{
-    This operation loads the content of a modifiable lvalue into an SSA value. 
-    Modifications of the lvalue executed after the load are not observable on 
+    This operation loads the content of a modifiable lvalue into an SSA value.
+    Modifications of the lvalue executed after the load are not observable on
     the produced value.
 
     Example:
@@ -1090,11 +1090,11 @@ def EmitC_LoadOp : EmitC_Op<"load", [CExpressionInterface,
     ```
   }];
 
-  let arguments = (ins 
+  let arguments = (ins
       Res<EmitC_LValueType, "", [MemRead<DefaultResource, 0, FullEffect>]>:$operand);
   let results = (outs AnyType:$result);
 
-  let assemblyFormat = "$operand attr-dict `:` type($operand)"; 
+  let assemblyFormat = "$operand attr-dict `:` type($operand)";
 }
 
 def EmitC_MulOp : EmitC_BinaryOp<"mul", []> {
@@ -1325,7 +1325,7 @@ def EmitC_VariableOp : EmitC_Op<"variable", []> {
     %0 = "emitc.variable"(){value = 42 : i32} : () -> !emitc.lvalue<i32>
 
     // Variable emitted as `int32_t* = NULL;`
-    %1 = "emitc.variable"() {value = #emitc.opaque<"NULL">} 
+    %1 = "emitc.variable"() {value = #emitc.opaque<"NULL">}
       : () -> !emitc.lvalue<!emitc.ptr<!emitc.opaque<"int32_t">>>
     ```
 
@@ -1457,7 +1457,7 @@ def EmitC_VerbatimOp : EmitC_Op<"verbatim"> {
     #endif
 
     ...
-    
+
     #ifdef __cplusplus
     }
     #endif
@@ -1519,7 +1519,7 @@ def EmitC_AssignOp : EmitC_Op<"assign", []> {
     ```
   }];
 
-  let arguments = (ins 
+  let arguments = (ins
       Res<EmitC_LValueType, "", [MemWrite<DefaultResource, 1, FullEffect>]>:$var,
       EmitCType:$value);
   let results = (outs);
@@ -1824,7 +1824,7 @@ def EmitC_FieldOp : EmitC_Op<"field", [Symbol]> {
   let summary = "A field within a class";
   let description = [{
     The `emitc.field` operation declares a named field within an `emitc.class`
-    operation. The field's type must be an EmitC type. 
+    operation. The field's type must be an EmitC type.
 
     Example:
 
@@ -1901,8 +1901,8 @@ def EmitC_DoOp : EmitC_Op<"do",
       2. An `emitc.yield` passing through the expression result
     - The expression's body contains the actual condition logic
 
-    The body region is executed before the first evaluation of the 
-    condition. Thus, there is a guarantee that the loop will be executed 
+    The body region is executed before the first evaluation of the
+    condition. Thus, there is a guarantee that the loop will be executed
     at least once. The loop terminates when the condition yields false.
 
     The canonical structure of `emitc.do` is:
@@ -1919,7 +1919,7 @@ def EmitC_DoOp : EmitC_Op<"do",
         emitc.yield %result : i1
       }
       // Forward expression result
-      emitc.yield %condition : i1  
+      emitc.yield %condition : i1
     }
     ```
 
@@ -1966,7 +1966,7 @@ def EmitC_DoOp : EmitC_Op<"do",
   }];
 
   let arguments = (ins);
-  let results = (outs); 
+  let results = (outs);
   let regions = (region SizedRegion<1>:$bodyRegion,
                         SizedRegion<1>:$conditionRegion);
 
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index d57bf5e2faf55..bab9cb4a91102 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -388,7 +388,7 @@ LogicalResult emitc::CallOpaqueOp::verify() {
 LogicalResult emitc::MemberCallOpaqueOp::verify() {
   return verifyOpaqueCallCommon(getOperation(), getCallee(), getArgs(),
                                 getTemplateArgs(), getResultTypes(),
-                                getArgsOperands().size());
+                                getArgOperands().size());
 }
 
 //===----------------------------------------------------------------------===//
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index 595ff07f99455..74e608ea818cf 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -909,14 +909,15 @@ static LogicalResult printOperation(CppEmitter &emitter, emitc::CallOp callOp) {
   return printCallOperation(emitter, operation, callee);
 }
 
+template <typename OpTy>
 static LogicalResult
-printOpaqueCallCommon(CppEmitter &emitter, Operation &op, StringRef callee,
+printOpaqueCallCommon(CppEmitter &emitter, OpTy op, StringRef callee,
                       std::optional<ArrayAttr> templateArgs,
                       std::optional<ArrayAttr> args, bool isMemberCall,
                       Value receiver = nullptr) {
   raw_ostream &os = emitter.ostream();
 
-  if (failed(emitter.emitAssignPrefix(op)))
+  if (failed(emitter.emitAssignPrefix(*op.getOperation())))
     return failure();
 
   if (isMemberCall) {
@@ -951,8 +952,7 @@ printOpaqueCallCommon(CppEmitter &emitter, Operation &op, StringRef callee,
     if (auto t = dyn_cast<IntegerAttr>(attr)) {
       if (t.getType().isIndex()) {
         int64_t idx = t.getInt();
-        // Shift index by 1 for member calls to skip the receiver operand.
-        Value operand = op.getOperand(isMemberCall ? idx + 1 : idx);
+        Value operand = op.getArgOperands()[idx];
         return emitter.emitOperand(operand, /*isInBrackets=*/false);
       }
     }
@@ -968,13 +968,10 @@ printOpaqueCallCommon(CppEmitter &emitter, Operation &op, StringRef callee,
   if (args) {
     emittedArgs = interleaveCommaWithError(*args, os, emitArgs);
   } else {
-    auto operands = op.getOperands();
-    if (isMemberCall)
-      operands = operands.drop_front(1);
-
-    emittedArgs = interleaveCommaWithError(operands, os, [&](Value operand) {
-      return emitter.emitOperand(operand, /*isInBrackets=*/true);
-    });
+    emittedArgs =
+        interleaveCommaWithError(op.getArgOperands(), os, [&](Value operand) {
+          return emitter.emitOperand(operand, /*isInBrackets=*/true);
+        });
   }
   if (failed(emittedArgs))
     return failure();
@@ -984,19 +981,18 @@ printOpaqueCallCommon(CppEmitter &emitter, Operation &op, StringRef callee,
 
 static LogicalResult printOperation(CppEmitter &emitter,
                                     emitc::CallOpaqueOp callOpaqueOp) {
-  return printOpaqueCallCommon(
-      emitter, *callOpaqueOp.getOperation(), callOpaqueOp.getCallee(),
-      callOpaqueOp.getTemplateArgs(), callOpaqueOp.getArgs(),
-      /*isMemberCall=*/false);
+  return printOpaqueCallCommon(emitter, callOpaqueOp, callOpaqueOp.getCallee(),
+                               callOpaqueOp.getTemplateArgs(),
+                               callOpaqueOp.getArgs(),
+                               /*isMemberCall=*/false);
 }
 
 static LogicalResult
 printOperation(CppEmitter &emitter,
                emitc::MemberCallOpaqueOp memberCallOpaqueOp) {
   return printOpaqueCallCommon(
-      emitter, *memberCallOpaqueOp.getOperation(),
-      memberCallOpaqueOp.getCallee(), memberCallOpaqueOp.getTemplateArgs(),
-      memberCallOpaqueOp.getArgs(),
+      emitter, memberCallOpaqueOp, memberCallOpaqueOp.getCallee(),
+      memberCallOpaqueOp.getTemplateArgs(), memberCallOpaqueOp.getArgs(),
       /*isMemberCall=*/true, memberCallOpaqueOp.getReceiver());
 }
 



More information about the Mlir-commits mailing list