Issue with LLVM::InvokeOp

I’ve been running into an issue with my compiler lowering from its dialect to the LLVM dialect when lowering to llvm.invoke. The problem appears to be that some point after conversion from my dialect’s invoke to llvm.invoke, something is modifying the successor operands for the normal block to contain the return value of the llvm.invoke operation itself:

%12 = llvm.invoke @"abs/1"(%8, %12) to ^bb6(%12 : !llvm.i64) unwind ^bb8 : (!llvm.i64, !llvm.i64) -> !llvm.i64

Notice how the return value of the invoke is also somehow an operand to the successofr block ^bb6? The error I get for this is error: operand #1 does not dominate this use, which makes perfect sense; but I’m not sure how it is getting mangled into this form. My assumption at this point is that some transformation in MLIR is performing an optimization that requires converting the return value of the invoke into a block argument, but it never checks whether that transformation is legal.

For my part, in an effort to troubleshoot this, I have all of my optimizations turned off; I’m not performing any canonicalizations that combine blocks or operations, basically just converting directly from my dialect to LLVM dialect, without going through the Standard dialect at all. The only passes I’m running at this point are the conversion pass itself, and a canonicalization pass before and after (which for my IR is a no-op, because I’ve disabled them, and as far as I can tell, the LLVM dialect has no canonicalizations either).

Here’s the relevant chunk of my dialect’s IR that then gets lowered to LLVM dialect (completely unoptimized, this is essentially translated directly from the frontend); it basically represents the equivalent of try { return abs(non_number()); } catch (exception) { return exception; }:

eir.func @"init:start/0"() -> !eir.term attributes {personality = @lumen_eh_personality} {
    %0 = llvm.mlir.null : !llvm<"i8*">
    %1 = eir.invoke @"non_number/0" to ^bb1 unwind ^bb12 : !eir.term
  ^bb1:  // pred: ^bb0
    eir.br ^bb2(%1 : !eir.term)
  ^bb2(%2: !eir.term):  // pred: ^bb1
    %3 = eir.invoke @"abs/1"(%2 : !eir.term) to ^bb9 unwind ^bb10 : !eir.term
  ^bb3:  // pred: ^bb4
    eir.unreachable
  ^bb4:  // pred: ^bb16
    eir.br ^bb3
  ^bb5:  // pred: ^bb8
    eir.return %14 : !eir.term
..snip..
  ^bb9:  // pred: ^bb2
    eir.return %3 : !eir.term
  ^bb10: // pred: ^bb2
    ...code path which ultimately leads to ^bb5..

To confirm that this wasn’t something I was during prior to converting to llvm.invoke, here’s what I see in the debug output during conversion:

//===-------------------------------------------===//
    Legalizing operation : 'llvm.invoke'(0x7fdb8df0d7d0) {
      %8 = "llvm.invoke"()[^bb3, ^bb9] {callee = @"non_number/0", operand_segment_sizes = dense<0> : vector<3xi32>} : () -> !llvm.i64

    } -> SUCCESS : operation marked legal by the target

...snip..

//===-------------------------------------------===//
    Legalizing operation : 'llvm.invoke'(0x7fdb8df0ef50) {
      %13 = "llvm.invoke"(%9)[^bb8, ^bb9] {callee = @"abs/1", operand_segment_sizes = dense<[1, 0, 0]> : vector<3xi32>} : (!eir.term) -> !llvm.i64

    } -> SUCCESS : operation marked legal by the target

Conversion itself succeeds, but fails verification (for obvious reasons).

So it appears to me that the initial conversion preserves the representation in my own IR, which itself is modeled after the way LLVM itself requires invokes to be laid out - namely that the return value is referenced directly from the normal block, not passed as a block argument. If there is a way for me to represent the return value of the invoke as an implicit argument to the successor block, and have that address the issue, I’m happy to make that change, but when I initially started using MLIR that wasn’t possible, or at least I wasn’t able to figure out how to represent it that way.

I’m starting to bang my head against the wall a bit, so wanted to see if anyone has suggestions on how best to troubleshoot this, or if I’ve found a bug. Happy to help test things out or provide more details if that’s helpful. Just let me know!

Paul

It isn’t clear to me that the llvm.invoke operation should produce a result in the first place.
LLVM is doing this because there aren’t any block arguments and you need to create the value. However in MLIR the SSA value can be just a block argument of the successor. We should just verify that the returned types of the invoked functions match the block args.

Agreed, I was a bit surprised that the MLIR implementation behaved that way actually, but I just rolled with it. That said, it’s not clear to me how to express the notion of an implicit successor operand in the definition of an operation. I haven’t done a thorough search, but it appears to me that there are no other operations that behave this way in the standard/LLVM dialects. If there is one I can use as a reference, I’m happy to do some experimenting!

The current llvm.invoke may have been added before I added support for implicit successor operands.

Implicit successor operands are the default, operations with non-implicit operands expose that the operands are pre-existing values by implementing the BranchOpInterface.

– River

So just to be clear: if invoke is a terminator and implements BranchOpInterface (as the unwind/normal successors can take additional arguments above and beyond the return value of the function being invoked) - then it is sufficient/valid to add an additional argument to the successor block for the normal branch to represent the return value? What determines which block argument is the implicit one? Is that the purpose of getSuccessorBlockArgument?

I spent a little time looking at refactoring llvm.invoke today; but I feel like I’m still missing some details.

Is the proper way to define this to remove the result type from the operation, and use the callee function type to determine the number and type of successor block arguments that need to be present and match type when doing verification? Then, when building out a function, you’d terminate a block with invoke, create the normal and unwind blocks, adding the implicit block arguments to the normal block, in addition to any other block arguments that the code calls for unrelated to invoke; does that sound correct to you? Presumably it is also important to implement the various functions for BranchOpInterface in order for those implicit block arguments to be considered valid when verifying the module; otherwise it seems like the verifier would complain that there are missing successor operands.

I also noticed that invoke does not implement CallOpInterface, is that for any reason in particular? Should it implement that trait? And if so, will that present any issues anywhere with the operation itself not having results, but instead forwarding its results to a successor block implicitly?

Anyway, sorry for all the questions; just want to make sure I understand thoroughly how this aspect of the IR works.