[LLVMIR] Calling variadic function indirectly

Hi,

LLVM dialect supports functions with variadic signatures. One can call these functions directly:

llvm.func @variadic_args(i32, i32, ...)

func.func @call_va_arg_function() {
  %1 = llvm.mlir.constant(0: i32) : i32
  %2 = llvm.mlir.constant(1: i32) : i32
  %3 = llvm.mlir.constant(2: i32) : i32
  // OK
  llvm.call @variadic_args(%1, %2, %3) : (i32, i32, i32) -> ()
  return
}

However, when we try to call variadic functions indirectly, like the snippet below, an error will be raised from the parser:

llvm.func @variadic_args(i32, i32, ...)

func.func @call_va_arg_function(%arg0 : !llvm.ptr<!llvm.ptr<!llvm.func<i32 (i32, i32, ...)>>>) {
  %0 = llvm.load %arg0 : !llvm.ptr<!llvm.ptr<!llvm.func<i32 (i32, i32, ...)>>>
  %1 = llvm.mlir.constant(0: i32) : i32
  %2 = llvm.mlir.constant(1: i32) : i32
  %3 = llvm.mlir.constant(2: i32) : i32
  // OK
  llvm.call @variadic_args(%1, %2, %3) : (i32, i32, i32) -> ()

  // error: use of value '%0' expects different type than prior uses:
  // '!llvm.ptr<!llvm.func<i32 (i32, i32, i32)>>' vs '!llvm.ptr<!llvm.func<i32 (i32, i32, ...)>>'
  %5 = llvm.call %0(%1, %2, %3) : (i32, i32, i32) -> i32
  return
}

Here is the full error message: “error: use of value ‘%0’ expects different type than prior uses: ‘!llvm.ptr<!llvm.func<i32 (i32, i32, i32)>>’ vs ‘!llvm.ptr<!llvm.func<i32 (i32, i32, …)>>’

The root cause of this problem comes from the parser: when it’s parsing ‘%0’, namely the indirect callee, it tries to use the trailing type, (i32, i32, i32) -> i32, to resolve the operand before realizing the type of ‘%0’ does not match the trailing type.

%5 = "llvm.call"(%0, %1, %2, %3) : (!llvm.ptr<!llvm.func<i32 (i32, i32, ...)>>, i32, i32, i32) -> i32

Although the generic format, shown above, (not surprisingly) works, let’s explore some other options.

Trial #1

If we tried to fix this by changing the last operation into

%5 = llvm.call %0(%1, %2, %3) : (i32, i32, ...) -> i32

The parser bails out when trying to parse “(i32, i32, …) → i32”(which is a builtin FunctionType): error: expected non-function type
Because variadic argument type is actually not a first-class concept. MLIR provides facilities to parse variadic arguments but LLVM dialect is the only one that supports it.

Trial #2

What about this?

%5 = llvm.call %0(%1, %2, %3) : !llvm.func<i32 (i32, i32, ...)>

This time, the parser raises this error message: custom op ‘llvm.call’ expected function type. Because not surprisingly, it is expecting a (builtin) FunctionType there.

Discussion

I have several solutions to address this problem that I would like to discuss here.

Supporting variadic arguments in builtin.FunctionType

This approach creates the most ergonomic syntax (when printing llvm.call). And technically speaking, it doesn’t interference with existing usages of FunctionType (it does change its hash though)

The downside is the question of necessity: None of the dialect, other than LLVM, supports variadic arguments, but we need to change a really important component (i.e. FunctionType).

Printing !llvm.func type as trailing type

Basically supports what we did in Trial #2:

%5 = llvm.call %0(%1, %2, %3) : !llvm.func<i32 (i32, i32, ...)>

With this approach, we only need to make modifications in the LLVM dialect. The downside is probably the slightly inferior readability.

Always print generic op for indirect variadic function call

This is probably the easiest fix. Of course, having really verbose syntax is a major downside.

It will be great if you can share your thought on how to fix this issue :slight_smile:

Some potential questions…

Q: Why doesn’t the parser complain when calling variadic function directly, despite the fact that the trailing type is (i32, i32, i32) -> () rather than (i32, i32, ...) -> ()?
A: The parser doesn’t need the type information to correctly resolve the callee (which is a SymbolRef) thus no type checking is performed there.

Q: How does LLVM IR work out when calling variadic function indirectly?
A: When calling indirect function in LLVM IR, there is always a type notation up front, for instance:

%83 = call i32 (i32, i32, ...) %79(i32 %82, i32 %87, i32 %90)

So LLParser knows how to resolve ‘%79’.

The right solution is to fix the parser of the indirect call op to accept the variadic trailing type in some form. Whether this is done by requiring an !llvm.func or by mimicking the function type parser but with optional trailing ellipsis is a matter of taste. I don’t have a strong preference here.

However, I am strongly against the addition of variadic arguments to the builtin function type only to support a parser of a dialect operation. The dialect itself does not actually use the type!

I am also against dropping the custom format entirely in favor of a generic one for a single operation in the dialect. This creates a highly confusing inconsistency.

I’m planning to take the !llvm.func approach. To be fair, mimicking a function type parser is not hard but I would like to avoid as much fragmentation (or recreating the wheel) as possible.

Good point!

Update: We cannot parse actual parameters without knowing their types, so eventually we need syntax like this:

%5 = llvm.call %0(%1, %2, %3) : !llvm.func<i32 (i32, i32, ...)>(i32, i32, i32) -> i32

It’s not pretty, but knowing types for both the (indirect) callee and actual parameters is necessary. LLVM IR also has a similar syntax.

This looks redundant, and redundancy is usually problematic as it may lead to conflicts. I would consider just adding the list of types for the operands passed into the variadic part, something like !llvm.func<i32 (i32, i32, ...), i32 should do here.