Dialect conversion fails with illegal operation via the C++ API, but succeeds via the CLI

I have this MLIR code consisting of a mix of Standard, Linalg, and SCF dialects. I wish to lower the Linalg and SCF operations to the Standard dialect, and then lower the Standard dialect to the LLVM dialect.

module {
  func @main() {
    // Create constants for the lower and upper bounds of an integer range [1,10]
    %c1_i32 = constant 1 : i32
    %c10_i32 = constant 10 : i32

    // Compute the size of a rank 1 dynamic-size memref to contain all elements in the range [1,10]
    %c1_i32_0 = constant 1 : i32
    %0 = subi %c10_i32, %c1_i32 : i32
    %1 = addi %0, %c1_i32_0 : i32
    %2 = index_cast %1 : i32 to index

    // Allocate space and write the values of the range [1,10] into it
    %3 = alloca(%2) : memref<?xi32>
    %c0 = constant 0 : index
    %c1 = constant 1 : index
    scf.parallel (%arg0) = (%c0) to (%2) step (%c1) {
      %6 = index_cast %arg0 : index to i32
      %7 = addi %c1_i32, %6 : i32
      store %7, %3[%arg0] : memref<?xi32>

    // Make a copy of the above
    %c0_1 = constant 0 : index
    %4 = dim %3, %c0_1 : memref<?xi32>
    %5 = alloca(%4) : memref<?xi32>
    linalg.copy(%3, %5) : memref<?xi32>, memref<?xi32>


I can successfully lower the above MLIR code to the LLVM dialect by invoking mlir-opt -convert-scf-to-std -convert-linalg-to-std -convert-std-to-llvm.

However, when I try to accomplish the same task via C++ using the following pass:

#include "mlir/Conversion/StandardToLLVM/ConvertStandardToLLVM.h"
#include "mlir/Conversion/StandardToLLVM/ConvertStandardToLLVMPass.h"
#include "mlir/Conversion/SCFToStandard/SCFToStandard.h"
#include "mlir/Conversion/LinalgToStandard/LinalgToStandard.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/Dialect/SCF/SCF.h"
#include "mlir/Dialect/StandardOps/IR/Ops.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Transforms/DialectConversion.h"

using namespace mlir;

struct LLVMLoweringPass : public PassWrapper<LLVMLoweringPass, OperationPass<ModuleOp>> {
  void getDependentDialects(DialectRegistry &registry) const override {
  void runOnOperation() final {
    LLVMConversionTarget target(getContext());
    target.addLegalOp<ModuleOp, ModuleTerminatorOp>();

    LLVMTypeConverter typeConverter(&getContext());

    OwningRewritePatternList patterns;
    linalg::populateLinalgToStandardConversionPatterns(patterns, &getContext());
    populateLoopToStdConversionPatterns(patterns, &getContext());
    populateStdToLLVMConversionPatterns(typeConverter, patterns);

    auto module = getOperation();
    if (failed(applyFullConversion(module, target, std::move(patterns))))

I get an error: failed to legalize operation 'scf.parallel'
With debug info:

Legalizing operation : 'scf.parallel'(0xced760) {
  * Fold {
  } -> FAILURE : unable to fold

  * Pattern : 'scf.parallel -> ()' {
    ** Insert  : 'scf.yield'(0xd49a10)
    ** Insert  : 'scf.for'(0xd14510)
    ** Insert  : 'std.index_cast'(0xd296e0)
    ** Insert  : 'std.addi'(0xd49a70)
    ** Insert  : 'std.store'(0xd49b20)
    ** Replace : 'scf.parallel'(0xced760)

    Legalizing operation : 'scf.yield'(0xd49a10) {
      "scf.yield"() : () -> ()

      * Fold {
      } -> FAILURE : unable to fold
    } -> FAILURE : no matched legalization pattern
  } -> FAILURE : generated operation 'scf.yield'(0x0000000000D49A10) was illegal
} -> FAILURE : no matched legalization pattern

I am not sure how to resolve this issue and would like some help with debugging.
I’m still quite new to MLIR (and compilers in general), so please bear with me.

In-case this is useful: I am using LLVM version 12.0.0git, pulled from the master branch. Last commit: 8b87fdb2079fad046b0611ac5e14aca921877c51

The way you set up the conversion differs from how the scf-to-std conversion is set up. The latter is a conversion that only marks illegal ForOp, ParallelOp, IfOp and WhileOp from SCF. In your case, only LLVM dialect ops are declared legal in the target, and there is no pattern to lower scf.yield (in practice, it’s the pattern for the ForOp that would handle it), so the conversion fails immediately.

One way to support this is to declare scf.yield as dynamically legal in the target, with the condition that it is a terminator of one of a the control-flow ops mention above. This will make sure the infra doesn’t complain about it prematurely.

@River707 in case he knows a better way or has ideas on how to make this case better supported in the infra (parent ops should be in charge of terminators, we don’t necessarily want terminators to have special patterns)

Marking the scf.yield as dynamically legal using the following snippet appears to have solved the issue. Thank you!

  target.addDynamicallyLegalOp<scf::YieldOp>([](Operation *op) {
    Operation *parentOp = op->getParentOp();
    Block *block = op->getBlock();
    if (isa<scf::ForOp, scf::ParallelOp, scf::IfOp, scf::WhileOp>(parentOp)) {
      return op == block->getTerminator();
    return false;

It was also mentioned by @mehdi_amini on Discord that simply marking scf.yield as a legal op gives the same result. However, it feels awkward marking an operation as legal when I do not want that operation to persist after conversion; so I will keep scf.yield dynamically legal.