[Mlir-commits] [mlir] [mlir][emitc] Lower multiple results as a struct (PR #200659)
Gil Rapaport
llvmlistbot at llvm.org
Sun May 31 23:58:12 PDT 2026
https://github.com/aniragil updated https://github.com/llvm/llvm-project/pull/200659
>From 0c27ef8b04cf0a0a44170fa047992f35bf0ca18d Mon Sep 17 00:00:00 2001
From: Gil Rapaport <gil.rapaport at mobileye.com>
Date: Mon, 18 May 2026 12:14:08 +0300
Subject: [PATCH 1/2] [mlir][emitc] Lower multiple results as a struct
Previously, func to emitc lowering rejected func.{func,call,return}
with more than one result/operand. This patch lifts that restriction by
packing multiple return values into an automatically-generated struct,
e.g. for a function returning (i32, i32):
emitc.class struct @return_i32_i32 {
emitc.field @field0 : i32
emitc.field @field1 : i32
}
On return, the operands are packed into a local struct variable which is
then loaded and returned. On call sites, the struct is stored in a local
variable, and each field is extracted to recreate the individual SSA
values of the original results. As with single-result functions,
`emitc.array` return types are not supported.
If a class with that name already exists, it is verified to have exactly
the expected fields with the correct types and no methods. Two functions
with the same return type tuple share a single class definition.
Assisted-by: Copilot
---
.../Conversion/FuncToEmitC/FuncToEmitC.cpp | 230 ++++++++++++++++--
.../FuncToEmitC/func-to-emitc-failed.mlir | 81 ++++++
.../Conversion/FuncToEmitC/func-to-emitc.mlir | 94 +++++++
mlir/test/Target/Cpp/func.mlir | 63 +++++
4 files changed, 452 insertions(+), 16 deletions(-)
diff --git a/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp b/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
index d2fb359c9aabe..e772923fb25ad 100644
--- a/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
+++ b/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
@@ -16,12 +16,120 @@
#include "mlir/Conversion/ConvertToEmitC/ToEmitCInterface.h"
#include "mlir/Dialect/EmitC/IR/EmitC.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
+#include "mlir/IR/BuiltinAttributes.h"
+#include "mlir/IR/SymbolTable.h"
#include "mlir/Transforms/DialectConversion.h"
using namespace mlir;
namespace {
+//===----------------------------------------------------------------------===//
+// Multi-return struct helpers
+//===----------------------------------------------------------------------===//
+
+// Looks up or creates an `emitc.class` named after `types` in the nearest
+// enclosing symbol table of `op`, suitable for packing those types as plain
+// struct fields (field0, field1, ...). If the class already exists it is
+// verified to have exactly the right fields and no methods. Returns the
+// corresponding !emitc.opaque<"struct ..."> type on success.
+static FailureOr<emitc::OpaqueType>
+getOrCreateMultiReturnType(ConversionPatternRewriter &rewriter, Location loc,
+ Operation *op, TypeRange types) {
+ // Build the struct name from the types, e.g. "return_i32_i32". Each type is
+ // printed and non-alphanumeric characters are replaced with '_'.
+ std::string structName = "return";
+ for (Type type : types) {
+ std::string typeName;
+ llvm::raw_string_ostream os(typeName);
+ type.print(os);
+ std::replace_if(
+ typeName.begin(), typeName.end(),
+ [](char c) { return !llvm::isAlnum(c); }, '_');
+ structName += "_" + typeName;
+ }
+
+ // Find the enclosing symbol table and the direct child op within it that
+ // contains `op`; the class will be inserted immediately before that child.
+ Operation *symbolTableOp = SymbolTable::getNearestSymbolTable(op);
+ Operation *insertBefore = op;
+ while (insertBefore->getParentOp() != symbolTableOp)
+ insertBefore = insertBefore->getParentOp();
+
+ if (Operation *sym = SymbolTable::lookupSymbolIn(symbolTableOp, structName)) {
+ auto classOp = dyn_cast<emitc::ClassOp>(sym);
+ if (!classOp)
+ return emitError(loc) << "symbol '" << structName
+ << "' exists but is not an emitc.class";
+
+ if (classOp.getClassType() != emitc::ClassType::struct_)
+ return emitError(loc)
+ << "existing class '" << structName << "' is not a struct";
+
+ SmallVector<emitc::FieldOp> fields;
+ for (Operation &bodyOp : classOp.getBody().front()) {
+ if (isa<emitc::FuncOp>(bodyOp))
+ return emitError(loc) << "existing class '" << structName
+ << "' has methods; expected a plain struct";
+ if (auto fieldOp = dyn_cast<emitc::FieldOp>(bodyOp))
+ fields.push_back(fieldOp);
+ }
+ if (fields.size() != types.size())
+ return emitError(loc) << "existing class '" << structName
+ << "' has wrong number of fields";
+ for (auto [i, fieldOp] : llvm::enumerate(fields)) {
+ if (fieldOp.getSymName() != "field" + std::to_string(i))
+ return emitError(loc) << "existing class '" << structName
+ << "': unexpected field name at index " << i;
+ if (fieldOp.getTypeAttr().getValue() != types[i])
+ return emitError(loc) << "existing class '" << structName
+ << "': wrong type for field " << i;
+ }
+ } else {
+ // Create the ClassOp before `insertBefore`, then restore the insertion
+ // point.
+ auto savedIP = rewriter.saveInsertionPoint();
+ rewriter.setInsertionPoint(insertBefore);
+
+ emitc::ClassOp classOp = emitc::ClassOp::create(rewriter, loc, structName,
+ /*final_specifier=*/false,
+ emitc::ClassType::struct_);
+ rewriter.createBlock(&classOp.getBody());
+ rewriter.setInsertionPointToStart(&classOp.getBody().front());
+
+ for (auto [i, type] : llvm::enumerate(types)) {
+ auto fieldName = rewriter.getStringAttr("field" + std::to_string(i));
+ emitc::FieldOp::create(rewriter, loc, fieldName, TypeAttr::get(type),
+ nullptr);
+ }
+
+ rewriter.restoreInsertionPoint(savedIP);
+ }
+ return emitc::OpaqueType::get(rewriter.getContext(), "struct " + structName);
+}
+
+// Packs multiple SSA values into an emitc.class struct variable and loads the
+// result as a single SSA value of the opaque struct type.
+static Value packValuesIntoStruct(ConversionPatternRewriter &rewriter,
+ Location loc, ValueRange values,
+ emitc::OpaqueType structType) {
+ MLIRContext *ctx = rewriter.getContext();
+ auto noInit = emitc::OpaqueAttr::get(ctx, "");
+ Value structLv =
+ emitc::VariableOp::create(rewriter, loc,
+ emitc::LValueType::get(structType), noInit)
+ .getResult();
+ for (auto [i, val] : llvm::enumerate(values)) {
+ Value fieldLv =
+ emitc::MemberOp::create(
+ rewriter, loc, emitc::LValueType::get(val.getType()),
+ rewriter.getStringAttr("field" + std::to_string(i)), structLv)
+ .getResult();
+ emitc::AssignOp::create(rewriter, loc, fieldLv, val);
+ }
+ return emitc::LoadOp::create(rewriter, loc, structType, structLv).getResult();
+}
+
/// Implement the interface to convert Func to EmitC.
struct FuncToEmitCDialectInterface : public ConvertToEmitCPatternInterface {
FuncToEmitCDialectInterface(Dialect *dialect)
@@ -55,15 +163,66 @@ class CallOpConversion final : public OpConversionPattern<func::CallOp> {
LogicalResult
matchAndRewrite(func::CallOp callOp, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
- // Multiple results func cannot be converted to `emitc.func`.
- if (callOp.getNumResults() > 1)
- return rewriter.notifyMatchFailure(
- callOp, "only functions with zero or one result can be converted");
+ if (callOp.getNumResults() <= 1) {
+ rewriter.replaceOpWithNewOp<emitc::CallOp>(
+ callOp, callOp.getResultTypes(), adaptor.getOperands(),
+ callOp->getAttrs());
+ return success();
+ }
+
+ // Multi-result call: determine the struct type.
+ Location loc = callOp.getLoc();
+
+ SmallVector<Type> convertedResultTypes;
+ for (Type t : callOp.getResultTypes()) {
+ Type ct = getTypeConverter()->convertType(t);
+ if (!ct)
+ return rewriter.notifyMatchFailure(callOp,
+ "result type conversion failed");
+ if (isa<emitc::ArrayType>(ct))
+ return rewriter.notifyMatchFailure(
+ callOp,
+ "multi-result calls with array result types are not supported");
+ convertedResultTypes.push_back(ct);
+ }
+
+ auto structType =
+ getOrCreateMultiReturnType(rewriter, loc, callOp, convertedResultTypes);
+ if (failed(structType))
+ return rewriter.notifyMatchFailure(callOp,
+ "incompatible multi-return struct");
- rewriter.replaceOpWithNewOp<emitc::CallOp>(callOp, callOp.getResultTypes(),
- adaptor.getOperands(),
- callOp->getAttrs());
+ // Emit a call returning the packed struct.
+ Value structVal =
+ emitc::CallOp::create(rewriter, loc, callOp.getCalleeAttr(),
+ TypeRange{*structType}, adaptor.getOperands())
+ .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()) {
+ results.push_back(Value()); // No replacement needed.
+ continue;
+ }
+ 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());
+ }
+
+ rewriter.replaceOp(callOp, results);
return success();
}
};
@@ -77,10 +236,6 @@ class FuncOpConversion final : public OpConversionPattern<func::FuncOp> {
ConversionPatternRewriter &rewriter) const override {
FunctionType fnType = funcOp.getFunctionType();
- if (fnType.getNumResults() > 1)
- return rewriter.notifyMatchFailure(
- funcOp, "only functions with zero or one result can be converted");
-
TypeConverter::SignatureConversion signatureConverter(
fnType.getNumInputs());
for (const auto &argType : enumerate(fnType.getInputs())) {
@@ -97,6 +252,27 @@ class FuncOpConversion final : public OpConversionPattern<func::FuncOp> {
if (!resultType)
return rewriter.notifyMatchFailure(funcOp,
"result type conversion failed");
+ } else if (fnType.getNumResults() > 1) {
+ SmallVector<Type> convertedResultTypes;
+ for (Type t : fnType.getResults()) {
+ Type ct = getTypeConverter()->convertType(t);
+ if (!ct)
+ return rewriter.notifyMatchFailure(funcOp,
+ "result type conversion failed");
+ if (isa<emitc::ArrayType>(ct))
+ return rewriter.notifyMatchFailure(
+ funcOp, "multi-result functions with array result types are not "
+ "supported");
+ convertedResultTypes.push_back(ct);
+ }
+
+ auto structTypeOrErr = getOrCreateMultiReturnType(
+ rewriter, funcOp.getLoc(), funcOp, convertedResultTypes);
+ if (failed(structTypeOrErr))
+ return rewriter.notifyMatchFailure(funcOp,
+ "incompatible multi-return struct");
+
+ resultType = *structTypeOrErr;
}
// Create the converted `emitc.func` op.
@@ -146,13 +322,35 @@ class ReturnOpConversion final : public OpConversionPattern<func::ReturnOp> {
LogicalResult
matchAndRewrite(func::ReturnOp returnOp, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
- if (returnOp.getNumOperands() > 1)
+ unsigned numOperands = returnOp.getNumOperands();
+
+ if (numOperands <= 1) {
+ rewriter.replaceOpWithNewOp<emitc::ReturnOp>(
+ returnOp, numOperands ? adaptor.getOperands()[0] : nullptr);
+ return success();
+ }
+
+ // Multi-operand return: pack values into a struct.
+ Location loc = returnOp.getLoc();
+
+ SmallVector<Type> adaptedTypes;
+ for (Value v : adaptor.getOperands())
+ adaptedTypes.push_back(v.getType());
+
+ if (llvm::any_of(adaptedTypes,
+ [](Type t) { return isa<emitc::ArrayType>(t); }))
return rewriter.notifyMatchFailure(
- returnOp, "only zero or one operand is supported");
+ returnOp, "multi-result returns with array types are not supported");
+
+ auto structType =
+ getOrCreateMultiReturnType(rewriter, loc, returnOp, adaptedTypes);
+ if (failed(structType))
+ return rewriter.notifyMatchFailure(returnOp,
+ "incompatible multi-return struct");
- rewriter.replaceOpWithNewOp<emitc::ReturnOp>(
- returnOp,
- returnOp.getNumOperands() ? adaptor.getOperands()[0] : nullptr);
+ Value structVal =
+ packValuesIntoStruct(rewriter, loc, adaptor.getOperands(), *structType);
+ rewriter.replaceOpWithNewOp<emitc::ReturnOp>(returnOp, structVal);
return success();
}
};
diff --git a/mlir/test/Conversion/FuncToEmitC/func-to-emitc-failed.mlir b/mlir/test/Conversion/FuncToEmitC/func-to-emitc-failed.mlir
index 73b3adeedaecd..983f310d30679 100644
--- a/mlir/test/Conversion/FuncToEmitC/func-to-emitc-failed.mlir
+++ b/mlir/test/Conversion/FuncToEmitC/func-to-emitc-failed.mlir
@@ -4,3 +4,84 @@
func.func @unsuppoted_emitc_type(%arg0: i4) -> i4 {
return %arg0 : i4
}
+
+// -----
+
+// A symbol with the auto-generated struct name already exists but is not an
+// emitc.class (here it is an emitc.func).
+emitc.func @return_i32_i32() { emitc.return }
+// expected-error at +2 {{symbol 'return_i32_i32' exists but is not an emitc.class}}
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func @symbol_not_a_class(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+
+// -----
+
+// The existing emitc.class is not a struct (class_type != struct).
+emitc.class @return_i32_i32 {
+ emitc.field @field0 : i32
+ emitc.field @field1 : i32
+}
+// expected-error at +2 {{existing class 'return_i32_i32' is not a struct}}
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func @class_not_a_struct(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+
+// -----
+
+// The existing emitc.class has a method, so it cannot be used as a plain
+// struct.
+emitc.class struct @return_i32_i32 {
+ emitc.func @method() { emitc.return }
+}
+// expected-error at +2 {{existing class 'return_i32_i32' has methods; expected a plain struct}}
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func @class_has_methods(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+
+// -----
+
+// The existing emitc.class has fewer fields than the return types require.
+emitc.class struct @return_i32_i32 {
+ emitc.field @field0 : i32
+}
+// expected-error at +2 {{existing class 'return_i32_i32' has wrong number of fields}}
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func @class_wrong_field_count(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+
+// -----
+
+// The existing emitc.class has fields with unexpected names.
+emitc.class struct @return_i32_i32 {
+ emitc.field @a : i32
+ emitc.field @b : i32
+}
+// expected-error at +2 {{existing class 'return_i32_i32': unexpected field name at index 0}}
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func @class_wrong_field_names(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+
+// -----
+
+// The existing emitc.class has fields with the wrong types.
+emitc.class struct @return_i32_i32 {
+ emitc.field @field0 : i64
+ emitc.field @field1 : i32
+}
+// expected-error at +2 {{existing class 'return_i32_i32': wrong type for field 0}}
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func @class_wrong_field_types(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+
+// -----
+
+// Multi-result function where one result is an array type.
+// expected-error at +1 {{failed to legalize operation 'func.func'}}
+func.func private @multi_result_with_array() -> (i32, !emitc.array<10xi32>)
diff --git a/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir b/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir
index 6824a64dda3ef..ccda818585517 100644
--- a/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir
+++ b/mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir
@@ -75,3 +75,97 @@ func.func @call() {
call @return_void() : () -> ()
return
}
+
+// -----
+
+// Multi-result function: check that an emitc.class struct is created and the
+// function returns the packed struct.
+// CHECK-LABEL: emitc.class struct @return_i32_i32 {
+// CHECK: emitc.field @field0 : i32
+// CHECK: emitc.field @field1 : i32
+// CHECK: }
+// CHECK-LABEL: emitc.func @return_two(
+// CHECK-SAME: %[[ARG0:.*]]: i32,
+// CHECK-SAME: %[[ARG1:.*]]: i32) -> !emitc.opaque<"struct return_i32_i32"> {
+// CHECK: %[[VAL_0:.*]] = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
+// CHECK: %[[VAL_1:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field0"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK: assign %[[ARG0]] : i32 to %[[VAL_1]] : <i32>
+// CHECK: %[[VAL_2:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK: assign %[[ARG1]] : i32 to %[[VAL_2]] : <i32>
+// CHECK: %[[VAL_3:.*]] = load %[[VAL_0]] : <!emitc.opaque<"struct return_i32_i32">>
+// CHECK: return %[[VAL_3]] : !emitc.opaque<"struct return_i32_i32">
+// CHECK: }
+func.func @return_two(%arg0: i32, %arg1: i32) -> (i32, i32) {
+ return %arg0, %arg1 : i32, i32
+}
+
+// -----
+
+// Call to a multi-result function: check that the call returns the struct and
+// that only the field actually used is extracted.
+// CHECK-LABEL: emitc.class struct @return_i32_i32 {
+// CHECK: emitc.field @field0 : i32
+// CHECK: emitc.field @field1 : i32
+// CHECK: }
+// CHECK-LABEL: emitc.func @return_two(
+// CHECK-SAME: %[[ARG0:.*]]: i32,
+// CHECK-SAME: %[[ARG1:.*]]: i32) -> !emitc.opaque<"struct return_i32_i32"> {
+// CHECK-NEXT: %[[VAL_0:.*]] = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
+// CHECK-NEXT: %[[VAL_1:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field0"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK-NEXT: assign %[[ARG0]] : i32 to %[[VAL_1]] : <i32>
+// CHECK-NEXT: %[[VAL_2:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK-NEXT: assign %[[ARG1]] : i32 to %[[VAL_2]] : <i32>
+// CHECK-NEXT: %[[VAL_3:.*]] = load %[[VAL_0]] : <!emitc.opaque<"struct return_i32_i32">>
+// CHECK-NEXT: return %[[VAL_3]] : !emitc.opaque<"struct return_i32_i32">
+// CHECK-NEXT: }
+// 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: }
+func.func @return_two(%arg0: i32, %arg1: i32) -> (i32, i32) {
+ return %arg0, %arg1 : i32, i32
+}
+func.func @caller(%arg0: i32) -> i32 {
+ %0, %1 = call @return_two(%arg0, %arg0) : (i32, i32) -> (i32, i32)
+ return %1 : i32
+}
+
+// -----
+
+// Two functions returning the same type tuple share one emitc.class.
+// CHECK-LABEL: emitc.class struct @return_i32_i32 {
+// CHECK: emitc.field @field0 : i32
+// CHECK: emitc.field @field1 : i32
+// CHECK: }
+// CHECK-LABEL: emitc.func @first(
+// CHECK-SAME: %[[ARG0:.*]]: i32) -> !emitc.opaque<"struct return_i32_i32"> {
+// CHECK-NEXT: %[[VAL_0:.*]] = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
+// CHECK-NEXT: %[[VAL_1:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field0"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK-NEXT: assign %[[ARG0]] : i32 to %[[VAL_1]] : <i32>
+// CHECK-NEXT: %[[VAL_2:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK-NEXT: assign %[[ARG0]] : i32 to %[[VAL_2]] : <i32>
+// CHECK-NEXT: %[[VAL_3:.*]] = load %[[VAL_0]] : <!emitc.opaque<"struct return_i32_i32">>
+// CHECK-NEXT: return %[[VAL_3]] : !emitc.opaque<"struct return_i32_i32">
+// CHECK-NEXT: }
+// CHECK-LABEL: emitc.func @second(
+// CHECK-SAME: %[[ARG0:.*]]: i32) -> !emitc.opaque<"struct return_i32_i32"> {
+// CHECK-NEXT: %[[VAL_0:.*]] = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
+// CHECK-NEXT: %[[VAL_1:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field0"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK-NEXT: assign %[[ARG0]] : i32 to %[[VAL_1]] : <i32>
+// CHECK-NEXT: %[[VAL_2:.*]] = "emitc.member"(%[[VAL_0]]) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+// CHECK-NEXT: assign %[[ARG0]] : i32 to %[[VAL_2]] : <i32>
+// CHECK-NEXT: %[[VAL_3:.*]] = load %[[VAL_0]] : <!emitc.opaque<"struct return_i32_i32">>
+// CHECK-NEXT: return %[[VAL_3]] : !emitc.opaque<"struct return_i32_i32">
+// CHECK-NEXT: }
+// CHECK-NOT: emitc.class
+func.func @first(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
+func.func @second(%arg0: i32) -> (i32, i32) {
+ return %arg0, %arg0 : i32, i32
+}
diff --git a/mlir/test/Target/Cpp/func.mlir b/mlir/test/Target/Cpp/func.mlir
index 9c9ea55bfc4e1..82f1ee9f6ec2b 100644
--- a/mlir/test/Target/Cpp/func.mlir
+++ b/mlir/test/Target/Cpp/func.mlir
@@ -43,3 +43,66 @@ emitc.func private @extern_func(i32) attributes {specifiers = ["extern"]}
emitc.func private @array_arg(!emitc.array<3xi32>) attributes {specifiers = ["extern"]}
// CPP-DEFAULT: extern void array_arg(int32_t[3]);
+
+emitc.class struct @return_i32_i32 {
+ emitc.field @field0 : i32
+ emitc.field @field1 : i32
+}
+
+emitc.func @return_two(%arg0: i32, %arg1: i32) -> !emitc.opaque<"struct return_i32_i32"> {
+ %0 = "emitc.variable"() <{value = #emitc.opaque<"">}> : () -> !emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>
+ %1 = "emitc.member"(%0) <{member = "field0"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+ assign %arg0 : i32 to %1 : <i32>
+ %2 = "emitc.member"(%0) <{member = "field1"}> : (!emitc.lvalue<!emitc.opaque<"struct return_i32_i32">>) -> !emitc.lvalue<i32>
+ assign %arg1 : i32 to %2 : <i32>
+ %3 = load %0 : <!emitc.opaque<"struct return_i32_i32">>
+ return %3 : !emitc.opaque<"struct return_i32_i32">
+}
+
+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
+}
+
+// CPP-DEFAULT: struct return_i32_i32 {
+// CPP-DEFAULT-NEXT: int32_t field0;
+// CPP-DEFAULT-NEXT: int32_t field1;
+// CPP-DEFAULT-NEXT: };
+// CPP-DEFAULT-NEXT: struct return_i32_i32 return_two(int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
+// CPP-DEFAULT-NEXT: struct return_i32_i32 [[V3:[^ ]*]];
+// CPP-DEFAULT-NEXT: [[V3]].field0 = [[V1]];
+// CPP-DEFAULT-NEXT: [[V3]].field1 = [[V2]];
+// CPP-DEFAULT-NEXT: struct return_i32_i32 [[V4:[^ ]*]] = [[V3]];
+// CPP-DEFAULT-NEXT: return [[V4]];
+// 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-DECLTOP: struct return_i32_i32 {
+// CPP-DECLTOP-NEXT: int32_t field0;
+// CPP-DECLTOP-NEXT: int32_t field1;
+// CPP-DECLTOP-NEXT: };
+// CPP-DECLTOP-NEXT: struct return_i32_i32 return_two(int32_t [[V1:[^ ]*]], int32_t [[V2:[^ ]*]]) {
+// CPP-DECLTOP-NEXT: struct return_i32_i32 [[V3:[^ ]*]];
+// CPP-DECLTOP-NEXT: struct return_i32_i32 [[V4:[^ ]*]];
+// CPP-DECLTOP: [[V3]].field0 = [[V1]];
+// CPP-DECLTOP-NEXT: [[V3]].field1 = [[V2]];
+// CPP-DECLTOP-NEXT: [[V4]] = [[V3]];
+// CPP-DECLTOP-NEXT: return [[V4]];
+// 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: [[V2]] = return_two([[V1]], [[V1]]);
+// CPP-DECLTOP: [[V3]] = [[V2]];
+// CPP-DECLTOP-NEXT: [[V4]] = [[V3]].field1;
+// CPP-DECLTOP-NEXT: return [[V4]];
>From 8d181065a576239230b4d015d49c71828080bc98 Mon Sep 17 00:00:00 2001
From: Gil Rapaport <gil.rapaport at mobileye.com>
Date: Mon, 1 Jun 2026 09:57:54 +0300
Subject: [PATCH 2/2] Clang format
---
mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp b/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
index e772923fb25ad..2ab8e284aaea1 100644
--- a/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
+++ b/mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp
@@ -217,7 +217,8 @@ class CallOpConversion final : public OpConversionPattern<func::CallOp> {
rewriter.getStringAttr("field" + std::to_string(i));
Value fieldLv = emitc::MemberOp::create(rewriter, loc,
emitc::LValueType::get(fieldType),
- fieldName, structLv).getResult();
+ fieldName, structLv)
+ .getResult();
results.push_back(
emitc::LoadOp::create(rewriter, loc, fieldType, fieldLv).getResult());
}
More information about the Mlir-commits
mailing list