Skip to content

[WebAssembly] Add a new wasm_multivalue calling convention#200076

Open
alexcrichton wants to merge 10 commits into
llvm:mainfrom
alexcrichton:wasm-multivalue
Open

[WebAssembly] Add a new wasm_multivalue calling convention#200076
alexcrichton wants to merge 10 commits into
llvm:mainfrom
alexcrichton:wasm-multivalue

Conversation

@alexcrichton
Copy link
Copy Markdown
Contributor

This is an implementation of WebAssembly/tool-conventions#268 here in LLVM. This adds a new calling convention to Clang, named wasm_multivalue, which is intended to be used on WebAssembly targets to configure multiple return values and slightly tweak the ABI. Changes here are:

  • Parsing/validation of __attribute__((wasm_multivalue)). Note that validation here means that it's not only well-formed but on wasm targets the multivalue target feature is additionally enabled.
  • Clang-level ABI adjustments for the wasm_multivalue calling convention. These are defined by Define a new "wasm-multivalue" calling convention WebAssembly/tool-conventions#268 and notably includes expanding structs with exactly 2 scalar fields in parameter-position and directly returning structs with any number of scalar fields in return-position.
  • A new wasm_multivaluecc keyword/calling convention for LLVM IR. This is what Clang lowers to when using the wasm_multivalue calling convention.
  • Adjustments at the LLVM ABI layer to support returning multiple values with the wasm_multivaluecc calling convention.

My goal after this would be to start integrating this into Rust next, under and unstable feature, and then further continue testing/vetting/etc for component model usage.

@llvmorg-github-actions
Copy link
Copy Markdown

llvmorg-github-actions Bot commented May 27, 2026

@llvm/pr-subscribers-backend-webassembly
@llvm/pr-subscribers-llvm-binary-utilities
@llvm/pr-subscribers-debuginfo

@llvm/pr-subscribers-llvm-ir

Author: Alex Crichton (alexcrichton)

Changes

This is an implementation of WebAssembly/tool-conventions#268 here in LLVM. This adds a new calling convention to Clang, named wasm_multivalue, which is intended to be used on WebAssembly targets to configure multiple return values and slightly tweak the ABI. Changes here are:

  • Parsing/validation of __attribute__((wasm_multivalue)). Note that validation here means that it's not only well-formed but on wasm targets the multivalue target feature is additionally enabled.
  • Clang-level ABI adjustments for the wasm_multivalue calling convention. These are defined by WebAssembly/tool-conventions#268 and notably includes expanding structs with exactly 2 scalar fields in parameter-position and directly returning structs with any number of scalar fields in return-position.
  • A new wasm_multivaluecc keyword/calling convention for LLVM IR. This is what Clang lowers to when using the wasm_multivalue calling convention.
  • Adjustments at the LLVM ABI layer to support returning multiple values with the wasm_multivaluecc calling convention.

My goal after this would be to start integrating this into Rust next, under and unstable feature, and then further continue testing/vetting/etc for component model usage.


Patch is 39.14 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200076.diff

30 Files Affected:

  • (modified) clang/include/clang/Basic/Attr.td (+6)
  • (modified) clang/include/clang/Basic/AttrDocs.td (+15)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+4-1)
  • (modified) clang/include/clang/Basic/Specifiers.h (+1)
  • (modified) clang/lib/AST/ItaniumMangle.cpp (+1)
  • (modified) clang/lib/AST/Type.cpp (+3)
  • (modified) clang/lib/AST/TypePrinter.cpp (+6)
  • (modified) clang/lib/Basic/Targets/WebAssembly.h (+1)
  • (modified) clang/lib/CodeGen/CGCall.cpp (+2)
  • (modified) clang/lib/CodeGen/CGDebugInfo.cpp (+2)
  • (modified) clang/lib/CodeGen/Targets/WebAssembly.cpp (+68-6)
  • (modified) clang/lib/Sema/SemaDeclAttr.cpp (+13)
  • (modified) clang/lib/Sema/SemaType.cpp (+4-1)
  • (added) clang/test/CodeGen/WebAssembly/wasm-multivalue-abi.c (+77)
  • (added) clang/test/CodeGen/WebAssembly/wasm-multivalue-functype.c (+45)
  • (added) clang/test/Sema/attr-wasm-multivalue.c (+33)
  • (modified) llvm/include/llvm/AsmParser/LLToken.h (+1)
  • (modified) llvm/include/llvm/BinaryFormat/Dwarf.def (+1)
  • (modified) llvm/include/llvm/IR/CallingConv.h (+4)
  • (modified) llvm/lib/AsmParser/LLLexer.cpp (+1)
  • (modified) llvm/lib/AsmParser/LLParser.cpp (+4)
  • (modified) llvm/lib/IR/AsmWriter.cpp (+3)
  • (modified) llvm/lib/IR/Function.cpp (+1)
  • (modified) llvm/lib/Target/WebAssembly/GISel/WebAssemblyCallLowering.cpp (+3-2)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyISelLowering.cpp (+5-3)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyMachineFunctionInfo.cpp (+1-1)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyRuntimeLibcallSignatures.cpp (+13-13)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyUtilities.cpp (+9-4)
  • (modified) llvm/lib/Target/WebAssembly/WebAssemblyUtilities.h (+8-4)
  • (added) llvm/test/CodeGen/WebAssembly/wasm-multivalue-cc.ll (+38)
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 70b5773f95b08..42a68ff02975d 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -3559,6 +3559,12 @@ def M68kRTD: DeclOrTypeAttr {
   let Documentation = [M68kRTDDocs];
 }
 
