[Mlir-commits] [mlir] [mlir][acc] Add compute kernel launch codegen to ACCToLLVM (PR #232382)
Valentin Clement バレンタイン クレメン
llvmlistbot at llvm.org
Thu Oct 15 12:58:27 PDT 2026
================
@@ -0,0 +1,490 @@
+//===- ACCKernelLaunchPatterns.cpp - Kernel launch to LLVM ------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Lower acc.kernel_environment and the gpu.launch_func operations in its body
+// together. The environment establishes one implicit data region. Its entry
+// call returns the device addresses that every contained kernel launch uses.
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Conversion/OpenACCToLLVM/ACCDataRuntime.h"
+#include "mlir/Conversion/OpenACCToLLVM/ACCToLLVM.h"
+#include "mlir/Conversion/OpenACCToLLVM/ACCToLLVMUtils.h"
+
+#include "mlir/Conversion/LLVMCommon/Pattern.h"
+#include "mlir/Dialect/Func/IR/FuncOps.h"
+#include "mlir/Dialect/GPU/IR/GPUDialect.h"
+#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
+#include "mlir/Dialect/OpenACC/Analysis/OpenACCSupport.h"
+#include "mlir/Dialect/OpenACC/OpenACC.h"
+#include "mlir/Dialect/OpenACC/OpenACCUtilsCG.h"
+#include "mlir/Dialect/OpenACC/OpenACCUtilsType.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/raw_ostream.h"
+
+#include <array>
+
+using namespace mlir;
+using namespace mlir::acc;
+
+static bool isDevicePtrMapping(Value operand) {
+ Operation *mapOp = operand.getDefiningOp();
+ if (!mapOp)
+ return false;
+ if (std::optional<MapFlags> flags = getMapFlags(mapOp))
+ return bitEnumContainsAny(*flags, MapFlags::devptr);
+ if (std::optional<DataClause> clause = getDataClause(mapOp))
+ return *clause == DataClause::acc_deviceptr;
+ return false;
+}
+
+static bool isDataEntryInsideEnvironment(KernelEnvironmentOp environment,
+ Value operand) {
+ Operation *mapOp = operand.getDefiningOp();
+ return isDataEntryOp(mapOp) && environment->isAncestor(mapOp);
+}
+
+/// The flags of a data clause operation that maps a kernel argument. A kernel
+/// argument only looks up the mapping its clause established, so the clauses
+/// that establish or tear down something else contribute no flag for it.
+static MapFlags getKernelDataClauseMapFlags(Operation *entryOp) {
+ bool ptrAndObj = hasAttachPoint(entryOp);
+ MapFlags ptrAndObjFlag = ptrAndObj ? MapFlags::ptr_and_obj : MapFlags::none;
+ std::optional<DataClause> clause = getDataClause(entryOp);
+ if (clause == DataClause::acc_declare_device_resident)
+ return MapFlags::present | ptrAndObjFlag;
+ if (clause == DataClause::acc_attach || clause == DataClause::acc_detach)
+ return ptrAndObjFlag;
+ return bitEnumClear(computeDataClauseMapFlags(entryOp, ptrAndObj),
+ MapFlags::device_resident);
+}
+
+/// The flags a kernel argument is passed with. An operand the environment
+/// already mapped is passed as the device address its entry call returned. An
+/// operand that no clause maps is passed by value.
+static MapFlags getKernelArgMapFlags(Value operand, bool alreadyMapped,
+ const ACCRuntimeCallConfig &config) {
+ Operation *mapOp = operand.getDefiningOp();
+ if (!isDataEntryOp(mapOp))
+ return mapOp ? config.postProcessMapFlags(mapOp, MapFlags::literal)
+ : MapFlags::literal;
+
+ MapFlags flags;
+ if (alreadyMapped)
+ flags = MapFlags::devptr | MapFlags::literal;
+ else if (auto mapInfo = dyn_cast<MapInfoOp>(mapOp))
+ flags = mapInfo.getMapFlags();
+ else
+ flags = getKernelDataClauseMapFlags(mapOp);
+ if (getImplicitFlag(mapOp))
+ flags = flags | MapFlags::implicit;
+ return config.postProcessMapFlags(mapOp, flags);
+}
+
+/// The descriptor of a kernel argument. The device address the environment
+/// returned is passed as is, since the runtime would map an address that
+/// carries a descriptor again.
+static FailureOr<Value>
+getKernelArgDescriptor(Location loc, Value operand, Value converted,
+ bool alreadyMapped, OpenACCSupport &accSupport,
+ ConversionPatternRewriter &rewriter) {
+ Type ptrTy = LLVM::LLVMPointerType::get(rewriter.getContext());
+ Operation *mapOp = operand.getDefiningOp();
+ if (alreadyMapped)
+ return Value(LLVM::ZeroOp::create(rewriter, loc, ptrTy));
+ if (isDataEntryOp(mapOp))
+ return createACCArgumentDescriptor(mapOp, converted, accSupport, rewriter);
+ if (auto memrefType = dyn_cast<MemRefType>(operand.getType())) {
+ ModuleOp module = rewriter.getInsertionBlock()
+ ->getParentOp()
+ ->getParentOfType<ModuleOp>();
+ auto sizeAndAlignment = acc::getTypeSizeAndAlignment(
+ memrefType.getElementType(), module, &accSupport);
+ if (!sizeAndAlignment) {
+ std::string message;
+ llvm::raw_string_ostream os(message);
+ os << "unhandled type size computation for type: "
+ << memrefType.getElementType();
+ (void)accSupport.emitNYI(loc, message);
+ return failure();
+ }
+ return createACCMemRefDescriptorWrapperArg(
+ loc, memrefType, converted, /*bounds=*/{},
+ constantI64(loc, sizeAndAlignment->first.getFixedValue(), rewriter),
+ rewriter);
+ }
+ return Value(LLVM::ZeroOp::create(rewriter, loc, ptrTy));
+}
+
+/// Emits the argument arrays of the kernel launched by \p launch, one entry per
+/// kernel operand. Each operand is one parameter of the kernel.
+static FailureOr<ACCDataRuntimeArgs> emitKernelRuntimeArgs(
+ gpu::LaunchFuncOp launch, gpu::LaunchFuncOpAdaptor adaptor,
+ ValueRange alreadyMappedOperands, ConversionPatternRewriter &rewriter,
+ OpenACCSupport &accSupport, Region &globalSymbolRegion,
+ SymbolTable &symbolTable, const ACCRuntimeCallConfig &config) {
+ Location loc = launch.getLoc();
+ SmallVector<ACCMappedObjectArgs> objects;
+ for (auto [operand, converted] : llvm::zip_equal(
+ launch.getKernelOperands(), adaptor.getKernelOperands())) {
+ bool alreadyMapped = llvm::is_contained(alreadyMappedOperands, operand);
+ MapFlags mapFlags = getKernelArgMapFlags(operand, alreadyMapped, config);
+ Operation *mapOp = operand.getDefiningOp();
+
+ ACCMappedObjectArgs args;
+ // A kernel argument states the base of the mapping it looks up, which for
+ // an object the declare directive made resident is that of its entry.
+ args.base = createACCMappedObjectBase(
+ loc, isDataEntryOp(mapOp) ? mapOp : nullptr, mapFlags, rewriter);
+ args.pointer = createACCMappedObjectPointer(loc, operand, converted,
+ mapFlags, rewriter);
+ FailureOr<Value> size = createACCMappedObjectSize(
+ loc, operand, converted, mapFlags, accSupport, rewriter);
+ if (failed(size))
+ return failure();
+ args.size = *size;
+ args.type =
+ constantI64(loc, config.getMapFlagsRuntimeValue(mapFlags), rewriter);
+ args.name = createACCMappedObjectName(
+ loc, operand, accSupport, globalSymbolRegion, &symbolTable, rewriter);
+ FailureOr<Value> descriptor = getKernelArgDescriptor(
+ loc, operand, converted, alreadyMapped, accSupport, rewriter);
+ if (failed(descriptor))
+ return failure();
+ args.descriptor = *descriptor;
+ objects.push_back(args);
+ }
+
+ ACCDataRuntimeArgs runtimeArgs;
+ createACCMappedObjectArrays(loc, objects, runtimeArgs, rewriter);
+ return runtimeArgs;
+}
+
+static FailureOr<Value> createKernelArgumentStruct(
+ gpu::LaunchFuncOp launch, gpu::LaunchFuncOpAdaptor adaptor,
+ ValueRange alreadyMappedOperands, ConversionPatternRewriter &rewriter,
+ OpenACCSupport &accSupport, Region &globalSymbolRegion,
+ SymbolTable &symbolTable, const ACCRuntimeCallConfig &config) {
+ FailureOr<ACCDataRuntimeArgs> emitted = emitKernelRuntimeArgs(
+ launch, adaptor, alreadyMappedOperands, rewriter, accSupport,
+ globalSymbolRegion, symbolTable, config);
+ if (failed(emitted))
+ return failure();
+ ACCDataRuntimeArgs runtimeArgs = *emitted;
+
+ Location loc = launch.getLoc();
+ MLIRContext *context = rewriter.getContext();
+ Type i32Ty = rewriter.getI32Type();
+ Type ptrTy = LLVM::LLVMPointerType::get(context);
+ Type argsType = getKernelArgumentType(context);
+ Value args = LLVM::ZeroOp::create(rewriter, loc, argsType);
+ auto insert = [&](Value value, KernelArgumentField field) {
+ args = LLVM::InsertValueOp::create(
+ rewriter, loc, argsType, args, value,
+ ArrayRef<int64_t>{getKernelArgumentFieldIndex(field)});
+ };
+ insert(runtimeArgs.argNum, KernelArgumentField::ArgNum);
+ insert(runtimeArgs.argBasePtrs, KernelArgumentField::ArgBasePtrs);
+ insert(runtimeArgs.argPtrs, KernelArgumentField::ArgPtrs);
+ insert(runtimeArgs.argSizes, KernelArgumentField::ArgSizes);
+ insert(runtimeArgs.argTypes, KernelArgumentField::ArgTypes);
+ insert(runtimeArgs.argNames, KernelArgumentField::ArgNames);
+ insert(runtimeArgs.argMappers, KernelArgumentField::ArgMappers);
+ insert(runtimeArgs.argDescs, KernelArgumentField::ArgDescs);
+
+ // The vector length is the first block dimension. The worker count is the
+ // product of the remaining ones.
+ Value blockY = castToI64(loc, adaptor.getBlockSizeY(), rewriter);
+ Value blockZ = castToI64(loc, adaptor.getBlockSizeZ(), rewriter);
+ insert(LLVM::MulOp::create(rewriter, loc, blockY, blockZ),
+ KernelArgumentField::NumWorkers);
+ insert(castToI64(loc, adaptor.getBlockSizeX(), rewriter),
+ KernelArgumentField::VectorLength);
+ Value dynamicSharedMemorySize =
+ adaptor.getDynamicSharedMemorySize()
+ ? castToI64(loc, adaptor.getDynamicSharedMemorySize(), rewriter)
+ : constantI64(loc, 0, rewriter);
+ insert(dynamicSharedMemorySize, KernelArgumentField::SmemSize);
+
+ // The grid sizes are the entries of the array in the NumGangs field.
+ int64_t numGangsField =
+ getKernelArgumentFieldIndex(KernelArgumentField::NumGangs);
+ std::array<Value, 3> gridSizes = {
+ adaptor.getGridSizeX(), adaptor.getGridSizeY(), adaptor.getGridSizeZ()};
+ for (auto [dimension, gridSize] : llvm::enumerate(gridSizes))
+ args = LLVM::InsertValueOp::create(
+ rewriter, loc, argsType, args, castToI64(loc, gridSize, rewriter),
+ ArrayRef<int64_t>{numGangsField, static_cast<int64_t>(dimension)});
+
+ Value one = LLVM::ConstantOp::create(rewriter, loc, i32Ty, 1);
+ Value argsPtr = LLVM::AllocaOp::create(rewriter, loc, ptrTy, argsType, one);
+ LLVM::StoreOp::create(rewriter, loc, args, argsPtr);
+ return argsPtr;
+}
+
+static FailureOr<Value> getKernelPointer(gpu::LaunchFuncOp launch,
+ ConversionPatternRewriter &rewriter,
+ Region &globalSymbolRegion,
+ SymbolTable &symbolTable) {
+ Location loc = launch.getLoc();
+ Type ptrTy = LLVM::LLVMPointerType::get(rewriter.getContext());
+ StringRef kernelName = launch.getKernel().getLeafReference().getValue();
+ if (auto function = symbolTable.lookup<LLVM::LLVMFuncOp>(kernelName))
+ return LLVM::AddressOfOp::create(rewriter, loc, ptrTy, function.getName())
+ .getResult();
+ if (auto function = symbolTable.lookup<func::FuncOp>(kernelName))
+ return LLVM::AddressOfOp::create(rewriter, loc, ptrTy, function.getName())
+ .getResult();
+
+ auto gpuFunction = SymbolTable::lookupNearestSymbolFrom<gpu::GPUFuncOp>(
+ launch, launch.getKernel());
+ if (!gpuFunction)
+ return failure();
+
+ {
+ OpBuilder::InsertionGuard guard(rewriter);
+ rewriter.setInsertionPointToStart(&globalSymbolRegion.front());
+ auto functionType = LLVM::LLVMFunctionType::get(
+ LLVM::LLVMVoidType::get(rewriter.getContext()), {});
+ auto declaration =
+ LLVM::LLVMFuncOp::create(rewriter, loc, kernelName, functionType);
+ symbolTable.insert(declaration);
+ }
+ return LLVM::AddressOfOp::create(rewriter, loc, ptrTy, kernelName)
+ .getResult();
+}
+
+static void declareLibc(SymbolTable &symbolTable, Region &globalSymbolRegion,
+ Location loc, StringRef name, Type result,
+ ArrayRef<Type> arguments) {
+ if (symbolTable.lookup<LLVM::LLVMFuncOp>(name))
+ return;
+ OpBuilder builder = OpBuilder::atBlockEnd(&globalSymbolRegion.front());
+ auto functionType = LLVM::LLVMFunctionType::get(result, arguments);
+ symbolTable.insert(
+ LLVM::LLVMFuncOp::create(builder, loc, name, functionType));
+}
+
+/// When the kernel entry point reports that the kernel did not launch, report
+/// the failure and stop.
+static void emitHostFallbackNotImplemented(Location loc, Value status,
+ ConversionPatternRewriter &rewriter,
+ Region &globalSymbolRegion,
+ SymbolTable &symbolTable) {
+ MLIRContext *context = rewriter.getContext();
+ Type i32Ty = rewriter.getI32Type();
+ Type ptrTy = LLVM::LLVMPointerType::get(context);
+ Type voidTy = LLVM::LLVMVoidType::get(context);
+
+ Block *preBlock = rewriter.getInsertionBlock();
+ Block *continueBlock =
+ rewriter.splitBlock(preBlock, rewriter.getInsertionPoint());
+ Block *errorBlock =
+ rewriter.createBlock(preBlock->getParent(), continueBlock->getIterator());
+ rewriter.setInsertionPointToEnd(preBlock);
+ Value success = LLVM::ConstantOp::create(
+ rewriter, loc, i32Ty, static_cast<int32_t>(RuntimeResult::Success));
+ Value failed = LLVM::ICmpOp::create(rewriter, loc, LLVM::ICmpPredicate::ne,
+ status, success);
+ LLVM::CondBrOp::create(rewriter, loc, failed, errorBlock, ValueRange{},
+ continueBlock, ValueRange{});
+
+ // TODO: Runtime API should provide a mechanism to report this error.
+ rewriter.setInsertionPointToStart(errorBlock);
+ declareLibc(symbolTable, globalSymbolRegion, loc, "puts", i32Ty, {ptrTy});
+ declareLibc(symbolTable, globalSymbolRegion, loc, "fflush", i32Ty, {ptrTy});
+ declareLibc(symbolTable, globalSymbolRegion, loc, "abort", voidTy, {});
+ Value message = getOrCreateGlobalString(
+ loc, rewriter, getInternalGlobalName("message", "host_fallback_nyi"),
+ "host fallback execution is not implemented\n", globalSymbolRegion,
+ &symbolTable);
+ LLVM::CallOp::create(rewriter, loc, TypeRange{i32Ty},
+ SymbolRefAttr::get(context, "puts"),
+ ValueRange{message});
+ Value nullFile = LLVM::ZeroOp::create(rewriter, loc, ptrTy);
+ LLVM::CallOp::create(rewriter, loc, TypeRange{i32Ty},
+ SymbolRefAttr::get(context, "fflush"),
+ ValueRange{nullFile});
+ LLVM::CallOp::create(rewriter, loc, TypeRange{},
+ SymbolRefAttr::get(context, "abort"), ValueRange{});
+ LLVM::UnreachableOp::create(rewriter, loc);
+ rewriter.setInsertionPointToStart(continueBlock);
+}
+
+static LogicalResult
+emitKernelCall(gpu::LaunchFuncOp launch, gpu::LaunchFuncOpAdaptor adaptor,
+ Value asyncQueue, ValueRange alreadyMappedOperands,
+ ConversionPatternRewriter &rewriter, OpenACCSupport &accSupport,
+ Region &globalSymbolRegion, SymbolTable &symbolTable,
+ const ACCRuntimeCallConfig &config) {
+ Location loc = launch.getLoc();
+ FailureOr<Value> kernelPointer =
+ getKernelPointer(launch, rewriter, globalSymbolRegion, symbolTable);
+ if (failed(kernelPointer)) {
+ (void)accSupport.emitNYI(loc, "undefined kernel");
+ return failure();
+ }
+ FailureOr<Value> arguments = createKernelArgumentStruct(
+ launch, adaptor, alreadyMappedOperands, rewriter, accSupport,
+ globalSymbolRegion, symbolTable, config);
+ if (failed(arguments))
+ return failure();
+
+ StringRef kernelName = launch.getKernel().getLeafReference().getValue();
+ Value kernelNamePtr = getOrCreateGlobalString(
+ loc, rewriter, getInternalGlobalName("kernel_name", kernelName),
+ kernelName, globalSymbolRegion, &symbolTable);
+ Value ident = createIdent(loc, getParentFunctionName(launch), rewriter,
+ globalSymbolRegion, config, &symbolTable);
+ // The kernel call uses the explicit data-region flag.
+ Value flags = constantI64(loc, static_cast<int64_t>(ACCDataRegionFlags::None),
+ rewriter);
+ // The launch is made for the default device type, as the data calls of the
+ // environment are.
+ Value deviceType = constantI64(
+ loc, config.getDeviceTypeRuntimeValue(DeviceType::None), rewriter);
+ Value binaryDescriptor = config.createKernelBinaryDescriptor(loc, rewriter);
+ FailureOr<LLVM::CallOp> call =
+ createRuntimeCall(loc, rewriter, globalSymbolRegion, symbolTable,
+ RuntimeFunction::ACCRTL_tgt_acc_kernel, config,
+ {ident, *kernelPointer, flags, deviceType, *arguments,
+ asyncQueue, kernelNamePtr, binaryDescriptor});
+ if (failed(call))
+ return failure();
+ emitHostFallbackNotImplemented(loc, call->getResult(), rewriter,
+ globalSymbolRegion, symbolTable);
+ return success();
+}
+
+namespace {
+struct KernelEnvironmentLowering
+ : public ConvertOpToLLVMPattern<KernelEnvironmentOp> {
+ KernelEnvironmentLowering(const LLVMTypeConverter &converter,
+ OpenACCSupport &accSupport,
+ Region &globalSymbolRegion,
+ SymbolTable &symbolTable,
+ const ACCRuntimeCallConfig &config,
+ DeviceType clauseDeviceType,
+ PatternBenefit benefit = 1)
+ : ConvertOpToLLVMPattern<KernelEnvironmentOp>(converter, benefit),
+ accSupport(accSupport), globalSymbolRegion(globalSymbolRegion),
+ symbolTable(symbolTable), config(config),
+ clauseDeviceType(clauseDeviceType) {}
+
+ LogicalResult
+ matchAndRewrite(KernelEnvironmentOp environment, OpAdaptor adaptor,
+ ConversionPatternRewriter &rewriter) const override {
+ ValueRange mappingOperands = environment.getDataClauseOperands();
+ ValueRange convertedMappingOperands = adaptor.getDataClauseOperands();
+ Location beginLoc = mappingOperands.empty()
+ ? environment.getLoc()
+ : mappingOperands.front().getLoc();
+ Location endLoc = getMappingExitLoc(mappingOperands).value_or(beginLoc);
+ Value asyncQueue =
+ getAsyncQueue(environment, clauseDeviceType, rewriter, config);
+ if (failed(emitWaitClause(environment, clauseDeviceType, asyncQueue,
+ rewriter, accSupport, globalSymbolRegion,
+ symbolTable, config)))
+ return failure();
+
+ SmallVector<Value> alreadyMappedOperands;
+ for (Value operand : mappingOperands)
+ if (!isDevicePtrMapping(operand))
+ alreadyMappedOperands.push_back(operand);
+
+ ACCDataCallOptions beginOptions;
+ beginOptions.flags = static_cast<int64_t>(ACCDataRegionFlags::Implicit);
+ beginOptions.returnAddressOperands = alreadyMappedOperands;
+ FailureOr<SmallVector<Value>> devicePointers = emitACCDataRuntimeCall(
+ beginLoc, RuntimeFunction::ACCRTL_tgt_acc_data_begin, mappingOperands,
+ convertedMappingOperands, rewriter, accSupport, globalSymbolRegion,
+ symbolTable, config, {asyncQueue}, beginOptions);
+ if (failed(devicePointers))
+ return failure();
+
+ llvm::DenseMap<Value, Value> deviceAddress;
+ for (auto [operand, pointer] :
+ llvm::zip_equal(alreadyMappedOperands, *devicePointers))
+ deviceAddress[operand] = pointer;
+
+ SmallVector<gpu::LaunchFuncOp> launches;
+ environment.walk([&](gpu::LaunchFuncOp launch) {
+ if (launch->getParentOfType<KernelEnvironmentOp>() == environment)
+ launches.push_back(launch);
+ });
+ for (gpu::LaunchFuncOp launch : launches) {
+ SmallVector<Value> converted;
+ converted.reserve(launch->getNumOperands());
+ for (Value operand : launch->getOperands()) {
+ Value value;
+ // A data entry in the region is mapped by this kernel, so it is passed
+ // as the variable of its clause.
+ if (isDataEntryInsideEnvironment(environment, operand))
+ value = remapOrSelf(getVar(operand.getDefiningOp()), rewriter);
+ else
+ value = deviceAddress.lookup(operand);
+ if (!value)
+ value = rewriter.getRemappedValue(operand);
+ if (!value)
+ return rewriter.notifyMatchFailure(launch,
+ "unconverted kernel operand");
+ converted.push_back(value);
+ }
+
+ gpu::LaunchFuncOpAdaptor launchAdaptor(converted, launch);
+ Value queue = launchAdaptor.getAsyncObject();
+ queue = queue ? castToI64(launch.getLoc(), queue, rewriter) : asyncQueue;
+ rewriter.setInsertionPoint(launch);
+ if (failed(emitKernelCall(launch, launchAdaptor, queue,
+ alreadyMappedOperands, rewriter, accSupport,
+ globalSymbolRegion, symbolTable, config)))
+ return failure();
+ rewriter.eraseOp(launch);
+ }
+
+ rewriter.setInsertionPointAfter(environment);
+ ACCDataCallOptions endOptions;
+ endOptions.callKind = ACCDataCallKind::DataExit;
+ endOptions.flags = static_cast<int64_t>(ACCDataRegionFlags::Implicit);
+ if (failed(emitACCDataRuntimeCall(
+ endLoc, RuntimeFunction::ACCRTL_tgt_acc_data_end, mappingOperands,
+ convertedMappingOperands, rewriter, accSupport, globalSymbolRegion,
+ symbolTable, config, {asyncQueue}, endOptions)))
+ return failure();
+
+ spliceConstructRegion(environment, environment.getRegion(), rewriter);
+ rewriter.eraseOp(environment);
+ return success();
+ }
+
+ OpenACCSupport &accSupport;
+ Region &globalSymbolRegion;
+ SymbolTable &symbolTable;
+ ACCRuntimeCallConfig config;
----------------
clementval wrote:
Should this be a reference? Or do we need a value copy?
https://github.com/llvm/llvm-project/pull/232382
More information about the Mlir-commits
mailing list