[Mlir-commits] [mlir] [mlir][emitc] Lower multiple results as a struct (PR #200659)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Sun May 31 05:43:24 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-mlir

Author: Gil Rapaport (aniragil)

<details>
<summary>Changes</summary>

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

---

Patch is 25.08 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200659.diff


4 Files Affected:

- (modified) mlir/lib/Conversion/FuncToEmitC/FuncToEmitC.cpp (+214-16) 
- (modified) mlir/test/Conversion/FuncToEmitC/func-to-emitc-failed.mlir (+81) 
- (modified) mlir/test/Conversion/FuncToEmitC/func-to-emitc.mlir (+94) 
- (modified) mlir/test/Target/Cpp/func.mlir (+63) 


``````````diff
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:      ass...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/200659


More information about the Mlir-commits mailing list