+def WasmMultivalue : DeclOrTypeAttr,
+                     TargetSpecificAttr<TargetWebAssembly> {
+  let Spellings = [Clang<"wasm_multivalue">];
+  let Documentation = [WasmMultivalueDocs];
+}
+
 def PreserveNone : DeclOrTypeAttr,
                    TargetSpecificAttr<TargetArch<!listconcat(TargetAArch64.Arches, TargetAnyX86.Arches)>> {
   let Spellings = [Clang<"preserve_none">];
diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td
index 87b9053be7cb6..dafba0108506d 100644
--- a/clang/include/clang/Basic/AttrDocs.td
+++ b/clang/include/clang/Basic/AttrDocs.td
@@ -3732,6 +3732,21 @@ using the `rtd` instruction.
   }];
 }
 
+def WasmMultivalueDocs : Documentation {
+  let Category = DocCatCallingConvs;
+  let Content = [{
+On WebAssembly targets, this attribute selects the ``wasm-multivalue`` calling
+convention as defined in the WebAssembly/tool-conventions repository. Relative
+to the default calling convention this takes advantage of the multi-value
+proposal in its ABI definition.
+
+This calling convention requires the ``multivalue`` target feature. Using the
+attribute without this feature enabled is a compile-time error. Enabling
+``multivalue`` does not change the default calling convention; this attribute
+must be used to opt in to the new behavior on a per-function basis.
+  }];
+}
+
 def DocCatConsumed : DocumentationCategory<"Consumed Annotation Checking"> {
   let Content = [{
 Clang supports additional attributes for checking basic resource management
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index e330ea03d0544..71df166089342 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -13828,7 +13828,7 @@ def err_builtin_pass_in_regs_non_class : Error<
   "argument %0 is not an unqualified class type">;
 
 
-// WebAssembly reference type and table diagnostics.
+// WebAssembly-related diagnostics.
 def err_wasm_reference_pr : Error<
   "%select{pointer|reference}0 to WebAssembly reference type is not allowed">;
 def err_wasm_ca_reference : Error<
@@ -13871,6 +13871,9 @@ 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">;
+def err_wasm_multivalue_requires_feature : Error<
+  "the 'wasm_multivalue' calling convention requires the 'multivalue' target "
+  "feature to be enabled">;
 
 // OpenACC diagnostics.
 def warn_acc_routine_unimplemented
diff --git a/clang/include/clang/Basic/Specifiers.h b/clang/include/clang/Basic/Specifiers.h
index 8da6fd4cf454a..b50deb91eef2c 100644
--- a/clang/include/clang/Basic/Specifiers.h
+++ b/clang/include/clang/Basic/Specifiers.h
@@ -313,6 +313,7 @@ namespace clang {
     CC_RISCVVLSCall_16384, // __attribute__((riscv_vls_cc(16384)))
     CC_RISCVVLSCall_32768, // __attribute__((riscv_vls_cc(32768)))
     CC_RISCVVLSCall_65536, // __attribute__((riscv_vls_cc(65536)))
+    CC_WasmMultivalue,     // __attribute__((wasm_multivalue))
   };
 
   /// Checks whether the given calling convention supports variadic
diff --git a/clang/lib/AST/ItaniumMangle.cpp b/clang/lib/AST/ItaniumMangle.cpp
index 1cb6fa05f22ac..743dc390866d6 100644
--- a/clang/lib/AST/ItaniumMangle.cpp
+++ b/clang/lib/AST/ItaniumMangle.cpp
@@ -3534,6 +3534,7 @@ StringRef CXXNameMangler::getCallingConvQualifierName(CallingConv CC) {
   case CC_PreserveMost:
   case CC_PreserveAll:
   case CC_M68kRTD:
+  case CC_WasmMultivalue:
   case CC_PreserveNone:
   case CC_RISCVVectorCall:
 #define CC_VLS_CASE(ABI_VLEN) case CC_RISCVVLSCall_##ABI_VLEN:
diff --git a/clang/lib/AST/Type.cpp b/clang/lib/AST/Type.cpp
index 96a398aa21dad..fa66c146b1d87 100644
--- a/clang/lib/AST/Type.cpp
+++ b/clang/lib/AST/Type.cpp
@@ -3741,6 +3741,8 @@ StringRef FunctionType::getNameForCallConv(CallingConv CC) {
     return "preserve_all";
   case CC_M68kRTD:
     return "m68k_rtd";
+  case CC_WasmMultivalue:
+    return "wasm_multivalue";
   case CC_PreserveNone:
     return "preserve_none";
     // clang-format off
@@ -4552,6 +4554,7 @@ bool AttributedType::isCallingConv() const {
   case attr::PreserveMost:
   case attr::PreserveAll:
   case attr::M68kRTD:
+  case attr::WasmMultivalue:
   case attr::PreserveNone:
   case attr::RISCVVectorCC:
   case attr::RISCVVLSCC:
diff --git a/clang/lib/AST/TypePrinter.cpp b/clang/lib/AST/TypePrinter.cpp
index 80f5b90ba35c4..273e12168ac97 100644
--- a/clang/lib/AST/TypePrinter.cpp
+++ b/clang/lib/AST/TypePrinter.cpp
@@ -1173,6 +1173,9 @@ void TypePrinter::printFunctionAfter(const FunctionType::ExtInfo &Info,
     case CC_M68kRTD:
       OS << " __attribute__((m68k_rtd))";
       break;
+    case CC_WasmMultivalue:
+      OS << " __attribute__((wasm_multivalue))";
+      break;
     case CC_PreserveNone:
       OS << " __attribute__((preserve_none))";
       break;
@@ -2081,6 +2084,9 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
   case attr::M68kRTD:
     OS << "m68k_rtd";
     break;
+  case attr::WasmMultivalue:
+    OS << "wasm_multivalue";
+    break;
   case attr::RISCVVectorCC:
     OS << "riscv_vector_cc";
     break;
diff --git a/clang/lib/Basic/Targets/WebAssembly.h b/clang/lib/Basic/Targets/WebAssembly.h
index 6085197498163..165acaabe96df 100644
--- a/clang/lib/Basic/Targets/WebAssembly.h
+++ b/clang/lib/Basic/Targets/WebAssembly.h
@@ -184,6 +184,7 @@ class LLVM_LIBRARY_VISIBILITY WebAssemblyTargetInfo : public TargetInfo {
     switch (CC) {
     case CC_C:
     case CC_Swift:
+    case CC_WasmMultivalue:
       return CCCR_OK;
     case CC_SwiftAsync:
       return CCCR_Error;
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index 40cc275d40273..2dad85ec71e30 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -103,6 +103,8 @@ unsigned CodeGenTypes::ClangCallConvToLLVMCallConv(CallingConv CC) {
     return llvm::CallingConv::SwiftTail;
   case CC_M68kRTD:
     return llvm::CallingConv::M68k_RTD;
+  case CC_WasmMultivalue:
+    return llvm::CallingConv::WASM_Multivalue;
   case CC_PreserveNone:
     return llvm::CallingConv::PreserveNone;
     // clang-format off
diff --git a/clang/lib/CodeGen/CGDebugInfo.cpp b/clang/lib/CodeGen/CGDebugInfo.cpp
index 0d45df02a2a21..090741c1ff992 100644
--- a/clang/lib/CodeGen/CGDebugInfo.cpp
+++ b/clang/lib/CodeGen/CGDebugInfo.cpp
@@ -1838,6 +1838,8 @@ static unsigned getDwarfCC(CallingConv CC) {
     return llvm::dwarf::DW_CC_LLVM_X86RegCall;
   case CC_M68kRTD:
     return llvm::dwarf::DW_CC_LLVM_M68kRTD;
+  case CC_WasmMultivalue:
+    return llvm::dwarf::DW_CC_LLVM_WasmMultivalue;
   case CC_PreserveNone:
     return llvm::dwarf::DW_CC_LLVM_PreserveNone;
   case CC_RISCVVectorCall:
diff --git a/clang/lib/CodeGen/Targets/WebAssembly.cpp b/clang/lib/CodeGen/Targets/WebAssembly.cpp
index ebe996a4edd8d..8942e3e78b6de 100644
--- a/clang/lib/CodeGen/Targets/WebAssembly.cpp
+++ b/clang/lib/CodeGen/Targets/WebAssembly.cpp
@@ -28,17 +28,19 @@ class WebAssemblyABIInfo final : public ABIInfo {
       : ABIInfo(CGT), defaultInfo(CGT), Kind(Kind) {}
 
 private:
-  ABIArgInfo classifyReturnType(QualType RetTy) const;
-  ABIArgInfo classifyArgumentType(QualType Ty) const;
+  ABIArgInfo classifyReturnType(QualType RetTy, llvm::CallingConv::ID CC) const;
+  ABIArgInfo classifyArgumentType(QualType Ty, llvm::CallingConv::ID CC) const;
 
   // DefaultABIInfo's classifyReturnType and classifyArgumentType are
   // non-virtual, but computeInfo and EmitVAArg are virtual, so we
   // overload them.
   void computeInfo(CGFunctionInfo &FI) const override {
+    llvm::CallingConv::ID CC = FI.getCallingConvention();
     if (!getCXXABI().classifyReturnType(FI))
-      FI.getReturnInfo() = classifyReturnType(FI.getReturnType());
+      FI.getReturnInfo() =
+          classifyReturnType(FI.getReturnType(), CC);
     for (auto &Arg : FI.arguments())
-      Arg.info = classifyArgumentType(Arg.type);
+      Arg.info = classifyArgumentType(Arg.type, CC);
   }
 
   RValue EmitVAArg(CodeGenFunction &CGF, Address VAListAddr, QualType Ty,
@@ -95,8 +97,54 @@ class WebAssemblyTargetCodeGenInfo final : public TargetCodeGenInfo {
   }
 };
 
+/// Count the number of "scalar fields" in the given record type, as defined by
+/// WebAssembly/tool-conventions for the "wasm-multivalue" calling convention
+/// primarily. A scalar field is a field that recursively, through nested
+/// structs, unions, and arrays, contains just a single scalar value.
+///
+/// Returns the number of scalar fields, or std::nullopt if the record contains
+/// a field that is not a scalar field (e.g., a sub-aggregate with multiple
+/// scalars, a bit-field, or a flexible array member).
+///
+/// Note that this is similar to `isSingleElementStruct` in structure.
+static std::optional<unsigned> countScalarFields(ASTContext &Context, QualType T) {
+  const auto *RD = T->getAsRecordDecl();
+  if (!RD)
+    return std::nullopt;
+  if (RD->hasFlexibleArrayMember())
+    return std::nullopt;
+
+  unsigned Count = 0;
+
+  // Check bases first for C++ records.
+  if (const auto *CXXRD = dyn_cast<CXXRecordDecl>(RD)) {
+    for (const auto &Base : CXXRD->bases()) {
+      auto SubCount = countScalarFields(Context, Base.getType());
+      if (!SubCount)
+        return std::nullopt;
+      Count += *SubCount;
+    }
+  }
+
+  for (const auto *FD : RD->fields()) {
+    if (FD->isBitField())
+      return std::nullopt;
+    if (isEmptyField(Context, FD, true))
+      continue;
+
+    QualType T = FD->getType();
+    if (isAggregateTypeForABI(T) && !isSingleElementStruct(T, Context))
+      return std::nullopt;
+
+    ++Count;
+  }
+
+  return Count;
+}
+
 /// Classify argument of given type \p Ty.
-ABIArgInfo WebAssemblyABIInfo::classifyArgumentType(QualType Ty) const {
+ABIArgInfo WebAssemblyABIInfo::classifyArgumentType(QualType Ty,
+                                                    llvm::CallingConv::ID CC) const {
   Ty = useFirstFieldIfTransparentUnion(Ty);
 
   if (isAggregateTypeForABI(Ty)) {
@@ -113,6 +161,12 @@ ABIArgInfo WebAssemblyABIInfo::classifyArgumentType(QualType Ty) const {
     // though watch out for things like bitfields.
     if (const Type *SeltTy = isSingleElementStruct(Ty, getContext()))
       return ABIArgInfo::getDirect(CGT.ConvertType(QualType(SeltTy, 0)));
+    // For the wasm-multivalue calling convention, structs with exactly two
+    // scalar fields are passed directly as two arguments.
+    if (CC == llvm::CallingConv::WASM_Multivalue) {
+      if (auto N = countScalarFields(getContext(), Ty); N && *N == 2)
+        return ABIArgInfo::getExpand();
+    }
     // For the experimental multivalue ABI, fully expand all other aggregates
     if (Kind == WebAssemblyABIKind::ExperimentalMV) {
       const auto *RD = Ty->castAsRecordDecl();
@@ -132,7 +186,8 @@ ABIArgInfo WebAssemblyABIInfo::classifyArgumentType(QualType Ty) const {
   return defaultInfo.classifyArgumentType(Ty);
 }
 
-ABIArgInfo WebAssemblyABIInfo::classifyReturnType(QualType RetTy) const {
+ABIArgInfo WebAssemblyABIInfo::classifyReturnType(QualType RetTy,
+                                                  llvm::CallingConv::ID CC) const {
   if (isAggregateTypeForABI(RetTy)) {
     // Records with non-trivial destructors/copy-constructors should not be
     // returned by value.
@@ -145,6 +200,13 @@ ABIArgInfo WebAssemblyABIInfo::classifyReturnType(QualType RetTy) const {
       // ABIArgInfo::getDirect().
       if (const Type *SeltTy = isSingleElementStruct(RetTy, getContext()))
         return ABIArgInfo::getDirect(CGT.ConvertType(QualType(SeltTy, 0)));
+      // For the wasm-multivalue calling convention, structs whose fields are
+      // (recursively) scalars are returned directly via the multivalue
+      // proposal.
+      if (CC == llvm::CallingConv::WASM_Multivalue) {
+        if (auto N = countScalarFields(getContext(), RetTy); N && *N > 0)
+          return ABIArgInfo::getDirect();
+      }
       // For the experimental multivalue ABI, return all other aggregates
       if (Kind == WebAssemblyABIKind::ExperimentalMV)
         return ABIArgInfo::getDirect();
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index ae04d3855f01c..ac21210bd7112 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -5543,6 +5543,9 @@ static void handleCallConvAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
   case ParsedAttr::AT_M68kRTD:
     D->addAttr(::new (S.Context) M68kRTDAttr(S.Context, AL));
     return;
+  case ParsedAttr::AT_WasmMultivalue:
+    D->addAttr(::new (S.Context) WasmMultivalueAttr(S.Context, AL));
+    return;
   case ParsedAttr::AT_PreserveNone:
     D->addAttr(::new (S.Context) PreserveNoneAttr(S.Context, AL));
     return;
@@ -5814,6 +5817,15 @@ bool Sema::CheckCallingConvAttr(const ParsedAttr &Attrs, CallingConv &CC,
   case ParsedAttr::AT_M68kRTD:
     CC = CC_M68kRTD;
     break;
+  case ParsedAttr::AT_WasmMultivalue:
+    CC = CC_WasmMultivalue;
+    if (Context.getTargetInfo().getTriple().isWasm() &&
+        !Context.getTargetInfo().hasFeature("multivalue")) {
+      Attrs.setInvalid();
+      Diag(Attrs.getLoc(), diag::err_wasm_multivalue_requires_feature);
+      return true;
+    }
+    break;
   case ParsedAttr::AT_PreserveNone:
     CC = CC_PreserveNone;
     break;
@@ -8068,6 +8080,7 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
   case ParsedAttr::AT_PreserveNone:
   case ParsedAttr::AT_RISCVVectorCC:
   case ParsedAttr::AT_RISCVVLSCC:
+  case ParsedAttr::AT_WasmMultivalue:
     handleCallConvAttr(S, D, AL);
     break;
   case ParsedAttr::AT_DeviceKernel:
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index 44ac4f6630690..2811cb0ee95d4 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -143,7 +143,8 @@ static void diagnoseBadTypeAttribute(Sema &S, const ParsedAttr &attr,
   case ParsedAttr::AT_M68kRTD:                                                 \
   case ParsedAttr::AT_PreserveNone:                                            \
   case ParsedAttr::AT_RISCVVectorCC:                                           \
-  case ParsedAttr::AT_RISCVVLSCC
+  case ParsedAttr::AT_RISCVVLSCC:                                              \
+  case ParsedAttr::AT_WasmMultivalue
 
 // Function type attributes.
 #define FUNCTION_TYPE_ATTRS_CASELIST                                           \
@@ -7803,6 +7804,8 @@ static Attr *getCCTypeAttr(ASTContext &Ctx, ParsedAttr &Attr) {
     return createSimpleAttr<PreserveAllAttr>(Ctx, Attr);
   case ParsedAttr::AT_M68kRTD:
     return createSimpleAttr<M68kRTDAttr>(Ctx, Attr);
+  case ParsedAttr::AT_WasmMultivalue:
+    return createSimpleAttr<WasmMultivalueAttr>(Ctx, Attr);
   case ParsedAttr::AT_PreserveNone:
     return createSimpleAttr<PreserveNoneAttr>(Ctx, Attr);
   case ParsedAttr::AT_RISCVVectorCC:
diff --git a/clang/test/CodeGen/WebAssembly/wasm-multivalue-abi.c b/clang/test/CodeGen/WebAssembly/wasm-multivalue-abi.c
new file mode 100644
index 0000000000000..5194616a13733
--- /dev/null
+++ b/clang/test/CodeGen/WebAssembly/wasm-multivalue-abi.c
@@ -0,0 +1,77 @@
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -target-feature +multivalue \
+// RUN:   %s -emit-llvm -o - | FileCheck %s
+// RUN: %clang_cc1 -triple wasm64-unknown-unknown -target-feature +multivalue \
+// RUN:   %s -emit-llvm -o - | FileCheck %s
+
+// Verify that the `wasm_multivalue` calling convention produces the function
+// signatures described in the WebAssembly tool conventions PR.
+
+// CHECK-LABEL: define wasm_multivaluecc void @f1()
+__attribute__((wasm_multivalue))
+void f1(void) {}
+
+// CHECK-LABEL: define wasm_multivaluecc i32 @f2(float {{.*}}, double {{.*}})
+__attribute__((wasm_multivalue))
+int f2(float a, double b) { return (int)(a + b); }
+
+// CHECK-LABEL: define wasm_multivaluecc i128 @f3(fp128
+__attribute__((wasm_multivalue))
+__int128 f3(long double x) { return (__int128)x; }
+
+struct Foo4 { };
+union Bar4 { };
+
+// CHECK-LABEL: define wasm_multivaluecc void @f4()
+__attribute__((wasm_multivalue))
+union Bar4 f4(struct Foo4 x) { union Bar4 r; return r; }
+
+struct Foo5 { int a; };
+union Bar5 { int a; };
+
+// CHECK-LABEL: define wasm_multivaluecc i32 @f5(i32
+__attribute__((wasm_multivalue))
+union Bar5 f5(struct Foo5 x) { union Bar5 r; r.a = x.a; return r; }
+
+struct Foo6 { int a; int b; };
+
+// CHECK-LABEL: define wasm_multivaluecc %struct.Foo6 @f6(i32 {{.*}}, i32 {{.*}})
+__attribute__((wasm_multivalue))
+struct Foo6 f6(struct Foo6 x) { return x; }
+
+// CHECK-LABEL: define wasm_multivaluecc i128 @f7()
+__attribute__((wasm_multivalue))
+__int128 f7(void) { return 1; }
+
+struct Foo8 { int a; int b; int c; };
+// CHECK-LABEL: define wasm_multivaluecc %struct.Foo8 @f8(ptr
+__attribute__((wasm_multivalue))
+struct Foo8 f8(struct Foo8 x) { return x; }
+
+struct Foo9 {
+  struct Foo6 inner;
+};
+// CHECK-LABEL: define wasm_multivaluecc void @f9(ptr {{.*}} sret
+__attribute__((wasm_multivalue))
+struct Foo9 f9(void) { struct Foo9 r = {{0, 0}}; return r; }
+
+// bitfields force pointers
+struct Foo10 {
+  int a : 4;
+  int b : 4;
+};
+// CHECK-LABEL: define wasm_multivaluecc void @f10(ptr
+__attribute__((wasm_multivalue))
+struct Foo10 f10(void) { struct Foo10 r = {0, 0}; return r; }
+
+// The default calling convention isn't changed from `+multivalue`
+// CHECK-LABEL: define void @f11(ptr{{.*}}sret(%struct.Foo6){{.*}}, ptr {{.*}}byval(%struct.Foo6)
+struct Foo6 f11(struct Foo6 x) { return x; }
+
+// Test cross-calling-convention indierct calls
+typedef __attribute__((wasm_multivalue)) struct Foo6 (*mv_ptr)(struct Foo6);
+
+// CHECK-LABEL: define void @f12(
+// CHECK: call wasm_multivaluecc {{(noundef )?}}%struct.Foo6 %0(i32{{.*}}, i32
+struct Foo6 f12(mv_ptr fn, struct Foo6 x) {
+  return fn(x);
+}
diff --git a/clang/test/CodeGen/WebAssembly/wasm-multivalue-functype.c b/clang/test/CodeGen/WebAssembly/wasm-multivalue-functype.c
new file mode 100644
index 0000000000000..c522640901473
--- /dev/null
+++ b/clang/test/CodeGen/WebAssembly/wasm-multivalue-functype.c
@@ -0,0 +1,45 @@
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -target-feature +multivalue %s -S -O2 -o - | FileCheck %s
+
+// CHECK: .functype f1 () -> ()
+void f1(void) {}
+// CHECK: .functype f1mv () -> ()
+__attribute__((wasm_multivalue))
+void f1mv(void) {}
+
+// CHECK: .functype f2 (f32, f64) -> (i32)
+int f2(float a, double b) { return (int)(a + b); }
+// CHECK: .functype f2mv (f32, f64) -> (i32)
+__attribute__((wasm_multivalue))
+int f2mv(float a, double b) { return (int)(a + b); }
+
+// CHECK: .functype f3 (i32, i64, i64) -> ()
+__int128 f3(long double x) { return (__int128)x; }
+// CHECK: .functype f3mv (i64, i64) -> (i64, i64)
+__attribute__((wasm_multivalue))
+__int128 f3mv(long double x) { return (__int128)x; }
+
+struct Foo4 { };
+union Bar4 { };
+
+// CHECK: .functype f4 () -> ()
+union Bar4 f4(struct Foo4 x) { union Bar4 r; return r; }
+// CHECK: .functype f4mv () -> ()
+__attribute__((wasm_multivalue))
+union Bar4 f4mv(struct Foo4 x) { union Bar4 r; return r; }
+
+struct Foo5 { int a; };
+union Bar5 { int a; };
+
+// CHECK: .functype f5 (i32) -> (i32)
+union Bar5 f5(struct Foo5 x) { union Bar5 r; r.a = x.a; return r; }
+// CHECK: .functype f5mv (i32) -> (i32)
+__attribute__((wasm_multivalue))
+union Bar5 f5mv(struct Foo5 x) { union Bar5 r; r.a = x.a; return r; }
+
+struct Foo6 { int a; int b; };
+
+// CHECK: .functype f6 (i32, i32) -> ()
+struct Foo6 f6(struct Foo6 x) { return x; }
+// CHECK: .functype f6mv (i32, i32) -> (i32, i32)
+__attribute__((wasm_multivalue))
+struct Foo6 f6mv(struct Foo6 x) { return x; }
diff --git a/clang/test/Sema/attr-wasm-multivalue.c b/clang/test/Sema/attr-wasm-multivalue.c
new file mode 100644
index 0000000000000..51e9f494b657d
--- /dev/null
+++ b/clang/test/Sema/attr-wasm-multivalue.c
@@ -0,0 +1,33 @@
+// RUN: %clang_cc1 -triple wasm32-unknown-unknown -target-feature +multivalue -fsyntax-only -verify=enabled %s...
[truncated]

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

✅ With the latest revision this PR passed the C/C++ code formatter.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 28, 2026

🐧 Linux x64 Test Results

  • 204850 tests passed
  • 6567 tests skipped

✅ The build succeeded and all tests passed.

@llvmorg-github-actions llvmorg-github-actions Bot added the clang:as-a-library libclang and C++ API label May 28, 2026
Copy link
Copy Markdown
Contributor

@QuantumSegfault QuantumSegfault left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we unify the LLVM and Clang attribute under one name: wasm_multivalue_cc? Cause you have wasm_multivalue in clang, but wasm_multivaluecc in LLVM.

So, as I understand it, at the IR level, wasm_multivaluecc opts-in to the same ABI that has existed under -mabi=experimental-mv (or something like that...) for a while now? That is, ALL by-value IR aggregates are expanded recursively (allowing full use of Wasm MV in custom ABIs). And -mabi=experimental-mv will still do the opt-in for all? Do you intend to keep the ABI switch long-term? But it's at the Clang (front-end; which will extend to Rust eventually) level that the thresholds are enforced (<= aggregates with only 2 scalar fields split)?

Could you add some tests to illustrate what happens to (Clang) arrays (i.e. would a char[2] get split into a pair of i32 parameters?). I know IR [2 x i8] would.

This is an implementation of WebAssembly/tool-conventions#268 here in
LLVM. This adds a new calling convention to Clang, named
`wasm_multivalue`, which is intended to be used on WebAssembly targets
to configure multiple return values and slightly tweak the ABI. Changes
here are:

* Parsing/validation of `__attribute__((wasm_multivalue))`. Note that
  validation here means that it's not only well-formed but on wasm
  targets the `multivalue` target feature is additionally enabled.
* Clang-level ABI adjustments for the `wasm_multivalue` calling
  convention. These are defined by WebAssembly/tool-conventions#268 and
  notably includes expanding structs with exactly 2 scalar fields in
  parameter-position and directly returning structs with any number of
  scalar fields in return-position.
* A new `wasm_multivaluecc` keyword/calling convention for LLVM IR. This
  is what Clang lowers to when using the `wasm_multivalue` calling
  convention.
* Adjustments at the LLVM ABI layer to support returning multiple values
  with the `wasm_multivaluecc` calling convention.

My goal after this would be to start integrating this into Rust next,
under and unstable feature, and then further continue
testing/vetting/etc for component model usage.
Fixes a case where a function calls another function with a different
calling convention.
@alexcrichton
Copy link
Copy Markdown
Contributor Author

Good point @QuantumSegfault, I originally thought wasm_multivaluecc was more idiomatic in the LLVM IR layer, but I think that's just a mistaken conclusion. I've canonicalized on wasm_multivalue now.

Otherwise though, yes, using wasm_multivalue as an ABI, within LLVM, pretends that experimental-mv was enabled for just that function. I'm not a maintainer of the wasm backend so I definitely don't have the final say, but my intention is, yes, to eventually phase out a flag like experimental-mv. And yes, Clang/rustc are where the full ABI is implemented in terms of limits for structs/records/etc, and AFAIK that matches what happens for all other platforms too.

Could you add some tests to illustrate what happens to (Clang) arrays

Sure! My C chops aren't so great though, and my initial attempts to do this are all thwarted. Is it possible to take/return an array by-value in C? (I thought the answer was "no", and if so then this poses a distinct question for Rust and what to do about its ABI in that regard)

@QuantumSegfault
Copy link
Copy Markdown
Contributor

Is it possible to take/return an array by-value in C?

...huh...I guess not. Bad assumption on my part. Passing int arg[2] ends up a single pointer, and you can't return arrays.

Seems to work if you wrap it in a struct though. Passed by value.

@QuantumSegfault
Copy link
Copy Markdown
Contributor

Okay, so looking at those tests I would expect small arrays to be split the same way separate struct fields would (or alternatively, passed through as IR arrays by value, which will be split further down the line into distinct parameters during instruction selection due to the LLVM-side CC), rather than being passed as pointers.

@alexcrichton
Copy link
Copy Markdown
Contributor Author

For that I'd defer discussion to WebAssembly/tool-conventions#268. I believe this PR is an accurate reflection of the current definition over there where struct fields are only considered for flattening if they're a scalar (or a wrapped scalar). For arrays that's similar to a struct of multiple fields, and those are always done indirectly. For changing the definition itself, though, that's where WebAssembly/tool-conventions#268 I think is a better place to discuss

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend:WebAssembly clang:as-a-library libclang and C++ API clang:codegen IR generation bugs: mangling, exceptions, etc. clang:frontend Language frontend issues, e.g. anything involving "Sema" debuginfo llvm:binary-utilities llvm:ir

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants