Skip to content

[clang][WebAssembly] Support reftypes & varargs in test_function_pointer_signature #150921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -13234,9 +13234,9 @@ def err_wasm_builtin_arg_must_match_table_element_type : Error <
"%ordinal0 argument must match the element type of the WebAssembly table in the %ordinal1 argument">;
def err_wasm_builtin_arg_must_be_integer_type : Error <
"%ordinal0 argument must be an integer">;
def err_wasm_builtin_test_fp_sig_cannot_include_reference_type
: Error<"not supported for "
"function pointers with a reference type %select{return "
def err_wasm_builtin_test_fp_sig_cannot_include_struct_or_union
: Error<"not supported with the multivalue ABI for "
"function pointers with a struct/union as %select{return "
"value|parameter}0">;

// OpenACC diagnostics.
Expand Down
3 changes: 2 additions & 1 deletion clang/include/clang/Sema/SemaWasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class SemaWasm : public SemaBase {
bool BuiltinWasmTableGrow(CallExpr *TheCall);
bool BuiltinWasmTableFill(CallExpr *TheCall);
bool BuiltinWasmTableCopy(CallExpr *TheCall);
bool BuiltinWasmTestFunctionPointerSignature(CallExpr *TheCall);
bool BuiltinWasmTestFunctionPointerSignature(const TargetInfo &TI,
CallExpr *TheCall);

WebAssemblyImportNameAttr *
mergeImportNameAttr(Decl *D, const WebAssemblyImportNameAttr &AL);
Expand Down
29 changes: 10 additions & 19 deletions clang/lib/CodeGen/TargetBuiltins/WebAssembly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -246,35 +246,26 @@ Value *CodeGenFunction::EmitWebAssemblyBuiltinExpr(unsigned BuiltinID,
llvm::FunctionType *LLVMFuncTy =
cast<llvm::FunctionType>(ConvertType(QualType(FuncTy, 0)));

bool VarArg = LLVMFuncTy->isVarArg();
unsigned NParams = LLVMFuncTy->getNumParams();
std::vector<Value *> Args;
Args.reserve(NParams + 3);
Args.reserve(NParams + 3 + VarArg);
// The only real argument is the FuncRef
Args.push_back(FuncRef);

// Add the type information
auto addType = [this, &Args](llvm::Type *T) {
if (T->isVoidTy()) {
// Do nothing
} else if (T->isFloatingPointTy()) {
Args.push_back(ConstantFP::get(T, 0));
} else if (T->isIntegerTy()) {
Args.push_back(ConstantInt::get(T, 0));
} else if (T->isPointerTy()) {
Args.push_back(ConstantPointerNull::get(llvm::PointerType::get(
getLLVMContext(), T->getPointerAddressSpace())));
} else {
// TODO: Handle reference types. For now, we reject them in Sema.
llvm_unreachable("Unhandled type");
}
};

addType(LLVMFuncTy->getReturnType());
llvm::Type *RetType = LLVMFuncTy->getReturnType();
if (!RetType->isVoidTy()) {
Args.push_back(PoisonValue::get(RetType));
}
// The token type indicates the boundary between return types and param
// types.
Args.push_back(PoisonValue::get(llvm::Type::getTokenTy(getLLVMContext())));
for (unsigned i = 0; i < NParams; i++) {
addType(LLVMFuncTy->getParamType(i));
Args.push_back(PoisonValue::get(LLVMFuncTy->getParamType(i)));
}
if (VarArg) {
Args.push_back(PoisonValue::get(Builder.getPtrTy()));
}
Function *Callee = CGM.getIntrinsic(Intrinsic::wasm_ref_test_func);
return Builder.CreateCall(Callee, Args);
Expand Down
36 changes: 21 additions & 15 deletions clang/lib/Sema/SemaWasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "clang/Basic/AddressSpaces.h"
#include "clang/Basic/DiagnosticSema.h"
#include "clang/Basic/TargetBuiltins.h"
#include "clang/Basic/TargetInfo.h"
#include "clang/Sema/Attr.h"
#include "clang/Sema/Sema.h"

Expand Down Expand Up @@ -227,7 +228,8 @@ bool SemaWasm::BuiltinWasmTableCopy(CallExpr *TheCall) {
return false;
}

bool SemaWasm::BuiltinWasmTestFunctionPointerSignature(CallExpr *TheCall) {
bool SemaWasm::BuiltinWasmTestFunctionPointerSignature(const TargetInfo &TI,
CallExpr *TheCall) {
if (SemaRef.checkArgCount(TheCall, 1))
return true;

Expand All @@ -250,27 +252,31 @@ bool SemaWasm::BuiltinWasmTestFunctionPointerSignature(CallExpr *TheCall) {
<< ArgType << FuncPtrArg->getSourceRange();
}

// Check that the function pointer doesn't use reference types
if (FuncTy->getReturnType().isWebAssemblyReferenceType()) {
return Diag(
FuncPtrArg->getBeginLoc(),
diag::err_wasm_builtin_test_fp_sig_cannot_include_reference_type)
<< 0 << FuncTy->getReturnType() << FuncPtrArg->getSourceRange();
}
auto NParams = FuncTy->getNumParams();
for (unsigned I = 0; I < NParams; I++) {
if (FuncTy->getParamType(I).isWebAssemblyReferenceType()) {
if (TI.getABI() == "experimental-mv") {
auto isStructOrUnion = [](QualType T) {
return T->isUnionType() || T->isStructureType();
};
if (isStructOrUnion(FuncTy->getReturnType())) {
return Diag(
FuncPtrArg->getBeginLoc(),
diag::
err_wasm_builtin_test_fp_sig_cannot_include_reference_type)
<< 1 << FuncPtrArg->getSourceRange();
err_wasm_builtin_test_fp_sig_cannot_include_struct_or_union)
<< 0 << FuncTy->getReturnType() << FuncPtrArg->getSourceRange();
}
auto NParams = FuncTy->getNumParams();
for (unsigned I = 0; I < NParams; I++) {
if (isStructOrUnion(FuncTy->getParamType(I))) {
return Diag(
FuncPtrArg->getBeginLoc(),
diag::
err_wasm_builtin_test_fp_sig_cannot_include_struct_or_union)
<< 1 << FuncPtrArg->getSourceRange();
}
}
}

// Set return type to int (the result of the test)
TheCall->setType(getASTContext().IntTy);

return false;
}

Expand All @@ -297,7 +303,7 @@ bool SemaWasm::CheckWebAssemblyBuiltinFunctionCall(const TargetInfo &TI,
case WebAssembly::BI__builtin_wasm_table_copy:
return BuiltinWasmTableCopy(TheCall);
case WebAssembly::BI__builtin_wasm_test_function_pointer_signature:
return BuiltinWasmTestFunctionPointerSignature(TheCall);
return BuiltinWasmTestFunctionPointerSignature(TI, TheCall);
}

return false;
Expand Down
70 changes: 70 additions & 0 deletions clang/test/CodeGen/WebAssembly/builtins-test-fp-sig.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// RUN: %clang_cc1 -triple wasm32-unknown-unknown -target-feature +gc -O3 -emit-llvm -DSINGLE_VALUE -o - %s | FileCheck %s -check-prefixes WEBASSEMBLY,WEBASSEMBLY-SV
// RUN: %clang_cc1 -triple wasm64-unknown-unknown -target-feature +gc -O3 -emit-llvm -DSINGLE_VALUE -o - %s | FileCheck %s -check-prefixes WEBASSEMBLY,WEBASSEMBLY-SV
// RUN: %clang_cc1 -triple wasm64-unknown-unknown -target-feature +gc -target-abi experimental-mv -O3 -emit-llvm -o - %s 2>&1 | FileCheck %s -check-prefixes WEBASSEMBLY
// RUN: not %clang_cc1 -triple wasm64-unknown-unknown -O3 -emit-llvm -o - %s 2>&1 | FileCheck %s -check-prefixes MISSING-GC

void use(int);

typedef void (*Fvoid)(void);
void test_function_pointer_signature_void(Fvoid func) {
// MISSING-GC: error: '__builtin_wasm_test_function_pointer_signature' needs target feature gc
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

typedef float (*Ffloats)(float, double, int);
void test_function_pointer_signature_floats(Ffloats func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, float poison, token poison, float poison, double poison, i32 poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

typedef void (*Fpointers)(Fvoid, Ffloats, void*, int*, int***, char[5]);
void test_function_pointer_signature_pointers(Fpointers func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, ptr poison, ptr poison, ptr poison, ptr poison, ptr poison, ptr poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

typedef void (*FVarArgs)(int, ...);
void test_function_pointer_signature_varargs(FVarArgs func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, i32 poison, ptr poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

typedef __externref_t (*FExternRef)(__externref_t, __externref_t);
void test_function_pointer_externref(FExternRef func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, ptr addrspace(10) poison, token poison, ptr addrspace(10) poison, ptr addrspace(10) poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

typedef __funcref Fpointers (*FFuncRef)(__funcref Fvoid, __funcref Ffloats);
void test_function_pointer_funcref(FFuncRef func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, ptr addrspace(20) poison, token poison, ptr addrspace(20) poison, ptr addrspace(20) poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

#ifdef SINGLE_VALUE
// Some tests that we get struct ABIs correct. There is no special code in
// __builtin_wasm_test_function_pointer_signature for this, it gets handled by
// the normal type lowering code.
// Single element structs are unboxed, multi element structs are passed on
// stack.
typedef struct {double x;} (*Fstructs1)(struct {double x;}, struct {float x;}, struct {double x; float y;});
void test_function_pointer_structs1(Fstructs1 func) {
// WEBASSEMBLY-SV: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, double poison, token poison, double poison, float poison, ptr poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

// Two element return struct ==> return ptr on stack
typedef struct {double x; double y;} (*Fstructs2)(void);
void test_function_pointer_structs2(Fstructs2 func) {
// WEBASSEMBLY-SV: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, ptr poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

// Return union ==> return ptr on stack, one element union => unboxed
typedef union {double x; float y;} (*FUnions)(union {double x; float y;}, union {double x;});
void test_function_pointer_unions(FUnions func) {
// WEBASSEMBLY-SV: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, ptr poison, ptr poison, double poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}
#endif
21 changes: 0 additions & 21 deletions clang/test/CodeGen/builtins-wasm.c
Original file line number Diff line number Diff line change
Expand Up @@ -751,24 +751,3 @@ void *tp (void) {
return __builtin_thread_pointer ();
// WEBASSEMBLY: call {{.*}} @llvm.thread.pointer.p0()
}

typedef void (*Fvoid)(void);
typedef float (*Ffloats)(float, double, int);
typedef void (*Fpointers)(Fvoid, Ffloats, void*, int*, int***, char[5]);

void use(int);

void test_function_pointer_signature_void(Fvoid func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison)
use(__builtin_wasm_test_function_pointer_signature(func));
}

void test_function_pointer_signature_floats(Ffloats func) {
// WEBASSEMBLY: tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, float 0.000000e+00, token poison, float 0.000000e+00, double 0.000000e+00, i32 0)
use(__builtin_wasm_test_function_pointer_signature(func));
}

void test_function_pointer_signature_pointers(Fpointers func) {
// WEBASSEMBLY: %0 = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, ptr null, ptr null, ptr null, ptr null, ptr null, ptr null)
use(__builtin_wasm_test_function_pointer_signature(func));
}
17 changes: 13 additions & 4 deletions clang/test/Sema/builtins-wasm.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// RUN: %clang_cc1 -fsyntax-only -verify -triple wasm32 -target-feature +reference-types %s
// RUN: %clang_cc1 -fsyntax-only -verify -triple wasm32 -target-abi experimental-mv -DMULTIVALUE -target-feature +reference-types %s

#define EXPR_HAS_TYPE(expr, type) _Generic((expr), type : 1, default : 0)

Expand Down Expand Up @@ -57,8 +58,8 @@ void test_table_copy(int dst_idx, int src_idx, int nelem) {

typedef void (*F1)(void);
typedef int (*F2)(int);
typedef int (*F3)(__externref_t);
typedef __externref_t (*F4)(int);
typedef void (*F3)(struct {int x; double y;});
typedef struct {int x; double y;} (*F4)(void);

void test_function_pointer_signature() {
// Test argument count validation
Expand All @@ -68,13 +69,21 @@ void test_function_pointer_signature() {
// // Test argument type validation - should require function pointer
(void)__builtin_wasm_test_function_pointer_signature((void*)0); // expected-error {{used type 'void *' where function pointer is required}}
(void)__builtin_wasm_test_function_pointer_signature((int)0); // expected-error {{used type 'int' where function pointer is required}}
(void)__builtin_wasm_test_function_pointer_signature((F3)0); // expected-error {{not supported for function pointers with a reference type parameter}}
(void)__builtin_wasm_test_function_pointer_signature((F4)0); // expected-error {{not supported for function pointers with a reference type return value}}

// // Test valid usage
int res = __builtin_wasm_test_function_pointer_signature((F1)0);
res = __builtin_wasm_test_function_pointer_signature((F2)0);

// Test return type
_Static_assert(EXPR_HAS_TYPE(__builtin_wasm_test_function_pointer_signature((F1)0), int), "");

#ifdef MULTIVALUE
// Test that struct arguments and returns are rejected with multivalue abi
(void)__builtin_wasm_test_function_pointer_signature((F3)0); // expected-error {{not supported with the multivalue ABI for function pointers with a struct/union as parameter}}
(void)__builtin_wasm_test_function_pointer_signature((F4)0); // expected-error {{not supported with the multivalue ABI for function pointers with a struct/union as return value}}
#else
// with default abi they are fine
(void)__builtin_wasm_test_function_pointer_signature((F3)0);
(void)__builtin_wasm_test_function_pointer_signature((F4)0);
#endif
}
9 changes: 9 additions & 0 deletions llvm/lib/Target/WebAssembly/WebAssemblyISelDAGToDAG.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ static APInt encodeFunctionSignature(SelectionDAG *DAG, SDLoc &DL,
if (VT == MVT::f64) {
return wasm::ValType::F64;
}
if (VT == MVT::externref) {
return wasm::ValType::EXTERNREF;
}
if (VT == MVT::funcref) {
return wasm::ValType::FUNCREF;
}
if (VT == MVT::exnref) {
return wasm::ValType::EXNREF;
}
LLVM_DEBUG(errs() << "Unhandled type for llvm.wasm.ref.test.func: " << VT
<< "\n");
llvm_unreachable("Unhandled type for llvm.wasm.ref.test.func");
Expand Down
30 changes: 23 additions & 7 deletions llvm/test/CodeGen/WebAssembly/ref-test-func.ll
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ define void @test_fpsig_return_i32(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, i32 0)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, i32 poison)
tail call void @use(i32 noundef %res) #3
ret void
}
Expand All @@ -48,7 +48,7 @@ define void @test_fpsig_return_i64(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, i64 0)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, i64 poison)
tail call void @use(i32 noundef %res) #3
ret void
}
Expand All @@ -65,7 +65,7 @@ define void @test_fpsig_return_f32(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, float 0.)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, float poison)
tail call void @use(i32 noundef %res) #3
ret void
}
Expand All @@ -82,7 +82,7 @@ define void @test_fpsig_return_f64(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, double 0.)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, double poison)
tail call void @use(i32 noundef %res) #3
ret void
}
Expand All @@ -100,7 +100,7 @@ define void @test_fpsig_param_i32(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, double 0.)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, token poison, double poison)
tail call void @use(i32 noundef %res) #3
ret void
}
Expand All @@ -118,7 +118,7 @@ define void @test_fpsig_multiple_params_and_returns(ptr noundef %func) local_unn
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, i32 0, i64 0, float 0., double 0., token poison, i64 0, float 0., i64 0)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, i32 poison, i64 poison, float poison, double poison, token poison, i64 poison, float poison, i64 poison)
tail call void @use(i32 noundef %res) #3
ret void
}
Expand All @@ -137,10 +137,26 @@ define void @test_fpsig_ptrs(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, ptr null, token poison, ptr null, ptr null)
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, ptr poison, token poison, ptr poison, ptr poison)
tail call void @use(i32 noundef %res) #3
ret void
}

define void @test_reference_types(ptr noundef %func) local_unnamed_addr #0 {
; CHECK-LABEL: test_reference_types:
; CHK32: .functype test_reference_types (i32) -> ()
; CHK64: .functype test_reference_types (i64) -> ()
; CHECK-NEXT: # %bb.0: # %entry
; CHECK-NEXT: local.get 0
; CHK64-NEXT: i32.wrap_i64
; CHECK-NEXT: table.get __indirect_function_table
; CHECK-NEXT: ref.test (funcref, externref) -> (externref)
; CHECK-NEXT: call use
; CHECK-NEXT: # fallthrough-return
entry:
%res = tail call i32 (ptr, ...) @llvm.wasm.ref.test.func(ptr %func, ptr addrspace(10) poison, token poison, ptr addrspace(20) poison, ptr addrspace(10) poison)
tail call void @use(i32 noundef %res) #3
ret void
}

declare void @use(i32 noundef) local_unnamed_addr #1