Using DRR to rewrite LLVM::CallOp

DRR is a nice way to write concise peephole transformations. But we’ve hit a snag when trying to deploy it to rewriting LLVM::CallOp calls.

In IR that may be partially lowered to LLVM-IR dialect, we have a need to rewrite LLVM-IR calls such as

tail call void @f1(...) 

to something like

tail call void @f1_special(%new_arg, ...)

Using DRR, we can write something like

def Special : Pat<
        (LLVM_CallOp $callee, $args, $fn),
        (CreateNewCall $args),
        [constraints]>;

If we also write

def CreateNewCall : NativeCodeCallVoid< ... >;

we get an immediate error (not enough values returned) since an LLVM_CallOp has 1 (optional) return value.

If we instead use

def CreateNewCall : NativeCodeCall< ... >;

and pass back a null Value, then things crash at runtime when the null Value is dereferenced.

The autogenerated code from DLL contains the following immediately after constructing the new Op.

    for (auto v: ::llvm::SmallVector<::mlir::Value, 4>{ {nativeVar_0} }) {
      tblgen_repl_values.push_back(v);
    }

The initialization list makes sense, since the LLVM::CallOp has a type and types are associated with Values, normally. But in the LLVM-IR dialect, a CallOp will always have a type but need not always return a value (if the type is void).

So we can’t say, there is no return value (since there is a type) and we can’t return a value (since it is nonsensical and wrong).

Maybe another NativeCodeCallNoValue pattern here?

If the op is defined to return a value in ODS then it’ll expect a Value here. In the void case I’m assuming the op has NoneType as return type. Why can’t the new op have the same return type and same “value”? I’m assuming for these there are no consumers, so nothing should dereference it.

If the constraint here is that this pattern only fires where type is void, then you are changing the resultant type in the pattern. But DRR is specifying equality under constraints and changing types isn’t supported there (root node of source and target pattern must match). So you’d have to insert another op that retains the return type (often this would be a cast, here I’m not sure which op is best, a constant of unit attr returning NoneType perhaps).

Thank you for your reply.

The return types are precisely as shown in the example. void. They come from

LLVM::LLVMVoidType::get(context)

There are no difference between the return type/arity of the old and new calls.

Note: this is in LLVM-IR dialect, where mlir::NoneType has been erased.

(The nullptr is definitely dereferenced. I have logs. lol)