Supports functions with multiple results (multi-value) (#446)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Crypt Keeper
2022-04-13 09:22:39 +08:00
committed by GitHub
parent 927914ffb0
commit c3ff16d596
68 changed files with 5555 additions and 1155 deletions

View File

@@ -248,6 +248,16 @@ type Memory interface {
Write(offset uint32, v []byte) bool
}
// EncodeI32 encodes the input as a ValueTypeI32.
func EncodeI32(input int32) uint64 {
return uint64(uint32(input))
}
// EncodeI64 encodes the input as a ValueTypeI64.
func EncodeI64(input int64) uint64 {
return uint64(input)
}
// EncodeF32 encodes the input as a ValueTypeF32.
// See DecodeF32
func EncodeF32(input float32) uint64 {

View File

@@ -41,6 +41,7 @@ func TestEncodeDecodeF32(t *testing.T) {
t.Run(fmt.Sprintf("%f", v), func(t *testing.T) {
encoded := EncodeF32(v)
binary := DecodeF32(encoded)
require.Zero(t, encoded>>32) // Ensures high bits aren't set
if math.IsNaN(float64(binary)) { // NaN cannot be compared with themselves, so we have to use IsNaN
require.True(t, math.IsNaN(float64(binary)))
} else {
@@ -73,3 +74,32 @@ func TestEncodeDecodeF64(t *testing.T) {
})
}
}
func TestEncodeCastI32(t *testing.T) {
for _, v := range []int32{
0, 100, -100, 1, -1,
math.MaxInt32,
math.MinInt32,
} {
t.Run(fmt.Sprintf("%d", v), func(t *testing.T) {
encoded := EncodeI32(v)
require.Zero(t, encoded>>32) // Ensures high bits aren't set
binary := int32(encoded)
require.Equal(t, v, binary)
})
}
}
func TestEncodeCastI64(t *testing.T) {
for _, v := range []int64{
0, 100, -100, 1, -1,
math.MaxInt64,
math.MinInt64,
} {
t.Run(fmt.Sprintf("%d", v), func(t *testing.T) {
encoded := EncodeI64(v)
binary := int64(encoded)
require.Equal(t, v, binary)
})
}
}

View File

@@ -247,8 +247,13 @@ func (b *moduleBuilder) Build() (*CompiledCode, error) {
}
}
// TODO: we can use r.enabledFeatures to fail early on things like mutable globals
if module, err := wasm.NewHostModule(b.moduleName, b.nameToGoFunc, b.nameToMemory, b.nameToGlobal); err != nil {
if module, err := wasm.NewHostModule(
b.moduleName,
b.nameToGoFunc,
b.nameToMemory,
b.nameToGlobal,
b.r.enabledFeatures,
); err != nil {
return nil, err
} else {
return &CompiledCode{module: module}, nil

View File

@@ -16,25 +16,25 @@ import (
// RuntimeConfig controls runtime behavior, with the default implementation as NewRuntimeConfig
type RuntimeConfig struct {
newEngine func() wasm.Engine
ctx context.Context
enabledFeatures wasm.Features
newEngine func(wasm.Features) wasm.Engine
ctx context.Context
memoryMaxPages uint32
}
// engineLessConfig helps avoid copy/pasting the wrong defaults.
var engineLessConfig = &RuntimeConfig{
ctx: context.Background(),
enabledFeatures: wasm.Features20191205,
ctx: context.Background(),
memoryMaxPages: wasm.MemoryMaxPages,
}
// clone ensures all fields are coped even if nil.
func (c *RuntimeConfig) clone() *RuntimeConfig {
return &RuntimeConfig{
enabledFeatures: c.enabledFeatures,
newEngine: c.newEngine,
ctx: c.ctx,
enabledFeatures: c.enabledFeatures,
memoryMaxPages: c.memoryMaxPages,
}
}
@@ -89,10 +89,23 @@ func (c *RuntimeConfig) WithMemoryMaxPages(memoryMaxPages uint32) *RuntimeConfig
return ret
}
// WithFinishedFeatures enables currently supported "finished" feature proposals. Use this to improve compatibility with
// tools that enable all features by default.
//
// Note: The features implied can vary and can lead to unpredictable behavior during updates.
// Note: This only includes "finished" features, but "finished" is not an official W3C term: it is possible that
// "finished" features do not make the next W3C recommended WebAssembly core specification.
// See https://github.com/WebAssembly/spec/tree/main/proposals
func (c *RuntimeConfig) WithFinishedFeatures() *RuntimeConfig {
ret := c.clone()
ret.enabledFeatures = wasm.FeaturesFinished
return ret
}
// WithFeatureMutableGlobal allows globals to be mutable. This defaults to true as the feature was finished in
// WebAssembly 1.0 (20191205).
//
// When false, a api.Global can never be cast to a api.MutableGlobal, and any source that includes global vars
// When false, an api.Global can never be cast to an api.MutableGlobal, and any source that includes global vars
// will fail to parse.
func (c *RuntimeConfig) WithFeatureMutableGlobal(enabled bool) *RuntimeConfig {
ret := c.clone()
@@ -100,8 +113,11 @@ func (c *RuntimeConfig) WithFeatureMutableGlobal(enabled bool) *RuntimeConfig {
return ret
}
// WithFeatureSignExtensionOps enables sign-extend operations. This defaults to false as the feature was not finished in
// WebAssembly 1.0 (20191205).
// WithFeatureSignExtensionOps enables sign extension instructions ("sign-extension-ops"). This defaults to false as the
// feature was not finished in WebAssembly 1.0 (20191205).
//
// This has the following effects:
// * Adds instructions `i32.extend8_s`, `i32.extend16_s`, `i64.extend8_s`, `i64.extend16_s` and `i64.extend32_s`
//
// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
func (c *RuntimeConfig) WithFeatureSignExtensionOps(enabled bool) *RuntimeConfig {
@@ -110,6 +126,20 @@ func (c *RuntimeConfig) WithFeatureSignExtensionOps(enabled bool) *RuntimeConfig
return ret
}
// WithFeatureMultiValue enables multiple values ("multi-value"). This defaults to false as the feature was not finished
// in WebAssembly 1.0 (20191205).
//
// This has the following effects:
// * Function (`func`) types allow more than one result
// * Block types (`block`, `loop` and `if`) can be arbitrary function types
//
// See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
func (c *RuntimeConfig) WithFeatureMultiValue(enabled bool) *RuntimeConfig {
ret := c.clone()
ret.enabledFeatures = ret.enabledFeatures.Set(wasm.FeatureMultiValue, enabled)
return ret
}
// CompiledCode is a WebAssembly 1.0 (20191205) module ready to be instantiated (Runtime.InstantiateModule) as an\
// api.Module.
//

165
examples/results_test.go Normal file
View File

@@ -0,0 +1,165 @@
package examples
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
// Test_MultiReturn_V1_0 implements functions with multiple returns values, using an approach portable with any
// WebAssembly 1.0 (20191205) runtime.
//
// Note: This is the same approach used by WASI snapshot-01!
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md
func Test_MultipleResults(t *testing.T) {
r := wazero.NewRuntime()
// Instantiate a module that illustrates multiple results using functions defined as WebAssembly instructions.
wasm, err := resultOffsetWasmFunctions(r)
require.NoError(t, err)
defer wasm.Close()
// Instantiate a module that illustrates multiple results using functions defined in Go.
host, err := resultOffsetHostFunctions(r)
require.NoError(t, err)
defer wasm.Close()
// Prove both implementations have the same effects!
for _, mod := range []api.Module{wasm, host} {
callGetNumber := mod.ExportedFunction("call_get_age")
results, err := callGetNumber.Call(nil)
require.NoError(t, err)
require.Equal(t, []uint64{37}, results)
}
}
// resultOffsetWasmFunctions defines Wasm functions that illustrate multiple results using a technique compatible
// with any WebAssembly 1.0 (20191205) runtime.
//
// To return a value in WASM written to a result parameter, you have to define memory and pass a location to write
// the result. At the end of your function, you load that location.
func resultOffsetWasmFunctions(r wazero.Runtime) (api.Module, error) {
return r.InstantiateModuleFromCode([]byte(`(module $result-offset/wasm
;; To use result parameters, we need scratch memory. Allocate the least possible: 1 page (64KB).
(memory 1 1)
;; Define a function that returns a result, while a second result is written to memory.
(func $get_age (param $result_offset.age i32) (result (;errno;) i32)
local.get 0 ;; stack = [$result_offset.age]
i64.const 37 ;; stack = [$result_offset.age, 37]
i64.store ;; stack = []
i32.const 0 ;; stack = [0]
)
;; Now, define a function that shows the Wasm mechanics returning something written to a result parameter.
;; The caller provides a memory offset to the callee, so that it knows where to write the second result.
(func $call_get_age (result i64)
i32.const 8 ;; stack = [8] $result_offset.age parameter to get_age (arbitrary memory offset 8)
call $get_age ;; stack = [errno] result of get_age
drop ;; stack = []
i32.const 8 ;; stack = [8] same value as the $result_offset.age parameter
i64.load ;; stack = [age]
)
;; Export the function, so that we can test it!
(export "call_get_age" (func $call_get_age))
)`))
}
// resultOffsetHostFunctions defines host functions that illustrate multiple results using a technique compatible
// with any WebAssembly 1.0 (20191205) runtime.
//
// To return a value in WASM written to a result parameter, you have to define memory and pass a location to write
// the result. At the end of your function, you load that location.
func resultOffsetHostFunctions(r wazero.Runtime) (api.Module, error) {
return r.NewModuleBuilder("result-offset/host").
// To use result parameters, we need scratch memory. Allocate the least possible: 1 page (64KB).
ExportMemoryWithMax("mem", 1, 1).
// Define a function that returns a result, while a second result is written to memory.
ExportFunction("get_age", func(m api.Module, resultOffsetAge uint32) (errno uint32) {
if m.Memory().WriteUint64Le(resultOffsetAge, 37) {
return 0
}
return 1 // overflow
}).
// Now, define a function that shows the Wasm mechanics returning something written to a result parameter.
// The caller provides a memory offset to the callee, so that it knows where to write the second result.
ExportFunction("call_get_age", func(m api.Module) (age uint64) {
resultOffsetAge := uint32(8) // arbitrary memory offset (in bytes)
_, _ = m.ExportedFunction("get_age").Call(m, uint64(resultOffsetAge))
age, _ = m.Memory().ReadUint64Le(resultOffsetAge)
return
}).Instantiate()
}
// Test_MultipleResults_MultiValue implements functions with multiple returns values naturally, due to enabling the
// "multi-value" feature.
//
// Note: While "multi-value" is not yet a W3C recommendation, most WebAssembly runtimes support it by default.
// See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
func Test_MultipleResults_MultiValue(t *testing.T) {
// wazero enables only W3C recommended features by default. Opt-in to other features like so:
r := wazero.NewRuntimeWithConfig(
wazero.NewRuntimeConfig().WithFeatureMultiValue(true),
// ^^ Note: You can enable all features via WithFinishedFeatures.
)
// Instantiate a module that illustrates multi-value functions defined as WebAssembly instructions.
wasm, err := multiValueWasmFunctions(r)
require.NoError(t, err)
defer wasm.Close()
// Instantiate a module that illustrates multi-value functions using functions defined in Go.
host, err := multiValueHostFunctions(r)
require.NoError(t, err)
defer wasm.Close()
// Prove both implementations have the same effects!
for _, mod := range []api.Module{wasm, host} {
callGetNumber := mod.ExportedFunction("call_get_age")
results, err := callGetNumber.Call(nil)
require.NoError(t, err)
require.Equal(t, []uint64{37}, results)
}
}
// multiValueWasmFunctions defines Wasm functions that illustrate multiple results using the "multi-value" feature.
func multiValueWasmFunctions(r wazero.Runtime) (api.Module, error) {
return r.InstantiateModuleFromCode([]byte(`(module $multi-value/wasm
;; Define a function that returns two results
(func $get_age (result (;age;) i64 (;errno;) i32)
i64.const 37 ;; stack = [37]
i32.const 0 ;; stack = [37, 0]
)
;; Now, define a function that returns only the first result.
(func $call_get_age (result i64)
call $get_age ;; stack = [37, errno] result of get_age
drop ;; stack = [37]
)
;; Export the function, so that we can test it!
(export "call_get_age" (func $call_get_age))
)`))
}
// multiValueHostFunctions defines Wasm functions that illustrate multiple results using the "multi-value" feature.
func multiValueHostFunctions(r wazero.Runtime) (api.Module, error) {
return r.NewModuleBuilder("multi-value/host").
// Define a function that returns two results
ExportFunction("get_age", func() (age uint64, errno uint32) {
age = 37
errno = 0
return
}).
// Now, define a function that returns only the first result.
ExportFunction("call_get_age", func(m api.Module) (age uint64) {
results, _ := m.ExportedFunction("get_age").Call(m)
return results[0]
}).Instantiate()
}

View File

@@ -31,12 +31,12 @@ import (
)
type EngineTester interface {
NewEngine() wasm.Engine
NewEngine(enabledFeatures wasm.Features) wasm.Engine
InitTable(me wasm.ModuleEngine, initTableLen uint32, initTableIdxToFnIdx map[wasm.Index]wasm.Index) []interface{}
}
func RunTestEngine_NewModuleEngine(t *testing.T, et EngineTester) {
e := et.NewEngine()
e := et.NewEngine(wasm.Features20191205)
t.Run("sets module name", func(t *testing.T) {
me, err := e.NewModuleEngine(t.Name(), nil, nil, nil, nil)
@@ -47,7 +47,7 @@ func RunTestEngine_NewModuleEngine(t *testing.T, et EngineTester) {
}
func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
e := et.NewEngine()
e := et.NewEngine(wasm.Features20191205)
// Define a basic function which defines one parameter. This is used to test results when incorrect arity is used.
i64 := wasm.ValueTypeI64
@@ -84,7 +84,7 @@ func RunTestModuleEngine_Call(t *testing.T, et EngineTester) {
}
func RunTestEngine_NewModuleEngine_InitTable(t *testing.T, et EngineTester) {
e := et.NewEngine()
e := et.NewEngine(wasm.Features20191205)
t.Run("no table elements", func(t *testing.T) {
table := &wasm.TableInstance{Min: 2, Table: make([]interface{}, 2)}
@@ -185,9 +185,10 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester
return v
})
e := et.NewEngine()
features := wasm.Features20191205
e := et.NewEngine(features)
module := &wasm.ModuleInstance{Memory: memory}
modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(e, wasm.Features20191205), module, nil)
modCtx := wasm.NewModuleContext(context.Background(), wasm.NewStore(features, e), module, nil)
f := &wasm.FunctionInstance{
GoFunc: &hostFn,
@@ -218,7 +219,7 @@ func runTestModuleEngine_Call_HostFn_ModuleContext(t *testing.T, et EngineTester
func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
runTestModuleEngine_Call_HostFn_ModuleContext(t, et) // TODO: refactor to use the same test interface.
e := et.NewEngine()
e := et.NewEngine(wasm.Features20191205)
imported, importedMe, importing, importingMe := setupCallTests(t, e)
defer importingMe.Close()
@@ -265,7 +266,7 @@ func RunTestModuleEngine_Call_HostFn(t *testing.T, et EngineTester) {
}
func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) {
e := et.NewEngine()
e := et.NewEngine(wasm.Features20191205)
imported, importedMe, importing, importingMe := setupCallTests(t, e)
defer importingMe.Close()

View File

@@ -11,7 +11,7 @@ import (
// DecodeModule implements wasm.DecodeModule for the WebAssembly 1.0 (20191205) Binary Format
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-format%E2%91%A0
func DecodeModule(binary []byte, _ wasm.Features, memoryMaxPages uint32) (*wasm.Module, error) {
func DecodeModule(binary []byte, enabledFeatures wasm.Features, memoryMaxPages uint32) (*wasm.Module, error) {
r := bytes.NewReader(binary)
// Magic number.
@@ -69,7 +69,7 @@ func DecodeModule(binary []byte, _ wasm.Features, memoryMaxPages uint32) (*wasm.
}
case wasm.SectionIDType:
m.TypeSection, err = decodeTypeSection(r)
m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
case wasm.SectionIDImport:
if m.ImportSection, err = decodeImportSection(r, memoryMaxPages); err != nil {
return nil, err // avoid re-wrapping the error.

View File

@@ -1,6 +1,10 @@
package binary
import (
"bytes"
"fmt"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -46,7 +50,50 @@ func encodeFunctionType(t *wasm.FunctionType) []byte {
}
return append(append([]byte{0x60}, encodeValTypes(t.Params)...), 1, t.Results[0])
}
// This branch should never be reaches as WebAssembly 1.0 (20191205) supports at most 1 result
// Only reached when "multi-value" is enabled because WebAssembly 1.0 (20191205) supports at most 1 result.
data := append([]byte{0x60}, encodeValTypes(t.Params)...)
return append(data, encodeValTypes(t.Results)...)
}
func decodeFunctionType(enabledFeatures wasm.Features, r *bytes.Reader) (*wasm.FunctionType, error) {
b, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("read leading byte: %w", err)
}
if b != 0x60 {
return nil, fmt.Errorf("%w: %#x != 0x60", ErrInvalidByte, b)
}
paramCount, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("could not read parameter count: %w", err)
}
paramTypes, err := decodeValueTypes(r, paramCount)
if err != nil {
return nil, fmt.Errorf("could not read parameter types: %w", err)
}
resultCount, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("could not read result count: %w", err)
}
// Guard >1.0 feature multi-value
if resultCount > 1 {
if err = enabledFeatures.Require(wasm.FeatureMultiValue); err != nil {
return nil, fmt.Errorf("multiple result types invalid as %v", err)
}
}
resultTypes, err := decodeValueTypes(r, resultCount)
if err != nil {
return nil, fmt.Errorf("could not read result types: %w", err)
}
return &wasm.FunctionType{
Params: paramTypes,
Results: resultTypes,
}, nil
}

View File

@@ -1,6 +1,8 @@
package binary
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/require"
@@ -8,7 +10,7 @@ import (
"github.com/tetratelabs/wazero/internal/wasm"
)
func TestEncodeFunctionType(t *testing.T) {
func TestFunctionType(t *testing.T) {
i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64
tests := []struct {
name string
@@ -25,53 +27,38 @@ func TestEncodeFunctionType(t *testing.T) {
input: &wasm.FunctionType{Params: []wasm.ValueType{i32}},
expected: []byte{0x60, 1, i32, 0},
},
{
name: "undefined param no result", // ensure future spec changes don't panic
input: &wasm.FunctionType{Params: []wasm.ValueType{0x6f}},
expected: []byte{0x60, 1, 0x6f, 0},
},
{
name: "no param one result",
input: &wasm.FunctionType{Results: []wasm.ValueType{i32}},
expected: []byte{0x60, 0, 1, i32},
},
{
name: "no param undefined result", // ensure future spec changes don't panic
input: &wasm.FunctionType{Results: []wasm.ValueType{0x6f}},
expected: []byte{0x60, 0, 1, 0x6f},
},
{
name: "one param one result",
input: &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32}},
expected: []byte{0x60, 1, i64, 1, i32},
},
{
name: "undefined param undefined result", // ensure future spec changes don't panic
input: &wasm.FunctionType{Params: []wasm.ValueType{0x6f}, Results: []wasm.ValueType{0x6f}},
expected: []byte{0x60, 1, 0x6f, 1, 0x6f},
},
{
name: "two params no result",
input: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}},
expected: []byte{0x60, 2, i32, i64, 0},
},
{
name: "no param two results", // this is just for coverage as WebAssembly 1.0 (20191205) does not allow it!
input: &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}},
expected: []byte{0x60, 0, 2, i32, i64},
},
{
name: "one param two results", // this is just for coverage as WebAssembly 1.0 (20191205) does not allow it!
input: &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32, i64}},
expected: []byte{0x60, 1, i64, 2, i32, i64},
},
{
name: "two param one result",
input: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32}},
expected: []byte{0x60, 2, i32, i64, 1, i32},
},
{
name: "two param two results", // this is just for coverage as WebAssembly 1.0 (20191205) does not allow it!
name: "no param two results",
input: &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}},
expected: []byte{0x60, 0, 2, i32, i64},
},
{
name: "one param two results",
input: &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i32, i64}},
expected: []byte{0x60, 1, i64, 2, i32, i64},
},
{
name: "two param two results",
input: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{i32, i64}},
expected: []byte{0x60, 2, i32, i64, 2, i32, i64},
},
@@ -80,9 +67,65 @@ func TestEncodeFunctionType(t *testing.T) {
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
bytes := encodeFunctionType(tc.input)
require.Equal(t, tc.expected, bytes)
b := encodeFunctionType(tc.input)
t.Run(fmt.Sprintf("encode - %s", tc.name), func(t *testing.T) {
require.Equal(t, tc.expected, b)
})
t.Run(fmt.Sprintf("decode - %s", tc.name), func(t *testing.T) {
binary, err := decodeFunctionType(wasm.FeaturesFinished, bytes.NewReader(b))
require.NoError(t, err)
require.Equal(t, binary, tc.input)
})
}
}
func TestDecodeFunctionType_Errors(t *testing.T) {
i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64
tests := []struct {
name string
input []byte
enabledFeatures wasm.Features
expectedErr string
}{
{
name: "undefined param no result",
input: []byte{0x60, 1, 0x6f, 0},
expectedErr: "could not read parameter types: invalid value type: 111",
},
{
name: "no param undefined result",
input: []byte{0x60, 0, 1, 0x6f},
expectedErr: "could not read result types: invalid value type: 111",
},
{
name: "undefined param undefined result",
input: []byte{0x60, 1, 0x6f, 1, 0x6f},
expectedErr: "could not read parameter types: invalid value type: 111",
},
{
name: "no param two results - multi-value not enabled",
input: []byte{0x60, 0, 2, i32, i64},
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "one param two results - multi-value not enabled",
input: []byte{0x60, 1, i64, 2, i32, i64},
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "two param two results - multi-value not enabled",
input: []byte{0x60, 2, i32, i64, 2, i32, i64},
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
_, err := decodeFunctionType(wasm.Features20191205, bytes.NewReader(tc.input))
require.EqualError(t, err, tc.expectedErr)
})
}
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/tetratelabs/wazero/internal/wasm"
)
func decodeTypeSection(r *bytes.Reader) ([]*wasm.FunctionType, error) {
func decodeTypeSection(enabledFeatures wasm.Features, r *bytes.Reader) ([]*wasm.FunctionType, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("get size of vector: %w", err)
@@ -16,51 +16,13 @@ func decodeTypeSection(r *bytes.Reader) ([]*wasm.FunctionType, error) {
result := make([]*wasm.FunctionType, vs)
for i := uint32(0); i < vs; i++ {
if result[i], err = decodeFunctionType(r); err != nil {
if result[i], err = decodeFunctionType(enabledFeatures, r); err != nil {
return nil, fmt.Errorf("read %d-th type: %v", i, err)
}
}
return result, nil
}
func decodeFunctionType(r *bytes.Reader) (*wasm.FunctionType, error) {
b, err := r.ReadByte()
if err != nil {
return nil, fmt.Errorf("read leading byte: %w", err)
}
if b != 0x60 {
return nil, fmt.Errorf("%w: %#x != 0x60", ErrInvalidByte, b)
}
s, _, err := leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("could not read parameter count: %w", err)
}
paramTypes, err := decodeValueTypes(r, s)
if err != nil {
return nil, fmt.Errorf("could not read parameter types: %w", err)
}
s, _, err = leb128.DecodeUint32(r)
if err != nil {
return nil, fmt.Errorf("could not read result count: %w", err)
} else if s > 1 {
return nil, fmt.Errorf("multi value results not supported")
}
resultTypes, err := decodeValueTypes(r, s)
if err != nil {
return nil, fmt.Errorf("could not read result types: %w", err)
}
return &wasm.FunctionType{
Params: paramTypes,
Results: resultTypes,
}, nil
}
func decodeImportSection(r *bytes.Reader, memoryMaxPages uint32) ([]*wasm.Import, error) {
vs, _, err := leb128.DecodeUint32(r)
if err != nil {

View File

@@ -2,6 +2,7 @@ package wasm
import (
"fmt"
"strings"
)
// Features are the currently enabled features.
@@ -15,14 +16,34 @@ type Features uint64
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205
const Features20191205 = FeatureMutableGlobal
// FeaturesFinished include all supported finished features, regardless of W3C status.
//
// See https://github.com/WebAssembly/proposals/blob/main/finished-proposals.md
const FeaturesFinished = 0xffffffffffffffff
const (
// FeatureMutableGlobal decides if global vars are allowed to be imported or exported (ExternTypeGlobal)
// See https://github.com/WebAssembly/mutable-global
FeatureMutableGlobal Features = 1 << iota
// FeatureSignExtensionOps decides if parsing should succeed on wasm.GlobalType Mutable
// FeatureSignExtensionOps decides if parsing should succeed on the following instructions:
//
// * OpcodeI32Extend8S
// * OpcodeI32Extend16S
// * OpcodeI64Extend8S
// * OpcodeI64Extend16S
// * OpcodeI64Extend32S
//
// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
FeatureSignExtensionOps
// FeatureMultiValue decides if parsing should succeed on the following:
//
// * FunctionType.Results length greater than one.
// * `block`, `loop` and `if` can be arbitrary function types.
//
// See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
FeatureMultiValue
)
// Set assigns the value for the given feature.
@@ -41,23 +62,38 @@ func (f Features) Get(feature Features) bool {
// Require fails with a configuration error if the given feature is not enabled
func (f Features) Require(feature Features) error {
if f&feature == 0 {
return fmt.Errorf("feature %s is disabled", feature)
return fmt.Errorf("feature %q is disabled", feature)
}
return nil
}
// String implements fmt.Stringer by returning each enabled feature.
func (f Features) String() string {
var builder strings.Builder
for i := Features(0); i < 63; i++ { // cycle through all bits to reduce code and maintenance
if f.Get(i) {
if name := featureName(i); name != "" {
if builder.Len() > 0 {
builder.WriteByte('|')
}
builder.WriteString(name)
}
}
}
return builder.String()
}
func featureName(f Features) string {
switch f {
case 0:
return ""
case FeatureMutableGlobal:
// match https://github.com/WebAssembly/mutable-global
return "mutable-global"
case FeatureSignExtensionOps:
// match https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
return "sign-extension-ops"
default:
return "undefined" // TODO: when there are multiple features join known ones on pipe (|)
case FeatureMultiValue:
// match https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
return "multi-value"
}
return ""
}

View File

@@ -59,7 +59,10 @@ func TestFeatures_String(t *testing.T) {
{name: "none", feature: 0, expected: ""},
{name: "mutable-global", feature: FeatureMutableGlobal, expected: "mutable-global"},
{name: "sign-extension-ops", feature: FeatureSignExtensionOps, expected: "sign-extension-ops"},
{name: "undefined", feature: 1 << 63, expected: "undefined"},
{name: "multi-value", feature: FeatureMultiValue, expected: "multi-value"},
{name: "features", feature: FeatureMutableGlobal | FeatureMultiValue, expected: "mutable-global|multi-value"},
{name: "undefined", feature: 1 << 63, expected: ""},
{name: "all", feature: FeaturesFinished, expected: "mutable-global|sign-extension-ops|multi-value"},
}
for _, tt := range tests {
@@ -76,10 +79,11 @@ func TestFeatures_Require(t *testing.T) {
feature Features
expectedErr string
}{
{name: "none", feature: 0, expectedErr: "feature mutable-global is disabled"},
{name: "none", feature: 0, expectedErr: "feature \"mutable-global\" is disabled"},
{name: "mutable-global", feature: FeatureMutableGlobal},
{name: "sign-extension-ops", feature: FeatureSignExtensionOps, expectedErr: "feature mutable-global is disabled"},
{name: "undefined", feature: 1 << 63, expectedErr: "feature mutable-global is disabled"},
{name: "sign-extension-ops", feature: FeatureSignExtensionOps, expectedErr: "feature \"mutable-global\" is disabled"},
{name: "multi-value", feature: FeatureMultiValue, expectedErr: "feature \"mutable-global\" is disabled"},
{name: "undefined", feature: 1 << 63, expectedErr: "feature \"mutable-global\" is disabled"},
}
for _, tt := range tests {

View File

@@ -2,37 +2,50 @@ package wasm
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/leb128"
)
// The wazero specific limitation described at RATIONALE.md.
const maximumValuesOnStack = 1 << 27
// validateFunction validates the instruction sequence of a function.
// following the specification https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#instructions%E2%91%A2.
//
// f is the validation target function instance.
// functions is the list of function indexes which are declared on a module from which the target is instantiated.
// globals is the list of global types which are declared on a module from which the target is instantiated.
// memories is the list of memory types which are declared on a module from which the target is instantiated.
// tables is the list of table types which are declared on a module from which the target is instantiated.
// types is the list of function types which are declared on a module from which the target is instantiated.
// maxStackValues is the maximum height of values stack which the target is allowed to reach.
// * idx is the index in the FunctionSection
// * functions are the function index namespace, which is prefixed by imports. The value is the TypeSection index.
// * globals are the global index namespace, which is prefixed by imports.
// * memory is the potentially imported memory and can be nil.
// * table is the potentially imported table and can be nil.
//
// Returns an error if the instruction sequence is not valid,
// or potentially it can exceed the maximum number of values on the stack.
func validateFunction(
func (m *Module) validateFunction(enabledFeatures Features, idx Index, functions []Index, globals []*GlobalType, memory *Memory, table *Table) error {
return m.validateFunctionWithMaxStackValues(enabledFeatures, idx, functions, globals, memory, table, maximumValuesOnStack)
}
// validateFunctionWithMaxStackValues is like validateFunction, but allows overriding maxStackValues for testing.
//
// * maxStackValues is the maximum height of values stack which the target is allowed to reach.
func (m *Module) validateFunctionWithMaxStackValues(
enabledFeatures Features,
functionType *FunctionType,
body []byte,
localTypes []ValueType,
idx Index,
functions []Index,
globals []*GlobalType,
memory *Memory,
table *Table,
types []*FunctionType,
maxStackValues int,
) error {
functionType := m.TypeSection[m.FunctionSection[idx]]
body := m.CodeSection[idx].Body
localTypes := m.CodeSection[idx].LocalTypes
types := m.TypeSection
// We start with the outermost control block which is for function return if the code branches into it.
controlBlockStack := []*controlBlock{{blockType: functionType}}
// Create the valueTypeStack to track the state of Wasm value stacks at anypoint of execution.
@@ -287,7 +300,8 @@ func validateFunction(
case OpcodeLocalGet:
inputLen := uint32(len(functionType.Params))
if l := uint32(len(localTypes)) + inputLen; index >= l {
return fmt.Errorf("invalid local index for local.get %d >= %d(=len(locals)+len(parameters))", index, l)
return fmt.Errorf("invalid local index for %s %d >= %d(=len(locals)+len(parameters))",
OpcodeLocalGetName, index, l)
}
if index < inputLen {
valueTypeStack.push(functionType.Params[index])
@@ -297,7 +311,8 @@ func validateFunction(
case OpcodeLocalSet:
inputLen := uint32(len(functionType.Params))
if l := uint32(len(localTypes)) + inputLen; index >= l {
return fmt.Errorf("invalid local index for local.set %d >= %d(=len(locals)+len(parameters))", index, l)
return fmt.Errorf("invalid local index for %s %d >= %d(=len(locals)+len(parameters))",
OpcodeLocalSetName, index, l)
}
var expType ValueType
if index < inputLen {
@@ -311,7 +326,8 @@ func validateFunction(
case OpcodeLocalTee:
inputLen := uint32(len(functionType.Params))
if l := uint32(len(localTypes)) + inputLen; index >= l {
return fmt.Errorf("invalid local index for local.tee %d >= %d(=len(locals)+len(parameters))", index, l)
return fmt.Errorf("invalid local index for %s %d >= %d(=len(locals)+len(parameters))",
OpcodeLocalTeeName, index, l)
}
var expType ValueType
if index < inputLen {
@@ -325,14 +341,14 @@ func validateFunction(
valueTypeStack.push(expType)
case OpcodeGlobalGet:
if index >= uint32(len(globals)) {
return fmt.Errorf("invalid global index")
return fmt.Errorf("invalid index for %s", OpcodeGlobalGetName)
}
valueTypeStack.push(globals[index].ValType)
case OpcodeGlobalSet:
if index >= uint32(len(globals)) {
return fmt.Errorf("invalid global index")
} else if !globals[index].Mutable {
return fmt.Errorf("global.set when not mutable")
return fmt.Errorf("%s when not mutable", OpcodeGlobalSetName)
} else if err := valueTypeStack.popAndVerifyType(
globals[index].ValType); err != nil {
return err
@@ -344,19 +360,19 @@ func validateFunction(
if err != nil {
return fmt.Errorf("read immediate: %v", err)
} else if int(index) >= len(controlBlockStack) {
return fmt.Errorf("invalid br operation: index out of range")
return fmt.Errorf("invalid %s operation: index out of range", OpcodeBrName)
}
pc += num - 1
// Check type soundness.
target := controlBlockStack[len(controlBlockStack)-int(index)-1]
targetResultType := target.blockType.Results
if target.isLoop {
if target.op == OpcodeLoop {
// Loop operation doesn't require results since the continuation is
// the beginning of the loop.
targetResultType = []ValueType{}
}
if err := valueTypeStack.popResults(targetResultType, false); err != nil {
return fmt.Errorf("type mismatch on the br operation: %v", err)
if err = valueTypeStack.popResults(op, targetResultType, false); err != nil {
return err
}
// br instruction is stack-polymorphic.
valueTypeStack.unreachable()
@@ -367,23 +383,23 @@ func validateFunction(
return fmt.Errorf("read immediate: %v", err)
} else if int(index) >= len(controlBlockStack) {
return fmt.Errorf(
"invalid ln param given for br_if: index=%d with %d for the current lable stack length",
index, len(controlBlockStack))
"invalid ln param given for %s: index=%d with %d for the current lable stack length",
OpcodeBrIfName, index, len(controlBlockStack))
}
pc += num - 1
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the required operand for br_if")
return fmt.Errorf("cannot pop the required operand for %s", OpcodeBrIfName)
}
// Check type soundness.
target := controlBlockStack[len(controlBlockStack)-int(index)-1]
targetResultType := target.blockType.Results
if target.isLoop {
if target.op == OpcodeLoop {
// Loop operation doesn't require results since the continuation is
// the beginning of the loop.
targetResultType = []ValueType{}
}
if err := valueTypeStack.popResults(targetResultType, false); err != nil {
return fmt.Errorf("type mismatch on the br_if operation: %v", err)
if err := valueTypeStack.popResults(op, targetResultType, false); err != nil {
return err
}
// Push back the result
for _, t := range targetResultType {
@@ -411,43 +427,43 @@ func validateFunction(
return fmt.Errorf("read immediate: %w", err)
} else if int(ln) >= len(controlBlockStack) {
return fmt.Errorf(
"invalid ln param given for br_table: ln=%d with %d for the current lable stack length",
ln, len(controlBlockStack))
"invalid ln param given for %s: ln=%d with %d for the current lable stack length",
OpcodeBrTableName, ln, len(controlBlockStack))
}
pc += n + num - 1
// Check type soundness.
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the required operand for br_table")
return fmt.Errorf("cannot pop the required operand for %s", OpcodeBrTableName)
}
lnLabel := controlBlockStack[len(controlBlockStack)-1-int(ln)]
expType := lnLabel.blockType.Results
if lnLabel.isLoop {
if lnLabel.op == OpcodeLoop {
// Loop operation doesn't require results since the continuation is
// the beginning of the loop.
expType = []ValueType{}
}
for _, l := range list {
if int(l) >= len(controlBlockStack) {
return fmt.Errorf("invalid l param given for br_table")
return fmt.Errorf("invalid l param given for %s", OpcodeBrTableName)
}
label := controlBlockStack[len(controlBlockStack)-1-int(l)]
expType2 := label.blockType.Results
if label.isLoop {
if label.op == OpcodeLoop {
// Loop operation doesn't require results since the continuation is
// the beginning of the loop.
expType2 = []ValueType{}
}
if len(expType) != len(expType2) {
return fmt.Errorf("incosistent block type length for br_table at %d; %v (ln=%d) != %v (l=%d)", l, expType, ln, expType2, l)
return fmt.Errorf("incosistent block type length for %s at %d; %v (ln=%d) != %v (l=%d)", OpcodeBrTableName, l, expType, ln, expType2, l)
}
for i := range expType {
if expType[i] != expType2[i] {
return fmt.Errorf("incosistent block type for br_table at %d", l)
return fmt.Errorf("incosistent block type for %s at %d", OpcodeBrTableName, l)
}
}
}
if err := valueTypeStack.popResults(expType, false); err != nil {
return fmt.Errorf("type mismatch on the br_table operation: %v", err)
if err = valueTypeStack.popResults(op, expType, false); err != nil {
return err
}
// br_table instruction is stack-polymorphic.
valueTypeStack.unreachable()
@@ -464,7 +480,7 @@ func validateFunction(
funcType := types[functions[index]]
for i := 0; i < len(funcType.Params); i++ {
if err := valueTypeStack.popAndVerifyType(funcType.Params[len(funcType.Params)-1-i]); err != nil {
return fmt.Errorf("type mismatch on call operation param type")
return fmt.Errorf("type mismatch on %s operation param type", OpcodeCallName)
}
}
for _, exp := range funcType.Results {
@@ -479,21 +495,21 @@ func validateFunction(
pc += num - 1
pc++
if body[pc] != 0x00 {
return fmt.Errorf("call_indirect reserved bytes not zero but got %d", body[pc])
return fmt.Errorf("%s reserved bytes not zero but got %d", OpcodeCallIndirectName, body[pc])
}
if table == nil {
return fmt.Errorf("table not given while having call_indirect")
return fmt.Errorf("table not given while having %s", OpcodeCallIndirectName)
}
if err = valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the in table index's type for call_indirect")
return fmt.Errorf("cannot pop the in table index's type for %s", OpcodeCallIndirectName)
}
if int(typeIndex) >= len(types) {
return fmt.Errorf("invalid type index at call_indirect: %d", typeIndex)
return fmt.Errorf("invalid type index at %s: %d", OpcodeCallIndirectName, typeIndex)
}
funcType := types[typeIndex]
for i := 0; i < len(funcType.Params); i++ {
if err = valueTypeStack.popAndVerifyType(funcType.Params[len(funcType.Params)-1-i]); err != nil {
return fmt.Errorf("type mismatch on call_indirect operation input type")
return fmt.Errorf("type mismatch on %s operation input type", OpcodeCallIndirectName)
}
}
for _, exp := range funcType.Results {
@@ -503,53 +519,53 @@ func validateFunction(
switch op {
case OpcodeI32Eqz:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the operand for i32.eqz: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeI32EqzName, err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI32Eq, OpcodeI32Ne, OpcodeI32LtS,
OpcodeI32LtU, OpcodeI32GtS, OpcodeI32GtU, OpcodeI32LeS,
OpcodeI32LeU, OpcodeI32GeS, OpcodeI32GeU:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the 1st i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st i32 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the 2nd i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd i32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI64Eqz:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the operand for i64.eqz: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeI64EqzName, err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI64Eq, OpcodeI64Ne, OpcodeI64LtS,
OpcodeI64LtU, OpcodeI64GtS, OpcodeI64GtU,
OpcodeI64LeS, OpcodeI64LeU, OpcodeI64GeS, OpcodeI64GeU:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the 1st i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st i64 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the 2nd i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd i64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeF32Eq, OpcodeF32Ne, OpcodeF32Lt, OpcodeF32Gt, OpcodeF32Le, OpcodeF32Ge:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the 1st f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st f32 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the 2nd f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd f32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeF64Eq, OpcodeF64Ne, OpcodeF64Lt, OpcodeF64Gt, OpcodeF64Le, OpcodeF64Ge:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the 1st f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st f64 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the 2nd f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd f64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI32Clz, OpcodeI32Ctz, OpcodeI32Popcnt:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI32Add, OpcodeI32Sub, OpcodeI32Mul, OpcodeI32DivS,
@@ -557,15 +573,15 @@ func validateFunction(
OpcodeI32Or, OpcodeI32Xor, OpcodeI32Shl, OpcodeI32ShrS,
OpcodeI32ShrU, OpcodeI32Rotl, OpcodeI32Rotr:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the 1st i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the 2nd i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI64Clz, OpcodeI64Ctz, OpcodeI64Popcnt:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI64)
case OpcodeI64Add, OpcodeI64Sub, OpcodeI64Mul, OpcodeI64DivS,
@@ -573,124 +589,124 @@ func validateFunction(
OpcodeI64Or, OpcodeI64Xor, OpcodeI64Shl, OpcodeI64ShrS,
OpcodeI64ShrU, OpcodeI64Rotl, OpcodeI64Rotr:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the 1st i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st i64 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the 2nd i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd i64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI64)
case OpcodeF32Abs, OpcodeF32Neg, OpcodeF32Ceil,
OpcodeF32Floor, OpcodeF32Trunc, OpcodeF32Nearest,
OpcodeF32Sqrt:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the 1st f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st f32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF32)
case OpcodeF32Add, OpcodeF32Sub, OpcodeF32Mul,
OpcodeF32Div, OpcodeF32Min, OpcodeF32Max,
OpcodeF32Copysign:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the 1st f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st f32 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the 2nd f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd f32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF32)
case OpcodeF64Abs, OpcodeF64Neg, OpcodeF64Ceil,
OpcodeF64Floor, OpcodeF64Trunc, OpcodeF64Nearest,
OpcodeF64Sqrt:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the 1st f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st f64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF64)
case OpcodeF64Add, OpcodeF64Sub, OpcodeF64Mul,
OpcodeF64Div, OpcodeF64Min, OpcodeF64Max,
OpcodeF64Copysign:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the 1st f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 1st f64 operand for %s: %v", InstructionName(op), err)
}
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the 2nd f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the 2nd f64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF64)
case OpcodeI32WrapI64:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the operand for i32.wrap_i64: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeI32WrapI64Name, err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI32TruncF32S, OpcodeI32TruncF32U:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the f32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI32TruncF64S, OpcodeI32TruncF64U:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the f64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI64ExtendI32S, OpcodeI64ExtendI32U:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI64)
case OpcodeI64TruncF32S, OpcodeI64TruncF32U:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the f32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the f32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI64)
case OpcodeI64TruncF64S, OpcodeI64TruncF64U:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the f64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the f64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeI64)
case OpcodeF32ConvertI32s, OpcodeF32ConvertI32U:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF32)
case OpcodeF32ConvertI64S, OpcodeF32ConvertI64U:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF32)
case OpcodeF32DemoteF64:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the operand for f32.demote_f64: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeF32DemoteF64Name, err)
}
valueTypeStack.push(ValueTypeF32)
case OpcodeF64ConvertI32S, OpcodeF64ConvertI32U:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the i32 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i32 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF64)
case OpcodeF64ConvertI64S, OpcodeF64ConvertI64U:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the i64 operand for 0x%x: %v", op, err)
return fmt.Errorf("cannot pop the i64 operand for %s: %v", InstructionName(op), err)
}
valueTypeStack.push(ValueTypeF64)
case OpcodeF64PromoteF32:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the operand for f64.promote_f32: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeF64PromoteF32Name, err)
}
valueTypeStack.push(ValueTypeF64)
case OpcodeI32ReinterpretF32:
if err := valueTypeStack.popAndVerifyType(ValueTypeF32); err != nil {
return fmt.Errorf("cannot pop the operand for i32.reinterpret_f32: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeI32ReinterpretF32Name, err)
}
valueTypeStack.push(ValueTypeI32)
case OpcodeI64ReinterpretF64:
if err := valueTypeStack.popAndVerifyType(ValueTypeF64); err != nil {
return fmt.Errorf("cannot pop the operand for i64.reinterpret_f64: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeI64ReinterpretF64Name, err)
}
valueTypeStack.push(ValueTypeI64)
case OpcodeF32ReinterpretI32:
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the operand for f32.reinterpret_i32: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeF32ReinterpretI32Name, err)
}
valueTypeStack.push(ValueTypeF32)
case OpcodeF64ReinterpretI64:
if err := valueTypeStack.popAndVerifyType(ValueTypeI64); err != nil {
return fmt.Errorf("cannot pop the operand for f64.reinterpret_i64: %v", err)
return fmt.Errorf("cannot pop the operand for %s: %v", OpcodeF64ReinterpretI64Name, err)
}
valueTypeStack.push(ValueTypeF64)
case OpcodeI32Extend8S, OpcodeI32Extend16S:
@@ -713,7 +729,7 @@ func validateFunction(
return fmt.Errorf("invalid numeric instruction 0x%x", op)
}
} else if op == OpcodeBlock {
bt, num, err := decodeBlockType(types, bytes.NewReader(body[pc+1:]))
bt, num, err := decodeBlockType(types, bytes.NewReader(body[pc+1:]), enabledFeatures)
if err != nil {
return fmt.Errorf("read block: %w", err)
}
@@ -722,10 +738,10 @@ func validateFunction(
blockType: bt,
blockTypeBytes: num,
})
valueTypeStack.pushStackLimit()
valueTypeStack.pushStackLimit(len(bt.Params))
pc += num
} else if op == OpcodeLoop {
bt, num, err := decodeBlockType(types, bytes.NewReader(body[pc+1:]))
bt, num, err := decodeBlockType(types, bytes.NewReader(body[pc+1:]), enabledFeatures)
if err != nil {
return fmt.Errorf("read block: %w", err)
}
@@ -733,12 +749,19 @@ func validateFunction(
startAt: pc,
blockType: bt,
blockTypeBytes: num,
isLoop: true,
op: op,
})
valueTypeStack.pushStackLimit()
if err = valueTypeStack.popParams(op, bt.Params, false); err != nil {
return err
}
// Plus we have to push any block params again.
for _, p := range bt.Params {
valueTypeStack.push(p)
}
valueTypeStack.pushStackLimit(len(bt.Params))
pc += num
} else if op == OpcodeIf {
bt, num, err := decodeBlockType(types, bytes.NewReader(body[pc+1:]))
bt, num, err := decodeBlockType(types, bytes.NewReader(body[pc+1:]), enabledFeatures)
if err != nil {
return fmt.Errorf("read block: %w", err)
}
@@ -746,38 +769,64 @@ func validateFunction(
startAt: pc,
blockType: bt,
blockTypeBytes: num,
isIf: true,
op: op,
})
if err := valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
if err = valueTypeStack.popAndVerifyType(ValueTypeI32); err != nil {
return fmt.Errorf("cannot pop the operand for 'if': %v", err)
}
valueTypeStack.pushStackLimit()
if err = valueTypeStack.popParams(op, bt.Params, false); err != nil {
return err
}
// Plus we have to push any block params again.
for _, p := range bt.Params {
valueTypeStack.push(p)
}
valueTypeStack.pushStackLimit(len(bt.Params))
pc += num
} else if op == OpcodeElse {
bl := controlBlockStack[len(controlBlockStack)-1]
bl.elseAt = pc
// Check the type soundness of the instructions *before* entering this else Op.
if err := valueTypeStack.popResults(bl.blockType.Results, true); err != nil {
return fmt.Errorf("invalid instruction results in then instructions")
if err := valueTypeStack.popResults(OpcodeIf, bl.blockType.Results, true); err != nil {
return err
}
// Before entering instructions inside else, we pop all the values pushed by then block.
valueTypeStack.resetAtStackLimit()
// Plus we have to push any block params again.
for _, p := range bl.blockType.Params {
valueTypeStack.push(p)
}
} else if op == OpcodeEnd {
bl := controlBlockStack[len(controlBlockStack)-1]
bl.endAt = pc
controlBlockStack = controlBlockStack[:len(controlBlockStack)-1]
if bl.isIf && bl.elseAt <= bl.startAt {
if len(bl.blockType.Results) > 0 {
return fmt.Errorf("type mismatch between then and else blocks")
// OpcodeEnd can end a block or the function itself. Check to see what it is:
ifMissingElse := bl.op == OpcodeIf && bl.elseAt <= bl.startAt
if ifMissingElse {
// If this is the end of block without else, the number of block's results and params must be same.
// Otherwise, the value stack would result in the inconsistent state at runtime.
if !bytes.Equal(bl.blockType.Results, bl.blockType.Params) {
return typeCountError(false, OpcodeElseName, bl.blockType.Params, bl.blockType.Results)
}
// To handle if block without else properly,
// we set ElseAt to EndAt-1 so we can just skip else.
// -1 skips else, to handle if block without else properly.
bl.elseAt = bl.endAt - 1
}
// Check type soundness.
if err := valueTypeStack.popResults(bl.blockType.Results, true); err != nil {
return fmt.Errorf("invalid instruction results at end instruction; expected %v: %v", bl.blockType.Results, err)
// Determine the block context
ctx := "" // the outer-most block: the function return
if bl.op == OpcodeIf && !ifMissingElse && bl.elseAt > 0 {
ctx = OpcodeElseName
} else if bl.op != 0 {
ctx = InstructionName(bl.op)
}
// Check return types match
if err := valueTypeStack.requireStackValues(false, ctx, bl.blockType.Results, true); err != nil {
return err
}
// Put the result types at the end after resetting at the stack limit
// since we might have Any type between the limit and the current top.
valueTypeStack.resetAtStackLimit()
@@ -788,11 +837,9 @@ func validateFunction(
// on values previously pushed by outer blocks.
valueTypeStack.popStackLimit()
} else if op == OpcodeReturn {
expTypes := functionType.Results
for i := 0; i < len(expTypes); i++ {
if err := valueTypeStack.popAndVerifyType(expTypes[len(expTypes)-1-i]); err != nil {
return fmt.Errorf("return type mismatch on return: %v; want %v", err, expTypes)
}
// Same formatting as OpcodeEnd on the outer-most block
if err := valueTypeStack.requireStackValues(false, "", functionType.Results, false); err != nil {
return err
}
// return instruction is stack-polymorphic.
valueTypeStack.unreachable()
@@ -850,30 +897,41 @@ const (
valueTypeUnknown = ValueType(0xFF)
)
func (s *valueTypeStack) pop() (ValueType, error) {
limit := 0
func (s *valueTypeStack) tryPop() (vt ValueType, limit int, ok bool) {
if len(s.stackLimits) > 0 {
limit = s.stackLimits[len(s.stackLimits)-1]
}
if len(s.stack) <= limit {
return 0, fmt.Errorf("invalid operation: trying to pop at %d with limit %d",
len(s.stack), limit)
} else if len(s.stack) == limit+1 && s.stack[limit] == valueTypeUnknown {
return valueTypeUnknown, nil
stackLen := len(s.stack)
if stackLen <= limit {
return
} else if stackLen == limit+1 && s.stack[limit] == valueTypeUnknown {
vt = valueTypeUnknown
ok = true
return
} else {
ret := s.stack[len(s.stack)-1]
s.stack = s.stack[:len(s.stack)-1]
return ret, nil
vt = s.stack[stackLen-1]
s.stack = s.stack[:stackLen-1]
ok = true
return
}
}
func (s *valueTypeStack) popAndVerifyType(expected ValueType) error {
actual, err := s.pop()
if err != nil {
return err
func (s *valueTypeStack) pop() (ValueType, error) {
if vt, limit, ok := s.tryPop(); ok {
return vt, nil
} else {
return 0, fmt.Errorf("invalid operation: trying to pop at %d with limit %d", len(s.stack), limit)
}
if actual != expected && actual != valueTypeUnknown && expected != valueTypeUnknown {
return fmt.Errorf("type mismatch")
}
// popAndVerifyType returns an error if the stack value is unexpected.
func (s *valueTypeStack) popAndVerifyType(expected ValueType) error {
have, _, ok := s.tryPop()
if !ok {
return fmt.Errorf("%s missing", ValueTypeName(expected))
}
if have != expected && have != valueTypeUnknown && expected != valueTypeUnknown {
return fmt.Errorf("type mismatch: expected %s, but was %s", ValueTypeName(expected), ValueTypeName(have))
}
return nil
}
@@ -904,28 +962,129 @@ func (s *valueTypeStack) popStackLimit() {
}
}
func (s *valueTypeStack) pushStackLimit() {
s.stackLimits = append(s.stackLimits, len(s.stack))
// pushStackLimit pushes the control frame's bottom of the stack.
func (s *valueTypeStack) pushStackLimit(params int) {
limit := len(s.stack) - params
s.stackLimits = append(s.stackLimits, limit)
}
func (s *valueTypeStack) popResults(expResults []ValueType, checkAboveLimit bool) error {
func (s *valueTypeStack) popParams(oc Opcode, want []ValueType, checkAboveLimit bool) error {
return s.requireStackValues(true, InstructionName(oc), want, checkAboveLimit)
}
func (s *valueTypeStack) popResults(oc Opcode, want []ValueType, checkAboveLimit bool) error {
return s.requireStackValues(false, InstructionName(oc), want, checkAboveLimit)
}
func (s *valueTypeStack) requireStackValues(
isParam bool,
context string,
want []ValueType,
checkAboveLimit bool,
) error {
limit := 0
if len(s.stackLimits) > 0 {
limit = s.stackLimits[len(s.stackLimits)-1]
}
for _, exp := range expResults {
if err := s.popAndVerifyType(exp); err != nil {
return err
// Iterate backwards as we are comparing the desired slice against stack value types.
countWanted := len(want)
// First, check if there are enough values on the stack.
have := make([]ValueType, 0, countWanted)
for i := countWanted - 1; i >= 0; i-- {
popped, _, ok := s.tryPop()
if !ok {
if len(have) > len(want) {
return typeCountError(isParam, context, have, want)
}
return typeCountError(isParam, context, have, want)
}
have = append(have, popped)
}
// Now, check if there are too many values.
if checkAboveLimit {
if !(limit == len(s.stack) || (limit+1 == len(s.stack) && s.stack[limit] == valueTypeUnknown)) {
return fmt.Errorf("leftovers found in the stack")
return typeCountError(isParam, context, append(s.stack, want...), want)
}
}
// Finally, check the types of the values:
for i, v := range have {
nextWant := want[countWanted-i-1] // have is in reverse order (stack)
if v != nextWant && v != valueTypeUnknown && nextWant != valueTypeUnknown {
return typeMismatchError(isParam, context, v, nextWant, i)
}
}
return nil
}
// typeMismatchError returns an error similar to go compiler's error on type mismatch.
func typeMismatchError(isParam bool, context string, have ValueType, want ValueType, i int) error {
var ret strings.Builder
ret.WriteString("cannot use ")
ret.WriteString(ValueTypeName(have))
if context != "" {
ret.WriteString(" in ")
ret.WriteString(context)
ret.WriteString(" block")
}
if isParam {
ret.WriteString(" as param")
} else {
ret.WriteString(" as result")
}
ret.WriteString("[")
ret.WriteString(strconv.Itoa(i))
ret.WriteString("] type ")
ret.WriteString(ValueTypeName(want))
return errors.New(ret.String())
}
// typeCountError returns an error similar to go compiler's error on type count mismatch.
func typeCountError(isParam bool, context string, have []ValueType, want []ValueType) error {
var ret strings.Builder
if len(have) > len(want) {
ret.WriteString("too many ")
} else {
ret.WriteString("not enough ")
}
if isParam {
ret.WriteString("params")
} else {
ret.WriteString("results")
}
if context != "" {
if isParam {
ret.WriteString(" for ")
} else {
ret.WriteString(" in ")
}
ret.WriteString(context)
ret.WriteString(" block")
}
ret.WriteString("\n\thave (")
writeValueTypes(have, &ret)
ret.WriteString(")\n\twant (")
writeValueTypes(want, &ret)
ret.WriteByte(')')
return errors.New(ret.String())
}
func writeValueTypes(vts []ValueType, ret *strings.Builder) {
switch len(vts) {
case 0:
case 1:
ret.WriteString(api.ValueTypeName(vts[0]))
default:
ret.WriteString(api.ValueTypeName(vts[0]))
for _, vt := range vts[1:] {
ret.WriteString(", ")
ret.WriteString(api.ValueTypeName(vt))
}
}
}
func (s *valueTypeStack) String() string {
var typeStrs, limits []string
for _, v := range s.stack {
@@ -954,30 +1113,36 @@ type controlBlock struct {
startAt, elseAt, endAt uint64
blockType *FunctionType
blockTypeBytes uint64
isLoop bool
isIf bool
// op is zero when the outermost block
op Opcode
}
func decodeBlockType(types []*FunctionType, r *bytes.Reader) (*FunctionType, uint64, error) {
func decodeBlockType(types []*FunctionType, r *bytes.Reader, enabledFeatures Features) (*FunctionType, uint64, error) {
return decodeBlockTypeImpl(func(index int64) (*FunctionType, error) {
if index < 0 || (index >= int64(len(types))) {
return nil, fmt.Errorf("type index out of range: %d", index)
}
return types[index], nil
}, r)
}, r, enabledFeatures)
}
// DecodeBlockType is exported for use in the compiler
func DecodeBlockType(types []*TypeInstance, r *bytes.Reader) (*FunctionType, uint64, error) {
func DecodeBlockType(types []*TypeInstance, r *bytes.Reader, enabledFeatures Features) (*FunctionType, uint64, error) {
return decodeBlockTypeImpl(func(index int64) (*FunctionType, error) {
if index < 0 || (index >= int64(len(types))) {
return nil, fmt.Errorf("type index out of range: %d", index)
}
return types[index].Type, nil
}, r)
}, r, enabledFeatures)
}
func decodeBlockTypeImpl(functionTypeResolver func(index int64) (*FunctionType, error), r *bytes.Reader) (*FunctionType, uint64, error) {
// decodeBlockTypeImpl decodes the type index from a positive 33-bit signed integer. Negative numbers indicate up to one
// WebAssembly 1.0 (20191205) compatible result type. Positive numbers are decoded when `enabledFeatures` include
// FeatureMultiValue and include an index in the Module.TypeSection.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-blocktype
// See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
func decodeBlockTypeImpl(functionTypeResolver func(index int64) (*FunctionType, error), r *bytes.Reader, enabledFeatures Features) (*FunctionType, uint64, error) {
raw, num, err := leb128.DecodeInt33AsInt64(r)
if err != nil {
return nil, 0, fmt.Errorf("decode int33: %w", err)
@@ -996,6 +1161,9 @@ func decodeBlockTypeImpl(functionTypeResolver func(index int64) (*FunctionType,
case -4: // 0x7c in original byte = f64
ret = &FunctionType{Results: []ValueType{ValueTypeF64}}
default:
if err = enabledFeatures.Require(FeatureMultiValue); err != nil {
return nil, num, fmt.Errorf("block with function type return invalid as %v", err)
}
ret, err = functionTypeResolver(raw)
}
return ret, num, err

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ package wasm
import (
"context"
"errors"
"fmt"
"reflect"
@@ -48,7 +47,7 @@ func GetHostFunctionCallContextValue(fk FunctionKind, ctx *ModuleContext) *refle
}
// getFunctionType returns the function type corresponding to the function signature or errs if invalid.
func getFunctionType(fn *reflect.Value, allowErrorResult bool) (fk FunctionKind, ft *FunctionType, hasErrorResult bool, err error) {
func getFunctionType(fn *reflect.Value, enabledFeatures Features) (fk FunctionKind, ft *FunctionType, err error) {
p := fn.Type()
if fn.Kind() != reflect.Func {
@@ -62,16 +61,12 @@ func getFunctionType(fn *reflect.Value, allowErrorResult bool) (fk FunctionKind,
}
rCount := p.NumOut()
if (allowErrorResult && rCount > 2) || (!allowErrorResult && rCount > 1) {
err = errors.New("multiple results are unsupported")
return
}
if allowErrorResult && rCount > 0 {
maybeErrIdx := rCount - 1
if p.Out(maybeErrIdx).Implements(errorType) {
hasErrorResult = true
rCount--
if rCount > 1 {
// Guard >1.0 feature multi-value
if err = enabledFeatures.Require(FeatureMultiValue); err != nil {
err = fmt.Errorf("multiple result types invalid as %v", err)
return
}
}
@@ -100,21 +95,21 @@ func getFunctionType(fn *reflect.Value, allowErrorResult bool) (fk FunctionKind,
return
}
if rCount == 0 {
return
}
for i := 0; i < len(ft.Results); i++ {
rI := p.Out(i)
if t, ok := getTypeOf(rI.Kind()); ok {
ft.Results[i] = t
continue
}
result := p.Out(0)
if t, ok := getTypeOf(result.Kind()); ok {
ft.Results[0] = t
// Now, we will definitely err, decide which message is best
if rI.Implements(errorType) {
err = fmt.Errorf("result[%d] is an error, which is unsupported", i)
} else {
err = fmt.Errorf("result[%d] is unsupported: %s", i, rI.Kind())
}
return
}
if result.Implements(errorType) {
err = errors.New("result[0] is an error, which is unsupported")
} else {
err = fmt.Errorf("result[0] is unsupported: %s", result.Kind())
}
return
}

View File

@@ -10,18 +10,14 @@ import (
"github.com/tetratelabs/wazero/api"
)
type errno uint32
func TestGetFunctionType(t *testing.T) {
i32, i64, f32, f64 := ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64
tests := []struct {
name string
inputFunc interface{}
allowErrorResult bool
expectedKind FunctionKind
expectedType *FunctionType
expectErrorResult bool
var tests = []struct {
name string
inputFunc interface{}
expectedKind FunctionKind
expectedType *FunctionType
}{
{
name: "nullary",
@@ -29,43 +25,6 @@ func TestGetFunctionType(t *testing.T) {
expectedKind: FunctionKindGoNoContext,
expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
},
{
name: "nullary allowErrorResult",
inputFunc: func() {},
allowErrorResult: true,
expectedKind: FunctionKindGoNoContext,
expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
},
{
name: "void error result",
inputFunc: func() error { return nil },
allowErrorResult: true,
expectedKind: FunctionKindGoNoContext,
expectErrorResult: true,
expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{}},
},
{
name: "void uint32 allowErrorResult",
inputFunc: func() uint32 { return 0 },
allowErrorResult: true,
expectedKind: FunctionKindGoNoContext,
expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{i32}},
},
{
name: "void type uint32 allowErrorResult",
inputFunc: func() errno { return 0 },
allowErrorResult: true,
expectedKind: FunctionKindGoNoContext,
expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{i32}},
},
{
name: "void (uint32,error) results",
inputFunc: func() (uint32, error) { return 0, nil },
allowErrorResult: true,
expectedKind: FunctionKindGoNoContext,
expectErrorResult: true,
expectedType: &FunctionType{Params: []ValueType{}, Results: []ValueType{i32}},
},
{
name: "wasm.Module void return",
inputFunc: func(api.Module) {},
@@ -84,6 +43,12 @@ func TestGetFunctionType(t *testing.T) {
expectedKind: FunctionKindGoNoContext,
expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
},
{
name: "all supported params and all supported results",
inputFunc: func(uint32, uint64, float32, float64) (uint32, uint64, float32, float64) { return 0, 0, 0, 0 },
expectedKind: FunctionKindGoNoContext,
expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32, i64, f32, f64}},
},
{
name: "all supported params and i32 result - wasm.Module",
inputFunc: func(api.Module, uint32, uint64, float32, float64) uint32 { return 0 },
@@ -97,17 +62,15 @@ func TestGetFunctionType(t *testing.T) {
expectedType: &FunctionType{Params: []ValueType{i32, i64, f32, f64}, Results: []ValueType{i32}},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
rVal := reflect.ValueOf(tc.inputFunc)
fk, ft, hasErrorResult, err := getFunctionType(&rVal, tc.allowErrorResult)
fk, ft, err := getFunctionType(&rVal, Features20191205|FeatureMultiValue)
require.NoError(t, err)
require.Equal(t, tc.expectedKind, fk)
require.Equal(t, tc.expectedType, ft)
require.Equal(t, tc.expectErrorResult, hasErrorResult)
})
}
}
@@ -140,9 +103,9 @@ func TestGetFunctionTypeErrors(t *testing.T) {
expectedErr: "result[0] is an error, which is unsupported",
},
{
name: "multiple results",
name: "multiple results - multi-value not enabled",
input: func() (uint64, uint32) { return 0, 0 },
expectedErr: "multiple results are unsupported",
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "multiple context types",
@@ -166,7 +129,7 @@ func TestGetFunctionTypeErrors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
rVal := reflect.ValueOf(tc.input)
_, _, _, err := getFunctionType(&rVal, tc.allowErrorResult)
_, _, err := getFunctionType(&rVal, Features20191205)
require.EqualError(t, err, tc.expectedErr)
})
}

View File

@@ -10,7 +10,13 @@ import (
)
// NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small.
func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameToMemory map[string]*Memory, nameToGlobal map[string]*Global) (m *Module, err error) {
func NewHostModule(
moduleName string,
nameToGoFunc map[string]interface{},
nameToMemory map[string]*Memory,
nameToGlobal map[string]*Global,
enabledFeatures Features,
) (m *Module, err error) {
if moduleName != "" {
m = &Module{NameSection: &NameSection{ModuleName: moduleName}}
} else {
@@ -26,7 +32,7 @@ func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameT
}
if funcCount > 0 {
if err = addFuncs(m, nameToGoFunc); err != nil {
if err = addFuncs(m, nameToGoFunc, enabledFeatures); err != nil {
return
}
}
@@ -37,6 +43,7 @@ func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameT
}
}
// TODO: we can use enabledFeatures to fail early on things like mutable globals (once supported)
if globalCount > 0 {
if err = addGlobals(m, nameToGlobal); err != nil {
return
@@ -45,7 +52,7 @@ func NewHostModule(moduleName string, nameToGoFunc map[string]interface{}, nameT
return
}
func addFuncs(m *Module, nameToGoFunc map[string]interface{}) error {
func addFuncs(m *Module, nameToGoFunc map[string]interface{}, enabledFeatures Features) error {
funcCount := uint32(len(nameToGoFunc))
funcNames := make([]string, 0, funcCount)
if m.NameSection == nil {
@@ -64,7 +71,7 @@ func addFuncs(m *Module, nameToGoFunc map[string]interface{}) error {
for idx := Index(0); idx < funcCount; idx++ {
name := funcNames[idx]
fn := reflect.ValueOf(nameToGoFunc[name])
_, functionType, _, err := getFunctionType(&fn, false)
_, functionType, err := getFunctionType(&fn, enabledFeatures)
if err != nil {
return fmt.Errorf("func[%s] %w", name, err)
}

View File

@@ -26,6 +26,10 @@ func (a *wasiAPI) FdWrite(ctx api.Module, fd, iovs, iovsCount, resultSize uint32
return 0
}
func swap(x, y uint32) (uint32, uint32) {
return y, x
}
func TestNewHostModule(t *testing.T) {
i32 := ValueTypeI32
@@ -34,6 +38,8 @@ func TestNewHostModule(t *testing.T) {
fnArgsSizesGet := reflect.ValueOf(a.ArgsSizesGet)
functionFdWrite := "fd_write"
fnFdWrite := reflect.ValueOf(a.FdWrite)
functionSwap := "swap"
fnSwap := reflect.ValueOf(swap)
tests := []struct {
name, moduleName string
@@ -78,6 +84,20 @@ func TestNewHostModule(t *testing.T) {
},
},
},
{
name: "multi-value",
moduleName: "swapper",
nameToGoFunc: map[string]interface{}{
functionSwap: swap,
},
expected: &Module{
TypeSection: []*FunctionType{{Params: []ValueType{i32, i32}, Results: []ValueType{i32, i32}}},
FunctionSection: []Index{0},
HostFunctionSection: []*reflect.Value{&fnSwap},
ExportSection: map[string]*Export{"swap": {Name: "swap", Type: ExternTypeFunc, Index: 0}},
NameSection: &NameSection{ModuleName: "swapper", FunctionNames: NameMap{{Index: 0, Name: "swap"}}},
},
},
{
name: "memory",
nameToMemory: map[string]*Memory{"memory": {1, 2}},
@@ -164,7 +184,13 @@ func TestNewHostModule(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
m, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal)
m, e := NewHostModule(
tc.moduleName,
tc.nameToGoFunc,
tc.nameToMemory,
tc.nameToGlobal,
Features20191205|FeatureMultiValue,
)
require.NoError(t, e)
requireHostModuleEquals(t, tc.expected, m)
})
@@ -206,6 +232,12 @@ func TestNewHostModule_Errors(t *testing.T) {
nameToGoFunc: map[string]interface{}{"fn": t},
expectedErr: "func[fn] kind != func: ptr",
},
{
name: "function has multiple results",
nameToGoFunc: map[string]interface{}{"fn": func() (uint32, uint32) { return 0, 0 }},
nameToMemory: map[string]*Memory{"fn": {1, 1}},
expectedErr: "func[fn] multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "memory collides on func name",
nameToGoFunc: map[string]interface{}{"fn": ArgsSizesGet},
@@ -234,7 +266,7 @@ func TestNewHostModule_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal)
_, e := NewHostModule(tc.moduleName, tc.nameToGoFunc, tc.nameToMemory, tc.nameToGlobal, Features20191205)
require.EqualError(t, e, tc.expectedErr)
})
}

View File

@@ -21,8 +21,22 @@ const (
// breaks out to after the OpcodeEnd on the enclosing OpcodeIf.
OpcodeElse Opcode = 0x05
// OpcodeEnd terminates a control instruction OpcodeBlock, OpcodeLoop or OpcodeIf.
OpcodeEnd Opcode = 0x0b
OpcodeBr Opcode = 0x0c
OpcodeEnd Opcode = 0x0b
// OpcodeBr is a stack-polymorphic opcode that performs an unconditional branch. How the stack is modified depends
// on whether the "br" is enclosed by a loop, and if FeatureMultiValue is enabled.
//
// Here are the rules in pseudocode about how the stack is modified based on the "br" operand L (label):
// if L is loop: append(L.originalStackWithoutInputs, N-values popped from the stack) where N == L.inputs
// else: append(L.originalStackWithoutInputs, N-values popped from the stack) where N == L.results
//
// In WebAssembly 1.0 (20191205), N can be zero or one. When FeatureMultiValue is enabled, N can be more than one,
// depending on the type use of the label L.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-controlmathsfbrl
OpcodeBr Opcode = 0x0c
// ^^ TODO: Add a diagram to help explain br l means that branch into AFTER l for non-loop labels
OpcodeBrIf Opcode = 0x0d
OpcodeBrTable Opcode = 0x0e
OpcodeReturn Opcode = 0x0f
@@ -240,186 +254,369 @@ const (
LastOpcode = OpcodeI64Extend32S
)
var instructionNames = [256]string{
OpcodeUnreachable: "unreachable",
OpcodeNop: "nop",
OpcodeBlock: "block",
OpcodeLoop: "loop",
OpcodeIf: "if",
OpcodeElse: "else",
OpcodeEnd: "end",
OpcodeBr: "br",
OpcodeBrIf: "br_if",
OpcodeBrTable: "br_table",
OpcodeReturn: "return",
OpcodeCall: "call",
OpcodeCallIndirect: "call_indirect",
OpcodeDrop: "drop",
OpcodeSelect: "select",
OpcodeLocalGet: "local.get",
OpcodeLocalSet: "local.set",
OpcodeLocalTee: "local.tee",
OpcodeGlobalGet: "global.get",
OpcodeGlobalSet: "global.set",
OpcodeI32Load: "i32.load",
OpcodeI64Load: "i64.load",
OpcodeF32Load: "f32.load",
OpcodeF64Load: "f64.load",
OpcodeI32Load8S: "i32.load8_s",
OpcodeI32Load8U: "i32.load8_u",
OpcodeI32Load16S: "i32.load16_s",
OpcodeI32Load16U: "i32.load16_u",
OpcodeI64Load8S: "i64.load8_s",
OpcodeI64Load8U: "i64.load8_u",
OpcodeI64Load16S: "i64.load16_s",
OpcodeI64Load16U: "i64.load16_u",
OpcodeI64Load32S: "i64.load32_s",
OpcodeI64Load32U: "i64.load32_u",
OpcodeI32Store: "i32.store",
OpcodeI64Store: "i64.store",
OpcodeF32Store: "f32.store",
OpcodeF64Store: "f64.store",
OpcodeI32Store8: "i32.store8",
OpcodeI32Store16: "i32.store16",
OpcodeI64Store8: "i64.store8",
OpcodeI64Store16: "i64.store16",
OpcodeI64Store32: "i64.store32",
OpcodeMemorySize: "memory.size",
OpcodeMemoryGrow: "memory.grow",
OpcodeI32Const: "i32.const",
OpcodeI64Const: "i64.const",
OpcodeF32Const: "f32.const",
OpcodeF64Const: "f64.const",
OpcodeI32Eqz: "i32.eqz",
OpcodeI32Eq: "i32.eq",
OpcodeI32Ne: "i32.ne",
OpcodeI32LtS: "i32.lt_s",
OpcodeI32LtU: "i32.lt_u",
OpcodeI32GtS: "i32.gt_s",
OpcodeI32GtU: "i32.gt_u",
OpcodeI32LeS: "i32.le_s",
OpcodeI32LeU: "i32.le_u",
OpcodeI32GeS: "i32.ge_s",
OpcodeI32GeU: "i32.ge_u",
OpcodeI64Eqz: "i64.eqz",
OpcodeI64Eq: "i64.eq",
OpcodeI64Ne: "i64.ne",
OpcodeI64LtS: "i64.lt_s",
OpcodeI64LtU: "i64.lt_u",
OpcodeI64GtS: "i64.gt_s",
OpcodeI64GtU: "i64.gt_u",
OpcodeI64LeS: "i64.le_s",
OpcodeI64LeU: "i64.le_u",
OpcodeI64GeS: "i64.ge_s",
OpcodeI64GeU: "i64.ge_u",
OpcodeF32Eq: "f32.eq",
OpcodeF32Ne: "f32.ne",
OpcodeF32Lt: "f32.lt",
OpcodeF32Gt: "f32.gt",
OpcodeF32Le: "f32.le",
OpcodeF32Ge: "f32.ge",
OpcodeF64Eq: "f64.eq",
OpcodeF64Ne: "f64.ne",
OpcodeF64Lt: "f64.lt",
OpcodeF64Gt: "f64.gt",
OpcodeF64Le: "f64.le",
OpcodeF64Ge: "f64.ge",
OpcodeI32Clz: "i32.clz",
OpcodeI32Ctz: "i32.ctz",
OpcodeI32Popcnt: "i32.popcnt",
OpcodeI32Add: "i32.add",
OpcodeI32Sub: "i32.sub",
OpcodeI32Mul: "i32.mul",
OpcodeI32DivS: "i32.div_s",
OpcodeI32DivU: "i32.div_u",
OpcodeI32RemS: "i32.rem_s",
OpcodeI32RemU: "i32.rem_u",
OpcodeI32And: "i32.and",
OpcodeI32Or: "i32.or",
OpcodeI32Xor: "i32.xor",
OpcodeI32Shl: "i32.shl",
OpcodeI32ShrS: "i32.shr_s",
OpcodeI32ShrU: "i32.shr_u",
OpcodeI32Rotl: "i32.rotl",
OpcodeI32Rotr: "i32.rotr",
OpcodeI64Clz: "i64.clz",
OpcodeI64Ctz: "i64.ctz",
OpcodeI64Popcnt: "i64.popcnt",
OpcodeI64Add: "i64.add",
OpcodeI64Sub: "i64.sub",
OpcodeI64Mul: "i64.mul",
OpcodeI64DivS: "i64.div_s",
OpcodeI64DivU: "i64.div_u",
OpcodeI64RemS: "i64.rem_s",
OpcodeI64RemU: "i64.rem_u",
OpcodeI64And: "i64.and",
OpcodeI64Or: "i64.or",
OpcodeI64Xor: "i64.xor",
OpcodeI64Shl: "i64.shl",
OpcodeI64ShrS: "i64.shr_s",
OpcodeI64ShrU: "i64.shr_u",
OpcodeI64Rotl: "i64.rotl",
OpcodeI64Rotr: "i64.rotr",
OpcodeF32Abs: "f32.abs",
OpcodeF32Neg: "f32.neg",
OpcodeF32Ceil: "f32.ceil",
OpcodeF32Floor: "f32.floor",
OpcodeF32Trunc: "f32.trunc",
OpcodeF32Nearest: "f32.nearest",
OpcodeF32Sqrt: "f32.sqrt",
OpcodeF32Add: "f32.add",
OpcodeF32Sub: "f32.sub",
OpcodeF32Mul: "f32.mul",
OpcodeF32Div: "f32.div",
OpcodeF32Min: "f32.min",
OpcodeF32Max: "f32.max",
OpcodeF32Copysign: "f32.copysign",
OpcodeF64Abs: "f64.abs",
OpcodeF64Neg: "f64.neg",
OpcodeF64Ceil: "f64.ceil",
OpcodeF64Floor: "f64.floor",
OpcodeF64Trunc: "f64.trunc",
OpcodeF64Nearest: "f64.nearest",
OpcodeF64Sqrt: "f64.sqrt",
OpcodeF64Add: "f64.add",
OpcodeF64Sub: "f64.sub",
OpcodeF64Mul: "f64.mul",
OpcodeF64Div: "f64.div",
OpcodeF64Min: "f64.min",
OpcodeF64Max: "f64.max",
OpcodeF64Copysign: "f64.copysign",
OpcodeI32WrapI64: "i32.wrap_i64",
OpcodeI32TruncF32S: "i32.trunc_f32_s",
OpcodeI32TruncF32U: "i32.trunc_f32_u",
OpcodeI32TruncF64S: "i32.trunc_f64_s",
OpcodeI32TruncF64U: "i32.trunc_f64_u",
OpcodeI64ExtendI32S: "i64.extend_i32_s",
OpcodeI64ExtendI32U: "i64.extend_i32_u",
OpcodeI64TruncF32S: "i64.trunc_f32_s",
OpcodeI64TruncF32U: "i64.trunc_f32_u",
OpcodeI64TruncF64S: "i64.trunc_f64_s",
OpcodeI64TruncF64U: "i64.trunc_f64_u",
OpcodeF32ConvertI32s: "f32.convert_i32_s",
OpcodeF32ConvertI32U: "f32.convert_i32_u",
OpcodeF32ConvertI64S: "f32.convert_i64_s",
OpcodeF32ConvertI64U: "f32.convert_i64u",
OpcodeF32DemoteF64: "f32.demote_f64",
OpcodeF64ConvertI32S: "f64.convert_i32_s",
OpcodeF64ConvertI32U: "f64.convert_i32_u",
OpcodeF64ConvertI64S: "f64.convert_i64_s",
OpcodeF64ConvertI64U: "f64.convert_i64_u",
OpcodeF64PromoteF32: "f64.promote_f32",
OpcodeI32ReinterpretF32: "i32.reinterpret_f32",
OpcodeI64ReinterpretF64: "i64.reinterpret_f64",
OpcodeF32ReinterpretI32: "f32.reinterpret_i32",
OpcodeF64ReinterpretI64: "f64.reinterpret_i64",
const (
OpcodeUnreachableName = "unreachable"
OpcodeNopName = "nop"
OpcodeBlockName = "block"
OpcodeLoopName = "loop"
OpcodeIfName = "if"
OpcodeElseName = "else"
OpcodeEndName = "end"
OpcodeBrName = "br"
OpcodeBrIfName = "br_if"
OpcodeBrTableName = "br_table"
OpcodeReturnName = "return"
OpcodeCallName = "call"
OpcodeCallIndirectName = "call_indirect"
OpcodeDropName = "drop"
OpcodeSelectName = "select"
OpcodeLocalGetName = "local.get"
OpcodeLocalSetName = "local.set"
OpcodeLocalTeeName = "local.tee"
OpcodeGlobalGetName = "global.get"
OpcodeGlobalSetName = "global.set"
OpcodeI32LoadName = "i32.load"
OpcodeI64LoadName = "i64.load"
OpcodeF32LoadName = "f32.load"
OpcodeF64LoadName = "f64.load"
OpcodeI32Load8SName = "i32.load8_s"
OpcodeI32Load8UName = "i32.load8_u"
OpcodeI32Load16SName = "i32.load16_s"
OpcodeI32Load16UName = "i32.load16_u"
OpcodeI64Load8SName = "i64.load8_s"
OpcodeI64Load8UName = "i64.load8_u"
OpcodeI64Load16SName = "i64.load16_s"
OpcodeI64Load16UName = "i64.load16_u"
OpcodeI64Load32SName = "i64.load32_s"
OpcodeI64Load32UName = "i64.load32_u"
OpcodeI32StoreName = "i32.store"
OpcodeI64StoreName = "i64.store"
OpcodeF32StoreName = "f32.store"
OpcodeF64StoreName = "f64.store"
OpcodeI32Store8Name = "i32.store8"
OpcodeI32Store16Name = "i32.store16"
OpcodeI64Store8Name = "i64.store8"
OpcodeI64Store16Name = "i64.store16"
OpcodeI64Store32Name = "i64.store32"
OpcodeMemorySizeName = "memory.size"
OpcodeMemoryGrowName = "memory.grow"
OpcodeI32ConstName = "i32.const"
OpcodeI64ConstName = "i64.const"
OpcodeF32ConstName = "f32.const"
OpcodeF64ConstName = "f64.const"
OpcodeI32EqzName = "i32.eqz"
OpcodeI32EqName = "i32.eq"
OpcodeI32NeName = "i32.ne"
OpcodeI32LtSName = "i32.lt_s"
OpcodeI32LtUName = "i32.lt_u"
OpcodeI32GtSName = "i32.gt_s"
OpcodeI32GtUName = "i32.gt_u"
OpcodeI32LeSName = "i32.le_s"
OpcodeI32LeUName = "i32.le_u"
OpcodeI32GeSName = "i32.ge_s"
OpcodeI32GeUName = "i32.ge_u"
OpcodeI64EqzName = "i64.eqz"
OpcodeI64EqName = "i64.eq"
OpcodeI64NeName = "i64.ne"
OpcodeI64LtSName = "i64.lt_s"
OpcodeI64LtUName = "i64.lt_u"
OpcodeI64GtSName = "i64.gt_s"
OpcodeI64GtUName = "i64.gt_u"
OpcodeI64LeSName = "i64.le_s"
OpcodeI64LeUName = "i64.le_u"
OpcodeI64GeSName = "i64.ge_s"
OpcodeI64GeUName = "i64.ge_u"
OpcodeF32EqName = "f32.eq"
OpcodeF32NeName = "f32.ne"
OpcodeF32LtName = "f32.lt"
OpcodeF32GtName = "f32.gt"
OpcodeF32LeName = "f32.le"
OpcodeF32GeName = "f32.ge"
OpcodeF64EqName = "f64.eq"
OpcodeF64NeName = "f64.ne"
OpcodeF64LtName = "f64.lt"
OpcodeF64GtName = "f64.gt"
OpcodeF64LeName = "f64.le"
OpcodeF64GeName = "f64.ge"
OpcodeI32ClzName = "i32.clz"
OpcodeI32CtzName = "i32.ctz"
OpcodeI32PopcntName = "i32.popcnt"
OpcodeI32AddName = "i32.add"
OpcodeI32SubName = "i32.sub"
OpcodeI32MulName = "i32.mul"
OpcodeI32DivSName = "i32.div_s"
OpcodeI32DivUName = "i32.div_u"
OpcodeI32RemSName = "i32.rem_s"
OpcodeI32RemUName = "i32.rem_u"
OpcodeI32AndName = "i32.and"
OpcodeI32OrName = "i32.or"
OpcodeI32XorName = "i32.xor"
OpcodeI32ShlName = "i32.shl"
OpcodeI32ShrSName = "i32.shr_s"
OpcodeI32ShrUName = "i32.shr_u"
OpcodeI32RotlName = "i32.rotl"
OpcodeI32RotrName = "i32.rotr"
OpcodeI64ClzName = "i64.clz"
OpcodeI64CtzName = "i64.ctz"
OpcodeI64PopcntName = "i64.popcnt"
OpcodeI64AddName = "i64.add"
OpcodeI64SubName = "i64.sub"
OpcodeI64MulName = "i64.mul"
OpcodeI64DivSName = "i64.div_s"
OpcodeI64DivUName = "i64.div_u"
OpcodeI64RemSName = "i64.rem_s"
OpcodeI64RemUName = "i64.rem_u"
OpcodeI64AndName = "i64.and"
OpcodeI64OrName = "i64.or"
OpcodeI64XorName = "i64.xor"
OpcodeI64ShlName = "i64.shl"
OpcodeI64ShrSName = "i64.shr_s"
OpcodeI64ShrUName = "i64.shr_u"
OpcodeI64RotlName = "i64.rotl"
OpcodeI64RotrName = "i64.rotr"
OpcodeF32AbsName = "f32.abs"
OpcodeF32NegName = "f32.neg"
OpcodeF32CeilName = "f32.ceil"
OpcodeF32FloorName = "f32.floor"
OpcodeF32TruncName = "f32.trunc"
OpcodeF32NearestName = "f32.nearest"
OpcodeF32SqrtName = "f32.sqrt"
OpcodeF32AddName = "f32.add"
OpcodeF32SubName = "f32.sub"
OpcodeF32MulName = "f32.mul"
OpcodeF32DivName = "f32.div"
OpcodeF32MinName = "f32.min"
OpcodeF32MaxName = "f32.max"
OpcodeF32CopysignName = "f32.copysign"
OpcodeF64AbsName = "f64.abs"
OpcodeF64NegName = "f64.neg"
OpcodeF64CeilName = "f64.ceil"
OpcodeF64FloorName = "f64.floor"
OpcodeF64TruncName = "f64.trunc"
OpcodeF64NearestName = "f64.nearest"
OpcodeF64SqrtName = "f64.sqrt"
OpcodeF64AddName = "f64.add"
OpcodeF64SubName = "f64.sub"
OpcodeF64MulName = "f64.mul"
OpcodeF64DivName = "f64.div"
OpcodeF64MinName = "f64.min"
OpcodeF64MaxName = "f64.max"
OpcodeF64CopysignName = "f64.copysign"
OpcodeI32WrapI64Name = "i32.wrap_i64"
OpcodeI32TruncF32SName = "i32.trunc_f32_s"
OpcodeI32TruncF32UName = "i32.trunc_f32_u"
OpcodeI32TruncF64SName = "i32.trunc_f64_s"
OpcodeI32TruncF64UName = "i32.trunc_f64_u"
OpcodeI64ExtendI32SName = "i64.extend_i32_s"
OpcodeI64ExtendI32UName = "i64.extend_i32_u"
OpcodeI64TruncF32SName = "i64.trunc_f32_s"
OpcodeI64TruncF32UName = "i64.trunc_f32_u"
OpcodeI64TruncF64SName = "i64.trunc_f64_s"
OpcodeI64TruncF64UName = "i64.trunc_f64_u"
OpcodeF32ConvertI32sName = "f32.convert_i32_s"
OpcodeF32ConvertI32UName = "f32.convert_i32_u"
OpcodeF32ConvertI64SName = "f32.convert_i64_s"
OpcodeF32ConvertI64UName = "f32.convert_i64u"
OpcodeF32DemoteF64Name = "f32.demote_f64"
OpcodeF64ConvertI32SName = "f64.convert_i32_s"
OpcodeF64ConvertI32UName = "f64.convert_i32_u"
OpcodeF64ConvertI64SName = "f64.convert_i64_s"
OpcodeF64ConvertI64UName = "f64.convert_i64_u"
OpcodeF64PromoteF32Name = "f64.promote_f32"
OpcodeI32ReinterpretF32Name = "i32.reinterpret_f32"
OpcodeI64ReinterpretF64Name = "i64.reinterpret_f64"
OpcodeF32ReinterpretI32Name = "f32.reinterpret_i32"
OpcodeF64ReinterpretI64Name = "f64.reinterpret_i64"
// Below are toggled with FeatureSignExtensionOps
OpcodeI32Extend8S: "i32.extend8_s",
OpcodeI32Extend16S: "i32.extend16_s",
OpcodeI64Extend8S: "i64.extend8_s",
OpcodeI64Extend16S: "i64.extend16_s",
OpcodeI64Extend32S: "i64.extend32_s",
OpcodeI32Extend8SName = "i32.extend8_s"
OpcodeI32Extend16SName = "i32.extend16_s"
OpcodeI64Extend8SName = "i64.extend8_s"
OpcodeI64Extend16SName = "i64.extend16_s"
OpcodeI64Extend32SName = "i64.extend32_s"
)
var instructionNames = [256]string{
OpcodeUnreachable: OpcodeUnreachableName,
OpcodeNop: OpcodeNopName,
OpcodeBlock: OpcodeBlockName,
OpcodeLoop: OpcodeLoopName,
OpcodeIf: OpcodeIfName,
OpcodeElse: OpcodeElseName,
OpcodeEnd: OpcodeEndName,
OpcodeBr: OpcodeBrName,
OpcodeBrIf: OpcodeBrIfName,
OpcodeBrTable: OpcodeBrTableName,
OpcodeReturn: OpcodeReturnName,
OpcodeCall: OpcodeCallName,
OpcodeCallIndirect: OpcodeCallIndirectName,
OpcodeDrop: OpcodeDropName,
OpcodeSelect: OpcodeSelectName,
OpcodeLocalGet: OpcodeLocalGetName,
OpcodeLocalSet: OpcodeLocalSetName,
OpcodeLocalTee: OpcodeLocalTeeName,
OpcodeGlobalGet: OpcodeGlobalGetName,
OpcodeGlobalSet: OpcodeGlobalSetName,
OpcodeI32Load: OpcodeI32LoadName,
OpcodeI64Load: OpcodeI64LoadName,
OpcodeF32Load: OpcodeF32LoadName,
OpcodeF64Load: OpcodeF64LoadName,
OpcodeI32Load8S: OpcodeI32Load8SName,
OpcodeI32Load8U: OpcodeI32Load8UName,
OpcodeI32Load16S: OpcodeI32Load16SName,
OpcodeI32Load16U: OpcodeI32Load16UName,
OpcodeI64Load8S: OpcodeI64Load8SName,
OpcodeI64Load8U: OpcodeI64Load8UName,
OpcodeI64Load16S: OpcodeI64Load16SName,
OpcodeI64Load16U: OpcodeI64Load16UName,
OpcodeI64Load32S: OpcodeI64Load32SName,
OpcodeI64Load32U: OpcodeI64Load32UName,
OpcodeI32Store: OpcodeI32StoreName,
OpcodeI64Store: OpcodeI64StoreName,
OpcodeF32Store: OpcodeF32StoreName,
OpcodeF64Store: OpcodeF64StoreName,
OpcodeI32Store8: OpcodeI32Store8Name,
OpcodeI32Store16: OpcodeI32Store16Name,
OpcodeI64Store8: OpcodeI64Store8Name,
OpcodeI64Store16: OpcodeI64Store16Name,
OpcodeI64Store32: OpcodeI64Store32Name,
OpcodeMemorySize: OpcodeMemorySizeName,
OpcodeMemoryGrow: OpcodeMemoryGrowName,
OpcodeI32Const: OpcodeI32ConstName,
OpcodeI64Const: OpcodeI64ConstName,
OpcodeF32Const: OpcodeF32ConstName,
OpcodeF64Const: OpcodeF64ConstName,
OpcodeI32Eqz: OpcodeI32EqzName,
OpcodeI32Eq: OpcodeI32EqName,
OpcodeI32Ne: OpcodeI32NeName,
OpcodeI32LtS: OpcodeI32LtSName,
OpcodeI32LtU: OpcodeI32LtUName,
OpcodeI32GtS: OpcodeI32GtSName,
OpcodeI32GtU: OpcodeI32GtUName,
OpcodeI32LeS: OpcodeI32LeSName,
OpcodeI32LeU: OpcodeI32LeUName,
OpcodeI32GeS: OpcodeI32GeSName,
OpcodeI32GeU: OpcodeI32GeUName,
OpcodeI64Eqz: OpcodeI64EqzName,
OpcodeI64Eq: OpcodeI64EqName,
OpcodeI64Ne: OpcodeI64NeName,
OpcodeI64LtS: OpcodeI64LtSName,
OpcodeI64LtU: OpcodeI64LtUName,
OpcodeI64GtS: OpcodeI64GtSName,
OpcodeI64GtU: OpcodeI64GtUName,
OpcodeI64LeS: OpcodeI64LeSName,
OpcodeI64LeU: OpcodeI64LeUName,
OpcodeI64GeS: OpcodeI64GeSName,
OpcodeI64GeU: OpcodeI64GeUName,
OpcodeF32Eq: OpcodeF32EqName,
OpcodeF32Ne: OpcodeF32NeName,
OpcodeF32Lt: OpcodeF32LtName,
OpcodeF32Gt: OpcodeF32GtName,
OpcodeF32Le: OpcodeF32LeName,
OpcodeF32Ge: OpcodeF32GeName,
OpcodeF64Eq: OpcodeF64EqName,
OpcodeF64Ne: OpcodeF64NeName,
OpcodeF64Lt: OpcodeF64LtName,
OpcodeF64Gt: OpcodeF64GtName,
OpcodeF64Le: OpcodeF64LeName,
OpcodeF64Ge: OpcodeF64GeName,
OpcodeI32Clz: OpcodeI32ClzName,
OpcodeI32Ctz: OpcodeI32CtzName,
OpcodeI32Popcnt: OpcodeI32PopcntName,
OpcodeI32Add: OpcodeI32AddName,
OpcodeI32Sub: OpcodeI32SubName,
OpcodeI32Mul: OpcodeI32MulName,
OpcodeI32DivS: OpcodeI32DivSName,
OpcodeI32DivU: OpcodeI32DivUName,
OpcodeI32RemS: OpcodeI32RemSName,
OpcodeI32RemU: OpcodeI32RemUName,
OpcodeI32And: OpcodeI32AndName,
OpcodeI32Or: OpcodeI32OrName,
OpcodeI32Xor: OpcodeI32XorName,
OpcodeI32Shl: OpcodeI32ShlName,
OpcodeI32ShrS: OpcodeI32ShrSName,
OpcodeI32ShrU: OpcodeI32ShrUName,
OpcodeI32Rotl: OpcodeI32RotlName,
OpcodeI32Rotr: OpcodeI32RotrName,
OpcodeI64Clz: OpcodeI64ClzName,
OpcodeI64Ctz: OpcodeI64CtzName,
OpcodeI64Popcnt: OpcodeI64PopcntName,
OpcodeI64Add: OpcodeI64AddName,
OpcodeI64Sub: OpcodeI64SubName,
OpcodeI64Mul: OpcodeI64MulName,
OpcodeI64DivS: OpcodeI64DivSName,
OpcodeI64DivU: OpcodeI64DivUName,
OpcodeI64RemS: OpcodeI64RemSName,
OpcodeI64RemU: OpcodeI64RemUName,
OpcodeI64And: OpcodeI64AndName,
OpcodeI64Or: OpcodeI64OrName,
OpcodeI64Xor: OpcodeI64XorName,
OpcodeI64Shl: OpcodeI64ShlName,
OpcodeI64ShrS: OpcodeI64ShrSName,
OpcodeI64ShrU: OpcodeI64ShrUName,
OpcodeI64Rotl: OpcodeI64RotlName,
OpcodeI64Rotr: OpcodeI64RotrName,
OpcodeF32Abs: OpcodeF32AbsName,
OpcodeF32Neg: OpcodeF32NegName,
OpcodeF32Ceil: OpcodeF32CeilName,
OpcodeF32Floor: OpcodeF32FloorName,
OpcodeF32Trunc: OpcodeF32TruncName,
OpcodeF32Nearest: OpcodeF32NearestName,
OpcodeF32Sqrt: OpcodeF32SqrtName,
OpcodeF32Add: OpcodeF32AddName,
OpcodeF32Sub: OpcodeF32SubName,
OpcodeF32Mul: OpcodeF32MulName,
OpcodeF32Div: OpcodeF32DivName,
OpcodeF32Min: OpcodeF32MinName,
OpcodeF32Max: OpcodeF32MaxName,
OpcodeF32Copysign: OpcodeF32CopysignName,
OpcodeF64Abs: OpcodeF64AbsName,
OpcodeF64Neg: OpcodeF64NegName,
OpcodeF64Ceil: OpcodeF64CeilName,
OpcodeF64Floor: OpcodeF64FloorName,
OpcodeF64Trunc: OpcodeF64TruncName,
OpcodeF64Nearest: OpcodeF64NearestName,
OpcodeF64Sqrt: OpcodeF64SqrtName,
OpcodeF64Add: OpcodeF64AddName,
OpcodeF64Sub: OpcodeF64SubName,
OpcodeF64Mul: OpcodeF64MulName,
OpcodeF64Div: OpcodeF64DivName,
OpcodeF64Min: OpcodeF64MinName,
OpcodeF64Max: OpcodeF64MaxName,
OpcodeF64Copysign: OpcodeF64CopysignName,
OpcodeI32WrapI64: OpcodeI32WrapI64Name,
OpcodeI32TruncF32S: OpcodeI32TruncF32SName,
OpcodeI32TruncF32U: OpcodeI32TruncF32UName,
OpcodeI32TruncF64S: OpcodeI32TruncF64SName,
OpcodeI32TruncF64U: OpcodeI32TruncF64UName,
OpcodeI64ExtendI32S: OpcodeI64ExtendI32SName,
OpcodeI64ExtendI32U: OpcodeI64ExtendI32UName,
OpcodeI64TruncF32S: OpcodeI64TruncF32SName,
OpcodeI64TruncF32U: OpcodeI64TruncF32UName,
OpcodeI64TruncF64S: OpcodeI64TruncF64SName,
OpcodeI64TruncF64U: OpcodeI64TruncF64UName,
OpcodeF32ConvertI32s: OpcodeF32ConvertI32sName,
OpcodeF32ConvertI32U: OpcodeF32ConvertI32UName,
OpcodeF32ConvertI64S: OpcodeF32ConvertI64SName,
OpcodeF32ConvertI64U: OpcodeF32ConvertI64UName,
OpcodeF32DemoteF64: OpcodeF32DemoteF64Name,
OpcodeF64ConvertI32S: OpcodeF64ConvertI32SName,
OpcodeF64ConvertI32U: OpcodeF64ConvertI32UName,
OpcodeF64ConvertI64S: OpcodeF64ConvertI64SName,
OpcodeF64ConvertI64U: OpcodeF64ConvertI64UName,
OpcodeF64PromoteF32: OpcodeF64PromoteF32Name,
OpcodeI32ReinterpretF32: OpcodeI32ReinterpretF32Name,
OpcodeI64ReinterpretF64: OpcodeI64ReinterpretF64Name,
OpcodeF32ReinterpretI32: OpcodeF32ReinterpretI32Name,
OpcodeF64ReinterpretI64: OpcodeF64ReinterpretI64Name,
// Below are toggled with FeatureSignExtensionOps
OpcodeI32Extend8S: OpcodeI32Extend8SName,
OpcodeI32Extend16S: OpcodeI32Extend16SName,
OpcodeI64Extend8S: OpcodeI64Extend8SName,
OpcodeI64Extend16S: OpcodeI64Extend16SName,
OpcodeI64Extend32S: OpcodeI64Extend32SName,
}
// InstructionName returns the instruction corresponding to this binary Opcode.

View File

@@ -21,12 +21,14 @@ var callStackCeiling = buildoptions.CallStackCeiling
// engine is an interpreter implementation of wasm.Engine
type engine struct {
enabledFeatures wasm.Features
compiledFunctions map[*wasm.FunctionInstance]*compiledFunction // guarded by mutex.
mux sync.RWMutex
}
func NewEngine() wasm.Engine {
func NewEngine(enabledFeatures wasm.Features) wasm.Engine {
return &engine{
enabledFeatures: enabledFeatures,
compiledFunctions: make(map[*wasm.FunctionInstance]*compiledFunction),
}
}
@@ -172,7 +174,7 @@ func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions
for i, f := range moduleFunctions {
var compiled *compiledFunction
if f.Kind == wasm.FunctionKindWasm {
ir, err := wazeroir.Compile(f)
ir, err := wazeroir.Compile(e.enabledFeatures, f)
if err != nil {
me.Close()
// TODO(Adrian): extract Module.funcDesc so that errors here have more context
@@ -311,7 +313,7 @@ func (e *engine) lowerIROps(f *wasm.FunctionInstance,
op.us[1] = uint64(f.Module.Types[o.TypeIndex].TypeID)
case *wazeroir.OperationDrop:
op.rs = make([]*wazeroir.InclusiveRange, 1)
op.rs[0] = o.Range
op.rs[0] = o.Depth
case *wazeroir.OperationSelect:
case *wazeroir.OperationPick:
op.us = make([]uint64, 1)
@@ -629,7 +631,7 @@ func (ce *callEngine) callNativeFunc(ctx *wasm.ModuleContext, f *compiledFunctio
}
case wazeroir.OperationKindBrTable:
{
if v := int(ce.pop()); v < len(op.us)-1 {
if v := uint64(ce.pop()); v < uint64(len(op.us)-1) {
ce.drop(op.rs[v+1])
frame.pc = op.us[v+1]
} else {

View File

@@ -51,8 +51,8 @@ var et = &engineTester{}
type engineTester struct {
}
func (e engineTester) NewEngine() wasm.Engine {
return NewEngine()
func (e engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine {
return NewEngine(enabledFeatures)
}
func (e engineTester) InitTable(me wasm.ModuleEngine, initTableLen uint32, initTableIdxToFnIdx map[wasm.Index]wasm.Index) []interface{} {
@@ -198,7 +198,7 @@ func TestInterpreter_CallEngine_callNativeFunc_signExtend(t *testing.T) {
func TestInterpreter_EngineCompile_Errors(t *testing.T) {
t.Run("invalid import", func(t *testing.T) {
e := et.NewEngine().(*engine)
e := et.NewEngine(wasm.Features20191205).(*engine)
_, err := e.NewModuleEngine(t.Name(),
[]*wasm.FunctionInstance{{Module: &wasm.ModuleInstance{Name: "uncompiled"}, DebugName: "uncompiled.fn"}},
nil, // moduleFunctions
@@ -209,7 +209,7 @@ func TestInterpreter_EngineCompile_Errors(t *testing.T) {
})
t.Run("release on compilation error", func(t *testing.T) {
e := et.NewEngine().(*engine)
e := et.NewEngine(wasm.Features20191205).(*engine)
importedFunctions := []*wasm.FunctionInstance{
{DebugName: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
@@ -269,7 +269,7 @@ func TestInterpreter_Close(t *testing.T) {
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
e := et.NewEngine().(*engine)
e := et.NewEngine(wasm.Features20191205).(*engine)
if len(tc.importedFunctions) > 0 {
// initialize the module-engine containing imported functions
me, err := e.NewModuleEngine(t.Name(), nil, tc.importedFunctions, nil, nil)

View File

@@ -18,6 +18,7 @@ import (
type (
// engine is an JIT implementation of wasm.Engine
engine struct {
enabledFeatures wasm.Features
compiledFunctions map[*wasm.FunctionInstance]*compiledFunction // guarded by mutex.
mux sync.RWMutex
// setFinalizer defaults to runtime.SetFinalizer, but overridable for tests.
@@ -367,7 +368,7 @@ func (e *engine) NewModuleEngine(name string, importedFunctions, moduleFunctions
var compiled *compiledFunction
var err error
if f.Kind == wasm.FunctionKindWasm {
compiled, err = compileWasmFunction(f)
compiled, err = compileWasmFunction(e.enabledFeatures, f)
} else {
compiled, err = compileHostFunction(f)
}
@@ -513,12 +514,13 @@ func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, pa
return
}
func NewEngine() wasm.Engine {
return newEngine()
func NewEngine(enabledFeatures wasm.Features) wasm.Engine {
return newEngine(enabledFeatures)
}
func newEngine() *engine {
func newEngine(enabledFeatures wasm.Features) *engine {
return &engine{
enabledFeatures: enabledFeatures,
compiledFunctions: map[*wasm.FunctionInstance]*compiledFunction{},
setFinalizer: runtime.SetFinalizer,
}
@@ -814,8 +816,8 @@ func compileHostFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
}, nil
}
func compileWasmFunction(f *wasm.FunctionInstance) (*compiledFunction, error) {
ir, err := wazeroir.Compile(f)
func compileWasmFunction(enabledFeatures wasm.Features, f *wasm.FunctionInstance) (*compiledFunction, error) {
ir, err := wazeroir.Compile(enabledFeatures, f)
if err != nil {
return nil, fmt.Errorf("failed to lower to wazeroir: %w", err)
}

View File

@@ -103,8 +103,8 @@ var et = &engineTester{}
type engineTester struct{}
func (e *engineTester) NewEngine() wasm.Engine {
return newEngine()
func (e *engineTester) NewEngine(enabledFeatures wasm.Features) wasm.Engine {
return newEngine(enabledFeatures)
}
func (e *engineTester) InitTable(me wasm.ModuleEngine, initTableLen uint32, initTableIdxToFnIdx map[wasm.Index]wasm.Index) []interface{} {
@@ -149,7 +149,7 @@ func requireSupportedOSArch(t *testing.T) {
func TestJIT_EngineCompile_Errors(t *testing.T) {
t.Run("invalid import", func(t *testing.T) {
e := et.NewEngine()
e := et.NewEngine(wasm.Features20191205)
_, err := e.NewModuleEngine(
t.Name(),
[]*wasm.FunctionInstance{{Module: &wasm.ModuleInstance{Name: "uncompiled"}, DebugName: "uncompiled.fn"}},
@@ -161,7 +161,7 @@ func TestJIT_EngineCompile_Errors(t *testing.T) {
})
t.Run("release on compilation error", func(t *testing.T) {
e := et.NewEngine().(*engine)
e := et.NewEngine(wasm.Features20191205).(*engine)
importedFunctions := []*wasm.FunctionInstance{
{DebugName: "1", Type: &wasm.FunctionType{}, Body: []byte{wasm.OpcodeEnd}, Module: &wasm.ModuleInstance{}},
@@ -213,7 +213,7 @@ func TestJIT_NewModuleEngine_CompiledFunctions(t *testing.T) {
}
}
e := et.NewEngine().(*engine)
e := et.NewEngine(wasm.Features20191205).(*engine)
importedFinalizer := fakeFinalizer{}
e.setFinalizer = importedFinalizer.setFinalizer
@@ -325,7 +325,7 @@ func TestJIT_ModuleEngine_Close(t *testing.T) {
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
e := et.NewEngine().(*engine)
e := et.NewEngine(wasm.Features20191205).(*engine)
var imported *moduleEngine
if len(tc.importedFunctions) > 0 {
// Instantiate the imported module
@@ -387,8 +387,9 @@ func TestJIT_ModuleEngine_Close(t *testing.T) {
// allows us to safely access to their data region from native code.
// See comments on initialValueStackSize and initialCallFrameStackSize.
func TestJIT_SliceAllocatedOnHeap(t *testing.T) {
e := newEngine()
store := wasm.NewStore(e, wasm.Features20191205)
enabledFeatures := wasm.Features20191205
e := newEngine(enabledFeatures)
store := wasm.NewStore(enabledFeatures, e)
const hostModuleName = "env"
const hostFnName = "grow_and_shrink_goroutine_stack"
@@ -408,7 +409,7 @@ func TestJIT_SliceAllocatedOnHeap(t *testing.T) {
// Trigger relocation of goroutine stack because at this point we have the majority of
// goroutine stack unused after recursive call.
runtime.GC()
}}, map[string]*wasm.Memory{}, map[string]*wasm.Global{})
}}, map[string]*wasm.Memory{}, map[string]*wasm.Global{}, enabledFeatures)
require.NoError(t, err)
_, err = store.Instantiate(context.Background(), hm, hostModuleName, nil)

View File

@@ -836,7 +836,7 @@ func (c *amd64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
// compileDrop implements compiler.compileDrop for the amd64 architecture.
func (c *amd64Compiler) compileDrop(o *wazeroir.OperationDrop) error {
return c.emitDropRange(o.Range)
return c.emitDropRange(o.Depth)
}
func (c *amd64Compiler) emitDropRange(r *wazeroir.InclusiveRange) error {

View File

@@ -1143,7 +1143,7 @@ func (c *arm64Compiler) compileCallIndirect(o *wazeroir.OperationCallIndirect) e
// compileDrop implements compiler.compileDrop for the arm64 architecture.
func (c *arm64Compiler) compileDrop(o *wazeroir.OperationDrop) error {
return c.compileDropRange(o.Range)
return c.compileDropRange(o.Depth)
}
// compileDropRange is the implementation of compileDrop. See compiler.compileDrop.

View File

@@ -268,7 +268,7 @@ func TestCompiler_compileDrop(t *testing.T) {
}
require.Equal(t, uint64(liveNum), compiler.valueLocationStack().sp)
err = compiler.compileDrop(&wazeroir.OperationDrop{Range: nil})
err = compiler.compileDrop(&wazeroir.OperationDrop{Depth: nil})
require.NoError(t, err)
// After the nil range drop, the stack must remain the same.
@@ -306,7 +306,7 @@ func TestCompiler_compileDrop(t *testing.T) {
}
require.Equal(t, uint64(liveNum+dropTargetNum), compiler.valueLocationStack().sp)
err = compiler.compileDrop(&wazeroir.OperationDrop{Range: r})
err = compiler.compileDrop(&wazeroir.OperationDrop{Depth: r})
require.NoError(t, err)
// After the drop operation, the stack contains only live contents.
@@ -355,7 +355,7 @@ func TestCompiler_compileDrop(t *testing.T) {
require.Equal(t, uint64(total), compiler.valueLocationStack().sp)
err = compiler.compileDrop(&wazeroir.OperationDrop{Range: r})
err = compiler.compileDrop(&wazeroir.OperationDrop{Depth: r})
require.NoError(t, err)
// After the drop operation, the stack contains only live contents.

View File

@@ -292,20 +292,12 @@ func (m *Module) validateFunctions(enabledFeatures Features, functions []Index,
return fmt.Errorf("code count (%d) != function count (%d)", codeCount, functionCount)
}
// The wazero specific limitation described at RATIONALE.md.
const maximumValuesOnStack = 1 << 27
for idx, typeIndex := range m.FunctionSection {
if typeIndex >= typeCount {
return fmt.Errorf("invalid %s: type section index %d out of range", m.funcDesc(SectionIDFunction, Index(idx)), typeIndex)
}
if err := validateFunction(
enabledFeatures,
m.TypeSection[typeIndex],
m.CodeSection[idx].Body,
m.CodeSection[idx].LocalTypes,
functions, globals, memory, table, m.TypeSection, maximumValuesOnStack); err != nil {
if err := m.validateFunction(enabledFeatures, Index(idx), functions, globals, memory, table); err != nil {
return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err)
}
}

View File

@@ -530,7 +530,7 @@ func TestModule_validateImports(t *testing.T) {
Type: ExternTypeGlobal,
DescGlobal: &GlobalType{ValType: ValueTypeI32, Mutable: true},
},
expectedErr: `invalid import["m"."n"] global: feature mutable-global is disabled`,
expectedErr: `invalid import["m"."n"] global: feature "mutable-global" is disabled`,
},
{
name: "table",
@@ -611,7 +611,7 @@ func TestModule_validateExports(t *testing.T) {
enabledFeatures: Features20191205.Set(FeatureMutableGlobal, false),
exportSection: map[string]*Export{"e1": {Type: ExternTypeGlobal, Index: 0}},
globals: []*GlobalType{{ValType: ValueTypeI32, Mutable: true}},
expectedErr: `invalid export["e1"] global[0]: feature mutable-global is disabled`,
expectedErr: `invalid export["e1"] global[0]: feature "mutable-global" is disabled`,
},
{
name: "global out of range",

View File

@@ -25,12 +25,12 @@ type (
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#store%E2%91%A0
Store struct {
// Engine is a global context for a Store which is in responsible for compilation and execution of Wasm modules.
engine Engine
// EnabledFeatures are read-only to allow optimizations.
EnabledFeatures Features
// Engine is a global context for a Store which is in responsible for compilation and execution of Wasm modules.
engine Engine
// moduleNames ensures no race conditions instantiating two modules of the same name
moduleNames map[string]struct{} // guarded by mux
@@ -228,10 +228,10 @@ func (m *ModuleInstance) getExport(name string, et ExternType) (*ExportInstance,
return exp, nil
}
func NewStore(engine Engine, enabledFeatures Features) *Store {
func NewStore(enabledFeatures Features, engine Engine) *Store {
return &Store{
engine: engine,
EnabledFeatures: enabledFeatures,
engine: engine,
moduleNames: map[string]struct{}{},
modules: map[string]*ModuleInstance{},
typeIDs: map[string]FunctionTypeID{},

View File

@@ -91,7 +91,13 @@ func TestModuleInstance_Memory(t *testing.T) {
func TestStore_Instantiate(t *testing.T) {
s := newStore()
m, err := NewHostModule("", map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
m, err := NewHostModule(
"",
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err)
type key string
@@ -121,7 +127,13 @@ func TestStore_CloseModule(t *testing.T) {
{
name: "Module imports HostModule",
initializer: func(t *testing.T, s *Store) {
m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
m, err := NewHostModule(
importedModuleName,
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err)
_, err = s.Instantiate(context.Background(), m, importedModuleName, nil)
require.NoError(t, err)
@@ -178,7 +190,13 @@ func TestStore_CloseModule(t *testing.T) {
func TestStore_hammer(t *testing.T) {
const importedModuleName = "imported"
m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
m, err := NewHostModule(
importedModuleName,
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err)
s := newStore()
@@ -228,7 +246,13 @@ func TestStore_Instantiate_Errors(t *testing.T) {
const importedModuleName = "imported"
const importingModuleName = "test"
m, err := NewHostModule(importedModuleName, map[string]interface{}{"fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
m, err := NewHostModule(
importedModuleName,
map[string]interface{}{"fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err)
t.Run("Fails if module name already in use", func(t *testing.T) {
@@ -313,7 +337,13 @@ func TestStore_Instantiate_Errors(t *testing.T) {
}
func TestModuleContext_ExportedFunction(t *testing.T) {
host, err := NewHostModule("host", map[string]interface{}{"host_fn": func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
host, err := NewHostModule(
"host",
map[string]interface{}{"host_fn": func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err)
s := newStore()
@@ -342,13 +372,19 @@ func TestModuleContext_ExportedFunction(t *testing.T) {
}
func TestFunctionInstance_Call(t *testing.T) {
store := NewStore(&mockEngine{shouldCompileFail: false, callFailIndex: -1}, Features20191205)
store := NewStore(Features20191205, &mockEngine{shouldCompileFail: false, callFailIndex: -1})
// Add the host module
functionName := "fn"
// This is a fake engine, so we don't capture inside the function body.
m, err := NewHostModule("host", map[string]interface{}{functionName: func(api.Module) {}}, map[string]*Memory{}, map[string]*Global{})
m, err := NewHostModule(
"host",
map[string]interface{}{functionName: func(api.Module) {}},
map[string]*Memory{},
map[string]*Global{},
Features20191205,
)
require.NoError(t, err)
// Add the host module
@@ -399,7 +435,7 @@ type mockModuleEngine struct {
}
func newStore() *Store {
return NewStore(&mockEngine{shouldCompileFail: false, callFailIndex: -1}, Features20191205)
return NewStore(Features20191205, &mockEngine{shouldCompileFail: false, callFailIndex: -1})
}
// NewModuleEngine implements the same method as documented on wasm.Engine.

View File

@@ -45,12 +45,12 @@ const (
// are also enforced in module instantiation, they are also enforced here, to allow relevant source line/col in errors.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#modules%E2%91%A3
type moduleParser struct {
// source is the entire WebAssembly text format source code being parsed.
source []byte
// enabledFeatures ensure parsing errs at the correct line and column number when a feature is disabled.
enabledFeatures wasm.Features
// source is the entire WebAssembly text format source code being parsed.
source []byte
// module holds the fields incrementally parsed from tokens in the source.
module *wasm.Module
@@ -147,10 +147,10 @@ func newModuleParser(module *wasm.Module, enabledFeatures wasm.Features, memoryM
funcNamespace: newIndexNamespace(module.SectionElementCount),
memoryNamespace: newIndexNamespace(module.SectionElementCount),
}
p.typeParser = newTypeParser(p.typeNamespace, p.onTypeEnd)
p.typeUseParser = newTypeUseParser(module, p.typeNamespace)
p.typeParser = newTypeParser(enabledFeatures, p.typeNamespace, p.onTypeEnd)
p.typeUseParser = newTypeUseParser(enabledFeatures, module, p.typeNamespace)
p.funcParser = newFuncParser(enabledFeatures, p.typeUseParser, p.funcNamespace, p.endFunc)
p.memoryParser = newMemoryParser(uint32(memoryMaxPages), p.memoryNamespace, p.endMemory)
p.memoryParser = newMemoryParser(memoryMaxPages, p.memoryNamespace, p.endMemory)
return &p
}
@@ -204,7 +204,7 @@ func (p *moduleParser) beginModuleField(tok tokenType, tokenBytes []byte, _, _ u
return p.parseExportName, nil
case "start":
if p.module.SectionElementCount(wasm.SectionIDStart) > 0 {
return nil, moreThanOneInvalid("start")
return nil, moreThanOneInvalidInSection(wasm.SectionIDStart)
}
p.pos = positionStart
return p.parseStart, nil

View File

@@ -14,9 +14,8 @@ func TestDecodeModule(t *testing.T) {
localGet0End := []byte{wasm.OpcodeLocalGet, 0x00, wasm.OpcodeEnd}
tests := []struct {
name, input string
enabledFeatures wasm.Features
expected *wasm.Module
name, input string
expected *wasm.Module
}{
{
name: "empty",
@@ -60,6 +59,24 @@ func TestDecodeModule(t *testing.T) {
},
},
},
{
name: "type func multiple abbreviated results",
input: "(module (type (func (param i32 i32) (result i32 i32))))",
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}},
},
},
},
{
name: "type func multiple results",
input: "(module (type (func (param i32) (param i32) (result i32) (result i32))))",
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{
{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}},
},
},
},
{
name: "import func empty",
input: "(module (import \"foo\" \"bar\" (func)))", // ok empty sig
@@ -208,7 +225,6 @@ func TestDecodeModule(t *testing.T) {
input: `(module
(func (param i64) (result i64) local.get 0 i64.extend16_s)
)`,
enabledFeatures: wasm.FeatureSignExtensionOps,
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{i64_i64},
FunctionSection: []wasm.Index{0},
@@ -621,6 +637,21 @@ func TestDecodeModule(t *testing.T) {
}},
},
},
{
name: "import func multiple abbreviated results",
input: `(module (import "misc" "swap" (func $swap (param i32 i32) (result i32 i32))))`,
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}}},
ImportSection: []*wasm.Import{{
Module: "misc", Name: "swap",
Type: wasm.ExternTypeFunc,
DescFunc: 0,
}},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "swap"}},
},
},
},
{
name: "func empty",
input: "(module (func))", // ok empty sig
@@ -1099,6 +1130,18 @@ func TestDecodeModule(t *testing.T) {
},
},
},
{
name: "func multiple abbreviated results",
input: "(module (func $swap (param i32 i32) (result i32 i32) local.get 1 local.get 0))",
expected: &wasm.Module{
TypeSection: []*wasm.FunctionType{{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}}},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{wasm.OpcodeLocalGet, 0x01, wasm.OpcodeLocalGet, 0x00, wasm.OpcodeEnd}}},
NameSection: &wasm.NameSection{
FunctionNames: wasm.NameMap{{Index: wasm.Index(0), Name: "swap"}},
},
},
},
{
name: "func call - index",
input: `(module
@@ -1507,10 +1550,7 @@ func TestDecodeModule(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
if tc.enabledFeatures == 0 {
tc.enabledFeatures = wasm.Features20191205
}
m, err := DecodeModule([]byte(tc.input), tc.enabledFeatures, wasm.MemoryMaxPages)
m, err := DecodeModule([]byte(tc.input), wasm.FeaturesFinished, wasm.MemoryMaxPages)
require.NoError(t, err)
require.Equal(t, tc.expected, m)
})
@@ -1519,10 +1559,9 @@ func TestDecodeModule(t *testing.T) {
func TestParseModule_Errors(t *testing.T) {
tests := []struct {
name, input string
enabledFeatures wasm.Features
memoryMaxPages uint32
expectedErr string
name, input string
memoryMaxPages uint32
expectedErr string
}{
{
name: "forgot parens",
@@ -1580,10 +1619,20 @@ func TestParseModule_Errors(t *testing.T) {
expectedErr: "1:30: unexpected ID: $Math in module.import[0]",
},
{
name: "type ID clash",
name: "type func ID clash",
input: "(module (type $1 (func)) (type $1 (func (param i32))))",
expectedErr: "1:32: duplicate ID $1 in module.type[1]",
},
{
name: "type func multiple abbreviated results - multi-value disabled",
input: "(module (type (func (param i32 i32) (result i32 i32))))",
expectedErr: "1:49: multiple result types invalid as feature \"multi-value\" is disabled in module.type[0].func.result[0]",
},
{
name: "type func multiple results - multi-value disabled",
input: "(module (type (func (param i32) (param i32) (result i32) (result i32))))",
expectedErr: "1:59: multiple result types invalid as feature \"multi-value\" is disabled in module.type[0].func",
},
{
name: "import missing module",
input: "(module (import))",
@@ -1684,16 +1733,6 @@ func TestParseModule_Errors(t *testing.T) {
input: "(module (import \"\" \"\" (func (param $x i32) (param i32) (param $x i32)))_",
expectedErr: "1:63: duplicate ID $x in module.import[0].func.param[2]",
},
{
name: "import func missing param0 type",
input: "(module (import \"\" \"\" (func (param))))",
expectedErr: "1:35: expected a type in module.import[0].func.param[0]",
},
{
name: "import func missing param1 type",
input: "(module (import \"\" \"\" (func (param i32) (param))))",
expectedErr: "1:47: expected a type in module.import[0].func.param[1]",
},
{
name: "import func wrong param0 type",
input: "(module (import \"\" \"\" (func (param f65))))",
@@ -1705,29 +1744,19 @@ func TestParseModule_Errors(t *testing.T) {
expectedErr: "1:48: unknown type: f65 in module.import[0].func.param[1]",
},
{
name: "import func double result",
input: "(module (import \"\" \"\" (func (param i32) (result i32) (result i32))))",
expectedErr: "1:54: unexpected '(' in module.import[0].func",
name: "import func multiple abbreviated results - multi-value disabled",
input: `(module (import "misc" "swap" (func $swap (param i32 i32) (result i32 i32))))`,
expectedErr: "1:71: multiple result types invalid as feature \"multi-value\" is disabled in module.import[0].func.result[0]",
},
{
name: "import func double result type",
input: "(module (import \"\" \"\" (func (param i32) (result i32 i32))))",
expectedErr: "1:53: redundant type in module.import[0].func.result",
name: "import func multiple results - multi-value disabled",
input: `(module (import "misc" "swap" (func $swap (param i32) (param i32) (result i32) (result i32))))`,
expectedErr: "1:81: multiple result types invalid as feature \"multi-value\" is disabled in module.import[0].func",
},
{
name: "import func wrong result type",
input: "(module (import \"\" \"\" (func (param i32) (result f65))))",
expectedErr: "1:49: unknown type: f65 in module.import[0].func.result",
},
{
name: "import func wrong no param type",
input: "(module (import \"\" \"\" (func (param))))",
expectedErr: "1:35: expected a type in module.import[0].func.param[0]",
},
{
name: "import func no result type",
input: "(module (import \"\" \"\" (func (param i32) (result))))",
expectedErr: "1:48: expected a type in module.import[0].func.result",
expectedErr: "1:49: unknown type: f65 in module.import[0].func.result[0]",
},
{
name: "import func wrong param token",
@@ -1737,7 +1766,7 @@ func TestParseModule_Errors(t *testing.T) {
{
name: "import func wrong result token",
input: "(module (import \"\" \"\" (func (result () ))))",
expectedErr: "1:37: unexpected '(' in module.import[0].func.result",
expectedErr: "1:37: unexpected '(' in module.import[0].func.result[0]",
},
{
name: "import func ID after param",
@@ -1777,7 +1806,7 @@ func TestParseModule_Errors(t *testing.T) {
{
name: "import func param after result",
input: "(module (import \"\" \"\" (func (result i32) (param i32))))",
expectedErr: "1:42: unexpected '(' in module.import[0].func",
expectedErr: "1:43: param after result in module.import[0].func",
},
{
name: "import func double desc",
@@ -1829,16 +1858,6 @@ func TestParseModule_Errors(t *testing.T) {
input: "(module (func (param $x i32 i64) ))",
expectedErr: "1:29: cannot assign IDs to parameters in abbreviated form in module.func[0].param[0]",
},
{
name: "func missing param0 type",
input: "(module (func (param)))",
expectedErr: "1:21: expected a type in module.func[0].param[0]",
},
{
name: "func missing param1 type",
input: "(module (func (param i32) (param)))",
expectedErr: "1:33: expected a type in module.func[0].param[1]",
},
{
name: "func wrong param0 type",
input: "(module (func (param f65)))",
@@ -1850,29 +1869,19 @@ func TestParseModule_Errors(t *testing.T) {
expectedErr: "1:34: unknown type: f65 in module.func[0].param[1]",
},
{
name: "func duplicate result",
input: "(module (func (param i32) (result i32) (result i32)))",
expectedErr: "1:41: at most one result allowed in module.func[0]",
name: "func multiple abbreviated results - multi-value disabled",
input: "(module (func $swap (param i32 i32) (result i32 i32) local.get 1 local.get 0))",
expectedErr: "1:49: multiple result types invalid as feature \"multi-value\" is disabled in module.func[0].result[0]",
},
{
name: "func double result type",
input: "(module (func (param i32) (result i32 i32)))",
expectedErr: "1:39: redundant type in module.func[0].result",
name: "func multiple results - multi-value disabled",
input: "(module (func $swap (param i32) (param i32) (result i32) (result i32) local.get 1 local.get 0))",
expectedErr: "1:59: multiple result types invalid as feature \"multi-value\" is disabled in module.func[0]",
},
{
name: "func wrong result type",
input: "(module (func (param i32) (result f65)))",
expectedErr: "1:35: unknown type: f65 in module.func[0].result",
},
{
name: "func wrong no param type",
input: "(module (func (param)))",
expectedErr: "1:21: expected a type in module.func[0].param[0]",
},
{
name: "func no result type",
input: "(module (func (param i32) (result)))",
expectedErr: "1:34: expected a type in module.func[0].result",
expectedErr: "1:35: unknown type: f65 in module.func[0].result[0]",
},
{
name: "func wrong param token",
@@ -1882,7 +1891,7 @@ func TestParseModule_Errors(t *testing.T) {
{
name: "func wrong result token",
input: "(module (func (result () )))",
expectedErr: "1:23: unexpected '(' in module.func[0].result",
expectedErr: "1:23: unexpected '(' in module.func[0].result[0]",
},
{
name: "func ID after param",
@@ -1965,7 +1974,7 @@ func TestParseModule_Errors(t *testing.T) {
input: `(module
(func (param i64) (result i64) local.get 0 i64.extend16_s)
)`,
expectedErr: "2:47: i64.extend16_s invalid as feature sign-extension-ops is disabled in module.func[0]",
expectedErr: "2:47: i64.extend16_s invalid as feature \"sign-extension-ops\" is disabled in module.func[0]",
},
{
name: "memory over max",
@@ -2115,13 +2124,10 @@ func TestParseModule_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
if tc.enabledFeatures == 0 {
tc.enabledFeatures = wasm.Features20191205
}
if tc.memoryMaxPages == 0 {
tc.memoryMaxPages = wasm.MemoryMaxPages
}
_, err := DecodeModule([]byte(tc.input), tc.enabledFeatures, tc.memoryMaxPages)
_, err := DecodeModule([]byte(tc.input), wasm.Features20191205, tc.memoryMaxPages)
require.EqualError(t, err, tc.expectedErr)
})
}

View File

@@ -67,12 +67,7 @@ func importAfterModuleDefined(section wasm.SectionID) error {
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#tables%E2%91%A0
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memories%E2%91%A0
func moreThanOneInvalidInSection(section wasm.SectionID) error {
return moreThanOneInvalid(wasm.SectionIDName(section))
}
// moreThanOneInvalid is the failure when a declaration that can result in more than one item.
func moreThanOneInvalid(context string) error {
return fmt.Errorf("at most one %s allowed", context)
return fmt.Errorf("at most one %s allowed", wasm.SectionIDName(section))
}
func unhandledSection(section wasm.SectionID) error {

View File

@@ -121,8 +121,6 @@ func sExpressionsUnsupported(tok tokenType, tokenBytes []byte, _, _ uint32) (tok
switch string(tokenBytes) {
case "param":
return nil, errors.New("param after result")
case "result":
return nil, moreThanOneInvalid("result")
case "local":
return nil, errors.New("TODO: local")
}
@@ -145,33 +143,47 @@ func (p *funcParser) beginFieldOrInstruction(tok tokenType, tokenBytes []byte, _
func (p *funcParser) beginInstruction(tokenBytes []byte) (next tokenParser, err error) {
var opCode wasm.Opcode
switch string(tokenBytes) {
case "call": // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-controlmathsfcallx
case wasm.OpcodeCallName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-controlmathsfcallx
opCode = wasm.OpcodeCall
next = p.parseFuncIndex
case "drop": // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-parametricmathsfdrop
case wasm.OpcodeDropName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-parametricmathsfdrop
opCode = wasm.OpcodeDrop
next = p.beginFieldOrInstruction
case "i32.add": // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric
case wasm.OpcodeI32AddName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric
opCode = wasm.OpcodeI32Add
next = p.beginFieldOrInstruction
case "i32.extend8_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
opCode = wasm.OpcodeI32Extend8S
next = p.beginFieldOrInstruction
case "i32.extend16_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
opCode = wasm.OpcodeI32Extend16S
next = p.beginFieldOrInstruction
case "i64.extend8_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
opCode = wasm.OpcodeI64Extend8S
next = p.beginFieldOrInstruction
case "i64.extend16_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
opCode = wasm.OpcodeI64Extend16S
next = p.beginFieldOrInstruction
case "i64.extend32_s": // See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
opCode = wasm.OpcodeI64Extend32S
next = p.beginFieldOrInstruction
case "local.get": // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#-hrefsyntax-instr-variablemathsflocalgetx%E2%91%A0
case wasm.OpcodeI32ConstName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric
opCode = wasm.OpcodeI32Const
next = p.parseI32
case wasm.OpcodeI64ConstName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#syntax-instr-numeric
opCode = wasm.OpcodeI64Const
next = p.parseI64
case wasm.OpcodeI64LoadName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A8
return p.encodeI64Instruction(wasm.OpcodeI64Load)
case wasm.OpcodeI64StoreName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A8
return p.encodeI64Instruction(wasm.OpcodeI64Store)
case wasm.OpcodeLocalGetName: // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#variable-instructions%E2%91%A0
opCode = wasm.OpcodeLocalGet
next = p.parseLocalIndex
// Next are sign-extension-ops
// See https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
case wasm.OpcodeI32Extend8SName:
opCode = wasm.OpcodeI32Extend8S
next = p.beginFieldOrInstruction
case wasm.OpcodeI32Extend16SName:
opCode = wasm.OpcodeI32Extend16S
next = p.beginFieldOrInstruction
case wasm.OpcodeI64Extend8SName:
opCode = wasm.OpcodeI64Extend8S
next = p.beginFieldOrInstruction
case wasm.OpcodeI64Extend16SName:
opCode = wasm.OpcodeI64Extend16S
next = p.beginFieldOrInstruction
case wasm.OpcodeI64Extend32SName:
opCode = wasm.OpcodeI64Extend32S
next = p.beginFieldOrInstruction
default:
return nil, fmt.Errorf("unsupported instruction: %s", tokenBytes)
}
@@ -187,6 +199,16 @@ func (p *funcParser) beginInstruction(tokenBytes []byte) (next tokenParser, err
return next, nil
}
func (p *funcParser) encodeI64Instruction(oc wasm.Opcode) (tokenParser, error) {
p.currentBody = append(
p.currentBody,
oc,
3, // alignment=3 (natural alignment) because 2^3 = size of I64 (8 bytes)
0, // offset=0 because that's the default
)
return p.beginFieldOrInstruction, nil
}
// end invokes onFunc to continue parsing
func (p *funcParser) end() (tokenParser, error) {
var code *wasm.Code
@@ -198,6 +220,32 @@ func (p *funcParser) end() (tokenParser, error) {
return p.onFunc(p.currentTypeIdx, code, p.currentName, p.currentParamNames)
}
// parseI32 parses a wasm.ValueTypeI32 and appends it to the currentBody.
func (p *funcParser) parseI32(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
if tok != tokenUN {
return nil, unexpectedToken(tok, tokenBytes)
}
if i, overflow := decodeUint32(tokenBytes); overflow { // TODO: negative and hex
return nil, fmt.Errorf("i32 outside range of uint32: %s", tokenBytes)
} else { // See /RATIONALE.md we can't tell the signed interpretation of a constant, so default to signed.
p.currentBody = append(p.currentBody, leb128.EncodeInt32(int32(i))...)
}
return p.beginFieldOrInstruction, nil
}
// parseI64 parses a wasm.ValueTypeI64 and appends it to the currentBody.
func (p *funcParser) parseI64(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
if tok != tokenUN {
return nil, unexpectedToken(tok, tokenBytes)
}
if i, overflow := decodeUint64(tokenBytes); overflow { // TODO: negative and hex
return nil, fmt.Errorf("i64 outside range of uint64: %s", tokenBytes)
} else { // See /RATIONALE.md we can't tell the signed interpretation of a constant, so default to signed.
p.currentBody = append(p.currentBody, leb128.EncodeInt64(int64(i))...)
}
return p.beginFieldOrInstruction, nil
}
// parseFuncIndex parses an index in the function namespace and appends it to the currentBody. If it was an ID, a
// placeholder byte(0) is added instead and will be resolved later.
func (p *funcParser) parseFuncIndex(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {

View File

@@ -11,9 +11,8 @@ import (
func TestFuncParser(t *testing.T) {
tests := []struct {
name, source string
enabledFeatures wasm.Features
expected *wasm.Code
name, source string
expected *wasm.Code
}{
{
name: "empty",
@@ -46,9 +45,41 @@ func TestFuncParser(t *testing.T) {
}},
},
{
name: "i32.extend8_s",
source: "(func (param i32) local.get 0 i32.extend8_s)",
enabledFeatures: wasm.FeatureSignExtensionOps,
name: "i32.const",
source: "(func i32.const 306)",
expected: &wasm.Code{Body: []byte{wasm.OpcodeI32Const, 0xb2, 0x02, wasm.OpcodeEnd}},
},
{
name: "i64.const",
source: "(func i64.const 356)",
expected: &wasm.Code{Body: []byte{wasm.OpcodeI64Const, 0xe4, 0x02, wasm.OpcodeEnd}},
},
{
name: "i64.load",
source: "(func i32.const 8 i64.load)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeI32Const, 8, // dynamic memory offset to load
wasm.OpcodeI64Load, 0x3, 0x0, // load alignment=3 (natural alignment) staticOffset=0
wasm.OpcodeEnd,
}},
},
{
name: "i64.store",
source: "(func i32.const 8 i64.const 37 i64.store)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeI32Const, 8, // dynamic memory offset to store
wasm.OpcodeI64Const, 37, // value to store
wasm.OpcodeI64Store, 0x3, 0x0, // load alignment=3 (natural alignment) staticOffset=0
wasm.OpcodeEnd,
}},
},
// Below are changes to test/core/i32 and i64.wast from the commit that added "sign-extension-ops" support.
// See https://github.com/WebAssembly/spec/commit/e308ca2ae04d5083414782e842a81f931138cf2e
{
name: "i32.extend8_s",
source: "(func (param i32) local.get 0 i32.extend8_s)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeLocalGet, 0x00,
wasm.OpcodeI32Extend8S,
@@ -56,9 +87,8 @@ func TestFuncParser(t *testing.T) {
}},
},
{
name: "i32.extend16_s",
source: "(func (param i32) local.get 0 i32.extend16_s)",
enabledFeatures: wasm.FeatureSignExtensionOps,
name: "i32.extend16_s",
source: "(func (param i32) local.get 0 i32.extend16_s)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeLocalGet, 0x00,
wasm.OpcodeI32Extend16S,
@@ -66,9 +96,8 @@ func TestFuncParser(t *testing.T) {
}},
},
{
name: "i64.extend8_s",
source: "(func (param i64) local.get 0 i64.extend8_s)",
enabledFeatures: wasm.FeatureSignExtensionOps,
name: "i64.extend8_s",
source: "(func (param i64) local.get 0 i64.extend8_s)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeLocalGet, 0x00,
wasm.OpcodeI64Extend8S,
@@ -76,9 +105,8 @@ func TestFuncParser(t *testing.T) {
}},
},
{
name: "i64.extend16_s",
source: "(func (param i64) local.get 0 i64.extend16_s)",
enabledFeatures: wasm.FeatureSignExtensionOps,
name: "i64.extend16_s",
source: "(func (param i64) local.get 0 i64.extend16_s)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeLocalGet, 0x00,
wasm.OpcodeI64Extend16S,
@@ -86,9 +114,8 @@ func TestFuncParser(t *testing.T) {
}},
},
{
name: "i64.extend32_s",
source: "(func (param i64) local.get 0 i64.extend32_s)",
enabledFeatures: wasm.FeatureSignExtensionOps,
name: "i64.extend32_s",
source: "(func (param i64) local.get 0 i64.extend32_s)",
expected: &wasm.Code{Body: []byte{
wasm.OpcodeLocalGet, 0x00,
wasm.OpcodeI64Extend32S,
@@ -108,7 +135,7 @@ func TestFuncParser(t *testing.T) {
}
module := &wasm.Module{}
fp := newFuncParser(tc.enabledFeatures, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), setFunc)
fp := newFuncParser(wasm.FeaturesFinished, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), setFunc)
require.NoError(t, parseFunc(fp, tc.source))
require.Equal(t, tc.expected, parsedCode)
})
@@ -118,7 +145,6 @@ func TestFuncParser(t *testing.T) {
func TestFuncParser_Call_Unresolved(t *testing.T) {
tests := []struct {
name, source string
enabledFeatures wasm.Features
expectedCode *wasm.Code
expectedUnresolvedIndex *unresolvedIndex
}{
@@ -167,7 +193,7 @@ func TestFuncParser_Call_Unresolved(t *testing.T) {
}
module := &wasm.Module{}
fp := newFuncParser(tc.enabledFeatures, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), setFunc)
fp := newFuncParser(wasm.Features20191205, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), setFunc)
require.NoError(t, parseFunc(fp, tc.source))
require.Equal(t, tc.expectedCode, parsedCode)
require.Equal(t, []*unresolvedIndex{tc.expectedUnresolvedIndex}, fp.funcNamespace.unresolvedIndices)
@@ -177,9 +203,8 @@ func TestFuncParser_Call_Unresolved(t *testing.T) {
func TestFuncParser_Call_Resolved(t *testing.T) {
tests := []struct {
name, source string
enabledFeatures wasm.Features
expected *wasm.Code
name, source string
expected *wasm.Code
}{
{
name: "index zero",
@@ -228,7 +253,7 @@ func TestFuncParser_Call_Resolved(t *testing.T) {
return parseErr, nil
}
fp := newFuncParser(tc.enabledFeatures, &typeUseParser{module: &wasm.Module{}}, funcNamespace, setFunc)
fp := newFuncParser(wasm.FeaturesFinished, &typeUseParser{module: &wasm.Module{}}, funcNamespace, setFunc)
require.NoError(t, parseFunc(fp, tc.source))
require.Equal(t, tc.expected, parsedCode)
})
@@ -237,9 +262,8 @@ func TestFuncParser_Call_Resolved(t *testing.T) {
func TestFuncParser_Errors(t *testing.T) {
tests := []struct {
name, source string
enabledFeatures wasm.Features
expectedErr string
name, source string
expectedErr string
}{
{
name: "not field",
@@ -261,6 +285,16 @@ func TestFuncParser_Errors(t *testing.T) {
source: "(func local.get 4294967296)",
expectedErr: "1:17: index outside range of uint32: 4294967296",
},
{
name: "i32.const overflow",
source: "(func i32.const 4294967296)",
expectedErr: "1:17: i32 outside range of uint32: 4294967296",
},
{
name: "i64.const overflow",
source: "(func i64.const 18446744073709551616)",
expectedErr: "1:17: i64 outside range of uint64: 18446744073709551616",
},
{
name: "instruction not yet supported",
source: "(func f32.const 1.1)",
@@ -279,32 +313,32 @@ func TestFuncParser_Errors(t *testing.T) {
{
name: "duplicate result",
source: "(func (result i32) (result i32))",
expectedErr: "1:21: at most one result allowed",
expectedErr: "1:21: multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "i32.extend8_s disabled",
source: "(func (param i32) local.get 0 i32.extend8_s)",
expectedErr: "1:31: i32.extend8_s invalid as feature sign-extension-ops is disabled",
expectedErr: "1:31: i32.extend8_s invalid as feature \"sign-extension-ops\" is disabled",
},
{
name: "i32.extend16_s disabled",
source: "(func (param i32) local.get 0 i32.extend16_s)",
expectedErr: "1:31: i32.extend16_s invalid as feature sign-extension-ops is disabled",
expectedErr: "1:31: i32.extend16_s invalid as feature \"sign-extension-ops\" is disabled",
},
{
name: "i64.extend8_s disabled",
source: "(func (param i64) local.get 0 i64.extend8_s)",
expectedErr: "1:31: i64.extend8_s invalid as feature sign-extension-ops is disabled",
expectedErr: "1:31: i64.extend8_s invalid as feature \"sign-extension-ops\" is disabled",
},
{
name: "i64.extend16_s disabled",
source: "(func (param i64) local.get 0 i64.extend16_s)",
expectedErr: "1:31: i64.extend16_s invalid as feature sign-extension-ops is disabled",
expectedErr: "1:31: i64.extend16_s invalid as feature \"sign-extension-ops\" is disabled",
},
{
name: "i64.extend32_s disabled",
source: "(func (param i64) local.get 0 i64.extend32_s)",
expectedErr: "1:31: i64.extend32_s invalid as feature sign-extension-ops is disabled",
expectedErr: "1:31: i64.extend32_s invalid as feature \"sign-extension-ops\" is disabled",
},
}
@@ -313,7 +347,7 @@ func TestFuncParser_Errors(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
module := &wasm.Module{}
fp := newFuncParser(tc.enabledFeatures, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), failOnFunc)
fp := newFuncParser(wasm.Features20191205, &typeUseParser{module: module}, newIndexNamespace(module.SectionElementCount), failOnFunc)
require.EqualError(t, parseFunc(fp, tc.source), tc.expectedErr)
})
}

View File

@@ -4,12 +4,11 @@ import (
"errors"
"fmt"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/wasm"
)
func newTypeParser(typeNamespace *indexNamespace, onType onType) *typeParser {
return &typeParser{typeNamespace: typeNamespace, onType: onType}
func newTypeParser(enabledFeatures wasm.Features, typeNamespace *indexNamespace, onType onType) *typeParser {
return &typeParser{enabledFeatures: enabledFeatures, typeNamespace: typeNamespace, onType: onType}
}
type onType func(ft *wasm.FunctionType) tokenParser
@@ -22,6 +21,9 @@ type onType func(ft *wasm.FunctionType) tokenParser
//
// Note: typeParser is reusable. The caller resets via begin.
type typeParser struct {
// enabledFeatures should be set to moduleParser.enabledFeatures
enabledFeatures wasm.Features
typeNamespace *indexNamespace
// onType is invoked on end
@@ -33,15 +35,16 @@ type typeParser struct {
// currentType is reset on begin and complete onType
currentType *wasm.FunctionType
// currentParamField is a field index and used to give an appropriate errorContext. Due to abbreviation it may be
// unrelated to the length of currentParams
currentParamField wasm.Index
// currentField is a field index and used to give an appropriate errorContext.
//
// Note: Due to abbreviation, this may be less than to the length of params or results.
currentField wasm.Index
// parsedParam allows us to check if we parsed a type in a "param" field. We can't use currentParamField because
// when parameters are abbreviated, ex. (param i32 i32), the currentParamField will be less than the type count.
parsedParam bool
// parsedParamType allows us to check if we parsed a type in a "param" field. This is used to enforce param names
// can't coexist with abbreviations.
parsedParamType bool
// parsedParamID is true when the field at currentParamField had an ID. Ex. (param $x i32)
// parsedParamID is true when the field at currentField had an ID. Ex. (param $x i32)
//
// Note: param IDs are allowed to be present on module types, but they serve no purpose. parsedParamID is only used
// to validate the grammar rules: ID validation is not necessary.
@@ -142,12 +145,15 @@ func (p *typeParser) beginParamOrResult(tok tokenType, tokenBytes []byte, _, _ u
return nil, unexpectedToken(tok, tokenBytes)
}
p.parsedParamType = false
switch string(tokenBytes) {
case "param":
p.pos = positionParam
p.parsedParam, p.parsedParamID = false, false
p.parsedParamID = false
return p.parseParamID, nil
case "result":
p.currentField = 0 // reset
p.pos = positionResult
return p.parseResult, nil
default:
@@ -165,6 +171,39 @@ func (p *typeParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte, l
return p.parseFuncEnd(tok, tokenBytes, line, col) // end of params, but no result. Ex. (func (param i32)) or (func)
}
// parseMoreResults looks for a '(', and if present returns beginResult to continue any additional results. Otherwise,
// it calls onType.
func (p *typeParser) parseMoreResults(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
if tok == tokenLParen {
p.pos = positionFunc
return p.beginResult, nil
}
return p.parseFuncEnd(tok, tokenBytes, line, col) // end of results
}
// beginResult attempts to begin a "result" field.
func (p *typeParser) beginResult(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
if tok != tokenKeyword {
return nil, unexpectedToken(tok, tokenBytes)
}
switch string(tokenBytes) {
case "param":
return nil, errors.New("param after result")
case "result":
// Guard >1.0 feature multi-value
if err := p.enabledFeatures.Require(wasm.FeatureMultiValue); err != nil {
err = fmt.Errorf("multiple result types invalid as %v", err)
return nil, err
}
p.pos = positionResult
return p.parseResult, nil
default:
return nil, unexpectedFieldName(tokenBytes)
}
}
// parseParamID ignores any ID if present and resumes with parseParam .
//
// Ex. A param ID is present `(param $x i32)`
@@ -188,12 +227,10 @@ func (p *typeParser) parseParamID(tok tokenType, tokenBytes []byte, line, col ui
// records i32 --^ ^
// parseMoreParamsOrResult resumes here --+
//
// Ex. One param type is present `(param i32)`
// records i32 --^ ^
// parseMoreParamsOrResult resumes here --+
//
// Ex. type is missing `(param)`
// errs here --^
// Ex. Multiple param types are present `(param i32 i64)`
// records i32 --^ ^ ^
// records i32 --+ |
// parseMoreParamsOrResult resumes here --+
func (p *typeParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
switch tok {
case tokenID: // Ex. $len
@@ -203,18 +240,15 @@ func (p *typeParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) (
if err != nil {
return nil, err
}
if p.parsedParam && p.parsedParamID {
if p.parsedParamType && p.parsedParamID {
return nil, errors.New("cannot assign IDs to parameters in abbreviated form")
}
p.currentType.Params = append(p.currentType.Params, vt)
p.parsedParam = true
p.parsedParamType = true
return p.parseParam, nil
case tokenRParen: // end of this field
if !p.parsedParam {
return nil, errors.New("expected a type")
}
// since multiple param fields are valid, ex `(func (param i32) (param i64))`, prepare for any next.
p.currentParamField++
p.currentField++
p.pos = positionFunc
return p.parseMoreParamsOrResult, nil
default:
@@ -222,44 +256,53 @@ func (p *typeParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) (
}
}
// parseResult parses the api.ValueType in the "result" field and returns onType to finish the type.
// parseResult records value type and continues if it is an abbreviated form with multiple value types. When complete,
// this returns parseMoreResults.
//
// Ex. One result type is present `(result i32)`
// records i32 --^ ^
// parseMoreResults resumes here --+
//
// Ex. Multiple result types are present `(result i32 i64)`
// records i32 --^ ^ ^
// records i32 --+ |
// parseMoreResults resumes here --+
func (p *typeParser) parseResult(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
switch tok {
case tokenID: // Ex. $len
return nil, fmt.Errorf("unexpected ID: %s", tokenBytes)
case tokenKeyword: // Ex. i32
if p.currentType.Results != nil {
return nil, errors.New("redundant type")
if len(p.currentType.Results) > 0 { // ex (result i32 i32)
// Guard >1.0 feature multi-value
if err := p.enabledFeatures.Require(wasm.FeatureMultiValue); err != nil {
err = fmt.Errorf("multiple result types invalid as %v", err)
return nil, err
}
}
var err error
p.currentType.Results, err = parseResultType(tokenBytes)
return p.parseResult, err
vt, err := parseValueType(tokenBytes)
if err != nil {
return nil, err
}
p.currentType.Results = append(p.currentType.Results, vt)
return p.parseResult, nil
case tokenRParen: // end of this field
if p.currentType.Results == nil {
return nil, errors.New("expected a type")
}
// since multiple result fields are valid, ex `(func (result i32) (result i64))`, prepare for any next.
p.currentField++
p.pos = positionFunc
return p.parseFuncEnd, nil // end of result, and only one is allowed
return p.parseMoreResults, nil
default:
return nil, unexpectedToken(tok, tokenBytes)
}
}
func parseResultType(tokenBytes []byte) ([]wasm.ValueType, error) {
vt, err := parseValueType(tokenBytes)
if err != nil {
return nil, err
}
return leb128.EncodeUint32(uint32(vt)), nil // reuse cache
}
func (p *typeParser) errorContext() string {
switch p.pos {
case positionFunc:
return ".func"
case positionParam:
return fmt.Sprintf(".func.param[%d]", p.currentParamField)
return fmt.Sprintf(".func.param[%d]", p.currentField)
case positionResult:
return ".func.result"
return fmt.Sprintf(".func.result[%d]", p.currentField)
}
return ""
}

View File

@@ -9,9 +9,10 @@ import (
)
var (
f32, i32, i64 = wasm.ValueTypeF32, wasm.ValueTypeI32, wasm.ValueTypeI64
f32, f64, i32, i64 = wasm.ValueTypeF32, wasm.ValueTypeF64, wasm.ValueTypeI32, wasm.ValueTypeI64
i32_v = &wasm.FunctionType{Params: []wasm.ValueType{i32}}
v_i32 = &wasm.FunctionType{Results: []wasm.ValueType{i32}}
v_i32i64 = &wasm.FunctionType{Results: []wasm.ValueType{i32, i64}}
i64_i64 = &wasm.FunctionType{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}}
i32i64_v = &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}}
i32i32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}
@@ -63,6 +64,11 @@ func TestTypeParser(t *testing.T) {
expected: v_i32,
expectedID: "v_i32",
},
{
name: "results no param",
input: "(type (func (result i32) (result i64)))",
expected: v_i32i64,
},
{
name: "mixed param no result",
input: "(type (func (param i32) (param i64)))",
@@ -95,6 +101,84 @@ func TestTypeParser(t *testing.T) {
input: "(type (func (param i32 i32) (param i32) (param i64) (param f32)))",
expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i32, i32, i64, f32}},
},
// Below are changes to test/core/br.wast from the commit that added "multi-value" support.
// See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
{
name: "multi-value - v_i64f32 abbreviated",
input: "(type (func (result i64 f32)))",
expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
},
{
name: "multi-value - i32i64_f32f64 abbreviated",
input: "(type (func (param i32 i64) (result f32 f64)))",
expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
},
{
name: "multi-value - v_i64f32",
input: "(type (func (result i64) (result f32)))",
expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
},
{
name: "multi-value - i32i64_f32f64",
input: "(type (func (param i32) (param i64) (result f32) (result f64)))",
expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
},
{
name: "multi-value - i32i64_f32f64 named",
input: "(type (func (param $x i32) (param $y i64) (result f32) (result f64)))",
expected: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
},
{
name: "multi-value - i64i64f32_f32i32 results abbreviated in groups",
input: "(type (func (result i64 i64 f32) (result f32 i32)))",
expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32, f32, i32}},
},
{
name: "multi-value - i32i32i64i32_f32f64f64i32 params and results abbreviated in groups",
input: "(type (func (param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32)))",
expected: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
{
name: "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
input: "(type (func (param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32)))",
expected: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
{
name: "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
input: "(type (func (param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32)))",
expected: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
{
name: "multi-value - empty abbreviated results",
input: "(type (func (result) (result) (result i64 i64) (result) (result f32) (result)))",
// Abbreviations have min length zero, which implies no-op results are ok.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
expected: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32}},
},
{
name: "multi-value - empty abbreviated params and results",
input: `(type (func
(param i32 i32) (param i64 i32) (param) (param $x i32) (param)
(result) (result f32 f64) (result f64 i32) (result)
))`,
// Abbreviations have min length zero, which implies no-op results are ok.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
expected: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
}
for _, tt := range tests {
@@ -105,7 +189,7 @@ func TestTypeParser(t *testing.T) {
require.Equal(t, wasm.SectionIDType, sectionID)
return 0
})
parsed, tp, err := parseFunctionType(typeNamespace, tc.input)
parsed, tp, err := parseFunctionType(wasm.FeaturesFinished, typeNamespace, tc.input)
require.NoError(t, err)
require.Equal(t, tc.expected, parsed)
require.Equal(t, uint32(1), tp.typeNamespace.count)
@@ -120,7 +204,10 @@ func TestTypeParser(t *testing.T) {
}
func TestTypeParser_Errors(t *testing.T) {
tests := []struct{ name, input, expectedErr string }{
tests := []struct {
name, input, expectedErr string
enabledFeatures wasm.Features
}{
{
name: "invalid token",
input: "(type \"0\")",
@@ -161,14 +248,9 @@ func TestTypeParser_Errors(t *testing.T) {
input: "(type (func (type 0))",
expectedErr: "unexpected field: type",
},
{
name: "param missing type",
input: "(type (func (param))",
expectedErr: "expected a type",
},
{
name: "param wrong type",
input: "(type (func (param i33))",
input: "(type (func (param i33)))",
expectedErr: "unknown type: i33",
},
{
@@ -183,37 +265,59 @@ func TestTypeParser_Errors(t *testing.T) {
},
{
name: "param wrong end",
input: "(type (func (param i64 \"\"))",
input: "(type (func (param i64 \"\")))",
expectedErr: "unexpected string: \"\"",
},
{
name: "result has no ID",
input: "(type (func (result $x i64) )",
input: "(type (func (result $x i64)))",
expectedErr: "unexpected ID: $x",
},
{
name: "result missing type",
input: "(type (func (result))",
expectedErr: "expected a type",
},
{
name: "result wrong type",
input: "(type (func (result i33))",
input: "(type (func (result i33)))",
expectedErr: "unknown type: i33",
},
{
name: "result abbreviated",
input: "(type (func (result i32 i64)))",
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "result twice",
input: "(type (func (result i32) (result i32)))",
expectedErr: "multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "result second wrong",
input: "(type (func (result i32) (result i33)))",
enabledFeatures: wasm.FeaturesFinished,
expectedErr: "unknown type: i33",
},
{
name: "result second redundant type wrong",
input: "(type (func (result i32) (result i32 i33)))",
enabledFeatures: wasm.FeaturesFinished,
expectedErr: "unknown type: i33",
},
{
name: "param after result",
input: "(type (func (result i32) (param i32)))",
expectedErr: "param after result",
},
{
name: "result wrong end",
input: "(type (func (result i64 \"\"))",
input: "(type (func (result i64 \"\")))",
expectedErr: "unexpected string: \"\"",
},
{
name: "func has no ID",
input: "(type (func $v_v ))",
input: "(type (func $v_v )))",
expectedErr: "unexpected ID: $v_v",
},
{
name: "func invalid token",
input: "(type (func \"0\"))",
input: "(type (func \"0\")))",
expectedErr: "unexpected string: \"0\"",
},
{
@@ -223,7 +327,7 @@ func TestTypeParser_Errors(t *testing.T) {
},
{
name: "wrong end - after func",
input: "(type (func (param i32) \"\"))",
input: "(type (func (param i32) \"\")))",
expectedErr: "unexpected string: \"\"",
},
}
@@ -232,11 +336,15 @@ func TestTypeParser_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
enabledFeatures := tc.enabledFeatures
if enabledFeatures == 0 {
enabledFeatures = wasm.Features20191205
}
typeNamespace := newIndexNamespace(func(sectionID wasm.SectionID) uint32 {
require.Equal(t, wasm.SectionIDType, sectionID)
return 0
})
parsed, _, err := parseFunctionType(typeNamespace, tc.input)
parsed, _, err := parseFunctionType(enabledFeatures, typeNamespace, tc.input)
require.EqualError(t, err, tc.expectedErr)
require.Nil(t, parsed)
})
@@ -251,19 +359,23 @@ func TestTypeParser_Errors(t *testing.T) {
require.NoError(t, err)
typeNamespace.count++
parsed, _, err := parseFunctionType(typeNamespace, "(type $v_v (func))")
parsed, _, err := parseFunctionType(wasm.Features20191205, typeNamespace, "(type $v_v (func))")
require.EqualError(t, err, "duplicate ID $v_v")
require.Nil(t, parsed)
})
}
func parseFunctionType(typeNamespace *indexNamespace, input string) (*wasm.FunctionType, *typeParser, error) {
func parseFunctionType(
enabledFeatures wasm.Features,
typeNamespace *indexNamespace,
input string,
) (*wasm.FunctionType, *typeParser, error) {
var parsed *wasm.FunctionType
var setFunc onType = func(ft *wasm.FunctionType) tokenParser {
parsed = ft
return parseErr
}
tp := newTypeParser(typeNamespace, setFunc)
tp := newTypeParser(enabledFeatures, typeNamespace, setFunc)
// typeParser starts after the '(type', so we need to eat it first!
_, _, err := lex(skipTokens(2, tp.begin), []byte(input))
return parsed, tp, err
@@ -295,42 +407,8 @@ func TestParseValueType(t *testing.T) {
})
}
func TestParseResultType(t *testing.T) {
tests := []struct {
name string
tokenBytes string
expected []wasm.ValueType
expectedErr string
}{
{
name: "no value",
tokenBytes: "i32",
expected: []wasm.ValueType{wasm.ValueTypeI32},
},
{
name: "invalid token",
tokenBytes: "i33",
expectedErr: "unknown type: i33",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
rt, err := parseResultType([]byte(tc.tokenBytes))
if tc.expectedErr != "" {
require.EqualError(t, err, tc.expectedErr)
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, rt)
}
})
}
}
func TestTypeParser_ErrorContext(t *testing.T) {
p := typeParser{currentParamField: 3}
p := typeParser{currentField: 3, currentType: &wasm.FunctionType{}}
tests := []struct {
input string
pos parserPosition
@@ -339,7 +417,7 @@ func TestTypeParser_ErrorContext(t *testing.T) {
{input: "initial", pos: positionInitial, expected: ""},
{input: "func", pos: positionFunc, expected: ".func"},
{input: "param", pos: positionParam, expected: ".func.param[3]"},
{input: "result", pos: positionResult, expected: ".func.result"},
{input: "result", pos: positionResult, expected: ".func.result[3]"},
}
for _, tt := range tests {

View File

@@ -7,11 +7,11 @@ import (
"github.com/tetratelabs/wazero/internal/wasm"
)
func newTypeUseParser(module *wasm.Module, typeNamespace *indexNamespace) *typeUseParser {
return &typeUseParser{module: module, typeNamespace: typeNamespace}
func newTypeUseParser(enabledFeatures wasm.Features, module *wasm.Module, typeNamespace *indexNamespace) *typeUseParser {
return &typeUseParser{enabledFeatures: enabledFeatures, module: module, typeNamespace: typeNamespace}
}
// onTypeUse is invoked when the grammar "(param)* (result)?" completes.
// onTypeUse is invoked when the grammar "(param)* (result)*" completes.
//
// * typeIdx if unresolved, this is replaced in moduleParser.resolveTypeUses
// * paramNames is nil unless IDs existed on at least one "param" field.
@@ -31,6 +31,9 @@ type onTypeUse func(typeIdx wasm.Index, paramNames wasm.NameMap, pos callbackPos
// "type", "param" and "result" inner fields in the correct order.
// Note: typeUseParser is reusable. The caller resets via begin.
type typeUseParser struct {
// enabledFeatures should be set to moduleParser.enabledFeatures
enabledFeatures wasm.Features
// module during parsing is a read-only pointer to the TypeSection and SectionElementCount
module *wasm.Module
@@ -75,15 +78,16 @@ type typeUseParser struct {
// paramNames are the paramIndex formatted for the wasm.NameSection LocalNames
paramNames wasm.NameMap
// currentParamField is a field index and used to give an appropriate errorContext. Due to abbreviation it may be
// unrelated to the length of currentParams
currentParamField wasm.Index
// currentField is a field index and used to give an appropriate errorContext.
//
// Note: Due to abbreviation, this may be less than to the length of params or results.
currentField wasm.Index
// parsedParam allows us to check if we parsed a type in a "param" field. We can't use currentParamField because when
// parameters are abbreviated, ex. (param i32 i32), the currentParamField will be less than the type count.
parsedParam bool
// parsedParamType allows us to check if we parsed a type in a "param" field. This is used to enforce param names
// can't coexist with abbreviations.
parsedParamType bool
// parsedParamID is true when the field at currentParamField had an ID. Ex. (param $x i32)
// parsedParamID is true when the field at currentField had an ID. Ex. (param $x i32)
parsedParamID bool
}
@@ -152,7 +156,7 @@ func (p *typeUseParser) beginTypeParamOrResult(tok tokenType, tokenBytes []byte,
p.paramIndex = nil
p.paramNames = nil
}
p.currentParamField = 0
p.currentField = 0
p.parsedTypeField = false
if tok == tokenKeyword && string(tokenBytes) == "type" {
p.pos = positionType
@@ -198,12 +202,14 @@ func (p *typeUseParser) beginParamOrResult(tok tokenType, tokenBytes []byte, lin
return nil, unexpectedToken(tok, tokenBytes)
}
p.parsedParamType = false
switch string(tokenBytes) {
case "param":
p.pos = positionParam
p.parsedParam, p.parsedParamID = false, false
p.parsedParamID = false
return p.parseParamID, nil
case "result":
p.currentField = 0 // reset
p.pos = positionResult
return p.parseResult, nil
case "type":
@@ -222,6 +228,41 @@ func (p *typeUseParser) parseMoreParamsOrResult(tok tokenType, tokenBytes []byte
return p.parseEnd(tok, tokenBytes, line, col)
}
// parseMoreResults looks for a '(', and if present returns beginResult to continue any additional results. Otherwise,
// it calls onType.
func (p *typeUseParser) parseMoreResults(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
if tok == tokenLParen {
p.pos = positionFunc
return p.beginResult, nil
}
return p.parseEnd(tok, tokenBytes, line, col)
}
// beginResult attempts to begin a "result" field.
func (p *typeUseParser) beginResult(tok tokenType, tokenBytes []byte, line, col uint32) (tokenParser, error) {
if tok != tokenKeyword {
return nil, unexpectedToken(tok, tokenBytes)
}
switch string(tokenBytes) {
case "param":
return nil, errors.New("param after result")
case "result":
// Guard >1.0 feature multi-value
if err := p.enabledFeatures.Require(wasm.FeatureMultiValue); err != nil {
err = fmt.Errorf("multiple result types invalid as %v", err)
return nil, err
}
p.pos = positionResult
return p.parseResult, nil
case "type":
return nil, errors.New("type after result")
default:
return p.end(callbackPositionUnhandledField, tok, tokenBytes, line, col)
}
}
// parseParamID sets any ID if present and resumes with parseParam .
//
// Ex. A param ID is present `(param $x i32)`
@@ -243,7 +284,7 @@ func (p *typeUseParser) parseParamID(tok tokenType, tokenBytes []byte, line, col
// setParamID adds the normalized ('$' stripped) parameter ID to the paramIndex and the wasm.NameSection.
func (p *typeUseParser) setParamID(idToken []byte) error {
// Note: currentParamField is the index of the param field, but due to mixing and matching of abbreviated params
// Note: currentField is the index of the param field, but due to mixing and matching of abbreviated params
// it can be less than the param index. Ex. (param i32 i32) (param $v i32) is param field 2, but the 3rd param.
var idx wasm.Index
if p.currentInlinedType != nil {
@@ -273,9 +314,6 @@ func (p *typeUseParser) setParamID(idToken []byte) error {
// records i32 --^ ^ ^
// records i32 --+ |
// parseMoreParamsOrResult resumes here --+
//
// Ex. type is missing `(param)`
// errs here --^
func (p *typeUseParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
switch tok {
case tokenID: // Ex. $len
@@ -285,7 +323,7 @@ func (p *typeUseParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32
if err != nil {
return nil, err
}
if p.parsedParam && p.parsedParamID {
if p.parsedParamType && p.parsedParamID {
return nil, errors.New("cannot assign IDs to parameters in abbreviated form")
}
if p.currentInlinedType == nil {
@@ -293,14 +331,11 @@ func (p *typeUseParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32
} else {
p.currentInlinedType.Params = append(p.currentInlinedType.Params, vt)
}
p.parsedParam = true
p.parsedParamType = true
return p.parseParam, nil
case tokenRParen: // end of this field
if !p.parsedParam {
return nil, errors.New("expected a type")
}
// since multiple param fields are valid, ex `(func (param i32) (param i64))`, prepare for any next.
p.currentParamField++
p.currentField++
p.pos = positionInitial
return p.parseMoreParamsOrResult, nil
default:
@@ -308,31 +343,44 @@ func (p *typeUseParser) parseParam(tok tokenType, tokenBytes []byte, _, _ uint32
}
}
// parseResult parses the api.ValueType in the "result" field and returns onType to finish the type.
// parseResult records value type and continues if it is an abbreviated form with multiple value types. When complete,
// this returns parseMoreResults.
//
// Ex. One result type is present `(result i32)`
// records i32 --^ ^
// parseMoreResults resumes here --+
//
// Ex. Multiple result types are present `(result i32 i64)`
// records i32 --^ ^ ^
// records i32 --+ |
// parseMoreResults resumes here --+
func (p *typeUseParser) parseResult(tok tokenType, tokenBytes []byte, _, _ uint32) (tokenParser, error) {
switch tok {
case tokenID: // Ex. $len
return nil, fmt.Errorf("unexpected ID: %s", tokenBytes)
case tokenKeyword: // Ex. i32
if p.currentInlinedType != nil && p.currentInlinedType.Results != nil {
return nil, errors.New("redundant type")
if p.currentInlinedType != nil && len(p.currentInlinedType.Results) > 0 { // ex (result i32 i32)
// Guard >1.0 feature multi-value
if err := p.enabledFeatures.Require(wasm.FeatureMultiValue); err != nil {
err = fmt.Errorf("multiple result types invalid as %v", err)
return nil, err
}
}
results, err := parseResultType(tokenBytes)
vt, err := parseValueType(tokenBytes)
if err != nil {
return nil, err
}
if p.currentInlinedType == nil {
p.currentInlinedType = &wasm.FunctionType{Results: results}
p.currentInlinedType = &wasm.FunctionType{Results: []wasm.ValueType{vt}}
} else {
p.currentInlinedType.Results = results
p.currentInlinedType.Results = append(p.currentInlinedType.Results, vt)
}
return p.parseResult, err
return p.parseResult, nil
case tokenRParen: // end of this field
if p.currentInlinedType == nil || p.currentInlinedType.Results == nil {
return nil, errors.New("expected a type")
}
// since multiple result fields are valid, ex `(func (result i32) (result i64))`, prepare for any next.
p.currentField++
p.pos = positionInitial
return p.parseEnd, nil
return p.parseMoreResults, nil
default:
return nil, unexpectedToken(tok, tokenBytes)
}
@@ -348,12 +396,12 @@ func (p *typeUseParser) parseEnd(tok tokenType, tokenBytes []byte, line, col uin
func (p *typeUseParser) errorContext() string {
switch p.pos {
case positionParam:
return fmt.Sprintf(".param[%d]", p.currentParamField)
case positionResult:
return ".result"
case positionType:
return ".type"
case positionParam:
return fmt.Sprintf(".param[%d]", p.currentField)
case positionResult:
return fmt.Sprintf(".result[%d]", p.currentField)
}
return ""
}

View File

@@ -3,7 +3,6 @@ package text
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -13,7 +12,7 @@ import (
type typeUseParserTest struct {
name string
source string
input string
expectedInlinedType *wasm.FunctionType
expectedTypeIdx wasm.Index
expectedParamNames wasm.NameMap
@@ -27,62 +26,142 @@ func TestTypeUseParser_InlinesTypesWhenNotYetAdded(t *testing.T) {
tests := []*typeUseParserTest{
{
name: "empty",
source: "()",
input: "()",
expectedInlinedType: v_v,
},
{
name: "param no result",
source: "((param i32))",
input: "((param i32))",
expectedInlinedType: i32_v,
},
{
name: "param no result - ID",
source: "((param $x i32))",
input: "((param $x i32))",
expectedInlinedType: i32_v,
expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}},
},
{
name: "result no param",
source: "((result i32))",
input: "((result i32))",
expectedInlinedType: v_i32,
},
{
name: "mixed param no result",
source: "((param i32) (param i64))",
input: "((param i32) (param i64))",
expectedInlinedType: i32i64_v,
},
{
name: "mixed param no result - ID",
source: "((param $x i32) (param $y i64))",
input: "((param $x i32) (param $y i64))",
expectedInlinedType: i32i64_v,
expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}, &wasm.NameAssoc{Index: 1, Name: "y"}},
},
{
name: "mixed param result",
source: "((param i32) (param i64) (result i32))",
input: "((param i32) (param i64) (result i32))",
expectedInlinedType: i32i64_i32,
},
{
name: "mixed param result - ID",
source: "((param $x i32) (param $y i64) (result i32))",
input: "((param $x i32) (param $y i64) (result i32))",
expectedInlinedType: i32i64_i32,
expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}, &wasm.NameAssoc{Index: 1, Name: "y"}},
},
{
name: "abbreviated param result",
source: "((param i32 i64) (result i32))",
input: "((param i32 i64) (result i32))",
expectedInlinedType: i32i64_i32,
},
{
name: "mixed param abbreviation", // Verifies we can handle less param fields than param types
source: "((param i32 i32) (param i32) (param i64) (param f32))",
input: "((param i32 i32) (param i32) (param i64) (param f32))",
expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i32, i32, i64, f32}},
},
// Below are changes to test/core/br.wast from the commit that added "multi-value" support.
// See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
{
name: "multi-value - v_i64f32 abbreviated",
input: "((result i64 f32))",
expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
},
{
name: "multi-value - i32i64_f32f64 abbreviated",
input: "((param i32 i64) (result f32 f64))",
expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
},
{
name: "multi-value - v_i64f32",
input: "((result i64) (result f32))",
expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, f32}},
},
{
name: "multi-value - i32i64_f32f64",
input: "((param i32) (param i64) (result f32) (result f64))",
expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
},
{
name: "multi-value - i32i64_f32f64 named",
input: "((param $x i32) (param $y i64) (result f32) (result f64))",
expectedInlinedType: &wasm.FunctionType{Params: []wasm.ValueType{i32, i64}, Results: []wasm.ValueType{f32, f64}},
expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "x"}, &wasm.NameAssoc{Index: 1, Name: "y"}},
},
{
name: "multi-value - i64i64f32_f32i32 results abbreviated in groups",
input: "((result i64 i64 f32) (result f32 i32))",
expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32, f32, i32}},
},
{
name: "multi-value - i32i32i64i32_f32f64f64i32 params and results abbreviated in groups",
input: "((param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32))",
expectedInlinedType: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
{
name: "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
input: "((param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32))",
expectedInlinedType: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
{
name: "multi-value - i32i32i64i32_f32f64f64i32 abbreviated in groups",
input: "((param i32 i32) (param i64 i32) (result f32 f64) (result f64 i32))",
expectedInlinedType: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
},
{
name: "multi-value - empty abbreviated results",
input: "((result) (result) (result i64 i64) (result) (result f32) (result))",
// Abbreviations have min length zero, which implies no-op results are ok.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
expectedInlinedType: &wasm.FunctionType{Results: []wasm.ValueType{i64, i64, f32}},
},
{
name: "multi-value - empty abbreviated params and results",
input: `(
(param i32 i32) (param i64 i32) (param) (param $x i32) (param)
(result) (result f32 f64) (result f64 i32) (result)
)`,
// Abbreviations have min length zero, which implies no-op results are ok.
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#abbreviations%E2%91%A2
expectedInlinedType: &wasm.FunctionType{
Params: []wasm.ValueType{i32, i32, i64, i32, i32},
Results: []wasm.ValueType{f32, f64, f64, i32},
},
expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 4, Name: "x"}},
},
}
runTypeUseParserTests(t, tests, func(tc *typeUseParserTest) (*typeUseParser, func(t *testing.T)) {
module := &wasm.Module{}
tp := newTypeUseParser(module, newIndexNamespace(module.SectionElementCount))
tp := newTypeUseParser(wasm.FeaturesFinished, module, newIndexNamespace(module.SectionElementCount))
return tp, func(t *testing.T) {
// We should have inlined the type, and it is the first type use, which means the inlined index is zero
require.Zero(t, tp.inlinedTypeIndices[0].inlinedIdx)
@@ -95,30 +174,30 @@ func TestTypeUseParser_UnresolvedType(t *testing.T) {
tests := []*typeUseParserTest{
{
name: "unresolved type - index",
source: "((type 1))",
input: "((type 1))",
expectedTypeIdx: 1,
},
{
name: "unresolved type - ID",
source: "((type $v_v))",
input: "((type $v_v))",
expectedTypeIdx: 0,
},
{
name: "unresolved type - index - match",
source: "((type 3) (param i32 i64) (result i32))",
input: "((type 3) (param i32 i64) (result i32))",
expectedTypeIdx: 3,
expectedInlinedType: i32i64_i32,
},
{
name: "unresolved type - ID - match",
source: "((type $i32i64_i32) (param i32 i64) (result i32))",
input: "((type $i32i64_i32) (param i32 i64) (result i32))",
expectedTypeIdx: 0,
expectedInlinedType: i32i64_i32,
},
}
runTypeUseParserTests(t, tests, func(tc *typeUseParserTest) (*typeUseParser, func(t *testing.T)) {
module := &wasm.Module{}
tp := newTypeUseParser(module, newIndexNamespace(module.SectionElementCount))
tp := newTypeUseParser(wasm.FeaturesFinished, module, newIndexNamespace(module.SectionElementCount))
return tp, func(t *testing.T) {
require.NotNil(t, tp.typeNamespace.unresolvedIndices)
if tc.expectedInlinedType == nil {
@@ -134,77 +213,77 @@ func TestTypeUseParser_ReuseExistingType(t *testing.T) {
tests := []*typeUseParserTest{
{
name: "match existing - result",
source: "((result i32))",
input: "((result i32))",
expectedTypeIdx: 0,
},
{
name: "match existing - nullary",
source: "()",
input: "()",
expectedTypeIdx: 1,
},
{
name: "match existing - param",
source: "((param i32))",
input: "((param i32))",
expectedTypeIdx: 2,
},
{
name: "match existing - param and result",
source: "((param i32 i64) (result i32))",
input: "((param i32 i64) (result i32))",
expectedTypeIdx: 3,
},
{
name: "type field index - result",
source: "((type 0))",
input: "((type 0))",
expectedTypeIdx: 0,
},
{
name: "type field ID - result",
source: "((type $v_i32))",
input: "((type $v_i32))",
expectedTypeIdx: 0,
},
{
name: "type field ID - result - match",
source: "((type $v_i32) (result i32))",
input: "((type $v_i32) (result i32))",
expectedTypeIdx: 0,
},
{
name: "type field index - nullary",
source: "((type 1))",
input: "((type 1))",
expectedTypeIdx: 1,
},
{
name: "type field ID - nullary",
source: "((type $v_v))",
input: "((type $v_v))",
expectedTypeIdx: 1,
},
{
name: "type field index - param",
source: "((type 2))",
input: "((type 2))",
expectedTypeIdx: 2,
},
{
name: "type field ID - param",
source: "((type $i32_v))",
input: "((type $i32_v))",
expectedTypeIdx: 2,
},
{
name: "type field ID - param - match",
source: "((type $i32_v) (param i32))",
input: "((type $i32_v) (param i32))",
expectedTypeIdx: 2,
},
{
name: "type field index - param and result",
source: "((type 3))",
input: "((type 3))",
expectedTypeIdx: 3,
},
{
name: "type field ID - param and result",
source: "((type $i32i64_i32))",
input: "((type $i32i64_i32))",
expectedTypeIdx: 3,
},
{
name: "type field ID - param and result - matched",
source: "((type $i32i64_i32) (param i32 i64) (result i32))",
input: "((type $i32i64_i32) (param i32 i64) (result i32))",
expectedTypeIdx: 3,
},
}
@@ -228,7 +307,7 @@ func TestTypeUseParser_ReuseExistingType(t *testing.T) {
require.NoError(t, err)
typeNamespace.count++
tp := newTypeUseParser(module, typeNamespace)
tp := newTypeUseParser(wasm.FeaturesFinished, module, typeNamespace)
return tp, func(t *testing.T) {
require.Nil(t, tp.typeNamespace.unresolvedIndices)
require.Nil(t, tp.inlinedTypes)
@@ -241,32 +320,32 @@ func TestTypeUseParser_ReuseExistingInlinedType(t *testing.T) {
tests := []*typeUseParserTest{
{
name: "match existing - result",
source: "((result i32))",
input: "((result i32))",
expectedInlinedType: v_i32,
},
{
name: "nullary",
source: "()",
input: "()",
expectedInlinedType: v_v,
},
{
name: "param",
source: "((param i32))",
input: "((param i32))",
expectedInlinedType: i32_v,
},
{
name: "param and result",
source: "((param i32 i64) (result i32))",
input: "((param i32 i64) (result i32))",
expectedInlinedType: i32i64_i32,
},
}
runTypeUseParserTests(t, tests, func(tc *typeUseParserTest) (*typeUseParser, func(t *testing.T)) {
module := &wasm.Module{}
tp := newTypeUseParser(module, newIndexNamespace(module.SectionElementCount))
tp := newTypeUseParser(wasm.FeaturesFinished, module, newIndexNamespace(module.SectionElementCount))
// inline a type that doesn't match the test
require.NoError(t, parseTypeUse(tp, "((param i32 i64))", ignoreTypeUse))
// inline the test type
require.NoError(t, parseTypeUse(tp, tc.source, ignoreTypeUse))
require.NoError(t, parseTypeUse(tp, tc.input, ignoreTypeUse))
return tp, func(t *testing.T) {
// verify it wasn't duplicated
@@ -281,37 +360,37 @@ func TestTypeUseParser_BeginResets(t *testing.T) {
tests := []*typeUseParserTest{
{
name: "result",
source: "((result i32))",
input: "((result i32))",
expectedInlinedType: v_i32,
},
{
name: "nullary",
source: "()",
input: "()",
expectedInlinedType: v_v,
},
{
name: "param",
source: "((param i32))",
input: "((param i32))",
expectedInlinedType: i32_v,
},
{
name: "param and result",
source: "((param i32 i32) (result i32))",
input: "((param i32 i32) (result i32))",
expectedInlinedType: i32i32_i32,
},
{
name: "param and result - with IDs",
source: "((param $l i32) (param $r i32) (result i32))",
input: "((param $l i32) (param $r i32) (result i32))",
expectedInlinedType: i32i32_i32,
expectedParamNames: wasm.NameMap{&wasm.NameAssoc{Index: 0, Name: "l"}, &wasm.NameAssoc{Index: 1, Name: "r"}},
},
}
runTypeUseParserTests(t, tests, func(tc *typeUseParserTest) (*typeUseParser, func(t *testing.T)) {
module := &wasm.Module{}
tp := newTypeUseParser(module, newIndexNamespace(module.SectionElementCount))
tp := newTypeUseParser(wasm.FeaturesFinished, module, newIndexNamespace(module.SectionElementCount))
// inline a type that uses all fields
require.NoError(t, parseTypeUse(tp, "((type $i32i64_i32) (param $x i32) (param $y i64) (result i32))", ignoreTypeUse))
require.NoError(t, parseTypeUse(tp, tc.source, ignoreTypeUse))
require.NoError(t, parseTypeUse(tp, tc.input, ignoreTypeUse))
return tp, func(t *testing.T) {
// this is the second inlined type
@@ -334,7 +413,7 @@ func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseT
kt := *tt // copy
kt.name = fmt.Sprintf("%s - trailing keyword", tt.name)
kt.source = fmt.Sprintf("%s nop)", tt.source[:len(tt.source)-1])
kt.input = fmt.Sprintf("%s nop)", tt.input[:len(tt.input)-1])
kt.expectedOnTypeUsePosition = callbackPositionUnhandledToken
kt.expectedOnTypeUseToken = tokenKeyword // at 'nop' and ')' remains
kt.expectedTrailingTokens = []tokenType{tokenRParen}
@@ -342,17 +421,10 @@ func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseT
ft := *tt // copy
ft.name = fmt.Sprintf("%s - trailing field", tt.name)
ft.source = fmt.Sprintf("%s (nop))", tt.source[:len(tt.source)-1])
// Two outcomes, we've reached a field not named "type", "param" or "result" or we completed "result"
if strings.Contains(tt.source, "result") {
ft.expectedOnTypeUsePosition = callbackPositionUnhandledToken
ft.expectedOnTypeUseToken = tokenLParen // at '(' and 'nop))' remain
ft.expectedTrailingTokens = []tokenType{tokenKeyword, tokenRParen, tokenRParen}
} else {
ft.expectedOnTypeUsePosition = callbackPositionUnhandledField
ft.expectedOnTypeUseToken = tokenKeyword // at 'nop' and '))' remain
ft.expectedTrailingTokens = []tokenType{tokenRParen, tokenRParen}
}
ft.input = fmt.Sprintf("%s (nop))", tt.input[:len(tt.input)-1])
ft.expectedOnTypeUsePosition = callbackPositionUnhandledField
ft.expectedOnTypeUseToken = tokenKeyword // at 'nop' and '))' remain
ft.expectedTrailingTokens = []tokenType{tokenRParen, tokenRParen}
moreTests = append(moreTests, &ft)
}
@@ -372,7 +444,7 @@ func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseT
}
tp, test := tf(tc)
require.NoError(t, parseTypeUse(tp, tc.source, setTypeUse))
require.NoError(t, parseTypeUse(tp, tc.input, setTypeUse))
require.Equal(t, tc.expectedTrailingTokens, p.tokenTypes)
require.Equal(t, tc.expectedTypeIdx, parsedTypeIdx)
require.Equal(t, tc.expectedParamNames, parsedParamNames)
@@ -382,101 +454,121 @@ func runTypeUseParserTests(t *testing.T, tests []*typeUseParserTest, tf typeUseT
}
func TestTypeUseParser_Errors(t *testing.T) {
tests := []struct{ name, source, expectedErr string }{
tests := []struct {
name, input, expectedErr string
enabledFeatures wasm.Features
}{
{
name: "not param",
source: "((param i32) ($param i32))",
input: "((param i32) ($param i32))",
expectedErr: "1:15: unexpected ID: $param",
},
{
name: "param missing type",
source: "((param))",
expectedErr: "1:8: expected a type",
},
{
name: "param wrong type",
source: "((param i33))",
input: "((param i33))",
expectedErr: "1:9: unknown type: i33",
},
{
name: "param ID in abbreviation",
source: "((param $x i32 i64) ",
input: "((param $x i32 i64) ",
expectedErr: "1:16: cannot assign IDs to parameters in abbreviated form",
},
{
name: "param second ID",
source: "((param $x $x i64) ",
input: "((param $x $x i64) ",
expectedErr: "1:12: redundant ID $x",
},
{
name: "param duplicate ID",
source: "((param $x i32) (param $x i64) ",
input: "((param $x i32) (param $x i64) ",
expectedErr: "1:24: duplicate ID $x",
},
{
name: "param wrong end",
source: `((param i64 ""))`,
input: `((param i64 ""))`,
expectedErr: "1:13: unexpected string: \"\"",
},
{
name: "result has no ID",
source: "((result $x i64) ",
input: "((result $x i64) ",
expectedErr: "1:10: unexpected ID: $x",
},
{
name: "result missing type",
source: "((result))",
expectedErr: "1:9: expected a type",
},
{
name: "result wrong type",
source: "((result i33))",
input: "((result i33))",
expectedErr: "1:10: unknown type: i33",
},
{
name: "result second type",
source: "((result i32 i64))",
expectedErr: "1:14: redundant type",
name: "result abbreviated",
input: "((result i32 i64))",
expectedErr: "1:14: multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "result twice",
input: "((result i32) (result i32))",
expectedErr: "1:16: multiple result types invalid as feature \"multi-value\" is disabled",
},
{
name: "result second wrong",
input: "((result i32) (result i33))",
enabledFeatures: wasm.FeaturesFinished,
expectedErr: "1:23: unknown type: i33",
},
{
name: "result second redundant type wrong",
input: "((result i32) (result i32 i33))",
enabledFeatures: wasm.FeaturesFinished,
expectedErr: "1:27: unknown type: i33",
},
{
name: "param after result",
input: "((result i32) (param i32))",
expectedErr: "1:16: param after result",
},
{
name: "type after result",
input: "((result i32) (type i32))",
expectedErr: "1:16: type after result",
},
{
name: "result wrong end",
source: `((result i64 ""))`,
input: "((result i64 \"\"))",
expectedErr: "1:14: unexpected string: \"\"",
},
{
name: "type missing index",
source: "((type))",
input: "((type))",
expectedErr: "1:7: missing index",
},
{
name: "type wrong token",
source: "((type v_v))",
input: "((type v_v))",
expectedErr: "1:8: unexpected keyword: v_v",
},
{
name: "type redundant",
source: "((type 0) (type 1))",
input: "((type 0) (type 1))",
expectedErr: "1:12: redundant type",
},
{
name: "type second index",
source: "((type 0 1))",
input: "((type 0 1))",
expectedErr: "1:10: redundant index",
},
{
name: "type overflow index",
source: "((type 4294967296))",
input: "((type 4294967296))",
expectedErr: "1:8: index outside range of uint32: 4294967296",
},
{
name: "type second ID",
source: "((type $v_v $v_v i64) ",
input: "((type $v_v $v_v i64) ",
expectedErr: "1:13: redundant index",
},
{
name: "type wrong end",
source: `((type 0 ""))`,
input: `((type 0 ""))`,
expectedErr: "1:10: unexpected string: \"\"",
},
}
@@ -485,9 +577,13 @@ func TestTypeUseParser_Errors(t *testing.T) {
tc := tt
t.Run(tc.name, func(t *testing.T) {
enabledFeatures := tc.enabledFeatures
if enabledFeatures == 0 {
enabledFeatures = wasm.Features20191205
}
module := &wasm.Module{}
tp := newTypeUseParser(module, newIndexNamespace(module.SectionElementCount))
err := parseTypeUse(tp, tc.source, failOnTypeUse)
tp := newTypeUseParser(enabledFeatures, module, newIndexNamespace(module.SectionElementCount))
err := parseTypeUse(tp, tc.input, failOnTypeUse)
require.EqualError(t, err, tc.expectedErr)
})
}
@@ -505,7 +601,7 @@ func TestTypeUseParser_FailsMatch(t *testing.T) {
require.NoError(t, err)
typeNamespace.count++
tp := newTypeUseParser(module, typeNamespace)
tp := newTypeUseParser(wasm.FeaturesFinished, module, typeNamespace)
tests := []struct{ name, source, expectedErr string }{
{
name: "nullary index",
@@ -559,7 +655,7 @@ func parseTypeUse(tp *typeUseParser, source string, onTypeUse onTypeUse) error {
}
func TestTypeUseParser_ErrorContext(t *testing.T) {
p := typeUseParser{currentParamField: 3}
p := typeUseParser{currentField: 3}
tests := []struct {
source string
pos parserPosition
@@ -567,7 +663,7 @@ func TestTypeUseParser_ErrorContext(t *testing.T) {
}{
{source: "initial", pos: positionInitial, expected: ""},
{source: "param", pos: positionParam, expected: ".param[3]"},
{source: "result", pos: positionResult, expected: ".result"},
{source: "result", pos: positionResult, expected: ".result[3]"},
{source: "type", pos: positionType, expected: ".type"},
}

View File

@@ -26,10 +26,12 @@ const (
type (
controlFrame struct {
frameID uint32
originalStackLen int
returns []UnsignedType
kind controlFrameKind
frameID uint32
// originalStackLen holds the number of values on the stack
// when start executing this control frame minus params for the block.
originalStackLenWithoutParam int
blockType *wasm.FunctionType
kind controlFrameKind
}
controlFrames struct{ frames []*controlFrame }
)
@@ -103,6 +105,7 @@ func (c *controlFrames) push(frame *controlFrame) {
}
type compiler struct {
enabledFeatures wasm.Features
stack []UnsignedType
currentID uint32
controlFrames *controlFrames
@@ -154,8 +157,13 @@ type CompilationResult struct {
// Compile lowers given function instance into wazeroir operations
// so that the resulting operations can be consumed by the interpreter
// or the JIT compilation engine.
func Compile(f *wasm.FunctionInstance) (*CompilationResult, error) {
c := compiler{controlFrames: &controlFrames{}, f: f, result: CompilationResult{LabelCallers: map[string]uint32{}}}
func Compile(enabledFeatures wasm.Features, f *wasm.FunctionInstance) (*CompilationResult, error) {
c := compiler{
enabledFeatures: enabledFeatures,
controlFrames: &controlFrames{},
f: f,
result: CompilationResult{LabelCallers: map[string]uint32{}},
}
// Push function arguments.
for _, t := range f.Type.Params {
@@ -170,18 +178,13 @@ func Compile(f *wasm.FunctionInstance) (*CompilationResult, error) {
}
// Insert the function control frame.
returns := make([]UnsignedType, 0, len(f.Type.Results))
for _, t := range f.Type.Results {
returns = append(returns, wasmValueTypeToUnsignedType(t))
}
c.controlFrames.push(&controlFrame{
frameID: c.nextID(),
originalStackLen: len(f.Type.Params),
returns: returns,
kind: controlFrameKindFunction,
frameID: c.nextID(),
blockType: f.Type,
kind: controlFrameKindFunction,
})
// Now enter the function body.
// Now, enter the function body.
for !c.controlFrames.empty() {
if err := c.handleInstruction(); err != nil {
return nil, fmt.Errorf("handling instruction: %w", err)
@@ -221,7 +224,7 @@ operatorSwitch:
// Nop is noop!
case wasm.OpcodeBlock:
bt, num, err := wasm.DecodeBlockType(c.f.Module.Types,
bytes.NewReader(c.f.Body[c.pc+1:]))
bytes.NewReader(c.f.Body[c.pc+1:]), c.enabledFeatures)
if err != nil {
return fmt.Errorf("reading block type for block instruction: %w", err)
}
@@ -236,18 +239,15 @@ operatorSwitch:
// Create a new frame -- entering this block.
frame := &controlFrame{
frameID: c.nextID(),
originalStackLen: len(c.stack),
kind: controlFrameKindBlockWithoutContinuationLabel,
}
for _, t := range bt.Results {
frame.returns = append(frame.returns, wasmValueTypeToUnsignedType(t))
frameID: c.nextID(),
originalStackLenWithoutParam: len(c.stack) - len(bt.Params),
kind: controlFrameKindBlockWithoutContinuationLabel,
blockType: bt,
}
c.controlFrames.push(frame)
case wasm.OpcodeLoop:
bt, num, err := wasm.DecodeBlockType(c.f.Module.Types,
bytes.NewReader(c.f.Body[c.pc+1:]))
bt, num, err := wasm.DecodeBlockType(c.f.Module.Types, bytes.NewReader(c.f.Body[c.pc+1:]), c.enabledFeatures)
if err != nil {
return fmt.Errorf("reading block type for loop instruction: %w", err)
}
@@ -262,17 +262,15 @@ operatorSwitch:
// Create a new frame -- entering loop.
frame := &controlFrame{
frameID: c.nextID(),
originalStackLen: len(c.stack),
kind: controlFrameKindLoop,
}
for _, t := range bt.Results {
frame.returns = append(frame.returns, wasmValueTypeToUnsignedType(t))
frameID: c.nextID(),
originalStackLenWithoutParam: len(c.stack) - len(bt.Params),
kind: controlFrameKindLoop,
blockType: bt,
}
c.controlFrames.push(frame)
// Prep labels for inside and the continuation of this loop.
loopLabel := &Label{FrameID: frame.frameID, Kind: LabelKindHeader, OriginalStackLen: frame.originalStackLen}
loopLabel := &Label{FrameID: frame.frameID, Kind: LabelKindHeader}
c.result.LabelCallers[loopLabel.String()]++
// Emit the branch operation to enter inside the loop.
@@ -284,8 +282,7 @@ operatorSwitch:
)
case wasm.OpcodeIf:
bt, num, err := wasm.DecodeBlockType(c.f.Module.Types,
bytes.NewReader(c.f.Body[c.pc+1:]))
bt, num, err := wasm.DecodeBlockType(c.f.Module.Types, bytes.NewReader(c.f.Body[c.pc+1:]), c.enabledFeatures)
if err != nil {
return fmt.Errorf("reading block type for if instruction: %w", err)
}
@@ -300,20 +297,18 @@ operatorSwitch:
// Create a new frame -- entering if.
frame := &controlFrame{
frameID: c.nextID(),
originalStackLen: len(c.stack),
frameID: c.nextID(),
originalStackLenWithoutParam: len(c.stack) - len(bt.Params),
// Note this will be set to controlFrameKindIfWithElse
// when else opcode found later.
kind: controlFrameKindIfWithoutElse,
}
for _, t := range bt.Results {
frame.returns = append(frame.returns, wasmValueTypeToUnsignedType(t))
kind: controlFrameKindIfWithoutElse,
blockType: bt,
}
c.controlFrames.push(frame)
// Prep labels for if and else of this if.
thenLabel := &Label{Kind: LabelKindHeader, FrameID: frame.frameID, OriginalStackLen: frame.originalStackLen}
elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID, OriginalStackLen: frame.originalStackLen}
thenLabel := &Label{Kind: LabelKindHeader, FrameID: frame.frameID}
elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID}
c.result.LabelCallers[thenLabel.String()]++
c.result.LabelCallers[elseLabel.String()]++
@@ -337,12 +332,17 @@ operatorSwitch:
// If it is currently in unreachable, and the non-nested if,
// reset the stack so we can correctly handle the else block.
top := c.controlFrames.top()
c.stack = c.stack[:top.originalStackLen]
c.stack = c.stack[:top.originalStackLenWithoutParam]
top.kind = controlFrameKindIfWithElse
// Re-push the parameters to the if block so that else block can use them.
for _, t := range frame.blockType.Params {
c.stackPush(wasmValueTypeToUnsignedType(t))
}
// We are no longer unreachable in else frame,
// so emit the correct label, and reset the unreachable state.
elseLabel := &Label{FrameID: frame.frameID, Kind: LabelKindElse, OriginalStackLen: top.originalStackLen}
elseLabel := &Label{FrameID: frame.frameID, Kind: LabelKindElse}
c.resetUnreachable()
c.emit(
&OperationLabel{Label: elseLabel},
@@ -357,11 +357,17 @@ operatorSwitch:
// We need to reset the stack so that
// the values pushed inside the then block
// do not affect the else block.
dropOp := &OperationDrop{Range: c.getFrameDropRange(frame)}
c.stack = c.stack[:frame.originalStackLen]
dropOp := &OperationDrop{Depth: c.getFrameDropRange(frame, false)}
// Reset the stack manipulated by the then block, and re-push the block param types to the stack.
c.stack = c.stack[:frame.originalStackLenWithoutParam]
for _, t := range frame.blockType.Params {
c.stackPush(wasmValueTypeToUnsignedType(t))
}
// Prep labels for else and the continuation of this if block.
elseLabel := &Label{FrameID: frame.frameID, Kind: LabelKindElse, OriginalStackLen: frame.originalStackLen}
elseLabel := &Label{FrameID: frame.frameID, Kind: LabelKindElse}
continuationLabel := &Label{FrameID: frame.frameID, Kind: LabelKindContinuation}
c.result.LabelCallers[continuationLabel.String()]++
@@ -386,15 +392,15 @@ operatorSwitch:
return nil
}
c.stack = c.stack[:frame.originalStackLen]
for _, t := range frame.returns {
c.stackPush(t)
c.stack = c.stack[:frame.originalStackLenWithoutParam]
for _, t := range frame.blockType.Results {
c.stackPush(wasmValueTypeToUnsignedType(t))
}
continuationLabel := &Label{FrameID: frame.frameID, Kind: LabelKindContinuation, OriginalStackLen: len(c.stack)}
continuationLabel := &Label{FrameID: frame.frameID, Kind: LabelKindContinuation}
if frame.kind == controlFrameKindIfWithoutElse {
// Emit the else label.
elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID, OriginalStackLen: frame.originalStackLen}
elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID}
c.result.LabelCallers[continuationLabel.String()]++
c.emit(
&OperationLabel{Label: elseLabel},
@@ -414,12 +420,12 @@ operatorSwitch:
// We need to reset the stack so that
// the values pushed inside the block.
dropOp := &OperationDrop{Range: c.getFrameDropRange(frame)}
c.stack = c.stack[:frame.originalStackLen]
dropOp := &OperationDrop{Depth: c.getFrameDropRange(frame, true)}
c.stack = c.stack[:frame.originalStackLenWithoutParam]
// Push the result types onto the stack.
for _, t := range frame.returns {
c.stackPush(t)
for _, t := range frame.blockType.Results {
c.stackPush(wasmValueTypeToUnsignedType(t))
}
// Emit the instructions according to the kind of the current control frame.
@@ -437,8 +443,8 @@ operatorSwitch:
)
case controlFrameKindIfWithoutElse:
// This case we have to emit "empty" else label.
elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID, OriginalStackLen: frame.originalStackLen}
continuationLabel := &Label{Kind: LabelKindContinuation, FrameID: frame.frameID, OriginalStackLen: len(c.stack)}
elseLabel := &Label{Kind: LabelKindElse, FrameID: frame.frameID}
continuationLabel := &Label{Kind: LabelKindContinuation, FrameID: frame.frameID}
c.result.LabelCallers[continuationLabel.String()] += 2
c.emit(
dropOp,
@@ -451,7 +457,7 @@ operatorSwitch:
)
case controlFrameKindBlockWithContinuationLabel,
controlFrameKindIfWithElse:
continuationLabel := &Label{Kind: LabelKindContinuation, FrameID: frame.frameID, OriginalStackLen: len(c.stack)}
continuationLabel := &Label{Kind: LabelKindContinuation, FrameID: frame.frameID}
c.result.LabelCallers[continuationLabel.String()]++
c.emit(
dropOp,
@@ -476,7 +482,7 @@ operatorSwitch:
targetFrame := c.controlFrames.get(int(targetIndex))
targetFrame.ensureContinuation()
dropOp := &OperationDrop{Range: c.getFrameDropRange(targetFrame)}
dropOp := &OperationDrop{Depth: c.getFrameDropRange(targetFrame, false)}
target := targetFrame.asBranchTarget()
c.result.LabelCallers[target.Label.String()]++
c.emit(
@@ -496,7 +502,7 @@ operatorSwitch:
targetFrame := c.controlFrames.get(int(targetIndex))
targetFrame.ensureContinuation()
drop := c.getFrameDropRange(targetFrame)
drop := c.getFrameDropRange(targetFrame, false)
target := targetFrame.asBranchTarget()
c.result.LabelCallers[target.Label.String()]++
@@ -530,7 +536,7 @@ operatorSwitch:
c.pc += n
targetFrame := c.controlFrames.get(int(l))
targetFrame.ensureContinuation()
drop := c.getFrameDropRange(targetFrame)
drop := c.getFrameDropRange(targetFrame, false)
target := &BranchTargetDrop{ToDrop: drop, Target: targetFrame.asBranchTarget()}
targets[i] = target
c.result.LabelCallers[target.Target.Label.String()]++
@@ -544,7 +550,7 @@ operatorSwitch:
c.pc += n
defaultTargetFrame := c.controlFrames.get(int(l))
defaultTargetFrame.ensureContinuation()
defaultTargetDrop := c.getFrameDropRange(defaultTargetFrame)
defaultTargetDrop := c.getFrameDropRange(defaultTargetFrame, false)
defaultTarget := defaultTargetFrame.asBranchTarget()
c.result.LabelCallers[defaultTarget.Label.String()]++
@@ -562,7 +568,7 @@ operatorSwitch:
c.markUnreachable()
case wasm.OpcodeReturn:
functionFrame := c.controlFrames.functionFrame()
dropOp := &OperationDrop{Range: c.getFrameDropRange(functionFrame)}
dropOp := &OperationDrop{Depth: c.getFrameDropRange(functionFrame, false)}
// Cleanup the stack and then jmp to function frame's continuation (meaning return).
c.emit(
@@ -595,7 +601,7 @@ operatorSwitch:
)
case wasm.OpcodeDrop:
c.emit(
&OperationDrop{Range: &InclusiveRange{Start: 0, End: 0}},
&OperationDrop{Depth: &InclusiveRange{Start: 0, End: 0}},
)
case wasm.OpcodeSelect:
c.emit(
@@ -620,7 +626,7 @@ operatorSwitch:
// +1 because we already manipulated the stack before
// called localDepth ^^.
&OperationSwap{Depth: depth + 1},
&OperationDrop{Range: &InclusiveRange{Start: 0, End: 0}},
&OperationDrop{Depth: &InclusiveRange{Start: 0, End: 0}},
)
case wasm.OpcodeLocalTee:
if index == nil {
@@ -630,7 +636,7 @@ operatorSwitch:
c.emit(
&OperationPick{Depth: 0},
&OperationSwap{Depth: depth + 1},
&OperationDrop{Range: &InclusiveRange{Start: 0, End: 0}},
&OperationDrop{Depth: &InclusiveRange{Start: 0, End: 0}},
)
case wasm.OpcodeGlobalGet:
if index == nil {
@@ -647,7 +653,7 @@ operatorSwitch:
&OperationGlobalSet{Index: *index},
)
case wasm.OpcodeI32Load:
imm, err := c.readMemoryImmediate("i32.load")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32LoadName)
if err != nil {
return err
}
@@ -655,7 +661,7 @@ operatorSwitch:
&OperationLoad{Type: UnsignedTypeI32, Arg: imm},
)
case wasm.OpcodeI64Load:
imm, err := c.readMemoryImmediate("i64.load")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64LoadName)
if err != nil {
return err
}
@@ -663,7 +669,7 @@ operatorSwitch:
&OperationLoad{Type: UnsignedTypeI64, Arg: imm},
)
case wasm.OpcodeF32Load:
imm, err := c.readMemoryImmediate("f32.load")
imm, err := c.readMemoryImmediate(wasm.OpcodeF32LoadName)
if err != nil {
return err
}
@@ -671,7 +677,7 @@ operatorSwitch:
&OperationLoad{Type: UnsignedTypeF32, Arg: imm},
)
case wasm.OpcodeF64Load:
imm, err := c.readMemoryImmediate("f64.load")
imm, err := c.readMemoryImmediate(wasm.OpcodeF64LoadName)
if err != nil {
return err
}
@@ -679,7 +685,7 @@ operatorSwitch:
&OperationLoad{Type: UnsignedTypeF64, Arg: imm},
)
case wasm.OpcodeI32Load8S:
imm, err := c.readMemoryImmediate("i32.load8_s")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32Load8SName)
if err != nil {
return err
}
@@ -687,7 +693,7 @@ operatorSwitch:
&OperationLoad8{Type: SignedInt32, Arg: imm},
)
case wasm.OpcodeI32Load8U:
imm, err := c.readMemoryImmediate("i32.load8_u")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32Load8UName)
if err != nil {
return err
}
@@ -695,7 +701,7 @@ operatorSwitch:
&OperationLoad8{Type: SignedUint32, Arg: imm},
)
case wasm.OpcodeI32Load16S:
imm, err := c.readMemoryImmediate("i32.load16_s")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32Load16SName)
if err != nil {
return err
}
@@ -703,7 +709,7 @@ operatorSwitch:
&OperationLoad16{Type: SignedInt32, Arg: imm},
)
case wasm.OpcodeI32Load16U:
imm, err := c.readMemoryImmediate("i32.load16_u")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32Load16UName)
if err != nil {
return err
}
@@ -711,7 +717,7 @@ operatorSwitch:
&OperationLoad16{Type: SignedUint32, Arg: imm},
)
case wasm.OpcodeI64Load8S:
imm, err := c.readMemoryImmediate("i64.load8_s")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Load8SName)
if err != nil {
return err
}
@@ -719,7 +725,7 @@ operatorSwitch:
&OperationLoad8{Type: SignedInt64, Arg: imm},
)
case wasm.OpcodeI64Load8U:
imm, err := c.readMemoryImmediate("i64.load8_u")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Load8UName)
if err != nil {
return err
}
@@ -727,7 +733,7 @@ operatorSwitch:
&OperationLoad8{Type: SignedUint64, Arg: imm},
)
case wasm.OpcodeI64Load16S:
imm, err := c.readMemoryImmediate("i64.load16_s")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Load16SName)
if err != nil {
return err
}
@@ -735,7 +741,7 @@ operatorSwitch:
&OperationLoad16{Type: SignedInt64, Arg: imm},
)
case wasm.OpcodeI64Load16U:
imm, err := c.readMemoryImmediate("i64.load16_u")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Load16UName)
if err != nil {
return err
}
@@ -743,7 +749,7 @@ operatorSwitch:
&OperationLoad16{Type: SignedUint64, Arg: imm},
)
case wasm.OpcodeI64Load32S:
imm, err := c.readMemoryImmediate("i64.load32_s")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Load32SName)
if err != nil {
return err
}
@@ -751,7 +757,7 @@ operatorSwitch:
&OperationLoad32{Signed: true, Arg: imm},
)
case wasm.OpcodeI64Load32U:
imm, err := c.readMemoryImmediate("i64.load32_s")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Load32UName)
if err != nil {
return err
}
@@ -759,7 +765,7 @@ operatorSwitch:
&OperationLoad32{Signed: false, Arg: imm},
)
case wasm.OpcodeI32Store:
imm, err := c.readMemoryImmediate("i32.store")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32StoreName)
if err != nil {
return err
}
@@ -767,7 +773,7 @@ operatorSwitch:
&OperationStore{Type: UnsignedTypeI32, Arg: imm},
)
case wasm.OpcodeI64Store:
imm, err := c.readMemoryImmediate("i64.store")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64StoreName)
if err != nil {
return err
}
@@ -775,7 +781,7 @@ operatorSwitch:
&OperationStore{Type: UnsignedTypeI64, Arg: imm},
)
case wasm.OpcodeF32Store:
imm, err := c.readMemoryImmediate("f32.store")
imm, err := c.readMemoryImmediate(wasm.OpcodeF32StoreName)
if err != nil {
return err
}
@@ -783,7 +789,7 @@ operatorSwitch:
&OperationStore{Type: UnsignedTypeF32, Arg: imm},
)
case wasm.OpcodeF64Store:
imm, err := c.readMemoryImmediate("f64.store")
imm, err := c.readMemoryImmediate(wasm.OpcodeF64StoreName)
if err != nil {
return err
}
@@ -791,7 +797,7 @@ operatorSwitch:
&OperationStore{Type: UnsignedTypeF64, Arg: imm},
)
case wasm.OpcodeI32Store8:
imm, err := c.readMemoryImmediate("i32.store8")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32Store8Name)
if err != nil {
return err
}
@@ -799,7 +805,7 @@ operatorSwitch:
&OperationStore8{Type: UnsignedInt32, Arg: imm},
)
case wasm.OpcodeI32Store16:
imm, err := c.readMemoryImmediate("i32.store16")
imm, err := c.readMemoryImmediate(wasm.OpcodeI32Store16Name)
if err != nil {
return err
}
@@ -807,7 +813,7 @@ operatorSwitch:
&OperationStore16{Type: UnsignedInt32, Arg: imm},
)
case wasm.OpcodeI64Store8:
imm, err := c.readMemoryImmediate("i64.store8")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Store8Name)
if err != nil {
return err
}
@@ -815,7 +821,7 @@ operatorSwitch:
&OperationStore8{Type: UnsignedInt64, Arg: imm},
)
case wasm.OpcodeI64Store16:
imm, err := c.readMemoryImmediate("i64.store16")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Store16Name)
if err != nil {
return err
}
@@ -823,7 +829,7 @@ operatorSwitch:
&OperationStore16{Type: UnsignedInt64, Arg: imm},
)
case wasm.OpcodeI64Store32:
imm, err := c.readMemoryImmediate("i64.store32")
imm, err := c.readMemoryImmediate(wasm.OpcodeI64Store32Name)
if err != nil {
return err
}
@@ -1493,7 +1499,7 @@ func (c *compiler) emit(ops ...Operation) {
// we could remove such operations.
// That happens when drop operation is unnecessary.
// i.e. when there's no need to adjust stack before jmp.
if o.Range == nil {
if o.Depth == nil {
continue
}
}
@@ -1530,18 +1536,28 @@ func (c *compiler) localDepth(n uint32) int {
return int(len(c.stack)) - 1 - int(n)
}
// Returns the range (starting from top of the stack) that spans across
// the stack. The range is supposed to be dropped from the stack when
// the given frame exists.
func (c *compiler) getFrameDropRange(frame *controlFrame) *InclusiveRange {
start := len(frame.returns)
// getFrameDropRange returns the range (starting from top of the stack) that spans across the stack. The range is
// supposed to be dropped from the stack when the given frame exists or branch into it.
//
// * frame is the control frame which the call-site is trying to branch into or exit.
// * isEnd true if the call-site is handling wasm.OpcodeEnd.
func (c *compiler) getFrameDropRange(frame *controlFrame, isEnd bool) *InclusiveRange {
var start int
if !isEnd && frame.kind == controlFrameKindLoop {
// If this is not End and the call-site is trying to branch into the Loop control frame,
// we have to start executing from the beginning of the loop block.
// Therefore, we have to pass the inputs to the frame.
start = len(frame.blockType.Params)
} else {
start = len(frame.blockType.Results)
}
var end int
if frame.kind == controlFrameKindFunction {
// On the function return, we eliminate all the contents on the stack
// including locals (existing below of frame.originalStackLen)
end = len(c.stack) - 1
} else {
end = len(c.stack) - 1 - frame.originalStackLen
end = len(c.stack) - 1 - frame.originalStackLenWithoutParam
}
if start <= end {
return &InclusiveRange{Start: start, End: end}

View File

@@ -0,0 +1,428 @@
package wazeroir
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasm/text"
)
var (
f64, i32 = wasm.ValueTypeF64, wasm.ValueTypeI32
i32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}
i32i32_i32 = &wasm.FunctionType{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32}}
v_v = &wasm.FunctionType{}
v_f64f64 = &wasm.FunctionType{Results: []wasm.ValueType{f64, f64}}
)
func TestCompile(t *testing.T) {
tests := []struct {
name string
module *wasm.Module
expected *CompilationResult
enabledFeatures wasm.Features
}{
{
name: "nullary",
module: requireModuleText(t, `(module (func))`),
expected: &CompilationResult{
Operations: []Operation{ // begin with params: []
&OperationBr{Target: &BranchTarget{}}, // return!
},
LabelCallers: map[string]uint32{},
},
},
{
name: "identity",
module: requireModuleText(t, `(module
(func (param $x i32) (result i32) local.get 0)
)`),
expected: &CompilationResult{
Operations: []Operation{ // begin with params: [$x]
&OperationPick{Depth: 0}, // [$x, $x]
&OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [$x]
&OperationBr{Target: &BranchTarget{}}, // return!
},
LabelCallers: map[string]uint32{},
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
enabledFeatures := tc.enabledFeatures
if enabledFeatures == 0 {
enabledFeatures = wasm.FeaturesFinished
}
functions, err := compileFunctions(enabledFeatures, tc.module)
require.NoError(t, err)
require.Len(t, functions, 1)
res, err := Compile(enabledFeatures, functions[0])
require.NoError(t, err)
require.Equal(t, tc.expected, res)
})
}
}
func TestCompile_Block(t *testing.T) {
tests := []struct {
name string
module *wasm.Module
expected *CompilationResult
enabledFeatures wasm.Features
}{
{
name: "type-i32-i32",
module: &wasm.Module{
TypeSection: []*wasm.FunctionType{v_v},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{
wasm.OpcodeBlock, 0x40,
wasm.OpcodeBr, 0,
wasm.OpcodeI32Add,
wasm.OpcodeDrop,
wasm.OpcodeEnd,
wasm.OpcodeEnd,
}}},
},
// Above set manually until the text compiler supports this:
// (func (export "type-i32-i32") (block (drop (i32.add (br 0)))))
expected: &CompilationResult{
Operations: []Operation{ // begin with params: []
&OperationBr{
Target: &BranchTarget{
Label: &Label{FrameID: 2, Kind: LabelKindContinuation}, // arbitrary FrameID
},
},
&OperationLabel{
Label: &Label{FrameID: 2, Kind: LabelKindContinuation}, // arbitrary FrameID
},
&OperationBr{Target: &BranchTarget{}}, // return!
},
// Note: i32.add comes after br 0 so is unreachable. Compilation succeeds when it feels like it
// shouldn't because the br instruction is stack-polymorphic. In other words, (br 0) substitutes for the
// two i32 parameters to add.
LabelCallers: map[string]uint32{".L2_cont": 1},
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
requireCompilationResult(t, tc.enabledFeatures, tc.expected, tc.module)
})
}
}
func TestCompile_MultiValue(t *testing.T) {
tests := []struct {
name string
module *wasm.Module
expected *CompilationResult
enabledFeatures wasm.Features
}{
{
name: "swap",
module: requireModuleText(t, `(module
(func (param $x i32) (param $y i32) (result i32 i32) local.get 1 local.get 0)
)`),
expected: &CompilationResult{
Operations: []Operation{ // begin with params: [$x, $y]
&OperationPick{Depth: 0}, // [$x, $y, $y]
&OperationPick{Depth: 2}, // [$x, $y, $y, $x]
&OperationDrop{Depth: &InclusiveRange{Start: 2, End: 3}}, // [$y, $x]
&OperationBr{Target: &BranchTarget{}}, // return!
},
LabelCallers: map[string]uint32{},
},
},
{
name: "br.wast - type-f64-f64-value",
module: &wasm.Module{
TypeSection: []*wasm.FunctionType{v_f64f64},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{
wasm.OpcodeBlock, 0, // (block (result f64 f64)
wasm.OpcodeF64Const, 0, 0, 0, 0, 0, 0, 0x10, 0x40, // (f64.const 4)
wasm.OpcodeF64Const, 0, 0, 0, 0, 0, 0, 0x14, 0x40, // (f64.const 5)
wasm.OpcodeBr, 0,
wasm.OpcodeF64Add,
wasm.OpcodeF64Const, 0, 0, 0, 0, 0, 0, 0x18, 0x40, // (f64.const 6)
wasm.OpcodeEnd,
wasm.OpcodeEnd,
}}},
},
// Above set manually until the text compiler supports this:
// (func $type-f64-f64-value (result f64 f64)
// (block (result f64 f64)
// (f64.add (br 0 (f64.const 4) (f64.const 5))) (f64.const 6)
// )
// )
expected: &CompilationResult{
Operations: []Operation{ // begin with params: []
&OperationConstF64{Value: 4}, // [4]
&OperationConstF64{Value: 5}, // [4, 5]
&OperationBr{
Target: &BranchTarget{
Label: &Label{FrameID: 2, Kind: LabelKindContinuation}, // arbitrary FrameID
},
},
&OperationLabel{
Label: &Label{FrameID: 2, Kind: LabelKindContinuation}, // arbitrary FrameID
},
&OperationBr{Target: &BranchTarget{}}, // return!
},
// Note: f64.add comes after br 0 so is unreachable. This is why neither the add, nor its other operand
// are in the above compilation result.
LabelCallers: map[string]uint32{".L2_cont": 1}, // arbitrary label
},
},
{
name: "call.wast - $const-i32-i64",
module: requireModuleText(t, `(module
(func $const-i32-i64 (result i32 i64) i32.const 306 i64.const 356)
)`),
expected: &CompilationResult{
Operations: []Operation{ // begin with params: []
&OperationConstI32{Value: 306}, // [306]
&OperationConstI64{Value: 356}, // [306, 356]
&OperationBr{Target: &BranchTarget{}}, // return!
},
LabelCallers: map[string]uint32{},
},
},
{
name: "if.wast - param",
module: &wasm.Module{
TypeSection: []*wasm.FunctionType{i32_i32}, // (func (param i32) (result i32)
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{
wasm.OpcodeI32Const, 1, // (i32.const 1)
wasm.OpcodeLocalGet, 0, wasm.OpcodeIf, 0, // (if (param i32) (result i32) (local.get 0)
wasm.OpcodeI32Const, 2, wasm.OpcodeI32Add, // (then (i32.const 2) (i32.add))
wasm.OpcodeElse, wasm.OpcodeI32Const, 0x7e, wasm.OpcodeI32Add, // (else (i32.const -2) (i32.add))
wasm.OpcodeEnd, // )
wasm.OpcodeEnd, // )
}}},
},
// Above set manually until the text compiler supports this:
// (func (export "param") (param i32) (result i32)
// (i32.const 1)
// (if (param i32) (result i32) (local.get 0)
// (then (i32.const 2) (i32.add))
// (else (i32.const -2) (i32.add))
// )
// )
expected: &CompilationResult{
Operations: []Operation{ // begin with params: [$0]
&OperationConstI32{Value: 1}, // [$0, 1]
&OperationPick{Depth: 1}, // [$0, 1, $0]
&OperationBrIf{ // [$0, 1]
Then: &BranchTargetDrop{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindHeader}}},
Else: &BranchTargetDrop{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindElse}}},
},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindHeader}},
&OperationConstI32{Value: 2}, // [$0, 1, 2]
&OperationAdd{Type: UnsignedTypeI32}, // [$0, 3]
&OperationBr{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}}},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindElse}},
&OperationConstI32{Value: uint32(api.EncodeI32(-2))}, // [$0, 1, -2]
&OperationAdd{Type: UnsignedTypeI32}, // [$0, -1]
&OperationBr{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}}},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}},
&OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // .L2 = [3], .L2_else = [-1]
&OperationBr{Target: &BranchTarget{}},
},
LabelCallers: map[string]uint32{
".L2": 1,
".L2_cont": 2,
".L2_else": 1,
},
},
},
{
name: "if.wast - params",
module: &wasm.Module{
TypeSection: []*wasm.FunctionType{
i32_i32, // (func (param i32) (result i32)
i32i32_i32, // (if (param i32 i32) (result i32)
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{
wasm.OpcodeI32Const, 1, // (i32.const 1)
wasm.OpcodeI32Const, 2, // (i32.const 2)
wasm.OpcodeLocalGet, 0, wasm.OpcodeIf, 1, // (if (param i32) (result i32) (local.get 0)
wasm.OpcodeI32Add, // (then (i32.add))
wasm.OpcodeElse, wasm.OpcodeI32Sub, // (else (i32.sub))
wasm.OpcodeEnd, // )
wasm.OpcodeEnd, // )
}}},
},
// Above set manually until the text compiler supports this:
// (func (export "params") (param i32) (result i32)
// (i32.const 1)
// (i32.const 2)
// (if (param i32 i32) (result i32) (local.get 0)
// (then (i32.add))
// (else (i32.sub))
// )
// )
expected: &CompilationResult{
Operations: []Operation{ // begin with params: [$0]
&OperationConstI32{Value: 1}, // [$0, 1]
&OperationConstI32{Value: 2}, // [$0, 1, 2]
&OperationPick{Depth: 2}, // [$0, 1, 2, $0]
&OperationBrIf{ // [$0, 1, 2]
Then: &BranchTargetDrop{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindHeader}}},
Else: &BranchTargetDrop{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindElse}}},
},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindHeader}},
&OperationAdd{Type: UnsignedTypeI32}, // [$0, 3]
&OperationBr{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}}},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindElse}},
&OperationSub{Type: UnsignedTypeI32}, // [$0, -1]
&OperationBr{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}}},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}},
&OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // .L2 = [3], .L2_else = [-1]
&OperationBr{Target: &BranchTarget{}},
},
LabelCallers: map[string]uint32{
".L2": 1,
".L2_cont": 2,
".L2_else": 1,
},
},
},
{
name: "if.wast - params-break",
module: &wasm.Module{
TypeSection: []*wasm.FunctionType{
i32_i32, // (func (param i32) (result i32)
i32i32_i32, // (if (param i32 i32) (result i32)
},
FunctionSection: []wasm.Index{0},
CodeSection: []*wasm.Code{{Body: []byte{
wasm.OpcodeI32Const, 1, // (i32.const 1)
wasm.OpcodeI32Const, 2, // (i32.const 2)
wasm.OpcodeLocalGet, 0, wasm.OpcodeIf, 1, // (if (param i32) (result i32) (local.get 0)
wasm.OpcodeI32Add, wasm.OpcodeBr, 0, // (then (i32.add) (br 0))
wasm.OpcodeElse, wasm.OpcodeI32Sub, wasm.OpcodeBr, 0, // (else (i32.sub) (br 0))
wasm.OpcodeEnd, // )
wasm.OpcodeEnd, // )
}}},
},
// Above set manually until the text compiler supports this:
// (func (export "params-break") (param i32) (result i32)
// (i32.const 1)
// (i32.const 2)
// (if (param i32 i32) (result i32) (local.get 0)
// (then (i32.add) (br 0))
// (else (i32.sub) (br 0))
// )
// )
expected: &CompilationResult{
Operations: []Operation{ // begin with params: [$0]
&OperationConstI32{Value: 1}, // [$0, 1]
&OperationConstI32{Value: 2}, // [$0, 1, 2]
&OperationPick{Depth: 2}, // [$0, 1, 2, $0]
&OperationBrIf{ // [$0, 1, 2]
Then: &BranchTargetDrop{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindHeader}}},
Else: &BranchTargetDrop{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindElse}}},
},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindHeader}},
&OperationAdd{Type: UnsignedTypeI32}, // [$0, 3]
&OperationBr{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}}},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindElse}},
&OperationSub{Type: UnsignedTypeI32}, // [$0, -1]
&OperationBr{Target: &BranchTarget{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}}},
&OperationLabel{Label: &Label{FrameID: 2, Kind: LabelKindContinuation}},
&OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // .L2 = [3], .L2_else = [-1]
&OperationBr{Target: &BranchTarget{}},
},
LabelCallers: map[string]uint32{
".L2": 1,
".L2_cont": 2,
".L2_else": 1,
},
},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
enabledFeatures := tc.enabledFeatures
if enabledFeatures == 0 {
enabledFeatures = wasm.FeaturesFinished
}
functions, err := compileFunctions(enabledFeatures, tc.module)
require.NoError(t, err)
require.Len(t, functions, 1)
res, err := Compile(enabledFeatures, functions[0])
require.NoError(t, err)
require.Equal(t, tc.expected, res)
})
}
}
func requireCompilationResult(t *testing.T, enabledFeatures wasm.Features, expected *CompilationResult, module *wasm.Module) {
if enabledFeatures == 0 {
enabledFeatures = wasm.FeaturesFinished
}
functions, err := compileFunctions(enabledFeatures, module)
require.NoError(t, err)
require.Len(t, functions, 1)
res, err := Compile(enabledFeatures, functions[0])
require.NoError(t, err)
require.Equal(t, expected, res)
}
func requireModuleText(t *testing.T, source string) *wasm.Module {
m, err := text.DecodeModule([]byte(source), wasm.FeaturesFinished, wasm.MemoryMaxPages)
require.NoError(t, err)
return m
}
func compileFunctions(enabledFeatures wasm.Features, module *wasm.Module) ([]*wasm.FunctionInstance, error) {
cf := &catchFunctions{}
_, err := wasm.NewStore(enabledFeatures, cf).Instantiate(context.Background(), module, "", wasm.DefaultSysContext())
return cf.functions, err
}
type catchFunctions struct {
functions []*wasm.FunctionInstance
}
// NewModuleEngine implements the same method as documented on wasm.Engine.
func (e *catchFunctions) NewModuleEngine(_ string, _, functions []*wasm.FunctionInstance, _ *wasm.TableInstance, _ map[wasm.Index]wasm.Index) (wasm.ModuleEngine, error) {
e.functions = functions
return e, nil
}
// Name implements the same method as documented on wasm.ModuleEngine.
func (e *catchFunctions) Name() string {
return ""
}
// Call implements the same method as documented on wasm.ModuleEngine.
func (e *catchFunctions) Call(_ *wasm.ModuleContext, _ *wasm.FunctionInstance, _ ...uint64) ([]uint64, error) {
return nil, nil
}
// Close implements the same method as documented on wasm.ModuleEngine.
func (e *catchFunctions) Close() {
}

View File

@@ -44,7 +44,7 @@ func formatOperation(w io.StringWriter, b Operation) {
case *OperationCallIndirect:
str = fmt.Sprintf("call_indirect: type=%d, table=%d", o.TypeIndex, o.TableIndex)
case *OperationDrop:
str = fmt.Sprintf("drop %d..%d", o.Range.Start, o.Range.End)
str = fmt.Sprintf("drop %d..%d", o.Depth.Start, o.Depth.End)
case *OperationSelect:
str = "select"
case *OperationPick:

View File

@@ -343,9 +343,8 @@ const (
)
type Label struct {
FrameID uint32
OriginalStackLen int
Kind LabelKind
FrameID uint32
Kind LabelKind
}
func (l *Label) String() (ret string) {
@@ -471,7 +470,7 @@ func (o *OperationCallIndirect) Kind() OperationKind {
return OperationKindCallIndirect
}
type OperationDrop struct{ Range *InclusiveRange }
type OperationDrop struct{ Depth *InclusiveRange }
func (o *OperationDrop) Kind() OperationKind {
return OperationKindDrop
@@ -507,8 +506,19 @@ func (o *OperationGlobalSet) Kind() OperationKind {
return OperationKindGlobalSet
}
// MemoryImmediate is the "memarg" to all memory instructions.
//
// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-instructions%E2%91%A0
type MemoryImmediate struct {
Alignment, Offset uint32
// Alignment the expected alignment (expressed as the exponent of a power of 2). Default to the natural alignment.
//
// "Natural alignment" is defined here as the smallest power of two that can hold the size of the value type. Ex
// wasm.ValueTypeI64 is encoded in 8 little-endian bytes. 2^3 = 8, so the natural alignment is three.
Alignment uint32
// Offset is the address offset added to the instruction's dynamic address operand, yielding a 33-bit effective
// address that is the zero-based index at which the memory is accessed. Default to zero.
Offset uint32
}
type OperationLoad struct {

View File

@@ -0,0 +1,309 @@
package multi_value
import (
_ "embed"
"testing"
"github.com/stretchr/testify/require"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
)
func TestMultiValue_JIT(t *testing.T) {
if !wazero.JITSupported {
t.Skip()
}
testMultiValue(t, wazero.NewRuntimeConfigJIT)
}
func TestMultiValue_Interpreter(t *testing.T) {
testMultiValue(t, wazero.NewRuntimeConfigInterpreter)
}
// multiValueWasm was compiled from testdata/multi_value.wat
//go:embed testdata/multi_value.wasm
var multiValueWasm []byte
func testMultiValue(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
t.Run("disabled", func(t *testing.T) {
// multi-value is disabled by default.
r := wazero.NewRuntimeWithConfig(newRuntimeConfig())
_, err := r.InstantiateModuleFromCode(multiValueWasm)
require.Error(t, err)
})
t.Run("enabled", func(t *testing.T) {
r := wazero.NewRuntimeWithConfig(newRuntimeConfig().WithFeatureMultiValue(true))
module, err := r.InstantiateModuleFromCode(multiValueWasm)
require.NoError(t, err)
defer module.Close()
swap := module.ExportedFunction("swap")
results, err := swap.Call(nil, 100, 200)
require.NoError(t, err)
require.Equal(t, []uint64{200, 100}, results)
add64UWithCarry := module.ExportedFunction("add64_u_with_carry")
results, err = add64UWithCarry.Call(nil, 0x8000000000000000, 0x8000000000000000, 0)
require.NoError(t, err)
require.Equal(t, []uint64{0, 1}, results)
add64USaturated := module.ExportedFunction("add64_u_saturated")
results, err = add64USaturated.Call(nil, 1230, 23)
require.NoError(t, err)
require.Equal(t, []uint64{1253}, results)
fac := module.ExportedFunction("fac")
results, err = fac.Call(nil, 25)
require.NoError(t, err)
require.Equal(t, []uint64{7034535277573963776}, results)
t.Run("br.wast", func(t *testing.T) {
testBr(t, r)
})
t.Run("call.wast", func(t *testing.T) {
testCall(t, r)
})
t.Run("call_indirect.wast", func(t *testing.T) {
testCallIndirect(t, r)
})
t.Run("fac.wast", func(t *testing.T) {
testFac(t, r)
})
t.Run("func.wast", func(t *testing.T) {
testFunc(t, r)
})
t.Run("if.wast", func(t *testing.T) {
testIf(t, r)
})
t.Run("loop.wast", func(t *testing.T) {
testLoop(t, r)
})
})
}
// brWasm was compiled from testdata/br.wat
//go:embed testdata/br.wasm
var brWasm []byte
func testBr(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(brWasm)
require.NoError(t, err)
defer module.Close()
testFunctions(t, module, []funcTest{
{name: "type-i32-i32"}, {name: "type-i64-i64"}, {name: "type-f32-f32"}, {name: "type-f64-f64"},
{name: "type-f64-f64-value", expected: []uint64{api.EncodeF64(4), api.EncodeF64(5)}},
{name: "as-return-values", expected: []uint64{2, 7}},
{name: "as-select-all", expected: []uint64{8}},
{name: "as-call-all", expected: []uint64{15}},
{name: "as-call_indirect-all", expected: []uint64{24}},
{name: "as-store-both", expected: []uint64{32}},
{name: "as-storeN-both", expected: []uint64{34}},
{name: "as-binary-both", expected: []uint64{46}},
{name: "as-compare-both", expected: []uint64{44}},
})
}
// callWasm was compiled from testdata/call.wat
//go:embed testdata/call.wasm
var callWasm []byte
func testCall(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(callWasm)
require.NoError(t, err)
defer module.Close()
testFunctions(t, module, []funcTest{
{name: "type-i32-i64", expected: []uint64{0x132, 0x164}},
{name: "type-all-i32-f64", expected: []uint64{32, api.EncodeF64(1.64)}},
{name: "type-all-i32-i32", expected: []uint64{2, 1}},
{name: "type-all-f32-f64", expected: []uint64{api.EncodeF64(2), api.EncodeF32(1)}},
{name: "type-all-f64-i32", expected: []uint64{2, api.EncodeF64(1)}},
{name: "as-binary-all-operands", expected: []uint64{7}},
{name: "as-mixed-operands", expected: []uint64{32}},
{name: "as-call-all-operands", expected: []uint64{3, 4}},
})
}
// callIndirectWasm was compiled from testdata/call_indirect.wat
//go:embed testdata/call_indirect.wasm
var callIndirectWasm []byte
func testCallIndirect(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(callIndirectWasm)
require.NoError(t, err)
defer module.Close()
testFunctions(t, module, []funcTest{
{name: "type-f64-i32", expected: []uint64{api.EncodeF64(0xf64), 32}},
{name: "type-all-f64-i32", expected: []uint64{api.EncodeF64(0xf64), 32}},
{name: "type-all-i32-f64", expected: []uint64{1, api.EncodeF64(2)}},
{name: "type-all-i32-i64", expected: []uint64{2, 1}},
})
_, err = module.ExportedFunction("dispatch").Call(nil, 32, 2)
require.EqualError(t, err, `wasm error: invalid table access
wasm stack trace:
call_indirect.wast.[16](i32,i64) i64`)
}
// facWasm was compiled from testdata/fac.wat
//go:embed testdata/fac.wasm
var facWasm []byte
func testFac(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(facWasm)
require.NoError(t, err)
defer module.Close()
fac := module.ExportedFunction("fac-ssa")
results, err := fac.Call(nil, 25)
require.NoError(t, err)
require.Equal(t, []uint64{7034535277573963776}, results)
}
// funcWasm was compiled from testdata/func.wat
//go:embed testdata/func.wasm
var funcWasm []byte
func testFunc(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(funcWasm)
require.NoError(t, err)
defer module.Close()
testFunctions(t, module, []funcTest{
{name: "value-i32-f64", expected: []uint64{77, api.EncodeF64(7)}},
{name: "value-i32-i32-i32", expected: []uint64{1, 2, 3}},
{name: "value-block-i32-i64", expected: []uint64{1, 2}},
{name: "return-i32-f64", expected: []uint64{78, api.EncodeF64(78.78)}},
{name: "return-i32-i32-i32", expected: []uint64{1, 2, 3}},
{name: "return-block-i32-i64", expected: []uint64{1, 2}},
{name: "break-i32-f64", expected: []uint64{79, api.EncodeF64(79.79)}},
{name: "break-i32-i32-i32", expected: []uint64{1, 2, 3}},
{name: "break-block-i32-i64", expected: []uint64{1, 2}},
{name: "break-br_if-num-num", params: []uint64{0}, expected: []uint64{51, 52}},
{name: "break-br_if-num-num", params: []uint64{1}, expected: []uint64{50, 51}},
{name: "break-br_table-num-num", params: []uint64{0}, expected: []uint64{50, 51}},
{name: "break-br_table-num-num", params: []uint64{1}, expected: []uint64{50, 51}},
{name: "break-br_table-num-num", params: []uint64{10}, expected: []uint64{50, 51}},
{name: "break-br_table-num-num", params: []uint64{api.EncodeI32(-100)}, expected: []uint64{50, 51}},
{name: "break-br_table-nested-num-num", params: []uint64{0}, expected: []uint64{101, 52}},
{name: "break-br_table-nested-num-num", params: []uint64{1}, expected: []uint64{50, 51}},
{name: "break-br_table-nested-num-num", params: []uint64{2}, expected: []uint64{101, 52}},
{name: "break-br_table-nested-num-num", params: []uint64{api.EncodeI32(-3)}, expected: []uint64{101, 52}},
})
fac := module.ExportedFunction("large-sig")
results, err := fac.Call(nil,
0, 1, api.EncodeF32(2), api.EncodeF32(3),
4, api.EncodeF64(5), api.EncodeF32(6), 7,
8, 9, api.EncodeF32(10), api.EncodeF64(11),
api.EncodeF64(12), api.EncodeF64(13), 14, 15,
api.EncodeF32(16))
require.NoError(t, err)
require.Equal(t, []uint64{api.EncodeF64(5), api.EncodeF32(2), 0, 8,
7, 1, api.EncodeF32(3), 9,
4, api.EncodeF32(6), api.EncodeF64(13), api.EncodeF64(11),
15, api.EncodeF32(16), 14, api.EncodeF64(12),
}, results)
}
// ifWasm was compiled from testdata/if.wat
//go:embed testdata/if.wasm
var ifWasm []byte
func testIf(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(ifWasm)
require.NoError(t, err)
defer module.Close()
testFunctions(t, module, []funcTest{
{name: "multi", params: []uint64{0}, expected: []uint64{9, api.EncodeI32(-1)}},
{name: "multi", params: []uint64{1}, expected: []uint64{8, 1}},
{name: "multi", params: []uint64{13}, expected: []uint64{8, 1}},
{name: "multi", params: []uint64{api.EncodeI32(-5)}, expected: []uint64{8, 1}},
{name: "as-binary-operands", params: []uint64{0}, expected: []uint64{api.EncodeI32(-12)}},
{name: "as-binary-operands", params: []uint64{1}, expected: []uint64{api.EncodeI32(12)}},
{name: "as-compare-operands", params: []uint64{0}, expected: []uint64{1}},
{name: "as-compare-operands", params: []uint64{1}, expected: []uint64{0}},
{name: "as-mixed-operands", params: []uint64{0}, expected: []uint64{api.EncodeI32(-3)}},
{name: "as-mixed-operands", params: []uint64{1}, expected: []uint64{27}},
{name: "break-multi-value", params: []uint64{0}, expected: []uint64{api.EncodeI32(-18), 18, api.EncodeI64(-18)}},
{name: "break-multi-value", params: []uint64{1}, expected: []uint64{18, api.EncodeI32(-18), 18}},
{name: "param", params: []uint64{0}, expected: []uint64{api.EncodeI32(-1)}},
{name: "param", params: []uint64{1}, expected: []uint64{3}},
{name: "params", params: []uint64{0}, expected: []uint64{api.EncodeI32(-1)}},
{name: "params", params: []uint64{1}, expected: []uint64{3}},
{name: "params-id", params: []uint64{0}, expected: []uint64{3}},
{name: "params-id", params: []uint64{1}, expected: []uint64{3}},
{name: "param-break", params: []uint64{0}, expected: []uint64{api.EncodeI32(-1)}},
{name: "param-break", params: []uint64{1}, expected: []uint64{3}},
{name: "params-break", params: []uint64{0}, expected: []uint64{api.EncodeI32(-1)}},
{name: "params-break", params: []uint64{1}, expected: []uint64{3}},
{name: "params-id-break", params: []uint64{0}, expected: []uint64{3}},
{name: "params-id-break", params: []uint64{1}, expected: []uint64{3}},
{name: "add64_u_with_carry", params: []uint64{0, 0, 0}, expected: []uint64{0, 0}},
{name: "add64_u_with_carry", params: []uint64{100, 124, 0}, expected: []uint64{224, 0}},
{name: "add64_u_with_carry", params: []uint64{api.EncodeI64(-1), 0, 0}, expected: []uint64{api.EncodeI64(-1), 0}},
{name: "add64_u_with_carry", params: []uint64{api.EncodeI64(-1), 1, 0}, expected: []uint64{0, 1}},
{name: "add64_u_with_carry", params: []uint64{api.EncodeI64(-1), api.EncodeI64(-1), 0}, expected: []uint64{api.EncodeI64(-2), 1}},
{name: "add64_u_with_carry", params: []uint64{api.EncodeI64(-1), 0, 1}, expected: []uint64{0, 1}},
{name: "add64_u_with_carry", params: []uint64{api.EncodeI64(-1), 1, 1}, expected: []uint64{1, 1}},
{name: "add64_u_with_carry", params: []uint64{0x8000000000000000, 0x8000000000000000, 0}, expected: []uint64{0, 1}},
{name: "add64_u_saturated", params: []uint64{0, 0}, expected: []uint64{0}},
{name: "add64_u_saturated", params: []uint64{1230, 23}, expected: []uint64{1253}},
{name: "add64_u_saturated", params: []uint64{api.EncodeI64(-1), 0}, expected: []uint64{api.EncodeI64(-1)}},
{name: "add64_u_saturated", params: []uint64{api.EncodeI64(-1), 1}, expected: []uint64{api.EncodeI64(-1)}},
{name: "add64_u_saturated", params: []uint64{api.EncodeI64(-1), api.EncodeI64(-1)}, expected: []uint64{api.EncodeI64(-1)}},
{name: "add64_u_saturated", params: []uint64{0x8000000000000000, 0x8000000000000000}, expected: []uint64{api.EncodeI64(-1)}},
{name: "type-use"},
})
}
// loopWasm was compiled from testdata/loop.wat
//go:embed testdata/loop.wasm
var loopWasm []byte
func testLoop(t *testing.T, r wazero.Runtime) {
module, err := r.InstantiateModuleFromCode(loopWasm)
require.NoError(t, err)
defer module.Close()
testFunctions(t, module, []funcTest{
{name: "as-binary-operands", expected: []uint64{12}},
{name: "as-compare-operands", expected: []uint64{0}},
{name: "as-mixed-operands", expected: []uint64{27}},
{name: "break-multi-value", expected: []uint64{18, api.EncodeI32(-18), 18}},
{name: "param", expected: []uint64{3}},
{name: "params", expected: []uint64{3}},
{name: "params-id", expected: []uint64{3}},
{name: "param-break", expected: []uint64{13}},
{name: "params-break", expected: []uint64{12}},
{name: "params-id-break", expected: []uint64{3}},
{name: "type-use"},
})
}
type funcTest struct {
name string
params []uint64
expected []uint64
}
func testFunctions(t *testing.T, module api.Module, tests []funcTest) {
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
results, err := module.ExportedFunction(tc.name).Call(nil, tc.params...)
require.NoError(t, err)
if tc.expected == nil {
require.Empty(t, results)
} else {
require.Equal(t, tc.expected, results)
}
})
}
}

Binary file not shown.

View File

@@ -0,0 +1,69 @@
;; This file includes changes to test/core/br.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names br.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $br.wast
;; preconditions
(func $f (param i32 i32 i32) (result i32) (i32.const -1))
(type $sig (func (param i32 i32 i32) (result i32)))
(table funcref (elem $f))
(memory 1)
;; changes
(func (export "type-i32-i32") (block (drop (i32.add (br 0)))))
(func (export "type-i64-i64") (block (drop (i64.add (br 0)))))
(func (export "type-f32-f32") (block (drop (f32.add (br 0)))))
(func (export "type-f64-f64") (block (drop (f64.add (br 0)))))
(func (export "type-f64-f64-value") (result f64 f64)
(block (result f64 f64)
(f64.add (br 0 (f64.const 4) (f64.const 5))) (f64.const 6)
)
)
(func (export "as-return-values") (result i32 i64)
(i32.const 2)
(block (result i64) (return (br 0 (i32.const 1) (i64.const 7))))
)
(func (export "as-select-all") (result i32)
(block (result i32) (select (br 0 (i32.const 8))))
)
(func (export "as-call-all") (result i32)
(block (result i32) (call $f (br 0 (i32.const 15))))
)
(func (export "as-call_indirect-all") (result i32)
(block (result i32) (call_indirect (type $sig) (br 0 (i32.const 24))))
)
(func (export "as-store-both") (result i32)
(block (result i32)
(i64.store (br 0 (i32.const 32))) (i32.const -1)
)
)
(func (export "as-storeN-both") (result i32)
(block (result i32)
(i64.store16 (br 0 (i32.const 34))) (i32.const -1)
)
)
(func (export "as-binary-both") (result i32)
(block (result i32) (i32.add (br 0 (i32.const 46))))
)
(func (export "as-compare-both") (result i32)
(block (result i32) (f64.le (br 0 (i32.const 44))))
)
)

Binary file not shown.

View File

@@ -0,0 +1,65 @@
;; This file includes changes to test/core/call.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names call.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $call.wast
(func $const-i32-i64 (result i32 i64) (i32.const 0x132) (i64.const 0x164))
(func $id-i32-f64 (param i32 f64) (result i32 f64)
(local.get 0) (local.get 1)
)
(func $swap-i32-i32 (param i32 i32) (result i32 i32)
(local.get 1) (local.get 0)
)
(func $swap-f32-f64 (param f32 f64) (result f64 f32)
(local.get 1) (local.get 0)
)
(func $swap-f64-i32 (param f64 i32) (result i32 f64)
(local.get 1) (local.get 0)
)
(func (export "type-i32-i64") (result i32 i64) (call $const-i32-i64))
(func (export "type-all-i32-f64") (result i32 f64)
(call $id-i32-f64 (i32.const 32) (f64.const 1.64))
)
(func (export "type-all-i32-i32") (result i32 i32)
(call $swap-i32-i32 (i32.const 1) (i32.const 2))
)
(func (export "type-all-f32-f64") (result f64 f32)
(call $swap-f32-f64 (f32.const 1) (f64.const 2))
)
(func (export "type-all-f64-i32") (result i32 f64)
(call $swap-f64-i32 (f64.const 1) (i32.const 2))
)
(func (export "as-binary-all-operands") (result i32)
(i32.add (call $swap-i32-i32 (i32.const 3) (i32.const 4)))
)
(func (export "as-mixed-operands") (result i32)
(call $swap-i32-i32 (i32.const 3) (i32.const 4))
(i32.const 5)
(i32.add)
(i32.mul)
)
(func (export "as-call-all-operands") (result i32 i32)
(call $swap-i32-i32 (call $swap-i32-i32 (i32.const 3) (i32.const 4)))
)
)

Binary file not shown.

View File

@@ -0,0 +1,260 @@
;; This file includes changes to test/core/call_indirect.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names call_indirect.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $call_indirect.wast
;; preconditions
(type $proc (func))
(type $out-i32 (func (result i32)))
(type $out-i64 (func (result i64)))
(type $out-f32 (func (result f32)))
(type $out-f64 (func (result f64)))
(type $f32-i32 (func (param f32 i32) (result i32)))
(type $i32-i64 (func (param i32 i64) (result i64)))
(type $f64-f32 (func (param f64 f32) (result f32)))
(type $i64-f64 (func (param i64 f64) (result f64)))
(type $over-i32 (func (param i32) (result i32)))
(type $over-i64 (func (param i64) (result i64)))
(type $over-f32 (func (param f32) (result f32)))
(type $over-f64 (func (param f64) (result f64)))
(type $over-i32-duplicate (func (param i32) (result i32)))
(type $over-i64-duplicate (func (param i64) (result i64)))
(type $over-f32-duplicate (func (param f32) (result f32)))
(type $over-f64-duplicate (func (param f64) (result f64)))
(func $const-i32 (type $out-i32) (i32.const 0x132))
(func $const-i64 (type $out-i64) (i64.const 0x164))
(func $const-f32 (type $out-f32) (f32.const 0xf32))
(func $const-f64 (type $out-f64) (f64.const 0xf64))
(func $id-i32 (type $over-i32) (local.get 0))
(func $id-i64 (type $over-i64) (local.get 0))
(func $id-f32 (type $over-f32) (local.get 0))
(func $id-f64 (type $over-f64) (local.get 0))
(func $i32-i64 (type $i32-i64) (local.get 1))
(func $i64-f64 (type $i64-f64) (local.get 1))
(func $f32-i32 (type $f32-i32) (local.get 1))
(func $f64-f32 (type $f64-f32) (local.get 1))
(func $over-i32-duplicate (type $over-i32-duplicate) (local.get 0))
(func $over-i64-duplicate (type $over-i64-duplicate) (local.get 0))
(func $over-f32-duplicate (type $over-f32-duplicate) (local.get 0))
(func $over-f64-duplicate (type $over-f64-duplicate) (local.get 0))
(func (export "dispatch") (param i32 i64) (result i64)
(call_indirect (type $over-i64) (local.get 1) (local.get 0))
)
(func $fac-i64 (export "fac-i64") (type $over-i64)
(if (result i64) (i64.eqz (local.get 0))
(then (i64.const 1))
(else
(i64.mul
(local.get 0)
(call_indirect (type $over-i64)
(i64.sub (local.get 0) (i64.const 1))
(i32.const 12)
)
)
)
)
)
(func $fib-i64 (export "fib-i64") (type $over-i64)
(if (result i64) (i64.le_u (local.get 0) (i64.const 1))
(then (i64.const 1))
(else
(i64.add
(call_indirect (type $over-i64)
(i64.sub (local.get 0) (i64.const 2))
(i32.const 13)
)
(call_indirect (type $over-i64)
(i64.sub (local.get 0) (i64.const 1))
(i32.const 13)
)
)
)
)
)
(func $fac-i32 (export "fac-i32") (type $over-i32)
(if (result i32) (i32.eqz (local.get 0))
(then (i32.const 1))
(else
(i32.mul
(local.get 0)
(call_indirect (type $over-i32)
(i32.sub (local.get 0) (i32.const 1))
(i32.const 23)
)
)
)
)
)
(func $fac-f32 (export "fac-f32") (type $over-f32)
(if (result f32) (f32.eq (local.get 0) (f32.const 0.0))
(then (f32.const 1.0))
(else
(f32.mul
(local.get 0)
(call_indirect (type $over-f32)
(f32.sub (local.get 0) (f32.const 1.0))
(i32.const 24)
)
)
)
)
)
(func $fac-f64 (export "fac-f64") (type $over-f64)
(if (result f64) (f64.eq (local.get 0) (f64.const 0.0))
(then (f64.const 1.0))
(else
(f64.mul
(local.get 0)
(call_indirect (type $over-f64)
(f64.sub (local.get 0) (f64.const 1.0))
(i32.const 25)
)
)
)
)
)
(func $fib-i32 (export "fib-i32") (type $over-i32)
(if (result i32) (i32.le_u (local.get 0) (i32.const 1))
(then (i32.const 1))
(else
(i32.add
(call_indirect (type $over-i32)
(i32.sub (local.get 0) (i32.const 2))
(i32.const 26)
)
(call_indirect (type $over-i32)
(i32.sub (local.get 0) (i32.const 1))
(i32.const 26)
)
)
)
)
)
(func $fib-f32 (export "fib-f32") (type $over-f32)
(if (result f32) (f32.le (local.get 0) (f32.const 1.0))
(then (f32.const 1.0))
(else
(f32.add
(call_indirect (type $over-f32)
(f32.sub (local.get 0) (f32.const 2.0))
(i32.const 27)
)
(call_indirect (type $over-f32)
(f32.sub (local.get 0) (f32.const 1.0))
(i32.const 27)
)
)
)
)
)
(func $fib-f64 (export "fib-f64") (type $over-f64)
(if (result f64) (f64.le (local.get 0) (f64.const 1.0))
(then (f64.const 1.0))
(else
(f64.add
(call_indirect (type $over-f64)
(f64.sub (local.get 0) (f64.const 2.0))
(i32.const 28)
)
(call_indirect (type $over-f64)
(f64.sub (local.get 0) (f64.const 1.0))
(i32.const 28)
)
)
)
)
)
(func $even (export "even") (param i32) (result i32)
(if (result i32) (i32.eqz (local.get 0))
(then (i32.const 44))
(else
(call_indirect (type $over-i32)
(i32.sub (local.get 0) (i32.const 1))
(i32.const 15)
)
)
)
)
(func $odd (export "odd") (param i32) (result i32)
(if (result i32) (i32.eqz (local.get 0))
(then (i32.const 99))
(else
(call_indirect (type $over-i32)
(i32.sub (local.get 0) (i32.const 1))
(i32.const 14)
)
)
)
)
(func $runaway (export "runaway") (call_indirect (type $proc) (i32.const 16)))
(func $mutual-runaway1 (export "mutual-runaway") (call_indirect (type $proc) (i32.const 18)))
(func $mutual-runaway2 (call_indirect (type $proc) (i32.const 17)))
;; changes
(type $out-f64-i32 (func (result f64 i32)))
(type $over-i32-f64 (func (param i32 f64) (result i32 f64)))
(type $swap-i32-i64 (func (param i32 i64) (result i64 i32)))
(table funcref
(elem
$const-i32 $const-i64 $const-f32 $const-f64 ;; 0..3
$id-i32 $id-i64 $id-f32 $id-f64 ;; 4..7
$f32-i32 $i32-i64 $f64-f32 $i64-f64 ;; 9..11
$fac-i64 $fib-i64 $even $odd ;; 12..15
$runaway $mutual-runaway1 $mutual-runaway2 ;; 16..18
$over-i32-duplicate $over-i64-duplicate ;; 19..20
$over-f32-duplicate $over-f64-duplicate ;; 21..22
$fac-i32 $fac-f32 $fac-f64 ;; 23..25
$fib-i32 $fib-f32 $fib-f64 ;; 26..28
$const-f64-i32 $id-i32-f64 $swap-i32-i64 ;; 29..31
)
)
(func $const-f64-i32 (type $out-f64-i32) (f64.const 0xf64) (i32.const 32))
(func $id-i32-f64 (type $over-i32-f64) (local.get 0) (local.get 1))
(func $swap-i32-i64 (type $swap-i32-i64) (local.get 1) (local.get 0))
(func (export "type-f64-i32") (result f64 i32)
(call_indirect (type $out-f64-i32) (i32.const 29))
)
(func (export "type-all-f64-i32") (result f64 i32)
(call_indirect (type $out-f64-i32) (i32.const 29))
)
(func (export "type-all-i32-f64") (result i32 f64)
(call_indirect (type $over-i32-f64)
(i32.const 1) (f64.const 2) (i32.const 30)
)
)
(func (export "type-all-i32-i64") (result i64 i32)
(call_indirect (type $swap-i32-i64)
(i32.const 1) (i64.const 2) (i32.const 31)
)
)
)

Binary file not shown.

View File

@@ -0,0 +1,30 @@
;; This file includes changes to test/core/fac.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names fac.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $fac.wast
(func $pick0 (param i64) (result i64 i64)
(local.get 0) (local.get 0)
)
(func $pick1 (param i64 i64) (result i64 i64 i64)
(local.get 0) (local.get 1) (local.get 0)
)
(func (export "fac-ssa") (param i64) (result i64)
(i64.const 1) (local.get 0)
(loop $l (param i64 i64) (result i64)
(call $pick1) (call $pick1) (i64.mul)
(call $pick1) (i64.const 1) (i64.sub)
(call $pick0) (i64.const 0) (i64.gt_u)
(br_if $l)
(drop) (return)
)
)
)

Binary file not shown.

View File

@@ -0,0 +1,104 @@
;; This file includes changes to test/core/func.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names func.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $func.wast
;; preconditions
(func $dummy)
;; changes
(func (result))
(func (result) (result))
(func (result i32 f64 f32) (unreachable))
(func (result i32) (result f64) (unreachable))
(func (result i32 f32) (result i64) (result) (result i32 f64) (unreachable))
(func $complex
(param i32 f32) (param $x i64) (param) (param i32)
(result) (result i32) (result) (result i64 i32)
(local f32) (local $y i32) (local i64 i32) (local) (local f64 i32)
(unreachable) (unreachable)
)
(func (export "value-i32-f64") (result i32 f64) (i32.const 77) (f64.const 7))
(func (export "value-i32-i32-i32") (result i32 i32 i32)
(i32.const 1) (i32.const 2) (i32.const 3)
)
(func (export "value-block-i32-i64") (result i32 i64)
(block (result i32 i64) (call $dummy) (i32.const 1) (i64.const 2))
)
(func (export "return-i32-f64") (result i32 f64)
(return (i32.const 78) (f64.const 78.78))
)
(func (export "return-i32-i32-i32") (result i32 i32 i32)
(return (i32.const 1) (i32.const 2) (i32.const 3))
)
(func (export "return-block-i32-i64") (result i32 i64)
(return (block (result i32 i64) (call $dummy) (i32.const 1) (i64.const 2)))
)
(func (export "break-i32-f64") (result i32 f64)
(br 0 (i32.const 79) (f64.const 79.79))
)
(func (export "break-i32-i32-i32") (result i32 i32 i32)
(br 0 (i32.const 1) (i32.const 2) (i32.const 3))
)
(func (export "break-block-i32-i64") (result i32 i64)
(br 0 (block (result i32 i64) (call $dummy) (i32.const 1) (i64.const 2)))
)
(func (export "break-br_if-num-num") (param i32) (result i32 i64)
(drop (drop (br_if 0 (i32.const 50) (i64.const 51) (local.get 0))))
(i32.const 51) (i64.const 52)
)
(func (export "break-br_table-num-num") (param i32) (result i32 i64)
(br_table 0 0 (i32.const 50) (i64.const 51) (local.get 0))
(i32.const 51) (i64.const 52)
)
(func (export "break-br_table-nested-num-num") (param i32) (result i32 i32)
(i32.add
(block (result i32 i32)
(br_table 0 1 0 (i32.const 50) (i32.const 51) (local.get 0))
(i32.const 51) (i32.const -3)
)
)
(i32.const 52)
)
(func (export "large-sig")
(param i32 i64 f32 f32 i32 f64 f32 i32 i32 i32 f32 f64 f64 f64 i32 i32 f32)
(result f64 f32 i32 i32 i32 i64 f32 i32 i32 f32 f64 f64 i32 f32 i32 f64)
(local.get 5)
(local.get 2)
(local.get 0)
(local.get 8)
(local.get 7)
(local.get 1)
(local.get 3)
(local.get 9)
(local.get 4)
(local.get 6)
(local.get 13)
(local.get 11)
(local.get 15)
(local.get 16)
(local.get 14)
(local.get 12)
)
)

Binary file not shown.

View File

@@ -0,0 +1,176 @@
;; This file includes changes to test/core/if.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names if.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $if.wast
;; preconditions
(func $dummy)
;; changes
(func (export "multi") (param i32) (result i32 i32)
(if (local.get 0) (then (call $dummy) (call $dummy) (call $dummy)))
(if (local.get 0) (then) (else (call $dummy) (call $dummy) (call $dummy)))
(if (result i32) (local.get 0)
(then (call $dummy) (call $dummy) (i32.const 8) (call $dummy))
(else (call $dummy) (call $dummy) (i32.const 9) (call $dummy))
)
(if (result i32 i64 i32) (local.get 0)
(then
(call $dummy) (call $dummy) (i32.const 1) (call $dummy)
(call $dummy) (call $dummy) (i64.const 2) (call $dummy)
(call $dummy) (call $dummy) (i32.const 3) (call $dummy)
)
(else
(call $dummy) (call $dummy) (i32.const -1) (call $dummy)
(call $dummy) (call $dummy) (i64.const -2) (call $dummy)
(call $dummy) (call $dummy) (i32.const -3) (call $dummy)
)
)
(drop) (drop)
)
(func (export "as-binary-operands") (param i32) (result i32)
(i32.mul
(if (result i32 i32) (local.get 0)
(then (call $dummy) (i32.const 3) (call $dummy) (i32.const 4))
(else (call $dummy) (i32.const 3) (call $dummy) (i32.const -4))
)
)
)
(func (export "as-compare-operands") (param i32) (result i32)
(f32.gt
(if (result f32 f32) (local.get 0)
(then (call $dummy) (f32.const 3) (call $dummy) (f32.const 3))
(else (call $dummy) (f32.const -2) (call $dummy) (f32.const -3))
)
)
)
(func (export "as-mixed-operands") (param i32) (result i32)
(if (result i32 i32) (local.get 0)
(then (call $dummy) (i32.const 3) (call $dummy) (i32.const 4))
(else (call $dummy) (i32.const -3) (call $dummy) (i32.const -4))
)
(i32.const 5)
(i32.add)
(i32.mul)
)
(func (export "break-multi-value") (param i32) (result i32 i32 i64)
(if (result i32 i32 i64) (local.get 0)
(then
(br 0 (i32.const 18) (i32.const -18) (i64.const 18))
(i32.const 19) (i32.const -19) (i64.const 19)
)
(else
(br 0 (i32.const -18) (i32.const 18) (i64.const -18))
(i32.const -19) (i32.const 19) (i64.const -19)
)
)
)
(func (export "param") (param i32) (result i32)
(i32.const 1)
(if (param i32) (result i32) (local.get 0)
(then (i32.const 2) (i32.add))
(else (i32.const -2) (i32.add))
)
)
(func (export "params") (param i32) (result i32)
(i32.const 1)
(i32.const 2)
(if (param i32 i32) (result i32) (local.get 0)
(then (i32.add))
(else (i32.sub))
)
)
(func (export "params-id") (param i32) (result i32)
(i32.const 1)
(i32.const 2)
(if (param i32 i32) (result i32 i32) (local.get 0) (then))
(i32.add)
)
(func (export "param-break") (param i32) (result i32)
(i32.const 1)
(if (param i32) (result i32) (local.get 0)
(then (i32.const 2) (i32.add) (br 0))
(else (i32.const -2) (i32.add) (br 0))
)
)
(func (export "params-break") (param i32) (result i32)
(i32.const 1)
(i32.const 2)
(if (param i32 i32) (result i32) (local.get 0)
(then (i32.add) (br 0))
(else (i32.sub) (br 0))
)
)
(func (export "params-id-break") (param i32) (result i32)
(i32.const 1)
(i32.const 2)
(if (param i32 i32) (result i32 i32) (local.get 0) (then (br 0)))
(i32.add)
)
(func $add64_u_with_carry (export "add64_u_with_carry")
(param $i i64) (param $j i64) (param $c i32) (result i64 i32)
(local $k i64)
(local.set $k
(i64.add
(i64.add (local.get $i) (local.get $j))
(i64.extend_i32_u (local.get $c))
)
)
(return (local.get $k) (i64.lt_u (local.get $k) (local.get $i)))
)
(func $add64_u_saturated (export "add64_u_saturated")
(param i64 i64) (result i64)
(call $add64_u_with_carry (local.get 0) (local.get 1) (i32.const 0))
(if (param i64) (result i64)
(then (drop) (i64.const -1))
)
)
(type $block-sig-1 (func))
(type $block-sig-2 (func (result i32)))
(type $block-sig-3 (func (param $x i32)))
(type $block-sig-4 (func (param i32 f64 i32) (result i32 f64 i32)))
(func (export "type-use")
(if (type $block-sig-1) (i32.const 1) (then))
(if (type $block-sig-2) (i32.const 1)
(then (i32.const 0)) (else (i32.const 2))
)
(if (type $block-sig-3) (i32.const 1) (then (drop)) (else (drop)))
(i32.const 0) (f64.const 0) (i32.const 0)
(if (type $block-sig-4) (i32.const 1) (then))
(drop) (drop) (drop)
(if (type $block-sig-2) (result i32) (i32.const 1)
(then (i32.const 0)) (else (i32.const 2))
)
(if (type $block-sig-3) (param i32) (i32.const 1)
(then (drop)) (else (drop))
)
(i32.const 0) (f64.const 0) (i32.const 0)
(if (type $block-sig-4)
(param i32) (param f64 i32) (result i32 f64) (result i32)
(i32.const 1) (then)
)
(drop) (drop) (drop)
)
)

Binary file not shown.

View File

@@ -0,0 +1,163 @@
;; This file includes changes to test/core/loop.wast from the commit that added "multi-value" support.
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names loop.wat
;;
;; See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c
(module $loop.wast
;; preconditions
(func $dummy)
;; changes
(func (export "multi") (result i32)
(loop (call $dummy) (call $dummy) (call $dummy) (call $dummy))
(loop (result i32) (call $dummy) (call $dummy) (i32.const 8) (call $dummy))
(drop)
(loop (result i32 i64 i32)
(call $dummy) (call $dummy) (call $dummy) (i32.const 8) (call $dummy)
(call $dummy) (call $dummy) (call $dummy) (i64.const 7) (call $dummy)
(call $dummy) (call $dummy) (call $dummy) (i32.const 9) (call $dummy)
)
(drop) (drop)
)
(func (export "as-binary-operands") (result i32)
(i32.mul
(loop (result i32 i32)
(call $dummy) (i32.const 3) (call $dummy) (i32.const 4)
)
)
)
(func (export "as-compare-operands") (result i32)
(f32.gt
(loop (result f32 f32)
(call $dummy) (f32.const 3) (call $dummy) (f32.const 3)
)
)
)
(func (export "as-mixed-operands") (result i32)
(loop (result i32 i32)
(call $dummy) (i32.const 3) (call $dummy) (i32.const 4)
)
(i32.const 5)
(i32.add)
(i32.mul)
)
(func (export "break-value") (result i32)
(block (result i32)
(i32.const 0)
(loop (param i32)
(block (br 2 (i32.const 18)))
(br 0 (i32.const 20))
)
(i32.const 19)
)
)
(func (export "break-multi-value") (result i32 i32 i64)
(block (result i32 i32 i64)
(i32.const 0) (i32.const 0) (i64.const 0)
(loop (param i32 i32 i64)
(block (br 2 (i32.const 18) (i32.const -18) (i64.const 18)))
(br 0 (i32.const 20) (i32.const -20) (i64.const 20))
)
(i32.const 19) (i32.const -19) (i64.const 19)
)
)
(func (export "param") (result i32)
(i32.const 1)
(loop (param i32) (result i32)
(i32.const 2)
(i32.add)
)
)
(func (export "params") (result i32)
(i32.const 1)
(i32.const 2)
(loop (param i32 i32) (result i32)
(i32.add)
)
)
(func (export "params-id") (result i32)
(i32.const 1)
(i32.const 2)
(loop (param i32 i32) (result i32 i32))
(i32.add)
)
(func (export "param-break") (result i32)
(local $x i32)
(i32.const 1)
(loop (param i32) (result i32)
(i32.const 4)
(i32.add)
(local.tee $x)
(local.get $x)
(i32.const 10)
(i32.lt_u)
(br_if 0)
)
)
(func (export "params-break") (result i32)
(local $x i32)
(i32.const 1)
(i32.const 2)
(loop (param i32 i32) (result i32)
(i32.add)
(local.tee $x)
(i32.const 3)
(local.get $x)
(i32.const 10)
(i32.lt_u)
(br_if 0)
(drop)
)
)
(func (export "params-id-break") (result i32)
(local $x i32)
(local.set $x (i32.const 0))
(i32.const 1)
(i32.const 2)
(loop (param i32 i32) (result i32 i32)
(local.set $x (i32.add (local.get $x) (i32.const 1)))
(br_if 0 (i32.lt_u (local.get $x) (i32.const 10)))
)
(i32.add)
)
(type $block-sig-1 (func))
(type $block-sig-2 (func (result i32)))
(type $block-sig-3 (func (param $x i32)))
(type $block-sig-4 (func (param i32 f64 i32) (result i32 f64 i32)))
(func (export "type-use")
(loop (type $block-sig-1))
(loop (type $block-sig-2) (i32.const 0))
(loop (type $block-sig-3) (drop))
(i32.const 0) (f64.const 0) (i32.const 0)
(loop (type $block-sig-4))
(drop) (drop) (drop)
(loop (type $block-sig-2) (result i32) (i32.const 0))
(loop (type $block-sig-3) (param i32) (drop))
(i32.const 0) (f64.const 0) (i32.const 0)
(loop (type $block-sig-4)
(param i32) (param f64 i32) (result i32 f64) (result i32)
)
(drop) (drop) (drop)
)
)

Binary file not shown.

View File

@@ -0,0 +1,58 @@
;; multiValue is a WebAssembly 1.0 (20191205) Text Format source, plus the "multi-value" feature.
;; * allows multiple return values from `func`, `block`, `loop` and `if`
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names multi_value.wat
;;
;; See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
(module $multi-value
(func $swap (param i32 i32) (result i32 i32)
(local.get 1) (local.get 0)
)
(export "swap" (func $swap))
(func $add64_u_with_carry (param $i i64) (param $j i64) (param $c i32) (result i64 i32)
(local $k i64)
(local.set $k
(i64.add (i64.add (local.get $i) (local.get $j)) (i64.extend_i32_u (local.get $c)))
)
(return (local.get $k) (i64.lt_u (local.get $k) (local.get $i)))
)
(export "add64_u_with_carry" (func $add64_u_with_carry))
(func $add64_u_saturated (param i64 i64) (result i64)
(call $add64_u_with_carry (local.get 0) (local.get 1) (i32.const 0))
(if (param i64) (result i64)
(then (drop) (i64.const 0xffff_ffff_ffff_ffff))
)
)
(export "add64_u_saturated" (func $add64_u_saturated))
(func $pick0 (param i64) (result i64 i64)
(local.get 0) (local.get 0)
)
(func $pick1 (param i64 i64) (result i64 i64 i64)
(local.get 0) (local.get 1) (local.get 0)
)
;; Note: This implementation loops forever if the input is zero.
(func $fac (param i64) (result i64)
(i64.const 1) (local.get 0)
(loop $l (param i64 i64) (result i64)
(call $pick1) (call $pick1) (i64.mul)
(call $pick1) (i64.const 1) (i64.sub)
(call $pick0) (i64.const 0) (i64.gt_u)
(br_if $l)
(drop) (return)
)
)
(export "fac" (func $fac))
)

View File

@@ -1,4 +1,4 @@
package post1_0
package sign_extension_ops
import (
_ "embed"
@@ -10,25 +10,15 @@ import (
"github.com/tetratelabs/wazero"
)
func TestJIT(t *testing.T) {
func TestSignExtensionOps_JIT(t *testing.T) {
if !wazero.JITSupported {
t.Skip()
}
runOptionalFeatureTests(t, wazero.NewRuntimeConfigJIT)
testSignExtensionOps(t, wazero.NewRuntimeConfigJIT)
}
func TestInterpreter(t *testing.T) {
runOptionalFeatureTests(t, wazero.NewRuntimeConfigInterpreter)
}
// runOptionalFeatureTests tests features enabled by feature flags (wasm.Features) as they were unfinished when
// WebAssembly 1.0 (20191205) was released.
//
// See https://github.com/WebAssembly/proposals/blob/main/finished-proposals.md
func runOptionalFeatureTests(t *testing.T, newRuntimeConfig func() *wazero.RuntimeConfig) {
t.Run("sign-extension-ops", func(t *testing.T) {
testSignExtensionOps(t, newRuntimeConfig)
})
func TestSignExtensionOps_Interpreter(t *testing.T) {
testSignExtensionOps(t, wazero.NewRuntimeConfigInterpreter)
}
// signExtend is a WebAssembly 1.0 (20191205) Text Format source, except that it uses opcodes from 'sign-extension-ops'.

View File

@@ -283,7 +283,7 @@ func TestInterpreter(t *testing.T) {
runTest(t, interpreter.NewEngine)
}
func runTest(t *testing.T, newEngine func() wasm.Engine) {
func runTest(t *testing.T, newEngine func(wasm.Features) wasm.Engine) {
files, err := testcases.ReadDir("testdata")
require.NoError(t, err)
@@ -309,7 +309,8 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) {
wastName := basename(base.SourceFile)
t.Run(wastName, func(t *testing.T) {
store := wasm.NewStore(newEngine(), wasm.Features20191205)
enabledFeatures := wasm.Features20191205
store := wasm.NewStore(enabledFeatures, newEngine(enabledFeatures))
addSpectestModule(t, store)
var lastInstantiatedModuleName string
@@ -320,9 +321,9 @@ func runTest(t *testing.T, newEngine func() wasm.Engine) {
case "module":
buf, err := testcases.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryMaxPages)
mod, err := binary.DecodeModule(buf, enabledFeatures, wasm.MemoryMaxPages)
require.NoError(t, err, msg)
require.NoError(t, mod.Validate(wasm.Features20191205))
require.NoError(t, mod.Validate(enabledFeatures))
moduleName := c.Name
if moduleName == "" { // When "(module ...) directive doesn't have name.

View File

@@ -27,13 +27,13 @@ var ensureJITFastest = "false"
//go:embed testdata/fac.wasm
var facWasm []byte
// TestFacIter ensures that the code in BenchmarkFacIter works as expected.
func TestFacIter(t *testing.T) {
// TestFac ensures that the code in BenchmarkFac works as expected.
func TestFac(t *testing.T) {
const in = 30
expValue := uint64(0x865df5dd54000000)
t.Run("Interpreter", func(t *testing.T) {
mod, fn, err := newWazeroFacIterBench(wazero.NewRuntimeConfigInterpreter())
mod, fn, err := newWazeroFacBench(wazero.NewRuntimeConfigInterpreter().WithFinishedFeatures())
require.NoError(t, err)
defer mod.Close()
@@ -45,7 +45,7 @@ func TestFacIter(t *testing.T) {
})
t.Run("JIT", func(t *testing.T) {
mod, fn, err := newWazeroFacIterBench(wazero.NewRuntimeConfigJIT())
mod, fn, err := newWazeroFacBench(wazero.NewRuntimeConfigJIT().WithFinishedFeatures())
require.NoError(t, err)
defer mod.Close()
@@ -57,7 +57,7 @@ func TestFacIter(t *testing.T) {
})
t.Run("wasmer-go", func(t *testing.T) {
store, instance, fn, err := newWasmerForFacIterBench()
store, instance, fn, err := newWasmerForFacBench()
require.NoError(t, err)
defer store.Close()
defer instance.Close()
@@ -70,7 +70,7 @@ func TestFacIter(t *testing.T) {
})
t.Run("wasmtime-go", func(t *testing.T) {
store, run, err := newWasmtimeForFacIterBench()
store, run, err := newWasmtimeForFacBench()
require.NoError(t, err)
for i := 0; i < 10000; i++ {
res, err := run.Call(store, in)
@@ -82,7 +82,7 @@ func TestFacIter(t *testing.T) {
})
t.Run("go-wasm3", func(t *testing.T) {
env, runtime, run, err := newGoWasm3ForFacIterBench()
env, runtime, run, err := newGoWasm3ForFacBench()
require.NoError(t, err)
defer env.Destroy()
defer runtime.Destroy()
@@ -97,12 +97,12 @@ func TestFacIter(t *testing.T) {
})
}
// BenchmarkFacIter_Init tracks the time spent readying a function for use
func BenchmarkFacIter_Init(b *testing.B) {
// BenchmarkFac_Init tracks the time spent readying a function for use
func BenchmarkFac_Init(b *testing.B) {
b.Run("Interpreter", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
mod, _, err := newWazeroFacIterBench(wazero.NewRuntimeConfigInterpreter())
mod, _, err := newWazeroFacBench(wazero.NewRuntimeConfigInterpreter().WithFinishedFeatures())
if err != nil {
b.Fatal(err)
}
@@ -113,7 +113,7 @@ func BenchmarkFacIter_Init(b *testing.B) {
b.Run("JIT", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
mod, _, err := newWazeroFacIterBench(wazero.NewRuntimeConfigJIT())
mod, _, err := newWazeroFacBench(wazero.NewRuntimeConfigJIT().WithFinishedFeatures())
if err != nil {
b.Fatal(err)
}
@@ -124,7 +124,7 @@ func BenchmarkFacIter_Init(b *testing.B) {
b.Run("wasmer-go", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
store, instance, _, err := newWasmerForFacIterBench()
store, instance, _, err := newWasmerForFacBench()
if err != nil {
b.Fatal(err)
}
@@ -136,7 +136,7 @@ func BenchmarkFacIter_Init(b *testing.B) {
b.Run("wasmtime-go", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, _, err := newWasmtimeForFacIterBench(); err != nil {
if _, _, err := newWasmtimeForFacBench(); err != nil {
b.Fatal(err)
}
}
@@ -145,7 +145,7 @@ func BenchmarkFacIter_Init(b *testing.B) {
b.Run("go-wasm3", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
env, runtime, _, err := newGoWasm3ForFacIterBench()
env, runtime, _, err := newGoWasm3ForFacBench()
if err != nil {
b.Fatal(err)
}
@@ -155,17 +155,17 @@ func BenchmarkFacIter_Init(b *testing.B) {
})
}
var facIterArgumentU64 = uint64(30)
var facIterArgumentI64 = int64(facIterArgumentU64)
var facArgumentU64 = uint64(30)
var facArgumentI64 = int64(facArgumentU64)
// TestFacIter_JIT_Fastest ensures that JIT is the fastest engine for function invocations.
// TestFac_JIT_Fastest ensures that JIT is the fastest engine for function invocations.
// This is disabled by default, and can be run with -ldflags '-X github.com/tetratelabs/wazero/vs.ensureJITFastest=true'.
func TestFacIter_JIT_Fastest(t *testing.T) {
func TestFac_JIT_Fastest(t *testing.T) {
if ensureJITFastest != "true" {
t.Skip()
}
jitResult := testing.Benchmark(jitFacIterInvoke)
jitResult := testing.Benchmark(jitFacInvoke)
cases := []struct {
runtimeName string
@@ -173,19 +173,19 @@ func TestFacIter_JIT_Fastest(t *testing.T) {
}{
{
runtimeName: "interpreter",
result: testing.Benchmark(interpreterFacIterInvoke),
result: testing.Benchmark(interpreterFacInvoke),
},
{
runtimeName: "wasmer-go",
result: testing.Benchmark(wasmerGoFacIterInvoke),
result: testing.Benchmark(wasmerGoFacInvoke),
},
{
runtimeName: "wasmtime-go",
result: testing.Benchmark(wasmtimeGoFacIterInvoke),
result: testing.Benchmark(wasmtimeGoFacInvoke),
},
{
runtimeName: "go-wasm3",
result: testing.Benchmark(goWasm3FacIterInvoke),
result: testing.Benchmark(goWasm3FacInvoke),
},
}
@@ -202,57 +202,57 @@ func TestFacIter_JIT_Fastest(t *testing.T) {
// https://github.com/golang/go/blob/fd09e88722e0af150bf8960e95e8da500ad91001/src/testing/benchmark.go#L428-L432
nanoPerOp := float64(tc.result.T.Nanoseconds()) / float64(tc.result.N)
msg := fmt.Sprintf("JIT engine must be faster than %s. "+
"Run BenchmarkFacIter_Invoke with ensureJITFastest=false instead to see the detailed result",
"Run BenchmarkFac_Invoke with ensureJITFastest=false instead to see the detailed result",
tc.runtimeName)
require.Lessf(t, jitNanoPerOp, nanoPerOp, msg)
})
}
}
// BenchmarkFacIter_Invoke benchmarks the time spent invoking a factorial calculation.
func BenchmarkFacIter_Invoke(b *testing.B) {
// BenchmarkFac_Invoke benchmarks the time spent invoking a factorial calculation.
func BenchmarkFac_Invoke(b *testing.B) {
if ensureJITFastest == "true" {
// If ensureJITFastest == "true", the benchmark for invocation will be run by
// TestFacIter_JIT_Fastest so skip here.
// TestFac_JIT_Fastest so skip here.
b.Skip()
}
b.Run("Interpreter", interpreterFacIterInvoke)
b.Run("JIT", jitFacIterInvoke)
b.Run("wasmer-go", wasmerGoFacIterInvoke)
b.Run("wasmtime-go", wasmtimeGoFacIterInvoke)
b.Run("go-wasm3", goWasm3FacIterInvoke)
b.Run("Interpreter", interpreterFacInvoke)
b.Run("JIT", jitFacInvoke)
b.Run("wasmer-go", wasmerGoFacInvoke)
b.Run("wasmtime-go", wasmtimeGoFacInvoke)
b.Run("go-wasm3", goWasm3FacInvoke)
}
func interpreterFacIterInvoke(b *testing.B) {
mod, fn, err := newWazeroFacIterBench(wazero.NewRuntimeConfigInterpreter())
func interpreterFacInvoke(b *testing.B) {
mod, fn, err := newWazeroFacBench(wazero.NewRuntimeConfigInterpreter().WithFinishedFeatures())
if err != nil {
b.Fatal(err)
}
defer mod.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err = fn.Call(nil, facIterArgumentU64); err != nil {
if _, err = fn.Call(nil, facArgumentU64); err != nil {
b.Fatal(err)
}
}
}
func jitFacIterInvoke(b *testing.B) {
mod, fn, err := newWazeroFacIterBench(wazero.NewRuntimeConfigJIT())
func jitFacInvoke(b *testing.B) {
mod, fn, err := newWazeroFacBench(wazero.NewRuntimeConfigJIT().WithFinishedFeatures())
if err != nil {
b.Fatal(err)
}
defer mod.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err = fn.Call(nil, facIterArgumentU64); err != nil {
if _, err = fn.Call(nil, facArgumentU64); err != nil {
b.Fatal(err)
}
}
}
func wasmerGoFacIterInvoke(b *testing.B) {
store, instance, fn, err := newWasmerForFacIterBench()
func wasmerGoFacInvoke(b *testing.B) {
store, instance, fn, err := newWasmerForFacBench()
if err != nil {
b.Fatal(err)
}
@@ -260,35 +260,35 @@ func wasmerGoFacIterInvoke(b *testing.B) {
defer instance.Close()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err = fn(facIterArgumentI64); err != nil {
if _, err = fn(facArgumentI64); err != nil {
b.Fatal(err)
}
}
}
func wasmtimeGoFacIterInvoke(b *testing.B) {
store, run, err := newWasmtimeForFacIterBench()
func wasmtimeGoFacInvoke(b *testing.B) {
store, run, err := newWasmtimeForFacBench()
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// go-wasm3 only maps the int type
if _, err = run.Call(store, int(facIterArgumentI64)); err != nil {
if _, err = run.Call(store, int(facArgumentI64)); err != nil {
b.Fatal(err)
}
}
}
func goWasm3FacIterInvoke(b *testing.B) {
env, runtime, run, err := newGoWasm3ForFacIterBench()
func goWasm3FacInvoke(b *testing.B) {
env, runtime, run, err := newGoWasm3ForFacBench()
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
// go-wasm3 only maps the int type
if _, err = run(int(facIterArgumentI64)); err != nil {
if _, err = run(int(facArgumentI64)); err != nil {
b.Fatal(err)
}
}
@@ -296,7 +296,7 @@ func goWasm3FacIterInvoke(b *testing.B) {
env.Destroy()
}
func newWazeroFacIterBench(config *wazero.RuntimeConfig) (api.Module, api.Function, error) {
func newWazeroFacBench(config *wazero.RuntimeConfig) (api.Module, api.Function, error) {
r := wazero.NewRuntimeWithConfig(config)
m, err := r.InstantiateModuleFromCode(facWasm)
@@ -304,12 +304,12 @@ func newWazeroFacIterBench(config *wazero.RuntimeConfig) (api.Module, api.Functi
return nil, nil, err
}
return m, m.ExportedFunction("fac-iter"), nil
return m, m.ExportedFunction("fac"), nil
}
// newWasmerForFacIterBench returns the store and instance that scope the factorial function.
// newWasmerForFacBench returns the store and instance that scope the factorial function.
// Note: these should be closed
func newWasmerForFacIterBench() (*wasmer.Store, *wasmer.Instance, wasmer.NativeFunction, error) {
func newWasmerForFacBench() (*wasmer.Store, *wasmer.Instance, wasmer.NativeFunction, error) {
store := wasmer.NewStore(wasmer.NewEngine())
importObject := wasmer.NewImportObject()
module, err := wasmer.NewModule(store, facWasm)
@@ -320,7 +320,7 @@ func newWasmerForFacIterBench() (*wasmer.Store, *wasmer.Instance, wasmer.NativeF
if err != nil {
return nil, nil, nil, err
}
f, err := instance.Exports.GetFunction("fac-iter")
f, err := instance.Exports.GetFunction("fac")
if err != nil {
return nil, nil, nil, err
}
@@ -330,7 +330,7 @@ func newWasmerForFacIterBench() (*wasmer.Store, *wasmer.Instance, wasmer.NativeF
return store, instance, f, nil
}
func newWasmtimeForFacIterBench() (*wasmtime.Store, *wasmtime.Func, error) {
func newWasmtimeForFacBench() (*wasmtime.Store, *wasmtime.Func, error) {
store := wasmtime.NewStore(wasmtime.NewEngine())
module, err := wasmtime.NewModule(store.Engine, facWasm)
if err != nil {
@@ -342,14 +342,14 @@ func newWasmtimeForFacIterBench() (*wasmtime.Store, *wasmtime.Func, error) {
return nil, nil, err
}
run := instance.GetFunc(store, "fac-iter")
run := instance.GetFunc(store, "fac")
if run == nil {
return nil, nil, errors.New("not a function")
}
return store, run, nil
}
func newGoWasm3ForFacIterBench() (*wasm3.Environment, *wasm3.Runtime, wasm3.FunctionWrapper, error) {
func newGoWasm3ForFacBench() (*wasm3.Environment, *wasm3.Runtime, wasm3.FunctionWrapper, error) {
env := wasm3.NewEnvironment()
runtime := wasm3.NewRuntime(&wasm3.Config{
Environment: wasm3.NewEnvironment(),
@@ -366,7 +366,7 @@ func newGoWasm3ForFacIterBench() (*wasm3.Environment, *wasm3.Runtime, wasm3.Func
return nil, nil, nil, err
}
run, err := runtime.FindFunction("fac-iter")
run, err := runtime.FindFunction("fac")
if err != nil {
return nil, nil, nil, err
}

View File

@@ -29,8 +29,6 @@ var exampleText []byte
// exampleBinary is the exampleText encoded in the WebAssembly 1.0 binary format.
var exampleBinary = binary.EncodeModule(example)
var enabledFeatures = wasm.Features20191205.Set(wasm.FeatureSignExtensionOps, true)
func newExample() *wasm.Module {
three := wasm.Index(3)
i32, i64 := wasm.ValueTypeI32, wasm.ValueTypeI64
@@ -40,6 +38,7 @@ func newExample() *wasm.Module {
{},
{Params: []wasm.ValueType{i32, i32, i32, i32}, Results: []wasm.ValueType{i32}},
{Params: []wasm.ValueType{i64}, Results: []wasm.ValueType{i64}},
{Params: []wasm.ValueType{i32, i32}, Results: []wasm.ValueType{i32, i32}},
},
ImportSection: []*wasm.Import{
{
@@ -52,17 +51,19 @@ func newExample() *wasm.Module {
DescFunc: 2,
},
},
FunctionSection: []wasm.Index{wasm.Index(1), wasm.Index(1), wasm.Index(0), wasm.Index(3)},
FunctionSection: []wasm.Index{wasm.Index(1), wasm.Index(1), wasm.Index(0), wasm.Index(3), wasm.Index(4)},
CodeSection: []*wasm.Code{
{Body: []byte{wasm.OpcodeCall, 3, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeLocalGet, 1, wasm.OpcodeI32Add, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeI64Extend16S, wasm.OpcodeEnd}},
{Body: []byte{wasm.OpcodeLocalGet, 1, wasm.OpcodeLocalGet, 0, wasm.OpcodeEnd}},
},
MemorySection: &wasm.Memory{Min: 1, Max: three},
ExportSection: map[string]*wasm.Export{
"AddInt": {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(4)},
"": {Name: "", Type: wasm.ExternTypeFunc, Index: wasm.Index(3)},
"AddInt": {Name: "AddInt", Type: wasm.ExternTypeFunc, Index: wasm.Index(4)},
"swap": {Name: "swap", Type: wasm.ExternTypeFunc, Index: wasm.Index(6)},
"mem": {Name: "mem", Type: wasm.ExternTypeMemory, Index: wasm.Index(0)},
},
StartSection: &three,
@@ -74,6 +75,7 @@ func newExample() *wasm.Module {
{Index: wasm.Index(2), Name: "call_hello"},
{Index: wasm.Index(3), Name: "hello"},
{Index: wasm.Index(4), Name: "addInt"},
{Index: wasm.Index(6), Name: "swap"},
},
LocalNames: wasm.IndirectNameMap{
{Index: wasm.Index(1), NameMap: wasm.NameMap{
@@ -93,19 +95,19 @@ func newExample() *wasm.Module {
func TestExampleUpToDate(t *testing.T) {
t.Run("binary.DecodeModule", func(t *testing.T) {
m, err := binary.DecodeModule(exampleBinary, enabledFeatures, wasm.MemoryMaxPages)
m, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryMaxPages)
require.NoError(t, err)
require.Equal(t, example, m)
})
t.Run("text.DecodeModule", func(t *testing.T) {
m, err := text.DecodeModule(exampleText, enabledFeatures, wasm.MemoryMaxPages)
m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages)
require.NoError(t, err)
require.Equal(t, example, m)
})
t.Run("Executable", func(t *testing.T) {
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithFeatureSignExtensionOps(true))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfig().WithFinishedFeatures())
// Add WASI to satisfy import tests
wm, err := wasi.InstantiateSnapshotPreview1(r)
@@ -117,10 +119,10 @@ func TestExampleUpToDate(t *testing.T) {
require.NoError(t, err)
defer module.Close()
// Call the add function as a smoke test
results, err := module.ExportedFunction("AddInt").Call(nil, 1, 2)
// Call the swap function as a smoke test
results, err := module.ExportedFunction("swap").Call(nil, 1, 2)
require.NoError(t, err)
require.Equal(t, uint64(3), results[0])
require.Equal(t, []uint64{2, 1}, results)
})
}
@@ -128,7 +130,7 @@ func BenchmarkCodecExample(b *testing.B) {
b.Run("binary.DecodeModule", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := binary.DecodeModule(exampleBinary, enabledFeatures, wasm.MemoryMaxPages); err != nil {
if _, err := binary.DecodeModule(exampleBinary, wasm.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
b.Fatal(err)
}
}
@@ -142,7 +144,7 @@ func BenchmarkCodecExample(b *testing.B) {
b.Run("text.DecodeModule", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := text.DecodeModule(exampleText, enabledFeatures, wasm.MemoryMaxPages); err != nil {
if _, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
b.Fatal(err)
}
}
@@ -150,7 +152,7 @@ func BenchmarkCodecExample(b *testing.B) {
b.Run("wat2wasm via text.DecodeModule->binary.EncodeModule", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if m, err := text.DecodeModule(exampleText, enabledFeatures, wasm.MemoryMaxPages); err != nil {
if m, err := text.DecodeModule(exampleText, wasm.FeaturesFinished, wasm.MemoryMaxPages); err != nil {
b.Fatal(err)
} else {
_ = binary.EncodeModule(m)

View File

@@ -38,6 +38,12 @@
(export "mem" (memory $mem))
(memory $mem 1 3)
;; add >1.0 feature from https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
;; add function using "sign-extension-ops"
;; https://github.com/WebAssembly/spec/blob/main/proposals/sign-extension-ops/Overview.md
(func (param i64) (result i64) local.get 0 i64.extend16_s)
;; add function using "multi-value"
;; https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
(func $swap (param i32 i32) (result i32 i32) local.get 1 local.get 0)
(export "swap" (func $swap))
)

BIN
vs/testdata/fac.wasm vendored

Binary file not shown.

50
vs/testdata/fac.wat vendored
View File

@@ -1,21 +1,35 @@
;; This includes a factorial function that uses the "multi-value" feature
;;
;; Compile like so, in order to not add any other post 1.0 features to the resulting wasm.
;; wat2wasm \
;; --disable-saturating-float-to-int \
;; --disable-sign-extension \
;; --disable-simd \
;; --disable-bulk-memory \
;; --disable-reference-types \
;; --debug-names fac.wat
;;
;; See https://github.com/WebAssembly/spec/blob/main/proposals/multi-value/Overview.md
(module
(func (export "fac-iter") (param i64) (result i64)
(local i64 i64)
(local.set 1 (local.get 0))
(local.set 2 (i64.const 1))
(block
(loop
(if
(i64.eq (local.get 1) (i64.const 0))
(then (br 2))
(else
(local.set 2 (i64.mul (local.get 1) (local.get 2)))
(local.set 1 (i64.sub (local.get 1) (i64.const 1)))
)
)
(br 0)
)
)
(local.get 2)
(func $pick0 (param i64) (result i64 i64)
(local.get 0) (local.get 0)
)
(func $pick1 (param i64 i64) (result i64 i64 i64)
(local.get 0) (local.get 1) (local.get 0)
)
;; Note: This implementation loops forever if the input is zero.
(func $fac (param i64) (result i64)
(i64.const 1) (local.get 0)
(loop $l (param i64 i64) (result i64)
(call $pick1) (call $pick1) (i64.mul)
(call $pick1) (i64.const 1) (i64.sub)
(call $pick0) (i64.const 0) (i64.gt_u)
(br_if $l)
(drop) (return)
)
)
(export "fac" (func $fac))
)

View File

@@ -110,7 +110,7 @@ func NewRuntime() Runtime {
func NewRuntimeWithConfig(config *RuntimeConfig) Runtime {
return &runtime{
ctx: config.ctx,
store: wasm.NewStore(config.newEngine(), config.enabledFeatures),
store: wasm.NewStore(config.enabledFeatures, config.newEngine(config.enabledFeatures)),
enabledFeatures: config.enabledFeatures,
memoryMaxPages: config.memoryMaxPages,
}
@@ -118,9 +118,9 @@ func NewRuntimeWithConfig(config *RuntimeConfig) Runtime {
// runtime allows decoupling of public interfaces from internal representation.
type runtime struct {
enabledFeatures wasm.Features
ctx context.Context
store *wasm.Store
enabledFeatures wasm.Features
memoryMaxPages uint32
}