[Mlir-commits] [mlir] [mlir][emitc] Support member access for values (PR #203308)

Gil Rapaport llvmlistbot at llvm.org
Sat Jun 13 03:34:27 PDT 2026


https://github.com/aniragil updated https://github.com/llvm/llvm-project/pull/203308

>From 8c9dfd95111c58ce24ba423ac3c88e767150c8c0 Mon Sep 17 00:00:00 2001
From: Gil Rapaport <gil.rapaport at mobileye.com>
Date: Fri, 29 May 2026 20:36:33 +0300
Subject: [PATCH 1/2] [mlir][emitc] Support member access for values

The `emitc.member` op is currently limited to taking lvalues of opaque
types representing structs and returning either lvalues or arrays of
its fields. Accessing members of SSA values of opaque types, therefore,
requires assigning them to an `emitc.variable`, applying `emitc.member`
to it and `emitc.load` to the member's lvalue. For users only wishing
to read members of a struct value, this should be redundant.

This PR extends `emitc.member` to handle struct values directly by
accepting opaque types as argument, as long as the result type is
neither an lvalue nor an array, which imply memory location. This
provides similar semantics to extracting elements out of SSA
tensors/vectors.
---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td   |  8 ++++---
 .../Conversion/FuncToEmitC/FuncToEmitC.cpp    | 17 ++++-----------
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp           | 19 +++++++++++++++++
 mlir/lib/Target/Cpp/TranslateToCpp.cpp        | 10 ++++++---
 .../Conversion/FuncToEmitC/func-to-emitc.mlir |  7 ++-----
 mlir/test/Dialect/EmitC/invalid_ops.mlir      | 18 +++++++++++++++-
 mlir/test/Dialect/EmitC/ops.mlir              |  2 ++
 mlir/test/Target/Cpp/func.mlir                | 21 +++++++------------
 mlir/test/Target/Cpp/member.mlir              |  3 +++
 9 files changed, 66 insertions(+), 39 deletions(-)

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index 65361a987a08e..1591feda11b0b 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -1187,16 +1187,18 @@ def EmitC_MemberOp : EmitC_Op<"member", [CExpressionInterface]> {
 
   let arguments = (ins
     Arg<StrAttr, "the member to access">:$member,
-    EmitC_LValueOf<[EmitC_OpaqueType]>:$operand
+    AnyTypeOf<[EmitC_OpaqueType, EmitC_LValueOf<[EmitC_OpaqueType]>]>:$operand
   );
-  let results = (outs AnyTypeOf<[EmitC_ArrayType, EmitC_LValueType]>);
+  let results = (outs AnyTypeOf<[EmitCType, EmitC_LValueType]>);
+  let hasVerifier = 1;
 
   let extraClassDeclaration = [{
     bool hasSideEffects() {
       return false;
     }
     bool alwaysInline() {
-      return true; // C doesn't support references.
+      // C doesn't support references.
+      return isa<emitc::LValueType, emitc::ArrayType>(getResult().getType());
     }
   }];
 }
diff --git a/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp b/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
index 14352ea3595e3..a2279242fb86b 100644
--- a/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
+++ b/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
@@ -210,13 +210,6 @@ class CallOpConversion final : public OpConversionPattern<func::CallOp> {
             .getResult(0);
 
     // Unpack struct fields to replace the original multiple results.
-    MLIRContext *ctx = rewriter.getContext();
-    auto noInit = emitc::OpaqueAttr::get(ctx, "");
-    Value structLv =
-        emitc::VariableOp::create(rewriter, loc,
-                                  emitc::LValueType::get(*structType), noInit)
-            .getResult();
-    emitc::AssignOp::create(rewriter, loc, structLv, structVal);
     SmallVector<Value> results;
     for (auto [i, result] : llvm::enumerate(callOp.getResults())) {
       if (result.use_empty()) {
@@ -226,12 +219,10 @@ class CallOpConversion final : public OpConversionPattern<func::CallOp> {
       Type fieldType = convertedResultTypes[i];
       StringAttr fieldName =
           rewriter.getStringAttr("field" + std::to_string(i));
-      Value fieldLv = emitc::MemberOp::create(rewriter, loc,
-                                              emitc::LValueType::get(fieldType),
-                                              fieldName, structLv)
-                          .getResult();
-      results.push_back(
-          emitc::LoadOp::create(rewriter, loc, fieldType, fieldLv).getResult());
+      Value fieldValue = emitc::MemberOp::create(rewriter, loc, fieldType,
+                                                 fieldName, structVal)
+                             .getResult();
+      results.push_back(fieldValue);
     }
 
     rewriter.replaceOp(callOp, results);
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index bab9cb4a91102..836bd7209c14c 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -1088,6 +1088,25 @@ LogicalResult emitc::LiteralOp::verify() {
     return emitOpError() << "value must not be empty";
   return success();
 }
+
+//===----------------------------------------------------------------------===//
+// MemberOp
+//===----------------------------------------------------------------------===//
+
+LogicalResult MemberOp::verify() {
+  Type operandType = getOperand().getType();
+  Type resultType = getResult().getType();
+  bool resultIsWritable = isa<emitc::LValueType, emitc::ArrayType>(resultType);
+
+  if (isa<emitc::LValueType>(operandType) && !resultIsWritable)
+    return emitOpError("lvalues must return lvalues or arrays");
+
+  if (!isa<emitc::LValueType>(operandType) && resultIsWritable)
+    return emitOpError("non-lvalues cannot return lvalues or arrays");
+
+  return success();
+}
+
 //===----------------------------------------------------------------------===//
 // SubOp
 //===----------------------------------------------------------------------===//
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index 74e608ea818cf..1008e0a878fe0 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -477,9 +477,13 @@ static LogicalResult printOperation(CppEmitter &emitter,
 
 static LogicalResult printOperation(CppEmitter &emitter,
                                     emitc::MemberOp memberOp) {
-  if (!emitter.isPartOfCurrentExpression(memberOp.getOperation()))
-    return success();
-
+  if (memberOp.alwaysInline()) {
+    if (!emitter.isPartOfCurrentExpression(memberOp.getOperation()))
+      return success();
+  } else {
+    if (failed(emitter.emitAssignPrefix(*memberOp.getOperation())))
+      return failure();
+  }
   if (failed(emitter.emitOperand(memberOp.getOperand())))
     return failure();
   emitter.ostream() << "." << memberOp.getMember();
diff --git a/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir b/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir
index 1a2a8e764e22d..9ac863c4a8e9f 100644
--- a/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir
+++ b/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir
@@ -121,11 +121,8 @@ func.func @return_two(%arg0: i32, %arg1: i32) -> (i32, i32) {
 // CHECK-LABEL:   emitc.func @caller(
 // CHECK-SAME:      %[[ARG0:.*]]: i32) -> i32 {
 // CHECK-NEXT:      %[[VAL_0:.*]] = call @return_two(%[[ARG0]], %[[ARG0]]) : (i32, i32) -> !emitc.opaque<"struct return_i32_i32">
-// CHECK-NEXT:      %[[VAL_1:.*]] = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
-// CHECK-NEXT:      assign %[[VAL_0]] : !emitc.opaque<"struct return_i32_i32"> to %[[VAL_1]] : <!emitc.opaque<"struct return_i32_i32">>
-// CHECK-NEXT:      %[[VAL_2:.*]] = "emitc.member"(%[[VAL_1]]) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
-// CHECK-NEXT:      %[[VAL_3:.*]] = load %[[VAL_2]] : <i32>
-// CHECK-NEXT:      return %[[VAL_3]] : i32
+// CHECK-NEXT:      %[[VAL_1:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field1"}> : (!emitc.opaque<"struct return_i32_i32">) -> i32
+// CHECK-NEXT:      return %[[VAL_1]] : i32
 // CHECK-NEXT:    }
 func.func @return_two(%arg0: i32, %arg1: i32) -> (i32, i32) {
   return %arg0, %arg1 : i32, i32
diff --git a/mlir/test/Dialect/EmitC/invalid_ops.mlir b/mlir/test/Dialect/EmitC/invalid_ops.mlir
index e9b9987c9d73c..dfe0629f3ef4f 100644
--- a/mlir/test/Dialect/EmitC/invalid_ops.mlir
+++ b/mlir/test/Dialect/EmitC/invalid_ops.mlir
@@ -596,13 +596,29 @@ func.func @use_global() {
 // -----
 
 func.func @member(%arg0: !emitc.lvalue<i32>) {
-  // expected-error @+1 {{'emitc.member' op operand #0 must be emitc.lvalue of EmitC opaque type values, but got '!emitc.lvalue<i32>'}}
+  // expected-error @+1 {{'emitc.member' op operand #0 must be EmitC opaque type or emitc.lvalue of EmitC opaque type values, but got '!emitc.lvalue<i32>'}}
   %0 = "emitc.member" (%arg0) {member = "a"} : (!emitc.lvalue<i32>) -> !emitc.lvalue<i32>
   return
 }
 
 // -----
 
+func.func @member_of_value_as_lvalue(%arg0: !emitc.opaque<"mystruct">) {
+  // expected-error @+1 {{'emitc.member' op non-lvalues cannot return lvalues or arrays}}
+  %1 = "emitc.member" (%arg0) {member = "a"} : (!emitc.opaque<"mystruct">) -> !emitc.lvalue<i32>
+  return
+}
+
+// -----
+
+func.func @member_of_value_array(%arg0: !emitc.opaque<"mystruct">) {
+  // expected-error @+1 {{'emitc.member' op non-lvalues cannot return lvalues or arrays}}
+  %1 = "emitc.member" (%arg0) {member = "a"} : (!emitc.opaque<"mystruct">) -> !emitc.array<2xi32>
+  return
+}
+
+// -----
+
 func.func @member_of_ptr(%arg0: !emitc.lvalue<i32>) {
   // expected-error @+1 {{'emitc.member_of_ptr' op operand #0 must be emitc.lvalue of EmitC opaque type or EmitC pointer type values, but got '!emitc.lvalue<i32>'}}
   %0 = "emitc.member_of_ptr" (%arg0) {member = "a"} : (!emitc.lvalue<i32>) -> !emitc.lvalue<i32>
diff --git a/mlir/test/Dialect/EmitC/ops.mlir b/mlir/test/Dialect/EmitC/ops.mlir
index 82e8aa8d140c0..4c4a935aaeaed 100644
--- a/mlir/test/Dialect/EmitC/ops.mlir
+++ b/mlir/test/Dialect/EmitC/ops.mlir
@@ -323,6 +323,8 @@ func.func @member_access(%arg0: !emitc.lvalue<!emitc.opaque<"mystruct">>, %arg1:
   %3 = "emitc.member_of_ptr" (%arg1) {member = "b"} : (!emitc.lvalue<!emitc.opaque<"mystruct_ptr">>) -> !emitc.array<2xi32>
   %4 = "emitc.member_of_ptr" (%arg2) {member = "a"} : (!emitc.lvalue<!emitc.ptr<!emitc.opaque<"mystruct">>>) -> !emitc.lvalue<i32>
   %5 = "emitc.member_of_ptr" (%arg2) {member = "b"} : (!emitc.lvalue<!emitc.ptr<!emitc.opaque<"mystruct">>>) -> !emitc.array<2xi32>
+  %6 = emitc.load %arg0 : !emitc.lvalue<!emitc.opaque<"mystruct">>
+  %7 = "emitc.member" (%6) {member = "a"} : (!emitc.opaque<"mystruct">) -> i32
   return
 }
 
diff --git a/mlir/test/Target/Cpp/func.mlir b/mlir/test/Target/Cpp/func.mlir
index 82f1ee9f6ec2b..e7aa2f5cbbdb0 100644
--- a/mlir/test/Target/Cpp/func.mlir
+++ b/mlir/test/Target/Cpp/func.mlir
@@ -61,11 +61,8 @@ emitc.func @return_two(%arg0: i32, %arg1: i32) -> !emitc.opaque<"struct return_i
 
 emitc.func @call_two(%arg0: i32) -> i32 {
   %0 = call @return_two(%arg0, %arg0) : (i32, i32) -> !emitc.opaque<"struct return_i32_i32">
-  %1 = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
-  assign %0 : !emitc.opaque<"struct return_i32_i32"> to %1 : <!emitc.opaque<"struct return_i32_i32">>
-  %2 = "emitc.member"(%1) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
-  %3 = load %2 : <i32>
-  return %3 : i32
+  %1 = "emitc.member"(%0) <{member = "field1"}> : (!emitc.opaque<"struct return_i32_i32">) -> i32
+  return %1 : i32
 }
 
 // CPP-DEFAULT: struct return_i32_i32 {
@@ -81,10 +78,8 @@ emitc.func @call_two(%arg0: i32) -> i32 {
 // CPP-DEFAULT-NEXT: }
 // CPP-DEFAULT-NEXT: int32_t call_two(int32_t [[V1:[^ ]*]]) {
 // CPP-DEFAULT-NEXT:   struct return_i32_i32 [[V2:[^ ]*]] = return_two([[V1]], [[V1]]);
-// CPP-DEFAULT-NEXT:   struct return_i32_i32 [[V3:[^ ]*]];
-// CPP-DEFAULT-NEXT:   [[V3]] = [[V2]];
-// CPP-DEFAULT-NEXT:   int32_t [[V4:[^ ]*]] = [[V3]].field1;
-// CPP-DEFAULT-NEXT:   return [[V4]];
+// CPP-DEFAULT-NEXT:   int32_t [[V3:[^ ]*]] = [[V2]].field1;
+// CPP-DEFAULT-NEXT:   return [[V3]];
 
 // CPP-DECLTOP: struct return_i32_i32 {
 // CPP-DECLTOP-NEXT:   int32_t field0;
@@ -100,9 +95,7 @@ emitc.func @call_two(%arg0: i32) -> i32 {
 // CPP-DECLTOP-NEXT: }
 // CPP-DECLTOP-NEXT: int32_t call_two(int32_t [[V1:[^ ]*]]) {
 // CPP-DECLTOP-NEXT:   struct return_i32_i32 [[V2:[^ ]*]];
-// CPP-DECLTOP-NEXT:   struct return_i32_i32 [[V3:[^ ]*]];
-// CPP-DECLTOP-NEXT:   int32_t [[V4:[^ ]*]];
+// CPP-DECLTOP-NEXT:   int32_t [[V3:[^ ]*]];
 // CPP-DECLTOP-NEXT:   [[V2]] = return_two([[V1]], [[V1]]);
-// CPP-DECLTOP:        [[V3]] = [[V2]];
-// CPP-DECLTOP-NEXT:   [[V4]] = [[V3]].field1;
-// CPP-DECLTOP-NEXT:   return [[V4]];
+// CPP-DECLTOP-NEXT:   [[V3]] = [[V2]].field1;
+// CPP-DECLTOP-NEXT:   return [[V3]];
diff --git a/mlir/test/Target/Cpp/member.mlir b/mlir/test/Target/Cpp/member.mlir
index 45b6336f63ce0..0fd25e440f752 100644
--- a/mlir/test/Target/Cpp/member.mlir
+++ b/mlir/test/Target/Cpp/member.mlir
@@ -21,6 +21,8 @@ func.func @member(%arg0: !emitc.opaque<"mystruct">, %arg1: i32, %arg2: index) {
   %8 = emitc.subscript %7[%arg2] : (!emitc.array<2xi32>, index) -> !emitc.lvalue<i32>
   emitc.assign %arg1 : i32 to %8 : !emitc.lvalue<i32>
 
+  %9 = "emitc.member" (%arg0) {member = "a"} : (!emitc.opaque<"mystruct">) -> i32
+
   return
 }
 
@@ -34,6 +36,7 @@ func.func @member(%arg0: !emitc.opaque<"mystruct">, %arg1: i32, %arg2: index) {
 // CPP-DEFAULT-NEXT: int32_t [[V5:[^ ]*]] = ([[V2]].c)[[[Index]]];
 // CPP-DEFAULT-NEXT: [[V4]] = [[V5]];
 // CPP-DEFAULT-NEXT: ([[V2]].d)[[[Index]]] = [[V1]];
+// CPP-DEFAULT-NEXT: int32_t [[V6:[^ ]*]] = [[V0]].a;
 
 
 func.func @member_of_pointer(%arg0: !emitc.ptr<!emitc.opaque<"mystruct">>, %arg1: i32, %arg2: index) {

>From d0344edf9bbb801468647515782dfbb9ab527a3b Mon Sep 17 00:00:00 2001
From: Gil Rapaport <gil.rapaport at mobileye.com>
Date: Sat, 13 Jun 2026 13:32:28 +0300
Subject: [PATCH 2/2] Address review comment

Added explanation for member op's operand/return value/memory semantic
match requirement.
Added a non-lvalue example in the op's documentation.
---
 mlir/include/mlir/Dialect/EmitC/IR/EmitC.td | 2 ++
 mlir/lib/Dialect/EmitC/IR/EmitC.cpp         | 8 ++++++++
 2 files changed, 10 insertions(+)

diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index 1591feda11b0b..2b3cb74b0363e 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -1178,6 +1178,8 @@ def EmitC_MemberOp : EmitC_Op<"member", [CExpressionInterface]> {
     Example:
 
     ```mlir
+    %0 = "emitc.member" (%arg0) {member = "a"}
+        : (!emitc.opaque<"mystruct">) -> i32
     %0 = "emitc.member" (%arg0) {member = "a"}
         : (!emitc.lvalue<!emitc.opaque<"mystruct">>) -> !emitc.lvalue<i32>
     %1 = "emitc.member" (%arg0) {member = "b"}
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index 836bd7209c14c..683099f02dcc8 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -1098,6 +1098,14 @@ LogicalResult MemberOp::verify() {
   Type resultType = getResult().getType();
   bool resultIsWritable = isa<emitc::LValueType, emitc::ArrayType>(resultType);
 
+  // Make sure the operand and return type agree on value/memory semantics:
+  // If the operand is an lvalue it models a memory location and as such its
+  // elements are also memory locations: They require a load operation to use
+  // their value and they can be assigned new values.
+  // If the operand isn't an lvalue it models an aggregate SSA value and as
+  // such its elements are also SSA values: Their value can be used directly
+  // but they cannot be assigned to.
+
   if (isa<emitc::LValueType>(operandType) && !resultIsWritable)
     return emitOpError("lvalues must return lvalues or arrays");
 



More information about the Mlir-commits mailing list