mirror of
https://github.com/wazero/wazero
synced 2026-06-21 14:12:37 +00:00
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:
@@ -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) ./...
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+1
@@ -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"}]}]}
|
||||
+73
@@ -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))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -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"}]}]}
|
||||
+66
@@ -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))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
File diff suppressed because one or more lines are too long
+1465
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -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"}]}
|
||||
+208
@@ -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"
|
||||
)
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user