[llvm] [WebAssembly] Add call_ref (0x14) and return_call_ref (0x15) (PR #195942)

Jonas Devlieghere via llvm-commits llvm-commits at lists.llvm.org
Wed May 6 10:55:16 PDT 2026


https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/195942

>From 3c84bed97e6da6d4763573ce27c91e8280c1b73d Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Tue, 5 May 2026 14:09:10 -0700
Subject: [PATCH 1/2] [WebAssembly] Add call_ref (0x14) and return_call_ref
 (0x15)

Add MC-layer support for the typed function references opcodes:

  - Instruction definitions in WebAssemblyInstrCall.td, gated on
    HasReferenceTypes (and HasTailCall for return_call_ref).
  - Asm-parser hook that accepts the (ty) -> (ty) signature syntax,
    mirroring call_indirect / return_call_indirect.
  - Stack-effect modeling in WebAssemblyAsmTypeCheck so non-trivial
    signatures type-check correctly.
  - Encoding and disassembly tests under test/MC/WebAssembly.

Codegen does not yet select these opcodes. The motivation is to unblock
LLDB, which uses LLVM's disassembler.

rdar://170659547
---
 .../AsmParser/WebAssemblyAsmParser.cpp        |  4 ++
 .../AsmParser/WebAssemblyAsmTypeCheck.cpp     | 12 ++++++
 .../WebAssembly/WebAssemblyInstrCall.td       | 16 ++++++++
 .../WebAssembly/function-references.txt       | 25 +++++++++++++
 .../test/MC/WebAssembly/function-references.s | 37 +++++++++++++++++++
 5 files changed, 94 insertions(+)
 create mode 100644 llvm/test/MC/Disassembler/WebAssembly/function-references.txt
 create mode 100644 llvm/test/MC/WebAssembly/function-references.s

diff --git a/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp b/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp
index 33e2e1c0d48f6..00aa86087430e 100644
--- a/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp
+++ b/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp
@@ -706,6 +706,10 @@ class WebAssemblyAsmParser final : public MCTargetAsmParser {
       if (parseFunctionTableOperand(&FunctionTable))
         return true;
       ExpectFuncType = true;
+    } else if (Name == "call_ref" || Name == "return_call_ref") {
+      // The typed function references forms take a function signature as
+      // their sole explicit operand (the funcref is popped from the stack).
+      ExpectFuncType = true;
     } else if (Name == "ref.test") {
       // When we get support for wasm-gc types, this should become
       // ExpectRefType.
diff --git a/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmTypeCheck.cpp b/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmTypeCheck.cpp
index bf9f8e6e90817..6ad8967f4075f 100644
--- a/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmTypeCheck.cpp
+++ b/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmTypeCheck.cpp
@@ -617,6 +617,18 @@ bool WebAssemblyAsmTypeCheck::typeCheck(SMLoc ErrorLoc, const MCInst &Inst,
     return Error;
   }
 
+  if (Name == "call_ref" || Name == "return_call_ref") {
+    // Funcref target popped from the stack, followed by the signature's
+    // parameters; pushes the signature's results.
+    bool Error = popType(ErrorLoc, wasm::ValType::FUNCREF);
+    Error |= checkSig(ErrorLoc, LastSig);
+    if (Name == "return_call_ref") {
+      Error |= endOfFunction(ErrorLoc, false);
+      pushType(Polymorphic{});
+    }
+    return Error;
+  }
+
   if (Name == "call" || Name == "return_call") {
     bool Error = false;
     const wasm::WasmSignature *Sig = nullptr;
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td b/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td
index 485b12e256224..263c580ecaf99 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td
@@ -81,4 +81,20 @@ defm RET_CALL_INDIRECT :
     0x13>,
   Requires<[HasTailCall]>;
 
+// Typed function references: call_ref / return_call_ref pop a funcref from the
+// stack and branch to it, using the given type index to describe the signature.
+let variadicOpsAreDefs = 1 in
+defm CALL_REF :
+  I<(outs), (ins TypeIndex:$type, FUNCREF:$ref, variable_ops),
+    (outs), (ins TypeIndex:$type), [],
+    "call_ref\t$type, $ref", "call_ref\t$type", 0x14>,
+  Requires<[HasReferenceTypes]>;
+
+let isReturn = 1, isTerminator = 1, hasCtrlDep = 1, isBarrier = 1 in
+defm RET_CALL_REF :
+  I<(outs), (ins TypeIndex:$type, FUNCREF:$ref, variable_ops),
+    (outs), (ins TypeIndex:$type), [],
+    "return_call_ref\t$type, $ref", "return_call_ref\t$type", 0x15>,
+  Requires<[HasReferenceTypes, HasTailCall]>;
+
 } // Uses = [SP32,SP64], isCall = 1
diff --git a/llvm/test/MC/Disassembler/WebAssembly/function-references.txt b/llvm/test/MC/Disassembler/WebAssembly/function-references.txt
new file mode 100644
index 0000000000000..d0d48f6eccef4
--- /dev/null
+++ b/llvm/test/MC/Disassembler/WebAssembly/function-references.txt
@@ -0,0 +1,25 @@
+# RUN: llvm-mc --disassemble %s -triple=wasm32-unknown-unknown -mattr=+reference-types,+tail-call | FileCheck %s
+# RUN: llvm-mc --disassemble %s -triple=wasm64-unknown-unknown -mattr=+reference-types,+tail-call | FileCheck %s
+
+# The Requires<[...]> feature gates do not filter the disassembler tables, so
+# the same bytes should decode even without features enabled.
+# RUN: llvm-mc --disassemble %s -triple=wasm32-unknown-unknown | FileCheck %s
+
+# CHECK: call_ref 0
+0x14 0x00
+
+# CHECK: call_ref 127
+0x14 0x7F
+
+# Multi-byte ULEB type index.
+# CHECK: call_ref 128
+0x14 0x80 0x01
+
+# CHECK: return_call_ref 0
+0x15 0x00
+
+# CHECK: return_call_ref 42
+0x15 0x2A
+
+# CHECK: end
+0x0B
diff --git a/llvm/test/MC/WebAssembly/function-references.s b/llvm/test/MC/WebAssembly/function-references.s
new file mode 100644
index 0000000000000..3bfee9f35d608
--- /dev/null
+++ b/llvm/test/MC/WebAssembly/function-references.s
@@ -0,0 +1,37 @@
+# RUN: llvm-mc -show-encoding -triple=wasm32-unknown-unknown -mattr=+reference-types,+tail-call < %s | FileCheck %s
+# RUN: llvm-mc -show-encoding -triple=wasm64-unknown-unknown -mattr=+reference-types,+tail-call < %s | FileCheck %s
+
+# Verify that dropping the tail-call feature rejects return_call_ref. The
+# reference-types predicate is not enforced at the parser level today (shared
+# with other Requires<[HasReferenceTypes]> opcodes such as ref.null_func), so
+# we only assert the tail-call gate.
+# RUN: not llvm-mc -triple=wasm32-unknown-unknown -mattr=+reference-types %s 2>&1 \
+# RUN:   | FileCheck --check-prefix=NO-TAIL-CALL %s
+
+# NO-TAIL-CALL: error: instruction requires: tail-call
+
+call_ref_void:
+    .functype call_ref_void () -> ()
+    ref.null_func
+    # CHECK: call_ref     () -> () # encoding: [0x14,
+    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex0 at TYPEINDEX, kind: fixup_uleb128_i32
+    call_ref () -> ()
+    end_function
+
+call_ref_sig:
+    .functype call_ref_sig () -> (i32)
+    i32.const 1
+    i32.const 2
+    ref.null_func
+    # CHECK: call_ref     (i32, i32) -> (i32) # encoding: [0x14,
+    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex1 at TYPEINDEX, kind: fixup_uleb128_i32
+    call_ref (i32, i32) -> (i32)
+    end_function
+
+return_call_ref_void:
+    .functype return_call_ref_void () -> ()
+    ref.null_func
+    # CHECK: return_call_ref     () -> () # encoding: [0x15,
+    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex2 at TYPEINDEX, kind: fixup_uleb128_i32
+    return_call_ref () -> ()
+    end_function

>From 48ca50dcbfcfcdcfa55e6a0e2b9d3a47a3bbae79 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Wed, 6 May 2026 10:54:50 -0700
Subject: [PATCH 2/2] Address Demetrius' feedback

---
 .../AsmParser/WebAssemblyAsmParser.cpp         |  4 ++++
 .../Target/WebAssembly/WebAssemblyInstrCall.td |  6 ++++--
 .../Target/WebAssembly/WebAssemblyInstrRef.td  |  5 +++++
 .../WebAssembly/function-references.txt        | 10 ++++++++--
 llvm/test/MC/WebAssembly/function-references.s | 18 ++++++++++++------
 5 files changed, 33 insertions(+), 10 deletions(-)

diff --git a/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp b/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp
index 00aa86087430e..c7b1f9c80f6e3 100644
--- a/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp
+++ b/llvm/lib/Target/WebAssembly/AsmParser/WebAssemblyAsmParser.cpp
@@ -714,6 +714,10 @@ class WebAssemblyAsmParser final : public MCTargetAsmParser {
       // When we get support for wasm-gc types, this should become
       // ExpectRefType.
       ExpectFuncType = true;
+    } else if (Name == "ref.cast") {
+      // When we get support for wasm-gc types, this should become
+      // ExpectRefType.
+      ExpectFuncType = true;
     }
 
     if (Name.contains("atomic.")) {
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td b/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td
index 263c580ecaf99..5ee489e50fa96 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyInstrCall.td
@@ -83,18 +83,20 @@ defm RET_CALL_INDIRECT :
 
 // Typed function references: call_ref / return_call_ref pop a funcref from the
 // stack and branch to it, using the given type index to describe the signature.
+// These were introduced with the function-references proposal, which was folded
+// into the GC proposal, so they are gated on HasGC.
 let variadicOpsAreDefs = 1 in
 defm CALL_REF :
   I<(outs), (ins TypeIndex:$type, FUNCREF:$ref, variable_ops),
     (outs), (ins TypeIndex:$type), [],
     "call_ref\t$type, $ref", "call_ref\t$type", 0x14>,
-  Requires<[HasReferenceTypes]>;
+  Requires<[HasGC]>;
 
 let isReturn = 1, isTerminator = 1, hasCtrlDep = 1, isBarrier = 1 in
 defm RET_CALL_REF :
   I<(outs), (ins TypeIndex:$type, FUNCREF:$ref, variable_ops),
     (outs), (ins TypeIndex:$type), [],
     "return_call_ref\t$type, $ref", "return_call_ref\t$type", 0x15>,
-  Requires<[HasReferenceTypes, HasTailCall]>;
+  Requires<[HasTailCall, HasGC]>;
 
 } // Uses = [SP32,SP64], isCall = 1
diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyInstrRef.td b/llvm/lib/Target/WebAssembly/WebAssemblyInstrRef.td
index 304c4f3fcb028..f6e366f18f7e3 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyInstrRef.td
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyInstrRef.td
@@ -41,6 +41,11 @@ defm REF_TEST_FUNCREF : I<(outs I32:$res), (ins TypeIndex:$type, FUNCREF:$ref),
                           "ref.test\t$type, $ref", "ref.test $type", 0xfb14>,
                         Requires<[HasGC]>;
 
+defm REF_CAST_FUNCREF : I<(outs FUNCREF:$res), (ins TypeIndex:$type, FUNCREF:$ref),
+                          (outs), (ins TypeIndex:$type), [],
+                          "ref.cast\t$type, $ref", "ref.cast $type", 0xfb16>,
+                        Requires<[HasGC]>;
+
 defm REF_FUNC : I<(outs FUNCREF:$res), (ins function32_op:$func),
                     (outs), (ins function32_op:$func), [],
                     "ref.func\t$func", "ref.func $func", 0xd2>,
diff --git a/llvm/test/MC/Disassembler/WebAssembly/function-references.txt b/llvm/test/MC/Disassembler/WebAssembly/function-references.txt
index d0d48f6eccef4..4ad50e605e7d0 100644
--- a/llvm/test/MC/Disassembler/WebAssembly/function-references.txt
+++ b/llvm/test/MC/Disassembler/WebAssembly/function-references.txt
@@ -1,5 +1,5 @@
-# RUN: llvm-mc --disassemble %s -triple=wasm32-unknown-unknown -mattr=+reference-types,+tail-call | FileCheck %s
-# RUN: llvm-mc --disassemble %s -triple=wasm64-unknown-unknown -mattr=+reference-types,+tail-call | FileCheck %s
+# RUN: llvm-mc --disassemble %s -triple=wasm32-unknown-unknown -mattr=+reference-types,+tail-call,+gc | FileCheck %s
+# RUN: llvm-mc --disassemble %s -triple=wasm64-unknown-unknown -mattr=+reference-types,+tail-call,+gc | FileCheck %s
 
 # The Requires<[...]> feature gates do not filter the disassembler tables, so
 # the same bytes should decode even without features enabled.
@@ -21,5 +21,11 @@
 # CHECK: return_call_ref 42
 0x15 0x2A
 
+# CHECK: ref.cast 0
+0xFB 0x16 0x00
+
+# CHECK: ref.cast 127
+0xFB 0x16 0x7F
+
 # CHECK: end
 0x0B
diff --git a/llvm/test/MC/WebAssembly/function-references.s b/llvm/test/MC/WebAssembly/function-references.s
index 3bfee9f35d608..6bd750d114ee9 100644
--- a/llvm/test/MC/WebAssembly/function-references.s
+++ b/llvm/test/MC/WebAssembly/function-references.s
@@ -1,11 +1,11 @@
-# RUN: llvm-mc -show-encoding -triple=wasm32-unknown-unknown -mattr=+reference-types,+tail-call < %s | FileCheck %s
-# RUN: llvm-mc -show-encoding -triple=wasm64-unknown-unknown -mattr=+reference-types,+tail-call < %s | FileCheck %s
+# RUN: llvm-mc -show-encoding -triple=wasm32-unknown-unknown -mattr=+reference-types,+tail-call,+gc < %s | FileCheck %s
+# RUN: llvm-mc -show-encoding -triple=wasm64-unknown-unknown -mattr=+reference-types,+tail-call,+gc < %s | FileCheck %s
 
 # Verify that dropping the tail-call feature rejects return_call_ref. The
 # reference-types predicate is not enforced at the parser level today (shared
 # with other Requires<[HasReferenceTypes]> opcodes such as ref.null_func), so
 # we only assert the tail-call gate.
-# RUN: not llvm-mc -triple=wasm32-unknown-unknown -mattr=+reference-types %s 2>&1 \
+# RUN: not llvm-mc -triple=wasm32-unknown-unknown -mattr=+reference-types,+gc %s 2>&1 \
 # RUN:   | FileCheck --check-prefix=NO-TAIL-CALL %s
 
 # NO-TAIL-CALL: error: instruction requires: tail-call
@@ -13,8 +13,11 @@
 call_ref_void:
     .functype call_ref_void () -> ()
     ref.null_func
+    # CHECK: ref.cast     () -> () # encoding: [0xfb,0x16,
+    # CHECK-NEXT: fixup A - offset: 2, value: .Ltypeindex0 at TYPEINDEX, kind: fixup_uleb128_i32
+    ref.cast () -> ()
     # CHECK: call_ref     () -> () # encoding: [0x14,
-    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex0 at TYPEINDEX, kind: fixup_uleb128_i32
+    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex1 at TYPEINDEX, kind: fixup_uleb128_i32
     call_ref () -> ()
     end_function
 
@@ -23,8 +26,11 @@ call_ref_sig:
     i32.const 1
     i32.const 2
     ref.null_func
+    # CHECK: ref.cast     (i32, i32) -> (i32) # encoding: [0xfb,0x16,
+    # CHECK-NEXT: fixup A - offset: 2, value: .Ltypeindex2 at TYPEINDEX, kind: fixup_uleb128_i32
+    ref.cast (i32, i32) -> (i32)
     # CHECK: call_ref     (i32, i32) -> (i32) # encoding: [0x14,
-    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex1 at TYPEINDEX, kind: fixup_uleb128_i32
+    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex3 at TYPEINDEX, kind: fixup_uleb128_i32
     call_ref (i32, i32) -> (i32)
     end_function
 
@@ -32,6 +38,6 @@ return_call_ref_void:
     .functype return_call_ref_void () -> ()
     ref.null_func
     # CHECK: return_call_ref     () -> () # encoding: [0x15,
-    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex2 at TYPEINDEX, kind: fixup_uleb128_i32
+    # CHECK-NEXT: fixup A - offset: 1, value: .Ltypeindex4 at TYPEINDEX, kind: fixup_uleb128_i32
     return_call_ref () -> ()
     end_function



More information about the llvm-commits mailing list