feat: typed function references spec (#2497)

Add experimental support for the [Typed Function References](https://github.com/WebAssembly/function-references)
proposal.

Closes https://github.com/wazero/wazero/issues/2483, follows up to the
refactoring in https://github.com/wazero/wazero/pull/2495 and prepares
to WasmGC.

Typed function references extend WebAssembly's type system with
non-nullable reference types and concrete function type indices (`(ref
$t)`, `(ref null $t)`), enabling direct calls through typed references
(`call_ref`, `return_call_ref`) and null-aware branching (`br_on_null`,
`br_on_non_null`, `ref.as_non_null`).

Excluding tests and spec suites, the feature amounts to roughly 1,900
lines of code.

Feature flag: `experimental.CoreFeaturesTypedFunctionReferences`

## What's the use for this?

Sadly, very little. This proposal is a pretty much just a prerequisite
for the GC proposal.

On the flip side, it completes the exception handling spec
(https://github.com/wazero/wazero/pull/2489):
1. two EH spec tests were previously skipped because they required
distinguishing nullable from non-nullable references. Those tests now
pass.
2. The fuzzer is now enabled for both exception handling and typed
function references. It could not be enabled for EH because the fuzzer
would generate func refs.

So I guess, technically, it has a use :D

## Type System

`ValueType` (already `uint64` introduced in
https://github.com/wazero/wazero/pull/2495) is now extended with bit
flags to encode nullability and concrete type indices:

| Bits  | Purpose                         |
|-------|---------------------------------|
| 0-7   | Base type byte (same as before) |
| 8     | Non-nullable flag               |
| 9     | Concrete ref flag               |
| 32-63 | Type index (for `(ref $t)`)     |

This encoding should be fine for WasmGC too and should not require
further changes in the near future.

Subtyping rules: non-nullable is a subtype of nullable (same
kind/index); concrete function refs `(ref $t)` are subtypes of
`funcref`.

## New Instructions

| Opcode | Hex | Description |

|-------------------|--------|----------------------------------------------------------|
| `call_ref` | `0x14` | Indirect call through a typed function reference
|
| `return_call_ref` | `0x15` | Tail-call variant of `call_ref` |
| `ref.as_non_null` | `0xd4` | Assert ref is non-null, trap otherwise |
| `br_on_null` | `0xd5` | Branch if null, push non-null ref on
fall-through |
| `br_on_non_null` | `0xd6` | Branch if non-null (carrying the ref),
fall-through on null |

## Validation

Tricky bits:

- Non-nullable local initialization tracking: `local.get` on a
non-nullable ref local is rejected unless a `local.set`/`local.tee` has
been executed in the same or enclosing block scope. State is
saved/restored at block boundaries per the spec (needed an additional
field to keep track of init'd values)
- `ref.func` now pushes `(ref $t)` (the concrete non-nullable type of
the referenced function) onto the validation type stack instead of plain
`funcref`, so that passing it to `call_ref` type-checks without an
upcast.
- Block types, element segments, table types, and const expressions all
support concrete ref types.
- The type section now validates forward references (standalone types
can only reference previously defined types; rec group members can
reference each other).

## Interpreter

The five new opcodes are compiled to new IR operations and executed in
the interpreter loop. `call_ref` / `return_call_ref` load the function
instance from the opaque reference pointer, null-check, and dispatch.
`br_on_null` / `br_on_non_null` pop the reference, check nullity, and
either branch or fall through with the appropriate stack state.

## Compiler (wazevo)

Implemented entirely as SSA-level lowering with no backend-specific
code:

- `call_ref` / `return_call_ref`: load executable and module context
pointers from the function instance, null-check via
`ExitIfTrueWithCode(ExitCodeNullReference)`, then dispatch as an
indirect call.
- `br_on_null` / `br_on_non_null`: compare against zero, branch with
trampoline blocks for try-table exits and listener support.
- `ref.as_non_null`: null-check with trap.

## Binary Decoding

- `decodeRefType` helper extracted and shared across `value.go`,
`element.go`, `table.go`, and `code.go` for consistent handling of `(ref
null ht)` / `(ref ht)` prefixes.
- Tables support the `0x40 0x00` prefix for initializer expressions
(required for non-nullable table element types).
- `DecodeBlockType` handles concrete ref types as block results.

## Cross-Module Linking

`call_indirect` uses `FunctionTypeID` for fast runtime type checks. The
existing `FunctionType.key()` method builds the key from raw `ValueType`
bytes, but with concrete refs `(ref $0)` in module A and `(ref $0)` in
module B may refer to structurally identical types at different local
indices. `structuralTypeKey` fixes this by replacing local type indices
with the already-assigned `FunctionTypeID` of the referenced type, so
two modules with the same structural signature share a single
`FunctionTypeID`.

## Spec Suite

The spec test suite uses `wasm-tools json-from-wast` (same as exception
handling). All 22 test files pass.

## Fuzzing

The fuzzer (`nodiff`) now enables both `CoreFeaturesExceptionHandling`
and `CoreFeaturesTypedFunctionReferences`. Dummy import generation
handles `exnref` and concrete ref types.

---------

Signed-off-by: Edoardo Vacchi <evacchi@users.noreply.github.com>
This commit is contained in:
Edoardo Vacchi
2026-05-28 15:57:29 +02:00
committed by GitHub
parent 475a1f8f0d
commit 2c14bbff3d
677 changed files with 9799 additions and 215 deletions
+23
View File
@@ -132,6 +132,16 @@ spectest_exception_handling_dir := $(spectest_base_dir)/exception-handling
spectest_exception_handling_testdata_dir := $(spectest_exception_handling_dir)/testdata
spec_version_exception_handling := 13734f8fb871a5dab939070f893adbd90bffe28c
spectest_typed_function_references_dir := $(spectest_base_dir)/typed-function-references
spectest_typed_function_references_testdata_dir := $(spectest_typed_function_references_dir)/testdata
spec_version_typed_function_references := 74d2ec81d15efd3c0f2fba46a023f376101d8e46
typed_function_references_wast_files := \
br_on_non_null.wast br_on_null.wast br_table.wast call_ref.wast elem.wast \
func.wast linking.wast local_init.wast ref.wast ref_as_non_null.wast \
ref_func.wast ref_is_null.wast ref_null.wast return_call_indirect.wast \
return_call_ref.wast return_call.wast select.wast table-sub.wast table.wast \
type-equivalence.wast unreached-invalid.wast unreached-valid.wast
.PHONY: build.spectest
build.spectest:
@$(MAKE) build.spectest.v1
@@ -140,6 +150,7 @@ build.spectest:
@$(MAKE) build.spectest.tail_call
@$(MAKE) build.spectest.extended_const
@$(MAKE) build.spectest.exception_handling
@$(MAKE) build.spectest.typed_function_references
.PHONY: build.spectest.v1
build.spectest.v1: # Note: wabt by default uses >1.0 features, so wast2json flags might drift as they include more. See WebAssembly/wabt#1878
@@ -219,6 +230,18 @@ build.spectest.exception_handling:
wasm-tools json-from-wast --wasm-dir . -o $$(basename $$f .wast).json $$f || true; \
done
.PHONY: build.spectest.typed_function_references
build.spectest.typed_function_references:
@rm -rf $(spectest_typed_function_references_testdata_dir)
@mkdir -p $(spectest_typed_function_references_testdata_dir)
@cd $(spectest_typed_function_references_testdata_dir) \
&& for f in $(typed_function_references_wast_files); do \
curl -sJL "https://raw.githubusercontent.com/WebAssembly/function-references/$(spec_version_typed_function_references)/test/core/$$f" -O; \
done
@cd $(spectest_typed_function_references_testdata_dir) && for f in `find . -name '*.wast'`; do \
wasm-tools json-from-wast --wasm-dir . -o $$(basename $$f .wast).json $$f || true; \
done
.PHONY: test
test:
@go test $(go_test_options) ./...
+2
View File
@@ -221,6 +221,8 @@ func featureName(f CoreFeatures) string {
return "extended-const"
case CoreFeatureSIMD << 4: // experimental.CoreFeaturesExceptionHandling
return "exception-handling"
case CoreFeatureSIMD << 5: // experimental.CoreFeaturesTypedFunctionReferences
return "typed-function-references"
}
return ""
}
+5
View File
@@ -32,3 +32,8 @@ const CoreFeaturesExtendedConst = api.CoreFeatureSIMD << 3
//
// See https://github.com/WebAssembly/exception-handling for further details.
const CoreFeaturesExceptionHandling = api.CoreFeatureSIMD << 4
// CoreFeaturesTypedFunctionReferences enables typed function references.
//
// See https://github.com/WebAssembly/function-references for further details.
const CoreFeaturesTypedFunctionReferences = api.CoreFeatureSIMD << 5
+83 -2
View File
@@ -327,6 +327,7 @@ func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 in
funcTypeToSigs: funcTypeToIRSignatures{
indirectCalls: make([]*signature, len(types)),
directCalls: make([]*signature, len(types)),
callRefCalls: make([]*signature, len(types)),
wasmTypes: types,
},
needSourceOffset: module.DWARFLines != nil,
@@ -1715,7 +1716,18 @@ operatorSwitch:
newOperationRefFunc(index),
)
case wasm.OpcodeRefNull:
c.pc++ // Skip the type of reftype as every ref value is opaque pointer.
c.pc++
switch reftype := c.body[c.pc]; wasm.ValueType(reftype) {
case wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeExnref:
// Abstract ref types are a single byte; already skipped.
default:
// Concrete type index encoded as LEB128; skip it.
_, num, err := leb128.LoadUint32(c.body[c.pc:])
if err != nil {
return fmt.Errorf("failed to read type index for ref.null: %v", err)
}
c.pc += num - 1
}
c.emit(
newOperationConstI64(0),
)
@@ -3562,6 +3574,64 @@ operatorSwitch:
// and can be safely removed.
c.markUnreachable()
case wasm.OpcodeCallRef:
c.emit(newOperationCallRef(index))
case wasm.OpcodeReturnCallRef:
functionFrame := c.controlFrames.functionFrame()
dropRange := c.getFrameDropRange(functionFrame, false)
c.emit(newOperationReturnCallRef(index, dropRange, functionFrame.asLabel()))
c.markUnreachable()
case wasm.OpcodeRefAsNonNull:
c.emit(newOperationRefAsNonNull())
case wasm.OpcodeBrOnNull:
targetIndex, n, err := leb128.LoadUint32(c.body[c.pc+1:])
if err != nil {
return fmt.Errorf("read the target for br_on_null: %w", err)
}
c.pc += n
if c.unreachableState.on {
break operatorSwitch
}
targetFrame := c.controlFrames.get(int(targetIndex))
targetFrame.ensureContinuation()
drop := c.getFrameDropRange(targetFrame, false)
target := targetFrame.asLabel()
c.result.LabelCallers[target]++
continuationLabel := newLabel(labelKindHeader, c.nextFrameID())
c.result.LabelCallers[continuationLabel]++
c.emit(newOperationBrOnNull(target, continuationLabel, drop))
c.emit(newOperationLabel(continuationLabel))
// On fall-through (non-null), the ref is pushed back at runtime.
c.stackPush(unsignedTypeI64)
case wasm.OpcodeBrOnNonNull:
targetIndex, n, err := leb128.LoadUint32(c.body[c.pc+1:])
if err != nil {
return fmt.Errorf("read the target for br_on_non_null: %w", err)
}
c.pc += n
if c.unreachableState.on {
break operatorSwitch
}
targetFrame := c.controlFrames.get(int(targetIndex))
targetFrame.ensureContinuation()
drop := c.getFrameDropRange(targetFrame, false)
target := targetFrame.asLabel()
c.result.LabelCallers[target]++
continuationLabel := newLabel(labelKindHeader, c.nextFrameID())
c.result.LabelCallers[continuationLabel]++
c.emit(newOperationBrOnNonNull(target, continuationLabel, drop))
c.emit(newOperationLabel(continuationLabel))
default:
return fmt.Errorf("unsupported instruction in interpreterir: 0x%x", op)
}
@@ -3593,7 +3663,10 @@ func (c *compiler) applyToStack(opcode wasm.Opcode) (index uint32, err error) {
wasm.OpcodeTailCallReturnCall,
wasm.OpcodeTailCallReturnCallIndirect,
// exception handling - throw reads tag index
wasm.OpcodeThrow:
wasm.OpcodeThrow,
// typed function references
wasm.OpcodeCallRef,
wasm.OpcodeReturnCallRef:
// Assumes that we are at the opcode now so skip it before read immediates.
v, num, err := leb128.LoadUint32(c.body[c.pc+1:])
if err != nil {
@@ -3718,6 +3791,14 @@ func (c *compiler) emitDefaultValue(t wasm.ValueType) {
case wasm.ValueTypeV128:
c.stackPush(unsignedTypeV128)
c.emit(newOperationV128Const(0, 0))
default:
// Concrete ref types (ref $t) have variable bit patterns.
if t.IsRef() {
c.stackPush(unsignedTypeI64)
c.emit(newOperationConstI64(0))
} else {
panic(fmt.Sprintf("bug: unsupported value type for default value: 0x%x", t))
}
}
}
@@ -3716,3 +3716,11 @@ func TestCompiler_threads(t *testing.T) {
})
}
}
func TestEmitDefaultValue_PanicsOnUnknownType(t *testing.T) {
defer func() {
require.Contains(t, fmt.Sprintf("%v", recover()), "unsupported value type for default value")
}()
c := &compiler{}
c.emitDefaultValue(0xFF)
}
@@ -619,6 +619,14 @@ func (e *engine) lowerIR(ir *compilationResult, ret *compiledFunction) error {
}
case operationKindTailCallReturnCallIndirect:
e.setLabelAddress(&op.Us[1], label(op.Us[1]), labelAddressResolutions)
case operationKindBrOnNull:
e.setLabelAddress(&op.U1, label(op.U1), labelAddressResolutions)
e.setLabelAddress(&op.U2, label(op.U2), labelAddressResolutions)
case operationKindBrOnNonNull:
e.setLabelAddress(&op.U1, label(op.U1), labelAddressResolutions)
e.setLabelAddress(&op.U2, label(op.U2), labelAddressResolutions)
case operationKindReturnCallRef:
e.setLabelAddress(&op.Us[1], label(op.Us[1]), labelAddressResolutions)
}
}
@@ -4589,6 +4597,73 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance
ce.dropForTailCall(frame, tf)
body, bodyLen = ce.resetPc(frame, tf)
case operationKindCallRef:
ref := ce.popValue()
if ref == 0 {
panic(wasmruntime.ErrRuntimeNullReference)
}
tf := functionFromUintptr(uintptr(ref))
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf)
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
frame.pc++
case operationKindReturnCallRef:
ref := ce.popValue()
if ref == 0 {
panic(wasmruntime.ErrRuntimeNullReference)
}
tf := functionFromUintptr(uintptr(ref))
if tf.moduleInstance != f.moduleInstance {
frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf)
if frameUnwound {
frame = ce.frames[len(ce.frames)-1]
body = frame.f.parent.body
bodyLen = uint64(len(body))
continue
}
ce.drop(op.Us[0])
frame.pc = op.Us[1]
continue
}
ce.dropForTailCall(frame, tf)
body, bodyLen = ce.resetPc(frame, tf)
case operationKindRefAsNonNull:
ref := ce.popValue()
if ref == 0 {
panic(wasmruntime.ErrRuntimeNullReference)
}
ce.pushValue(ref)
frame.pc++
case operationKindBrOnNull:
ref := ce.popValue()
if ref == 0 {
ce.drop(op.U3)
frame.pc = op.U1
} else {
ce.pushValue(ref)
frame.pc = op.U2
}
case operationKindBrOnNonNull:
ref := ce.popValue()
if ref != 0 {
ce.drop(op.U3)
ce.pushValue(ref)
frame.pc = op.U1
} else {
frame.pc = op.U2
}
default:
frame.pc++
}
+75
View File
@@ -453,6 +453,16 @@ func (o operationKind) String() (ret string) {
ret = "operationKindThrow"
case operationKindThrowRef:
ret = "operationKindThrowRef"
case operationKindCallRef:
ret = "operationKindCallRef"
case operationKindReturnCallRef:
ret = "operationKindReturnCallRef"
case operationKindRefAsNonNull:
ret = "operationKindRefAsNonNull"
case operationKindBrOnNull:
ret = "operationKindBrOnNull"
case operationKindBrOnNonNull:
ret = "operationKindBrOnNonNull"
default:
panic(fmt.Errorf("unknown operation %d", o))
}
@@ -786,6 +796,17 @@ const (
// operationKindThrowRef is the Kind for throw_ref instruction.
operationKindThrowRef
// operationKindCallRef is the Kind for call_ref instruction.
operationKindCallRef
// operationKindReturnCallRef is the Kind for return_call_ref instruction.
operationKindReturnCallRef
// operationKindRefAsNonNull is the Kind for ref.as_non_null instruction.
operationKindRefAsNonNull
// operationKindBrOnNull is the Kind for br_on_null instruction.
operationKindBrOnNull
// operationKindBrOnNonNull is the Kind for br_on_non_null instruction.
operationKindBrOnNonNull
// operationKindEnd is always placed at the bottom of this iota definition to be used in the test.
operationKindEnd
)
@@ -1127,6 +1148,21 @@ func (o unionOperation) String() string {
case operationKindThrowRef:
return o.Kind.String()
case operationKindCallRef:
return fmt.Sprintf("%s %d", o.Kind, o.U1)
case operationKindReturnCallRef:
return fmt.Sprintf("%s %d", o.Kind, o.U1)
case operationKindRefAsNonNull:
return o.Kind.String()
case operationKindBrOnNull:
return fmt.Sprintf("%s %s %s", o.Kind, label(o.U1).String(), label(o.U2).String())
case operationKindBrOnNonNull:
return fmt.Sprintf("%s %s %s", o.Kind, label(o.U1).String(), label(o.U2).String())
default:
panic(fmt.Sprintf("TODO: %v", o.Kind))
}
@@ -2870,6 +2906,45 @@ func newOperationThrowRef() unionOperation {
return unionOperation{Kind: operationKindThrowRef}
}
// newOperationCallRef is a constructor for operationKindCallRef.
// U1 = type index.
func newOperationCallRef(typeIndex uint32) unionOperation {
return unionOperation{Kind: operationKindCallRef, U1: uint64(typeIndex)}
}
// newOperationReturnCallRef is a constructor for operationKindReturnCallRef.
// U1 = type index, U2 = table index (unused), Us = [dropDepth, label].
func newOperationReturnCallRef(typeIndex uint32, dropDepth inclusiveRange, l label) unionOperation {
return unionOperation{Kind: operationKindReturnCallRef, U1: uint64(typeIndex), Us: []uint64{dropDepth.AsU64(), uint64(l)}}
}
// newOperationRefAsNonNull is a constructor for operationKindRefAsNonNull.
func newOperationRefAsNonNull() unionOperation {
return unionOperation{Kind: operationKindRefAsNonNull}
}
// newOperationBrOnNull is a constructor for operationKindBrOnNull.
// If ref is null, branch to U1 (thenTarget) with drop U3; otherwise continue at U2 (elseTarget).
func newOperationBrOnNull(thenTarget, elseTarget label, thenDrop inclusiveRange) unionOperation {
return unionOperation{
Kind: operationKindBrOnNull,
U1: uint64(thenTarget),
U2: uint64(elseTarget),
U3: thenDrop.AsU64(),
}
}
// newOperationBrOnNonNull is a constructor for operationKindBrOnNonNull.
// If ref is non-null, branch to U1 (thenTarget) with drop U3; otherwise continue at U2 (elseTarget).
func newOperationBrOnNonNull(thenTarget, elseTarget label, thenDrop inclusiveRange) unionOperation {
return unionOperation{
Kind: operationKindBrOnNonNull,
U1: uint64(thenTarget),
U2: uint64(elseTarget),
U3: thenDrop.AsU64(),
}
}
// exceptionTableEntry represents one try_table's exception handling scope.
// Built at compile time and stored per compiledFunction.
type exceptionTableEntry struct {
+51
View File
@@ -279,6 +279,17 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
return c.funcTypeToSigs.get(c.funcs[index], false /* direct */), nil
case wasm.OpcodeCallIndirect, wasm.OpcodeTailCallReturnCallIndirect:
return c.funcTypeToSigs.get(index, true /* call_indirect */), nil
case wasm.OpcodeCallRef, wasm.OpcodeReturnCallRef:
return c.funcTypeToSigs.getCallRef(index), nil
case wasm.OpcodeRefAsNonNull:
// Pop a ref (i64), push it back (i64). Traps if null at runtime.
return signature_I64_I64, nil
case wasm.OpcodeBrOnNull:
// Pop a ref (i64). If null, branch; if non-null, push ref back.
return signature_I64_None, nil
case wasm.OpcodeBrOnNonNull:
// Pop a ref (i64). If non-null, push and branch; if null, fall through.
return signature_I64_None, nil
case wasm.OpcodeDrop:
return signature_Unknown_None, nil
case wasm.OpcodeSelect, wasm.OpcodeTypedSelect:
@@ -653,6 +664,7 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature
type funcTypeToIRSignatures struct {
directCalls []*signature
indirectCalls []*signature
callRefCalls []*signature
wasmTypes []wasm.FunctionType
}
@@ -697,6 +709,28 @@ func (f *funcTypeToIRSignatures) get(typeIndex wasm.Index, indirect bool) *signa
return sig
}
// getCallRef returns the *signature for call_ref, which is like a direct call
// but with an extra i64 input for the funcref operand.
func (f *funcTypeToIRSignatures) getCallRef(typeIndex wasm.Index) *signature {
if sig := f.callRefCalls[typeIndex]; sig != nil {
return sig
}
tp := &f.wasmTypes[typeIndex]
sig := &signature{
in: make([]unsignedType, 0, len(tp.Params)+1),
out: make([]unsignedType, 0, len(tp.Results)),
}
for _, vt := range tp.Params {
sig.in = append(sig.in, wasmValueTypeTounsignedType(vt))
}
sig.in = append(sig.in, unsignedTypeI64) // funcref operand
for _, vt := range tp.Results {
sig.out = append(sig.out, wasmValueTypeTounsignedType(vt))
}
f.callRefCalls[typeIndex] = sig
return sig
}
func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType {
switch vt {
case wasm.ValueTypeI32:
@@ -712,6 +746,11 @@ func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType {
return unsignedTypeF64
case wasm.ValueTypeV128:
return unsignedTypeV128
default:
// Concrete ref types (ref $t) have variable bit patterns.
if vt.IsRef() {
return unsignedTypeI64
}
}
panic("unreachable")
}
@@ -731,6 +770,10 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature {
return signature_None_F64
case wasm.ValueTypeV128:
return signature_None_V128
default:
if vt.IsRef() {
return signature_None_I64
}
}
panic("unreachable")
}
@@ -750,6 +793,10 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature {
return signature_F64_None
case wasm.ValueTypeV128:
return signature_V128_None
default:
if vt.IsRef() {
return signature_I64_None
}
}
panic("unreachable")
}
@@ -769,6 +816,10 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature {
return signature_F64_F64
case wasm.ValueTypeV128:
return signature_V128_V128
default:
if vt.IsRef() {
return signature_I64_I64
}
}
panic("unreachable")
}
@@ -0,0 +1,161 @@
package interpreter
import (
"testing"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
// TestCompile_LocalSetWithMultipleLocals_I32 tests that local.set depth
// calculations are correct with param + 2 locals (i32 baseline).
func TestCompile_LocalSetWithMultipleLocals_I32(t *testing.T) {
module := &wasm.Module{
TypeSection: []wasm.FunctionType{i32_i32},
FunctionSection: []wasm.Index{0},
CodeSection: []wasm.Code{{
LocalTypes: []wasm.ValueType{wasm.ValueTypeI32, wasm.ValueTypeI32},
Body: []byte{
wasm.OpcodeI32Const, 10,
wasm.OpcodeLocalSet, 1,
wasm.OpcodeI32Const, 20,
wasm.OpcodeLocalSet, 2,
wasm.OpcodeLocalGet, 1,
wasm.OpcodeLocalGet, 2,
wasm.OpcodeI32Add,
wasm.OpcodeEnd,
},
}},
}
for _, tp := range module.TypeSection {
tp.CacheNumInUint64()
}
c, err := newCompiler(api.CoreFeaturesV2, 0, module, false)
require.NoError(t, err)
result, err := c.Next()
require.NoError(t, err)
require.Equal(t, []unionOperation{
newOperationConstI32(0), // default local 1
newOperationConstI32(0), // default local 2
newOperationConstI32(10), // i32.const 10
newOperationSet(2, false), // local.set 1
newOperationConstI32(20), // i32.const 20
newOperationSet(1, false), // local.set 2
newOperationPick(1, false), // local.get 1
newOperationPick(1, false), // local.get 2
newOperationAdd(unsignedTypeI32), // i32.add
newOperationDrop(inclusiveRange{Start: 1, End: 3}), // drop locals+param, keep result
newOperationBr(newLabel(labelKindReturn, 0)), // return
}, result.Operations)
}
// TestCompile_LocalSetWithMultipleConcreteRefLocals tests that local.set depth
// calculations are correct with param + 2 concrete ref locals.
func TestCompile_LocalSetWithMultipleConcreteRefLocals(t *testing.T) {
concreteRefNullable := wasm.ValueTypeConcreteRef(0, true)
module := &wasm.Module{
TypeSection: []wasm.FunctionType{i32_i32},
FunctionSection: []wasm.Index{0, 0, 0},
CodeSection: []wasm.Code{
{
LocalTypes: []wasm.ValueType{concreteRefNullable, concreteRefNullable},
Body: []byte{
wasm.OpcodeRefFunc, 1,
wasm.OpcodeLocalSet, 1,
wasm.OpcodeRefFunc, 2,
wasm.OpcodeLocalSet, 2,
wasm.OpcodeLocalGet, 0,
wasm.OpcodeLocalGet, 1,
wasm.OpcodeCallRef, 0,
wasm.OpcodeLocalGet, 2,
wasm.OpcodeCallRef, 0,
wasm.OpcodeEnd,
},
},
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeI32Const, 42, wasm.OpcodeEnd}},
},
}
for i := range module.TypeSection {
module.TypeSection[i].CacheNumInUint64()
}
features := api.CoreFeaturesV2 | experimental.CoreFeaturesTypedFunctionReferences | experimental.CoreFeaturesTailCall
c, err := newCompiler(features, 0, module, false)
require.NoError(t, err)
result, err := c.Next()
require.NoError(t, err)
require.Equal(t, []unionOperation{
newOperationConstI64(0), // default local 1 (concrete ref)
newOperationConstI64(0), // default local 2 (concrete ref)
newOperationRefFunc(1), // ref.func 1
newOperationSet(2, false), // local.set 1
newOperationRefFunc(2), // ref.func 2
newOperationSet(1, false), // local.set 2
newOperationPick(2, false), // local.get 0 (param)
newOperationPick(2, false), // local.get 1
newOperationCallRef(0), // call_ref 0
newOperationPick(1, false), // local.get 2
newOperationCallRef(0), // call_ref 0
newOperationDrop(inclusiveRange{Start: 1, End: 3}), // drop locals+param, keep result
newOperationBr(newLabel(labelKindReturn, 0)), // return
}, result.Operations)
}
// TestCompile_LocalSetWithMultipleFuncrefLocals tests local.set with
// funcref locals (not concrete refs) as a comparison.
func TestCompile_LocalSetWithMultipleFuncrefLocals(t *testing.T) {
module := &wasm.Module{
TypeSection: []wasm.FunctionType{i32_i32},
FunctionSection: []wasm.Index{0, 0, 0},
CodeSection: []wasm.Code{
{
LocalTypes: []wasm.ValueType{wasm.ValueTypeFuncref, wasm.ValueTypeFuncref},
Body: []byte{
wasm.OpcodeRefFunc, 1,
wasm.OpcodeLocalSet, 1,
wasm.OpcodeRefFunc, 2,
wasm.OpcodeLocalSet, 2,
wasm.OpcodeLocalGet, 1,
wasm.OpcodeRefIsNull,
wasm.OpcodeLocalGet, 2,
wasm.OpcodeRefIsNull,
wasm.OpcodeI32Add,
wasm.OpcodeEnd,
},
},
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeI32Const, 42, wasm.OpcodeEnd}},
},
}
for i := range module.TypeSection {
module.TypeSection[i].CacheNumInUint64()
}
c, err := newCompiler(api.CoreFeaturesV2, 0, module, false)
require.NoError(t, err)
result, err := c.Next()
require.NoError(t, err)
require.Equal(t, []unionOperation{
newOperationConstI64(0), // default local 1 (funcref)
newOperationConstI64(0), // default local 2 (funcref)
newOperationRefFunc(1), // ref.func 1
newOperationSet(2, false), // local.set 1
newOperationRefFunc(2), // ref.func 2
newOperationSet(1, false), // local.set 2
newOperationPick(1, false), // local.get 1
newOperationEqz(unsignedInt64), // ref.is_null
newOperationPick(1, false), // local.get 2
newOperationEqz(unsignedInt64), // ref.is_null
newOperationAdd(unsignedTypeI32), // i32.add
newOperationDrop(inclusiveRange{Start: 1, End: 3}), // drop locals+param, keep result
newOperationBr(newLabel(labelKindReturn, 0)), // return
}, result.Operations)
}
+5 -17
View File
@@ -447,23 +447,7 @@ func (c *Compiler) declareNecessaryVariables() {
}
func (c *Compiler) declareWasmGlobal(typ wasm.ValueType, mutable bool) {
var st ssa.Type
switch typ {
case wasm.ValueTypeI32:
st = ssa.TypeI32
case wasm.ValueTypeI64,
// externref, funcref, and exnref are represented as I64 since we only support 64-bit platforms.
wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeExnref:
st = ssa.TypeI64
case wasm.ValueTypeF32:
st = ssa.TypeF32
case wasm.ValueTypeF64:
st = ssa.TypeF64
case wasm.ValueTypeV128:
st = ssa.TypeV128
default:
panic("TODO: " + wasm.ValueTypeName(typ))
}
st := WasmTypeToSSAType(typ)
v := c.ssaBuilder.DeclareVariable(st)
index := wasm.Index(len(c.globalVariables))
c.globalVariables = append(c.globalVariables, v)
@@ -490,6 +474,10 @@ func WasmTypeToSSAType(vt wasm.ValueType) ssa.Type {
case wasm.ValueTypeV128:
return ssa.TypeV128
default:
// Concrete ref types (ref $t) have variable bit patterns.
if vt.IsRef() {
return ssa.TypeI64
}
panic("TODO: " + wasm.ValueTypeName(vt))
}
}
+236 -1
View File
@@ -3380,7 +3380,12 @@ func (c *Compiler) lowerCurrentOpcode() {
state.push(refFuncRet)
case wasm.OpcodeRefNull:
c.loweringState.pc++ // skips the reference type as we treat both of them as i64(0).
switch reftype := c.wasmFunctionBody[c.loweringState.pc+1]; wasm.ValueType(reftype) {
case wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeExnref:
c.loweringState.pc++
default:
c.readI32u()
}
if state.unreachable {
break
}
@@ -3681,6 +3686,151 @@ func (c *Compiler) lowerCurrentOpcode() {
blockType: bt,
})
case wasm.OpcodeRefAsNonNull:
if state.unreachable {
break
}
r := state.pop()
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder)
checkNull := builder.AllocateInstruction().
AsIcmp(r, zero.Return(), ssa.IntegerCmpCondEqual).
Insert(builder).Return()
exitIfNull := builder.AllocateInstruction()
exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, checkNull, wazevoapi.ExitCodeNullReference)
builder.InsertInstruction(exitIfNull)
state.push(r)
case wasm.OpcodeBrOnNull:
labelIndex := c.readI32u()
if state.unreachable {
break
}
r := state.pop()
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder)
isNull := builder.AllocateInstruction().
AsIcmp(r, zero.Return(), ssa.IntegerCmpCondEqual).
Insert(builder).Return()
targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
args := c.nPeekDup(argNum)
var sealTargetBlk bool
if c.branchExitsTryTable(int(labelIndex)) {
current := builder.CurrentBlock()
trampolineBlk := builder.AllocateBasicBlock()
builder.SetCurrentBlock(trampolineBlk)
c.emitTryTableLeaves(int(labelIndex))
c.insertJumpToBlock(args, targetBlk)
builder.SetCurrentBlock(current)
targetBlk = trampolineBlk
sealTargetBlk = true
args = ssa.ValuesNil
}
if c.needListener && targetBlk.ReturnBlock() {
current := builder.CurrentBlock()
targetBlk = builder.AllocateBasicBlock()
builder.SetCurrentBlock(targetBlk)
sealTargetBlk = true
c.callListenerAfter()
instr := builder.AllocateInstruction()
instr.AsReturn(args)
builder.InsertInstruction(instr)
args = ssa.ValuesNil
builder.SetCurrentBlock(current)
}
brnz := builder.AllocateInstruction()
brnz.AsBrnz(isNull, args, targetBlk)
builder.InsertInstruction(brnz)
if sealTargetBlk {
builder.Seal(targetBlk)
}
// Fall-through: ref is non-null, push it back.
elseBlk := builder.AllocateBasicBlock()
c.insertJumpToBlock(ssa.ValuesNil, elseBlk)
builder.Seal(elseBlk)
builder.SetCurrentBlock(elseBlk)
state.push(r)
case wasm.OpcodeBrOnNonNull:
labelIndex := c.readI32u()
if state.unreachable {
break
}
r := state.pop()
zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder)
isNonNull := builder.AllocateInstruction().
AsIcmp(r, zero.Return(), ssa.IntegerCmpCondNotEqual).
Insert(builder).Return()
// When non-null, branch to label with args + the non-null ref.
targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
// The branch delivers argNum-1 values from the stack plus the ref.
// The ref is the last value delivered to the label target.
args := c.nPeekDup(argNum - 1)
args = args.Append(builder.VarLengthPool(), r)
var sealTargetBlk bool
if c.branchExitsTryTable(int(labelIndex)) {
current := builder.CurrentBlock()
trampolineBlk := builder.AllocateBasicBlock()
builder.SetCurrentBlock(trampolineBlk)
c.emitTryTableLeaves(int(labelIndex))
c.insertJumpToBlock(args, targetBlk)
builder.SetCurrentBlock(current)
targetBlk = trampolineBlk
sealTargetBlk = true
args = ssa.ValuesNil
}
if c.needListener && targetBlk.ReturnBlock() {
current := builder.CurrentBlock()
targetBlk = builder.AllocateBasicBlock()
builder.SetCurrentBlock(targetBlk)
sealTargetBlk = true
c.callListenerAfter()
instr := builder.AllocateInstruction()
instr.AsReturn(args)
builder.InsertInstruction(instr)
args = ssa.ValuesNil
builder.SetCurrentBlock(current)
}
brnz := builder.AllocateInstruction()
brnz.AsBrnz(isNonNull, args, targetBlk)
builder.InsertInstruction(brnz)
if sealTargetBlk {
builder.Seal(targetBlk)
}
// Fall-through: ref is null, nothing extra pushed.
elseBlk := builder.AllocateBasicBlock()
c.insertJumpToBlock(ssa.ValuesNil, elseBlk)
builder.Seal(elseBlk)
builder.SetCurrentBlock(elseBlk)
case wasm.OpcodeCallRef:
typeIndex := c.readI32u()
if state.unreachable {
break
}
c.lowerCallRef(typeIndex)
case wasm.OpcodeReturnCallRef:
typeIndex := c.readI32u()
if state.unreachable {
break
}
c.emitTryTableLeaves(len(c.state().controlFrames))
c.lowerTailCallReturnCallRef(typeIndex)
state.unreachable = true
default:
panic("TODO: unsupported in wazevo yet: " + wasm.InstructionName(op))
}
@@ -3985,6 +4135,91 @@ func (c *Compiler) lowerTailCallReturnCallIndirect(typeIndex, tableIndex uint32)
c.lowerReturn(builder)
}
func (c *Compiler) prepareCallRef(typeIndex uint32) (ssa.Value, *wasm.FunctionType, ssa.Values) {
builder := c.ssaBuilder
state := c.state()
functionInstancePtr := state.pop()
// Check if it is not the null pointer.
zero := builder.AllocateInstruction()
zero.AsIconst64(0)
builder.InsertInstruction(zero)
checkNull := builder.AllocateInstruction()
checkNull.AsIcmp(functionInstancePtr, zero.Return(), ssa.IntegerCmpCondEqual)
builder.InsertInstruction(checkNull)
exitIfNull := builder.AllocateInstruction()
exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, checkNull.Return(), wazevoapi.ExitCodeNullReference)
builder.InsertInstruction(exitIfNull)
// Load the executable and moduleContextOpaquePtr from the function instance.
loadExecutablePtr := builder.AllocateInstruction()
loadExecutablePtr.AsLoad(functionInstancePtr, wazevoapi.FunctionInstanceExecutableOffset, ssa.TypeI64)
builder.InsertInstruction(loadExecutablePtr)
executablePtr := loadExecutablePtr.Return()
loadModuleContextOpaquePtr := builder.AllocateInstruction()
loadModuleContextOpaquePtr.AsLoad(functionInstancePtr, wazevoapi.FunctionInstanceModuleContextOpaquePtrOffset, ssa.TypeI64)
builder.InsertInstruction(loadModuleContextOpaquePtr)
moduleContextOpaquePtr := loadModuleContextOpaquePtr.Return()
typ := &c.m.TypeSection[typeIndex]
tail := len(state.values) - len(typ.Params)
vs := state.values[tail:]
state.values = state.values[:tail]
args := c.allocateVarLengthValues(2+len(vs), c.execCtxPtrValue, moduleContextOpaquePtr)
args = args.Append(builder.VarLengthPool(), vs...)
c.storeCallerModuleContext()
return executablePtr, typ, args
}
func (c *Compiler) lowerCallRef(typeIndex uint32) {
builder := c.ssaBuilder
state := c.state()
executablePtr, typ, args := c.prepareCallRef(typeIndex)
call := builder.AllocateInstruction()
call.AsCallIndirect(executablePtr, c.signatures[typ], args)
builder.InsertInstruction(call)
first, rest := call.Returns()
if first.Valid() {
state.push(first)
}
for _, v := range rest {
state.push(v)
}
c.reloadAfterCall()
}
func (c *Compiler) lowerTailCallReturnCallRef(typeIndex uint32) {
builder := c.ssaBuilder
state := c.state()
executablePtr, typ, args := c.prepareCallRef(typeIndex)
call := builder.AllocateInstruction()
call.AsTailCallReturnCallIndirect(executablePtr, c.signatures[typ], args)
builder.InsertInstruction(call)
// In a proper tail call, the following code is unreachable since execution
// transfers to the callee. However, sometimes the backend might need to fall back to
// a regular call, so we include return handling and let the backend delete it
// when redundant.
// For details, see internal/engine/RATIONALE.md
first, rest := call.Returns()
if first.Valid() {
state.push(first)
}
for _, v := range rest {
state.push(v)
}
c.reloadAfterCall()
c.lowerReturn(builder)
}
// memOpSetup inserts the bounds check and calculates the address of the memory operation (loads/stores).
func (c *Compiler) memOpSetup(baseAddr ssa.Value, constOffset, operationSizeInBytes uint64) (address ssa.Value) {
address = ssa.ValueInvalid
@@ -0,0 +1,256 @@
package adhoc
import (
"context"
"os"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasmruntime"
)
func typedFuncRefsConfigs() []struct {
name string
config wazero.RuntimeConfig
} {
configs := []struct {
name string
config wazero.RuntimeConfig
}{
{"interpreter", wazero.NewRuntimeConfigInterpreter().WithCoreFeatures(
api.CoreFeaturesV2 | experimental.CoreFeaturesTypedFunctionReferences,
)},
}
if platform.CompilerSupported() {
configs = append(configs, struct {
name string
config wazero.RuntimeConfig
}{"compiler", wazero.NewRuntimeConfigCompiler().WithCoreFeatures(
api.CoreFeaturesV2 | experimental.CoreFeaturesTypedFunctionReferences,
)})
}
return configs
}
// TestCallRefWithConcreteRefLocals reproduces the call_ref.wast "run" function
// which uses (local (ref null $ii)) concrete ref locals, sets them with ref.func,
// and calls through them with call_ref.
func TestCallRefWithConcreteRefLocals(t *testing.T) {
buf, err := os.ReadFile("../spectest/typed-function-references/testdata/call_ref.0.wasm")
if err != nil {
t.Skipf("could not read call_ref.0.wasm: %v", err)
}
configs := []struct {
name string
config wazero.RuntimeConfig
}{
{"interpreter", wazero.NewRuntimeConfigInterpreter().WithCoreFeatures(
api.CoreFeaturesV2 | experimental.CoreFeaturesTypedFunctionReferences | experimental.CoreFeaturesTailCall,
)},
}
if platform.CompilerSupported() {
configs = append(configs, struct {
name string
config wazero.RuntimeConfig
}{"compiler", wazero.NewRuntimeConfigCompiler().WithCoreFeatures(
api.CoreFeaturesV2 | experimental.CoreFeaturesTypedFunctionReferences | experimental.CoreFeaturesTailCall,
)})
}
for _, tc := range configs {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
r := wazero.NewRuntimeWithConfig(ctx, tc.config)
defer r.Close(ctx)
inst, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
require.NoError(t, err)
fn := inst.ExportedFunction("run")
require.NotNil(t, fn)
results, err := fn.Call(ctx, 0)
require.NoError(t, err)
require.Equal(t, uint64(0), results[0])
results, err = fn.Call(ctx, 3)
require.NoError(t, err)
expected := uint64(0xfffffff7) // -9 as i32
require.Equal(t, expected, results[0])
})
}
}
// TestCallRefTrapOnNull verifies that call_ref on a null reference traps
// with ErrRuntimeNullReference.
func TestCallRefTrapOnNull(t *testing.T) {
// (module
// (type $t (func (result i32)))
// (func (export "test") (result i32)
// ref.null $t
// call_ref $t
// )
// )
mod := &wasm.Module{
TypeSection: []wasm.FunctionType{
{Results: []wasm.ValueType{wasm.ValueTypeI32}, ResultNumInUint64: 1},
},
FunctionSection: []wasm.Index{0},
CodeSection: []wasm.Code{
{Body: []byte{
wasm.OpcodeRefNull, 0x00, // ref.null type_index=0
wasm.OpcodeCallRef, 0x00, // call_ref type_index=0
wasm.OpcodeEnd,
}},
},
ExportSection: []wasm.Export{{Name: "test", Type: wasm.ExternTypeFunc, Index: 0}},
}
for _, tc := range typedFuncRefsConfigs() {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
r := wazero.NewRuntimeWithConfig(ctx, tc.config)
defer r.Close(ctx)
buf := binaryencoding.EncodeModule(mod)
inst, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
require.NoError(t, err)
_, err = inst.ExportedFunction("test").Call(ctx)
require.ErrorIs(t, err, wasmruntime.ErrRuntimeNullReference)
})
}
}
// TestRefAsNonNullTrapOnNull verifies that ref.as_non_null on a null funcref
// traps with ErrRuntimeNullReference.
func TestRefAsNonNullTrapOnNull(t *testing.T) {
// (module
// (type $t (func))
// (func (export "test")
// ref.null func
// ref.as_non_null
// drop
// )
// )
mod := &wasm.Module{
TypeSection: []wasm.FunctionType{{}},
FunctionSection: []wasm.Index{0},
CodeSection: []wasm.Code{
{Body: []byte{
wasm.OpcodeRefNull, wasm.ValueTypeFuncref.Kind(), // ref.null func
wasm.OpcodeRefAsNonNull, // trap if null
wasm.OpcodeDrop,
wasm.OpcodeEnd,
}},
},
ExportSection: []wasm.Export{{Name: "test", Type: wasm.ExternTypeFunc, Index: 0}},
}
for _, tc := range typedFuncRefsConfigs() {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
r := wazero.NewRuntimeWithConfig(ctx, tc.config)
defer r.Close(ctx)
buf := binaryencoding.EncodeModule(mod)
inst, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
require.NoError(t, err)
_, err = inst.ExportedFunction("test").Call(ctx)
require.ErrorIs(t, err, wasmruntime.ErrRuntimeNullReference)
})
}
}
// TestBrOnNull verifies that br_on_null branches when the ref is null
// and falls through (pushing the ref) when non-null.
func TestBrOnNull(t *testing.T) {
buf, err := os.ReadFile("../spectest/typed-function-references/testdata/br_on_null.0.wasm")
if err != nil {
t.Skipf("could not read br_on_null.0.wasm: %v", err)
}
for _, tc := range typedFuncRefsConfigs() {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
r := wazero.NewRuntimeWithConfig(ctx, tc.config)
defer r.Close(ctx)
inst, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
require.NoError(t, err)
// null ref → branches, returns -1
fn := inst.ExportedFunction("nullable-null")
require.NotNil(t, fn)
results, err := fn.Call(ctx)
require.NoError(t, err)
require.Equal(t, uint64(0xffffffff), results[0]) // -1 as i32
// non-null ref → falls through, calls $f, returns 7
fn = inst.ExportedFunction("nullable-f")
require.NotNil(t, fn)
results, err = fn.Call(ctx)
require.NoError(t, err)
require.Equal(t, uint64(7), results[0])
})
}
}
// TestBrOnNonNull verifies that br_on_non_null branches (carrying the ref)
// when the ref is non-null, and falls through when null.
func TestBrOnNonNull(t *testing.T) {
// (module
// (type $t (func (result i32)))
// (func $f (type $t) (i32.const 42))
// (func (export "null") (result i32)
// (block $l (result i32)
// ref.null 0
// br_on_non_null $l ;; null → fall through (no ref pushed)
// i32.const 1
// )
// )
// ... see below for "non_null"
// )
// For br_on_non_null targeting a block that expects (ref $t) on branch,
// we need the block's type signature to accept the ref. Using a type index
// for the block type is complex in binary encoding.
// Instead, test via a pre-compiled spec wasm.
buf, err := os.ReadFile("../spectest/typed-function-references/testdata/br_on_non_null.0.wasm")
if err != nil {
t.Skipf("could not read br_on_non_null.0.wasm: %v", err)
}
for _, tc := range typedFuncRefsConfigs() {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
r := wazero.NewRuntimeWithConfig(ctx, tc.config)
defer r.Close(ctx)
inst, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
require.NoError(t, err)
// null ref → falls through, returns -1
fn := inst.ExportedFunction("nullable-null")
require.NotNil(t, fn)
results, err := fn.Call(ctx)
require.NoError(t, err)
require.Equal(t, uint64(0xffffffff), results[0]) // -1 as i32
// non-null ref → branches carrying ref, calls $f, returns 7
fn = inst.ExportedFunction("nullable-f")
require.NotNil(t, fn)
results, err = fn.Call(ctx)
require.NoError(t, err)
require.Equal(t, uint64(7), results[0])
})
}
}
@@ -16,7 +16,7 @@ import (
//go:embed testdata
var testcases embed.FS
const enabledFeatures = api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesTailCall
const enabledFeatures = api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesTailCall | experimental.CoreFeaturesTypedFunctionReferences
func TestCompiler(t *testing.T) {
if !platform.CompilerSupported() {
@@ -37,11 +37,5 @@ func runCases(t *testing.T, ctx context.Context, config wazero.RuntimeConfig) {
spectest.RunCase(t, testcases, "throw", ctx, config, -1, 0, math.MaxInt)
spectest.RunCase(t, testcases, "throw_ref", ctx, config, -1, 0, math.MaxInt)
spectest.RunCase(t, testcases, "tag", ctx, config, -1, 0, math.MaxInt)
// Run try_table.wast in two ranges, skipping lines 470-495
// (two assert_invalid blocks for try_table type validation):
// we desugar non-nullable ref types to nullable, so we cannot
// detect the type mismatch between (ref null $t) and (ref $t).
spectest.RunCase(t, testcases, "try_table", ctx, config, -1, 0, 470)
spectest.RunCase(t, testcases, "try_table", ctx, config, -1, 496, math.MaxInt)
spectest.RunCase(t, testcases, "try_table", ctx, config, -1, 0, math.MaxInt)
}
@@ -323,6 +323,8 @@ func (c command) expectedError() (err error) {
err = wasmruntime.ErrRuntimeUnreachable
case "uncaught exception":
err = wasmruntime.ErrRuntimeUncaughtException
case "null", "null reference", "null function reference", "null function":
err = wasmruntime.ErrRuntimeNullReference
default:
if strings.HasPrefix(c.Text, "uninitialized") {
err = wasmruntime.ErrRuntimeInvalidTableAccess
@@ -0,0 +1,30 @@
package spectest
import (
"context"
"embed"
"testing"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/internal/integration_test/spectest"
"github.com/tetratelabs/wazero/internal/platform"
)
//go:embed testdata/*.wasm
//go:embed testdata/*.json
var testcases embed.FS
const enabledFeatures = api.CoreFeaturesV2 | experimental.CoreFeaturesTypedFunctionReferences | experimental.CoreFeaturesTailCall
func TestCompiler(t *testing.T) {
if !platform.CompilerSupported() {
t.Skip()
}
spectest.Run(t, testcases, context.Background(), wazero.NewRuntimeConfigCompiler().WithCoreFeatures(enabledFeatures))
}
func TestInterpreter(t *testing.T) {
spectest.Run(t, testcases, context.Background(), wazero.NewRuntimeConfigInterpreter().WithCoreFeatures(enabledFeatures))
}
@@ -0,0 +1 @@
{"source_filename":"./br_on_non_null.wast","commands":[{"type":"module","line":1,"filename":"br_on_non_null.0.wasm","module_type":"binary"},{"type":"assert_trap","line":37,"action":{"type":"invoke","field":"unreachable","args":[]},"text":"unreachable"},{"type":"assert_return","line":39,"action":{"type":"invoke","field":"nullable-null","args":[]},"expected":[{"type":"i32","value":"-1"}]},{"type":"assert_return","line":40,"action":{"type":"invoke","field":"nonnullable-f","args":[]},"expected":[{"type":"i32","value":"7"}]},{"type":"assert_return","line":41,"action":{"type":"invoke","field":"nullable-f","args":[]},"expected":[{"type":"i32","value":"7"}]},{"type":"module","line":43,"filename":"br_on_non_null.1.wasm","module_type":"binary"},{"type":"module","line":51,"filename":"br_on_non_null.2.wasm","module_type":"binary"},{"type":"assert_return","line":72,"action":{"type":"invoke","field":"args-null","args":[{"type":"i32","value":"3"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_return","line":73,"action":{"type":"invoke","field":"args-f","args":[{"type":"i32","value":"3"}]},"expected":[{"type":"i32","value":"9"}]}]}
@@ -0,0 +1,73 @@
(module
(type $t (func (result i32)))
(func $nn (param $r (ref $t)) (result i32)
(call_ref $t
(block $l (result (ref $t))
(br_on_non_null $l (local.get $r))
(return (i32.const -1))
)
)
)
(func $n (param $r (ref null $t)) (result i32)
(call_ref $t
(block $l (result (ref $t))
(br_on_non_null $l (local.get $r))
(return (i32.const -1))
)
)
)
(elem func $f)
(func $f (result i32) (i32.const 7))
(func (export "nullable-null") (result i32) (call $n (ref.null $t)))
(func (export "nonnullable-f") (result i32) (call $nn (ref.func $f)))
(func (export "nullable-f") (result i32) (call $n (ref.func $f)))
(func (export "unreachable") (result i32)
(block $l (result (ref $t))
(br_on_non_null $l (unreachable))
(return (i32.const -1))
)
(call_ref $t)
)
)
(assert_trap (invoke "unreachable") "unreachable")
(assert_return (invoke "nullable-null") (i32.const -1))
(assert_return (invoke "nonnullable-f") (i32.const 7))
(assert_return (invoke "nullable-f") (i32.const 7))
(module
(type $t (func))
(func (param $r (ref null $t)) (drop (block (result (ref $t)) (br_on_non_null 0 (local.get $r)) (unreachable))))
(func (param $r (ref null func)) (drop (block (result (ref func)) (br_on_non_null 0 (local.get $r)) (unreachable))))
(func (param $r (ref null extern)) (drop (block (result (ref extern)) (br_on_non_null 0 (local.get $r)) (unreachable))))
)
(module
(type $t (func (param i32) (result i32)))
(elem func $f)
(func $f (param i32) (result i32) (i32.mul (local.get 0) (local.get 0)))
(func $a (param $n i32) (param $r (ref null $t)) (result i32)
(call_ref $t
(block $l (result i32 (ref $t))
(return (br_on_non_null $l (local.get $n) (local.get $r)))
)
)
)
(func (export "args-null") (param $n i32) (result i32)
(call $a (local.get $n) (ref.null $t))
)
(func (export "args-f") (param $n i32) (result i32)
(call $a (local.get $n) (ref.func $f))
)
)
(assert_return (invoke "args-null" (i32.const 3)) (i32.const 3))
(assert_return (invoke "args-f" (i32.const 3)) (i32.const 9))
@@ -0,0 +1 @@
{"source_filename":"./br_on_null.wast","commands":[{"type":"module","line":1,"filename":"br_on_null.0.wasm","module_type":"binary"},{"type":"assert_trap","line":32,"action":{"type":"invoke","field":"unreachable","args":[]},"text":"unreachable"},{"type":"assert_return","line":34,"action":{"type":"invoke","field":"nullable-null","args":[]},"expected":[{"type":"i32","value":"-1"}]},{"type":"assert_return","line":35,"action":{"type":"invoke","field":"nonnullable-f","args":[]},"expected":[{"type":"i32","value":"7"}]},{"type":"assert_return","line":36,"action":{"type":"invoke","field":"nullable-f","args":[]},"expected":[{"type":"i32","value":"7"}]},{"type":"module","line":38,"filename":"br_on_null.1.wasm","module_type":"binary"},{"type":"module","line":46,"filename":"br_on_null.2.wasm","module_type":"binary"},{"type":"assert_return","line":65,"action":{"type":"invoke","field":"args-null","args":[{"type":"i32","value":"3"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_return","line":66,"action":{"type":"invoke","field":"args-f","args":[{"type":"i32","value":"3"}]},"expected":[{"type":"i32","value":"9"}]}]}
@@ -0,0 +1,66 @@
(module
(type $t (func (result i32)))
(func $nn (param $r (ref $t)) (result i32)
(block $l
(return (call_ref $t (br_on_null $l (local.get $r))))
)
(i32.const -1)
)
(func $n (param $r (ref null $t)) (result i32)
(block $l
(return (call_ref $t (br_on_null $l (local.get $r))))
)
(i32.const -1)
)
(elem func $f)
(func $f (result i32) (i32.const 7))
(func (export "nullable-null") (result i32) (call $n (ref.null $t)))
(func (export "nonnullable-f") (result i32) (call $nn (ref.func $f)))
(func (export "nullable-f") (result i32) (call $n (ref.func $f)))
(func (export "unreachable") (result i32)
(block $l
(return (call_ref $t (br_on_null $l (unreachable))))
)
(i32.const -1)
)
)
(assert_trap (invoke "unreachable") "unreachable")
(assert_return (invoke "nullable-null") (i32.const -1))
(assert_return (invoke "nonnullable-f") (i32.const 7))
(assert_return (invoke "nullable-f") (i32.const 7))
(module
(type $t (func))
(func (param $r (ref null $t)) (drop (br_on_null 0 (local.get $r))))
(func (param $r (ref null func)) (drop (br_on_null 0 (local.get $r))))
(func (param $r (ref null extern)) (drop (br_on_null 0 (local.get $r))))
)
(module
(type $t (func (param i32) (result i32)))
(elem func $f)
(func $f (param i32) (result i32) (i32.mul (local.get 0) (local.get 0)))
(func $a (param $n i32) (param $r (ref null $t)) (result i32)
(block $l (result i32)
(return (call_ref $t (br_on_null $l (local.get $n) (local.get $r))))
)
)
(func (export "args-null") (param $n i32) (result i32)
(call $a (local.get $n) (ref.null $t))
)
(func (export "args-f") (param $n i32) (result i32)
(call $a (local.get $n) (ref.func $f))
)
)
(assert_return (invoke "args-null" (i32.const 3)) (i32.const 3))
(assert_return (invoke "args-f" (i32.const 3)) (i32.const 9))
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
{"source_filename":"./call_ref.wast","commands":[{"type":"module","line":1,"filename":"call_ref.0.wasm","module_type":"binary"},{"type":"assert_return","line":94,"action":{"type":"invoke","field":"run","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":95,"action":{"type":"invoke","field":"run","args":[{"type":"i32","value":"3"}]},"expected":[{"type":"i32","value":"-9"}]},{"type":"assert_trap","line":97,"action":{"type":"invoke","field":"null","args":[]},"text":"null function reference"},{"type":"assert_return","line":99,"action":{"type":"invoke","field":"fac","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"1"}]},{"type":"assert_return","line":100,"action":{"type":"invoke","field":"fac","args":[{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"1"}]},{"type":"assert_return","line":101,"action":{"type":"invoke","field":"fac","args":[{"type":"i64","value":"5"}]},"expected":[{"type":"i64","value":"120"}]},{"type":"assert_return","line":102,"action":{"type":"invoke","field":"fac","args":[{"type":"i64","value":"25"}]},"expected":[{"type":"i64","value":"7034535277573963776"}]},{"type":"assert_return","line":103,"action":{"type":"invoke","field":"fac-acc","args":[{"type":"i64","value":"0"},{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"1"}]},{"type":"assert_return","line":104,"action":{"type":"invoke","field":"fac-acc","args":[{"type":"i64","value":"1"},{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"1"}]},{"type":"assert_return","line":105,"action":{"type":"invoke","field":"fac-acc","args":[{"type":"i64","value":"5"},{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"120"}]},{"type":"assert_return","line":107,"action":{"type":"invoke","field":"fac-acc","args":[{"type":"i64","value":"25"},{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"7034535277573963776"}]},{"type":"assert_return","line":111,"action":{"type":"invoke","field":"fib","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"1"}]},{"type":"assert_return","line":112,"action":{"type":"invoke","field":"fib","args":[{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"1"}]},{"type":"assert_return","line":113,"action":{"type":"invoke","field":"fib","args":[{"type":"i64","value":"2"}]},"expected":[{"type":"i64","value":"2"}]},{"type":"assert_return","line":114,"action":{"type":"invoke","field":"fib","args":[{"type":"i64","value":"5"}]},"expected":[{"type":"i64","value":"8"}]},{"type":"assert_return","line":115,"action":{"type":"invoke","field":"fib","args":[{"type":"i64","value":"20"}]},"expected":[{"type":"i64","value":"10946"}]},{"type":"assert_return","line":117,"action":{"type":"invoke","field":"even","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"44"}]},{"type":"assert_return","line":118,"action":{"type":"invoke","field":"even","args":[{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"99"}]},{"type":"assert_return","line":119,"action":{"type":"invoke","field":"even","args":[{"type":"i64","value":"100"}]},"expected":[{"type":"i64","value":"44"}]},{"type":"assert_return","line":120,"action":{"type":"invoke","field":"even","args":[{"type":"i64","value":"77"}]},"expected":[{"type":"i64","value":"99"}]},{"type":"assert_return","line":121,"action":{"type":"invoke","field":"odd","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"99"}]},{"type":"assert_return","line":122,"action":{"type":"invoke","field":"odd","args":[{"type":"i64","value":"1"}]},"expected":[{"type":"i64","value":"44"}]},{"type":"assert_return","line":123,"action":{"type":"invoke","field":"odd","args":[{"type":"i64","value":"200"}]},"expected":[{"type":"i64","value":"99"}]},{"type":"assert_return","line":124,"action":{"type":"invoke","field":"odd","args":[{"type":"i64","value":"77"}]},"expected":[{"type":"i64","value":"44"}]},{"type":"module","line":129,"filename":"call_ref.1.wasm","module_type":"binary"},{"type":"assert_trap","line":136,"action":{"type":"invoke","field":"unreachable","args":[]},"text":"unreachable"},{"type":"module","line":138,"filename":"call_ref.2.wasm","module_type":"binary"},{"type":"assert_trap","line":149,"action":{"type":"invoke","field":"unreachable","args":[]},"text":"unreachable"},{"type":"module","line":151,"filename":"call_ref.3.wasm","module_type":"binary"},{"type":"assert_trap","line":165,"action":{"type":"invoke","field":"unreachable","args":[]},"text":"unreachable"},{"type":"assert_invalid","line":168,"filename":"call_ref.4.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":184,"filename":"call_ref.5.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":201,"filename":"call_ref.6.wasm","module_type":"binary","text":"type mismatch"}]}
@@ -0,0 +1,208 @@
(module
(type $ii (func (param i32) (result i32)))
(func $apply (param $f (ref $ii)) (param $x i32) (result i32)
(call_ref $ii (local.get $x) (local.get $f))
)
(func $f (type $ii) (i32.mul (local.get 0) (local.get 0)))
(func $g (type $ii) (i32.sub (i32.const 0) (local.get 0)))
(elem declare func $f $g)
(func (export "run") (param $x i32) (result i32)
(local $rf (ref null $ii))
(local $rg (ref null $ii))
(local.set $rf (ref.func $f))
(local.set $rg (ref.func $g))
(call_ref $ii (call_ref $ii (local.get $x) (local.get $rf)) (local.get $rg))
)
(func (export "null") (result i32)
(call_ref $ii (i32.const 1) (ref.null $ii))
)
;; Recursion
(type $ll (func (param i64) (result i64)))
(type $lll (func (param i64 i64) (result i64)))
(elem declare func $fac)
(global $fac (ref $ll) (ref.func $fac))
(func $fac (export "fac") (type $ll)
(if (result i64) (i64.eqz (local.get 0))
(then (i64.const 1))
(else
(i64.mul
(local.get 0)
(call_ref $ll (i64.sub (local.get 0) (i64.const 1)) (global.get $fac))
)
)
)
)
(elem declare func $fac-acc)
(global $fac-acc (ref $lll) (ref.func $fac-acc))
(func $fac-acc (export "fac-acc") (type $lll)
(if (result i64) (i64.eqz (local.get 0))
(then (local.get 1))
(else
(call_ref $lll
(i64.sub (local.get 0) (i64.const 1))
(i64.mul (local.get 0) (local.get 1))
(global.get $fac-acc)
)
)
)
)
(elem declare func $fib)
(global $fib (ref $ll) (ref.func $fib))
(func $fib (export "fib") (type $ll)
(if (result i64) (i64.le_u (local.get 0) (i64.const 1))
(then (i64.const 1))
(else
(i64.add
(call_ref $ll (i64.sub (local.get 0) (i64.const 2)) (global.get $fib))
(call_ref $ll (i64.sub (local.get 0) (i64.const 1)) (global.get $fib))
)
)
)
)
(elem declare func $even $odd)
(global $even (ref $ll) (ref.func $even))
(global $odd (ref $ll) (ref.func $odd))
(func $even (export "even") (type $ll)
(if (result i64) (i64.eqz (local.get 0))
(then (i64.const 44))
(else (call_ref $ll (i64.sub (local.get 0) (i64.const 1)) (global.get $odd)))
)
)
(func $odd (export "odd") (type $ll)
(if (result i64) (i64.eqz (local.get 0))
(then (i64.const 99))
(else (call_ref $ll (i64.sub (local.get 0) (i64.const 1)) (global.get $even)))
)
)
)
(assert_return (invoke "run" (i32.const 0)) (i32.const 0))
(assert_return (invoke "run" (i32.const 3)) (i32.const -9))
(assert_trap (invoke "null") "null function reference")
(assert_return (invoke "fac" (i64.const 0)) (i64.const 1))
(assert_return (invoke "fac" (i64.const 1)) (i64.const 1))
(assert_return (invoke "fac" (i64.const 5)) (i64.const 120))
(assert_return (invoke "fac" (i64.const 25)) (i64.const 7034535277573963776))
(assert_return (invoke "fac-acc" (i64.const 0) (i64.const 1)) (i64.const 1))
(assert_return (invoke "fac-acc" (i64.const 1) (i64.const 1)) (i64.const 1))
(assert_return (invoke "fac-acc" (i64.const 5) (i64.const 1)) (i64.const 120))
(assert_return
(invoke "fac-acc" (i64.const 25) (i64.const 1))
(i64.const 7034535277573963776)
)
(assert_return (invoke "fib" (i64.const 0)) (i64.const 1))
(assert_return (invoke "fib" (i64.const 1)) (i64.const 1))
(assert_return (invoke "fib" (i64.const 2)) (i64.const 2))
(assert_return (invoke "fib" (i64.const 5)) (i64.const 8))
(assert_return (invoke "fib" (i64.const 20)) (i64.const 10946))
(assert_return (invoke "even" (i64.const 0)) (i64.const 44))
(assert_return (invoke "even" (i64.const 1)) (i64.const 99))
(assert_return (invoke "even" (i64.const 100)) (i64.const 44))
(assert_return (invoke "even" (i64.const 77)) (i64.const 99))
(assert_return (invoke "odd" (i64.const 0)) (i64.const 99))
(assert_return (invoke "odd" (i64.const 1)) (i64.const 44))
(assert_return (invoke "odd" (i64.const 200)) (i64.const 99))
(assert_return (invoke "odd" (i64.const 77)) (i64.const 44))
;; Unreachable typing.
(module
(type $t (func))
(func (export "unreachable") (result i32)
(unreachable)
(call_ref $t)
)
)
(assert_trap (invoke "unreachable") "unreachable")
(module
(elem declare func $f)
(type $t (func (param i32) (result i32)))
(func $f (param i32) (result i32) (local.get 0))
(func (export "unreachable") (result i32)
(unreachable)
(ref.func $f)
(call_ref $t)
)
)
(assert_trap (invoke "unreachable") "unreachable")
(module
(elem declare func $f)
(type $t (func (param i32) (result i32)))
(func $f (param i32) (result i32) (local.get 0))
(func (export "unreachable") (result i32)
(unreachable)
(i32.const 0)
(ref.func $f)
(call_ref $t)
(drop)
(i32.const 0)
)
)
(assert_trap (invoke "unreachable") "unreachable")
(assert_invalid
(module
(elem declare func $f)
(type $t (func (param i32) (result i32)))
(func $f (param i32) (result i32) (local.get 0))
(func (export "unreachable") (result i32)
(unreachable)
(i64.const 0)
(ref.func $f)
(call_ref $t)
)
)
"type mismatch"
)
(assert_invalid
(module
(elem declare func $f)
(type $t (func (param i32) (result i32)))
(func $f (param i32) (result i32) (local.get 0))
(func (export "unreachable") (result i32)
(unreachable)
(ref.func $f)
(call_ref $t)
(drop)
(i64.const 0)
)
)
"type mismatch"
)
(assert_invalid
(module
(type $t (func))
(func $f (param $r externref)
(call_ref $t (local.get $r))
)
)
"type mismatch"
)

Some files were not shown because too many files have changed in this diff Show More