[mlir][LLVMIR] Add support for va_start/copy/end intrinsics

This patch adds three new LLVM intrinsic operations: llvm.intr.vastart/copy/end.
And its translation from LLVM IR.

This effectively removes a restriction, imposed by 0126dcf1f0, where
non-external functions in LLVM dialect cannot be variadic. At that time
it was not clear how LLVM intrinsics are going to be modeled, which
indirectly affects va_start/copy/end, the core intrinsics used in
variadic functions. But since we have LLVM intrinsics as normal
MLIR operations, it's not a problem anymore.

Differential Revision: https://reviews.llvm.org/D127540
This commit is contained in:
Min-Yih Hsu 2022-05-23 13:52:38 -07:00
parent 3a1a404ae2
commit 856056d1b0
12 changed files with 228 additions and 13 deletions

View File

@ -182,6 +182,8 @@ Function types are converted to LLVM dialect function types as follows:
individual pointers;
- the conversion of `memref`-typed arguments is subject to
[calling conventions](TargetLLVMIR.md#calling-conventions).
- if a function type has boolean attribute `func.varargs` being set, the
converted LLVM function will be variadic.
Examples:
@ -252,6 +254,11 @@ Examples:
// potentially with other non-memref typed results.
!llvm.func<struct<(struct<(ptr<f32>, ptr<f32>, i64)>,
struct<(ptr<double>, ptr<double>, i64)>)> ()>
// If "func.varargs" attribute is set:
(i32) -> () attributes { "func.varargs" = true }
// the corresponding LLVM function will be variadic:
!llvm.func<void (i32, ...)>
```
Conversion patterns are available to convert built-in function operations and
@ -747,6 +754,18 @@ which introduces significant overhead. In such situations, auxiliary interface
functions are executed on host and only pass the values through device function
invocation mechanism.
Limitation: Right now we cannot generate C interface for variadic functions,
regardless of being non-external or external. Because C functions are unable to
"forward" variadic arguments like this:
```c
void bar(int, ...);
void foo(int x, ...) {
// ERROR: no way to forward variadic arguments.
void bar(x, ...);
}
```
### Address Computation
Accesses to a memref element are transformed into an access to an element of the

View File

@ -186,6 +186,28 @@ def LLVM_CoroResumeOp : LLVM_IntrOp<"coro.resume", [], [], [], 0> {
let assemblyFormat = "$handle attr-dict";
}
//
// Variadic function intrinsics.
//
def LLVM_VaStartOp : LLVM_ZeroResultIntrOp<"vastart">,
Arguments<(ins LLVM_i8Ptr:$arg_list)> {
let assemblyFormat = "$arg_list attr-dict";
let summary = "Initializes `arg_list` for subsequent variadic argument extractions.";
}
def LLVM_VaCopyOp : LLVM_ZeroResultIntrOp<"vacopy">,
Arguments<(ins LLVM_i8Ptr:$dest_list, LLVM_i8Ptr:$src_list)> {
let assemblyFormat = "$src_list `to` $dest_list attr-dict";
let summary = "Copies the current argument position from `src_list` to `dest_list`.";
}
def LLVM_VaEndOp : LLVM_ZeroResultIntrOp<"vaend">,
Arguments<(ins LLVM_i8Ptr:$arg_list)> {
let assemblyFormat = "$arg_list attr-dict";
let summary = "Destroys `arg_list`, which has been initialized by `intr.vastart` or `intr.vacopy`.";
}
//
// Exception handling intrinsics.
//

View File

@ -373,6 +373,10 @@ struct FuncOpConversion : public FuncOpConversionBase {
if (funcOp->getAttrOfType<UnitAttr>(
LLVM::LLVMDialect::getEmitCWrapperAttrName())) {
if (newFuncOp.isVarArg())
return funcOp->emitError("C interface for variadic functions is not "
"supported yet.");
if (newFuncOp.isExternal())
wrapExternalFunction(rewriter, funcOp.getLoc(), *getTypeConverter(),
funcOp, newFuncOp);

View File

@ -2132,7 +2132,6 @@ LogicalResult ShuffleVectorOp::verify() {
// Add the entry block to the function.
Block *LLVMFuncOp::addEntryBlock() {
assert(empty() && "function already has an entry block");
assert(!isVarArg() && "unimplemented: non-external variadic functions");
auto *entry = new Block;
push_back(entry);
@ -2331,9 +2330,6 @@ LogicalResult LLVMFuncOp::verify() {
return success();
}
if (isVarArg())
return emitOpError("only external functions can be variadic");
return success();
}

View File

@ -62,6 +62,17 @@ func.func @indirect_call(%arg0: (f32) -> i32, %arg1: f32) -> i32 {
return %0 : i32
}
func.func @variadic_func(%arg0: i32) attributes { "func.varargs" = true } {
return
}
// -----
func.func private @badllvmlinkage(i32) attributes { "llvm.linkage" = 3 : i64 } // expected-error {{Contains llvm.linkage attribute not of type LLVM::LinkageAttr}}
// -----
// expected-error@+1{{C interface for variadic functions is not supported yet.}}
func.func @variadic_func(%arg0: i32) attributes { "func.varargs" = true, "llvm.emit_c_interface" } {
return
}

View File

@ -159,6 +159,11 @@ module {
llvm.func weak fastcc @cconv3() {
llvm.return
}
// CHECK-LABEL: llvm.func @variadic_def
llvm.func @variadic_def(...) {
llvm.return
}
}
// -----
@ -232,15 +237,6 @@ module {
// -----
module {
// expected-error@+1 {{only external functions can be variadic}}
llvm.func @variadic_def(...) {
llvm.return
}
}
// -----
module {
// expected-error@+1 {{cannot attach result attributes to functions with a void return}}
llvm.func @variadic_def() -> (!llvm.void {llvm.noalias})

View File

@ -496,3 +496,29 @@ module {
llvm.return
}
}
// CHECK-LABEL: llvm.func @vararg_func
llvm.func @vararg_func(%arg0: i32, ...) {
// CHECK: %{{.*}} = llvm.mlir.constant(1 : i32) : i32
// CHECK: %{{.*}} = llvm.mlir.constant(1 : i32) : i32
%0 = llvm.mlir.constant(1 : i32) : i32
%1 = llvm.mlir.constant(1 : i32) : i32
// CHECK: %[[ALLOCA0:.+]] = llvm.alloca %{{.*}} x !llvm.struct<"struct.va_list", (ptr<i8>)> {alignment = 8 : i64} : (i32) -> !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>>
// CHECK: %[[CAST0:.+]] = llvm.bitcast %[[ALLOCA0]] : !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>> to !llvm.ptr<i8>
%2 = llvm.alloca %1 x !llvm.struct<"struct.va_list", (ptr<i8>)> {alignment = 8 : i64} : (i32) -> !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>>
%3 = llvm.bitcast %2 : !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>> to !llvm.ptr<i8>
// CHECK: llvm.intr.vastart %[[CAST0]]
llvm.intr.vastart %3
// CHECK: %[[ALLOCA1:.+]] = llvm.alloca %{{.*}} x !llvm.ptr<i8> {alignment = 8 : i64} : (i32) -> !llvm.ptr<ptr<i8>>
// CHECK: %[[CAST1:.+]] = llvm.bitcast %[[ALLOCA1]] : !llvm.ptr<ptr<i8>> to !llvm.ptr<i8>
%4 = llvm.alloca %0 x !llvm.ptr<i8> {alignment = 8 : i64} : (i32) -> !llvm.ptr<ptr<i8>>
%5 = llvm.bitcast %4 : !llvm.ptr<ptr<i8>> to !llvm.ptr<i8>
// CHECK: llvm.intr.vacopy %[[CAST0]] to %[[CAST1]]
llvm.intr.vacopy %3 to %5
// CHECK: llvm.intr.vaend %[[CAST1]]
// CHECK: llvm.intr.vaend %[[CAST0]]
llvm.intr.vaend %5
llvm.intr.vaend %3
// CHECK: llvm.return
llvm.return
}

View File

@ -629,3 +629,34 @@ define void @unreachable_inst() {
; CHECK: llvm.unreachable
unreachable
}
; Varadic function definition
%struct.va_list = type { i8* }
declare void @llvm.va_start(i8*)
declare void @llvm.va_copy(i8*, i8*)
declare void @llvm.va_end(i8*)
; CHECK-LABEL: llvm.func @variadic_function
define void @variadic_function(i32 %X, ...) {
; CHECK: %[[ALLOCA0:.+]] = llvm.alloca %{{.*}} x !llvm.struct<"struct.va_list", (ptr<i8>)> {{.*}} : (i32) -> !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>>
%ap = alloca %struct.va_list
; CHECK: %[[CAST0:.+]] = llvm.bitcast %[[ALLOCA0]] : !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>> to !llvm.ptr<i8>
%ap2 = bitcast %struct.va_list* %ap to i8*
; CHECK: llvm.intr.vastart %[[CAST0]]
call void @llvm.va_start(i8* %ap2)
; CHECK: %[[ALLOCA1:.+]] = llvm.alloca %{{.*}} x !llvm.ptr<i8> {{.*}} : (i32) -> !llvm.ptr<ptr<i8>>
%aq = alloca i8*
; CHECK: %[[CAST1:.+]] = llvm.bitcast %[[ALLOCA1]] : !llvm.ptr<ptr<i8>> to !llvm.ptr<i8>
%aq2 = bitcast i8** %aq to i8*
; CHECK: llvm.intr.vacopy %[[CAST0]] to %[[CAST1]]
call void @llvm.va_copy(i8* %aq2, i8* %ap2)
; CHECK: llvm.intr.vaend %[[CAST1]]
call void @llvm.va_end(i8* %aq2)
; CHECK: llvm.intr.vaend %[[CAST0]]
call void @llvm.va_end(i8* %ap2)
; CHECK: llvm.return
ret void
}

View File

@ -385,6 +385,17 @@ define void @umul_with_overflow_test(i32 %0, i32 %1, <8 x i32> %2, <8 x i32> %3)
ret void
}
; CHECK-LABEL: llvm.func @va_intrinsics_test
define void @va_intrinsics_test(i8* %0, i8* %1) {
; CHECK: llvm.intr.vastart %{{.*}}
call void @llvm.va_start(i8* %0)
; CHECK: llvm.intr.vacopy %{{.*}} to %{{.*}}
call void @llvm.va_copy(i8* %1, i8* %0)
; CHECK: llvm.intr.vaend %{{.*}}
call void @llvm.va_end(i8* %0)
ret void
}
; CHECK-LABEL: llvm.func @coro_id
define void @coro_id(i32 %0, i8* %1) {
; CHECK: llvm.intr.coro.id %{{.*}}, %{{.*}}, %{{.*}}, %{{.*}} : !llvm.token
@ -686,6 +697,9 @@ declare void @llvm.coro.resume(i8*)
declare i32 @llvm.eh.typeid.for(i8*)
declare i8* @llvm.stacksave()
declare void @llvm.stackrestore(i8*)
declare void @llvm.va_start(i8*)
declare void @llvm.va_copy(i8*, i8*)
declare void @llvm.va_end(i8*)
declare <8 x i32> @llvm.vp.add.v8i32(<8 x i32>, <8 x i32>, <8 x i1>, i32)
declare <8 x i32> @llvm.vp.sub.v8i32(<8 x i32>, <8 x i32>, <8 x i1>, i32)
declare <8 x i32> @llvm.vp.mul.v8i32(<8 x i32>, <8 x i32>, <8 x i1>, i32)

View File

@ -1909,3 +1909,31 @@ llvm.func @duplicate_block_with_args_in_switch(%cond : i32, %arg1: f32, %arg2: f
llvm.func @bar(f32)
llvm.func @baz()
llvm.func @qux(f32)
// -----
// Varaidic function definition
// CHECK: %struct.va_list = type { ptr }
// CHECK: define void @vararg_function(i32 %{{.*}}, ...)
llvm.func @vararg_function(%arg0: i32, ...) {
%0 = llvm.mlir.constant(1 : i32) : i32
%1 = llvm.mlir.constant(1 : i32) : i32
// CHECK: %[[ALLOCA0:.+]] = alloca %struct.va_list, align 8
%2 = llvm.alloca %1 x !llvm.struct<"struct.va_list", (ptr<i8>)> {alignment = 8 : i64} : (i32) -> !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>>
%3 = llvm.bitcast %2 : !llvm.ptr<struct<"struct.va_list", (ptr<i8>)>> to !llvm.ptr<i8>
// CHECK: call void @llvm.va_start(ptr %[[ALLOCA0]])
llvm.intr.vastart %3
// CHECK: %[[ALLOCA1:.+]] = alloca ptr, align 8
%4 = llvm.alloca %0 x !llvm.ptr<i8> {alignment = 8 : i64} : (i32) -> !llvm.ptr<ptr<i8>>
%5 = llvm.bitcast %4 : !llvm.ptr<ptr<i8>> to !llvm.ptr<i8>
// CHECK: call void @llvm.va_copy(ptr %[[ALLOCA1]], ptr %[[ALLOCA0]])
llvm.intr.vacopy %3 to %5
// CHECK: call void @llvm.va_end(ptr %[[ALLOCA1]])
// CHECK: call void @llvm.va_end(ptr %[[ALLOCA0]])
llvm.intr.vaend %5
llvm.intr.vaend %3
// CHECK: ret void
llvm.return
}

View File

@ -12,3 +12,5 @@ if 'msan' in config.available_features:
if 'native' not in config.available_features:
config.unsupported = True
config.available_features.add(
config.root.native_target.lower() + '-native-target')

View File

@ -0,0 +1,66 @@
// RUN: mlir-cpu-runner %s -e caller --entry-point-result=i32 | FileCheck %s
// Varaidic argument list (va_list) and the extraction logics are ABI-specific.
// REQUIRES: x86-native-target
// Check if variadic functions can be called and the correct variadic argument
// can be extracted.
llvm.func @caller() -> i32 {
%0 = llvm.mlir.constant(3 : i32) : i32
%1 = llvm.mlir.constant(2 : i32) : i32
%2 = llvm.mlir.constant(1 : i32) : i32
%3 = llvm.call @foo(%2, %1, %0) : (i32, i32, i32) -> i32
llvm.return %3 : i32
}
// Equivalent C code:
// int foo(int X, ...) {
// va_list args;
// va_start(args, X);
// int num = va_arg(args, int);
// va_end(args);
// return num;
//}
llvm.func @foo(%arg0: i32, ...) -> i32 {
%0 = llvm.mlir.constant(8 : i64) : i64
%1 = llvm.mlir.constant(2 : i32) : i32
%2 = llvm.mlir.constant(0 : i64) : i64
%3 = llvm.mlir.constant(0 : i64) : i64
%4 = llvm.mlir.constant(8 : i32) : i32
%5 = llvm.mlir.constant(3 : i32) : i32
%6 = llvm.mlir.constant(0 : i64) : i64
%7 = llvm.mlir.constant(0 : i64) : i64
%8 = llvm.mlir.constant(41 : i32) : i32
%9 = llvm.mlir.constant(0 : i32) : i32
%10 = llvm.mlir.constant(0 : i64) : i64
%11 = llvm.mlir.constant(0 : i64) : i64
%12 = llvm.mlir.constant(1 : i32) : i32
%13 = llvm.alloca %12 x !llvm.array<1 x struct<"struct.va_list", (i32, i32, ptr<i8>, ptr<i8>)>> {alignment = 8 : i64} : (i32) -> !llvm.ptr<array<1 x struct<"struct.va_list", (i32, i32, ptr<i8>, ptr<i8>)>>>
%14 = llvm.bitcast %13 : !llvm.ptr<array<1 x struct<"struct.va_list", (i32, i32, ptr<i8>, ptr<i8>)>>> to !llvm.ptr<i8>
llvm.intr.vastart %14
%15 = llvm.getelementptr %13[%11, %10, 0] : (!llvm.ptr<array<1 x struct<"struct.va_list", (i32, i32, ptr<i8>, ptr<i8>)>>>, i64, i64) -> !llvm.ptr<i32>
%16 = llvm.load %15 : !llvm.ptr<i32>
%17 = llvm.icmp "ult" %16, %8 : i32
llvm.cond_br %17, ^bb1, ^bb2
^bb1: // pred: ^bb0
%18 = llvm.getelementptr %13[%7, %6, 3] : (!llvm.ptr<array<1 x struct<"struct.va_list", (i32, i32, ptr<i8>, ptr<i8>)>>>, i64, i64) -> !llvm.ptr<ptr<i8>>
%19 = llvm.load %18 : !llvm.ptr<ptr<i8>>
%20 = llvm.zext %16 : i32 to i64
%21 = llvm.getelementptr %19[%20] : (!llvm.ptr<i8>, i64) -> !llvm.ptr<i8>
%22 = llvm.add %16, %4 : i32
llvm.store %22, %15 : !llvm.ptr<i32>
llvm.br ^bb3(%21 : !llvm.ptr<i8>)
^bb2: // pred: ^bb0
%23 = llvm.getelementptr %13[%3, %2, 2] : (!llvm.ptr<array<1 x struct<"struct.va_list", (i32, i32, ptr<i8>, ptr<i8>)>>>, i64, i64) -> !llvm.ptr<ptr<i8>>
%24 = llvm.load %23 : !llvm.ptr<ptr<i8>>
%25 = llvm.getelementptr %24[%0] : (!llvm.ptr<i8>, i64) -> !llvm.ptr<i8>
llvm.store %25, %23 : !llvm.ptr<ptr<i8>>
llvm.br ^bb3(%24 : !llvm.ptr<i8>)
^bb3(%26: !llvm.ptr<i8>): // 2 preds: ^bb1, ^bb2
%27 = llvm.bitcast %26 : !llvm.ptr<i8> to !llvm.ptr<i32>
%28 = llvm.load %27 : !llvm.ptr<i32>
llvm.intr.vaend %14
llvm.return %28 : i32
}
// CHECK: 2