From c3ff16d5962bf8ccdd5a22b6925d46d014dae56b Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Wed, 13 Apr 2022 09:22:39 +0800 Subject: [PATCH] Supports functions with multiple results (multi-value) (#446) Signed-off-by: Adrian Cole Signed-off-by: Takeshi Yoneda --- api/wasm.go | 10 + api/wasm_test.go | 30 + builder.go | 9 +- config.go | 44 +- examples/results_test.go | 165 ++ internal/testing/enginetest/enginetest.go | 17 +- internal/wasm/binary/decoder.go | 4 +- internal/wasm/binary/function.go | 49 +- internal/wasm/binary/function_test.go | 103 +- internal/wasm/binary/section.go | 42 +- internal/wasm/features.go | 48 +- internal/wasm/features_test.go | 12 +- internal/wasm/func_validation.go | 438 +++-- internal/wasm/func_validation_test.go | 1647 ++++++++++++++++- internal/wasm/gofunc.go | 41 +- internal/wasm/gofunc_test.go | 67 +- internal/wasm/host.go | 15 +- internal/wasm/host_test.go | 36 +- internal/wasm/instruction.go | 557 ++++-- internal/wasm/interpreter/interpreter.go | 10 +- internal/wasm/interpreter/interpreter_test.go | 10 +- internal/wasm/jit/engine.go | 14 +- internal/wasm/jit/engine_test.go | 19 +- internal/wasm/jit/jit_impl_amd64.go | 2 +- internal/wasm/jit/jit_impl_arm64.go | 2 +- internal/wasm/jit/jit_stack_test.go | 6 +- internal/wasm/module.go | 10 +- internal/wasm/module_test.go | 4 +- internal/wasm/store.go | 10 +- internal/wasm/store_test.go | 52 +- internal/wasm/text/decoder.go | 14 +- internal/wasm/text/decoder_test.go | 156 +- internal/wasm/text/errors.go | 7 +- internal/wasm/text/func_parser.go | 90 +- internal/wasm/text/func_parser_test.go | 104 +- internal/wasm/text/type_parser.go | 131 +- internal/wasm/text/type_parser_test.go | 200 +- internal/wasm/text/typeuse_parser.go | 126 +- internal/wasm/text/typeuse_parser_test.go | 286 ++- internal/wazeroir/compiler.go | 208 ++- internal/wazeroir/compiler_test.go | 428 +++++ internal/wazeroir/format.go | 2 +- internal/wazeroir/operations.go | 20 +- tests/post1_0/multi-value/multi_value_test.go | 309 ++++ tests/post1_0/multi-value/testdata/br.wasm | Bin 0 -> 548 bytes tests/post1_0/multi-value/testdata/br.wat | 69 + tests/post1_0/multi-value/testdata/call.wasm | Bin 0 -> 502 bytes tests/post1_0/multi-value/testdata/call.wat | 65 + .../multi-value/testdata/call_indirect.wasm | Bin 0 -> 1693 bytes .../multi-value/testdata/call_indirect.wat | 260 +++ tests/post1_0/multi-value/testdata/fac.wasm | Bin 0 -> 147 bytes tests/post1_0/multi-value/testdata/fac.wat | 30 + tests/post1_0/multi-value/testdata/func.wasm | Bin 0 -> 741 bytes tests/post1_0/multi-value/testdata/func.wat | 104 ++ tests/post1_0/multi-value/testdata/if.wasm | Bin 0 -> 979 bytes tests/post1_0/multi-value/testdata/if.wat | 176 ++ tests/post1_0/multi-value/testdata/loop.wasm | Bin 0 -> 773 bytes tests/post1_0/multi-value/testdata/loop.wat | 163 ++ .../multi-value/testdata/multi_value.wasm | Bin 0 -> 328 bytes .../multi-value/testdata/multi_value.wat | 58 + .../sign_extension_ops_test.go} | 20 +- tests/spectest/spec_test.go | 9 +- ...nch_fac_iter_test.go => bench_fac_test.go} | 112 +- vs/codec_test.go | 28 +- vs/testdata/example.wat | 8 +- vs/testdata/fac.wasm | Bin 85 -> 137 bytes vs/testdata/fac.wat | 50 +- wasm.go | 4 +- 68 files changed, 5555 insertions(+), 1155 deletions(-) create mode 100644 examples/results_test.go create mode 100644 internal/wazeroir/compiler_test.go create mode 100644 tests/post1_0/multi-value/multi_value_test.go create mode 100644 tests/post1_0/multi-value/testdata/br.wasm create mode 100644 tests/post1_0/multi-value/testdata/br.wat create mode 100644 tests/post1_0/multi-value/testdata/call.wasm create mode 100644 tests/post1_0/multi-value/testdata/call.wat create mode 100644 tests/post1_0/multi-value/testdata/call_indirect.wasm create mode 100644 tests/post1_0/multi-value/testdata/call_indirect.wat create mode 100644 tests/post1_0/multi-value/testdata/fac.wasm create mode 100644 tests/post1_0/multi-value/testdata/fac.wat create mode 100644 tests/post1_0/multi-value/testdata/func.wasm create mode 100644 tests/post1_0/multi-value/testdata/func.wat create mode 100644 tests/post1_0/multi-value/testdata/if.wasm create mode 100644 tests/post1_0/multi-value/testdata/if.wat create mode 100644 tests/post1_0/multi-value/testdata/loop.wasm create mode 100644 tests/post1_0/multi-value/testdata/loop.wat create mode 100644 tests/post1_0/multi-value/testdata/multi_value.wasm create mode 100644 tests/post1_0/multi-value/testdata/multi_value.wat rename tests/post1_0/{post1_0_test.go => sign-extension-ops/sign_extension_ops_test.go} (90%) rename vs/{bench_fac_iter_test.go => bench_fac_test.go} (64%) diff --git a/api/wasm.go b/api/wasm.go index 3c32735d..6e58d1ba 100644 --- a/api/wasm.go +++ b/api/wasm.go @@ -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 { diff --git a/api/wasm_test.go b/api/wasm_test.go index 456f958c..a14ac889 100644 --- a/api/wasm_test.go +++ b/api/wasm_test.go @@ -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) + }) + } +} diff --git a/builder.go b/builder.go index 4bbbbd3d..09062018 100644 --- a/builder.go +++ b/builder.go @@ -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 diff --git a/config.go b/config.go index 6aadcfc7..7addb6fc 100644 --- a/config.go +++ b/config.go @@ -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. // diff --git a/examples/results_test.go b/examples/results_test.go new file mode 100644 index 00000000..45d6dd5f --- /dev/null +++ b/examples/results_test.go @@ -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() +} diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go index 082cd134..795d5f50 100644 --- a/internal/testing/enginetest/enginetest.go +++ b/internal/testing/enginetest/enginetest.go @@ -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() diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index c09a21d3..6fc7f3ff 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -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. diff --git a/internal/wasm/binary/function.go b/internal/wasm/binary/function.go index c4c8c4da..8c01f370 100644 --- a/internal/wasm/binary/function.go +++ b/internal/wasm/binary/function.go @@ -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 +} diff --git a/internal/wasm/binary/function_test.go b/internal/wasm/binary/function_test.go index f61cf68f..aca78540 100644 --- a/internal/wasm/binary/function_test.go +++ b/internal/wasm/binary/function_test.go @@ -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) }) } } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index 77eb1940..440a2e07 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -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 { diff --git a/internal/wasm/features.go b/internal/wasm/features.go index c97e1d3f..33dd6d10 100644 --- a/internal/wasm/features.go +++ b/internal/wasm/features.go @@ -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 "" } diff --git a/internal/wasm/features_test.go b/internal/wasm/features_test.go index c6adb557..04ea2af5 100644 --- a/internal/wasm/features_test.go +++ b/internal/wasm/features_test.go @@ -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 { diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index caa66fc3..cbb02a7c 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -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 diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index 91b9e634..d3650fa7 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestValidateFunction_valueStackLimit(t *testing.T) { +func TestModule_ValidateFunction_validateFunctionWithMaxStackValues(t *testing.T) { const max = 100 const valuesNum = max + 1 @@ -25,43 +25,48 @@ func TestValidateFunction_valueStackLimit(t *testing.T) { // Plus all functions must end with End opcode. body = append(body, OpcodeEnd) + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: body}}, + } + t.Run("not exceed", func(t *testing.T) { - err := validateFunction(Features20191205, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, max+1) + err := m.validateFunctionWithMaxStackValues(Features20191205, 0, []Index{0}, nil, nil, nil, max+1) require.NoError(t, err) }) t.Run("exceed", func(t *testing.T) { - err := validateFunction(Features20191205, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, max) + err := m.validateFunctionWithMaxStackValues(Features20191205, 0, []Index{0}, nil, nil, nil, max) require.Error(t, err) expMsg := fmt.Sprintf("function may have %d stack values, which exceeds limit %d", valuesNum, max) require.Equal(t, expMsg, err.Error()) }) } -func TestValidateFunction_SignExtensionOps(t *testing.T) { - const maxStackHeight = 100 // arbitrary +func TestModule_ValidateFunction_SignExtensionOps(t *testing.T) { tests := []struct { input Opcode expectedErrOnDisable string }{ { input: OpcodeI32Extend8S, - expectedErrOnDisable: "i32.extend8_s invalid as feature sign-extension-ops is disabled", + expectedErrOnDisable: "i32.extend8_s invalid as feature \"sign-extension-ops\" is disabled", }, { input: OpcodeI32Extend16S, - expectedErrOnDisable: "i32.extend16_s invalid as feature sign-extension-ops is disabled", + expectedErrOnDisable: "i32.extend16_s invalid as feature \"sign-extension-ops\" is disabled", }, { input: OpcodeI64Extend8S, - expectedErrOnDisable: "i64.extend8_s invalid as feature sign-extension-ops is disabled", + expectedErrOnDisable: "i64.extend8_s invalid as feature \"sign-extension-ops\" is disabled", }, { input: OpcodeI64Extend16S, - expectedErrOnDisable: "i64.extend16_s invalid as feature sign-extension-ops is disabled", + expectedErrOnDisable: "i64.extend16_s invalid as feature \"sign-extension-ops\" is disabled", }, { input: OpcodeI64Extend32S, - expectedErrOnDisable: "i64.extend32_s invalid as feature sign-extension-ops is disabled", + expectedErrOnDisable: "i64.extend32_s invalid as feature \"sign-extension-ops\" is disabled", }, } @@ -69,7 +74,12 @@ func TestValidateFunction_SignExtensionOps(t *testing.T) { tc := tt t.Run(InstructionName(tc.input), func(t *testing.T) { t.Run("disabled", func(t *testing.T) { - err := validateFunction(Features20191205, &FunctionType{}, []byte{tc.input}, nil, nil, nil, nil, nil, nil, maxStackHeight) + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{tc.input}}}, + } + err := m.validateFunction(Features20191205, 0, []Index{0}, nil, nil, nil) require.EqualError(t, err, tc.expectedErrOnDisable) }) t.Run("enabled", func(t *testing.T) { @@ -81,9 +91,1622 @@ func TestValidateFunction_SignExtensionOps(t *testing.T) { body = append(body, OpcodeI64Const) } body = append(body, tc.input, 123, OpcodeDrop, OpcodeEnd) - err := validateFunction(FeatureSignExtensionOps, &FunctionType{}, body, nil, nil, nil, nil, nil, nil, maxStackHeight) + m := &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: body}}, + } + err := m.validateFunction(FeatureSignExtensionOps, 0, []Index{0}, nil, nil, nil) require.NoError(t, err) }) }) } } + +// TestModule_ValidateFunction_MultiValue only tests what can't yet be detected during compilation. These examples are +// from test/core/if.wast from the commit that added "multi-value" support. +// +// See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c +func TestModule_ValidateFunction_MultiValue(t *testing.T) { + tests := []struct { + name string + module *Module + expectedErrOnDisable string + }{ + { + name: "block with function type", + module: &Module{ + TypeSection: []*FunctionType{v_f64f64}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0, // (block (result f64 f64) + OpcodeF64Const, 0, 0, 0, 0, 0, 0, 0x10, 0x40, // (f64.const 4) + OpcodeF64Const, 0, 0, 0, 0, 0, 0, 0x14, 0x40, // (f64.const 5) + OpcodeBr, 0, + OpcodeF64Add, + OpcodeF64Const, 0, 0, 0, 0, 0, 0, 0x18, 0x40, // (f64.const 6) + OpcodeEnd, + OpcodeEnd, + }}}, + }, + expectedErrOnDisable: "read block: block with function type return invalid as feature \"multi-value\" is disabled", + }, + { + name: "if with function type", // a.k.a. "param" + module: &Module{ + TypeSection: []*FunctionType{i32_i32}, // (func (param i32) (result i32) + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, // (i32.const 1) + OpcodeLocalGet, 0, OpcodeIf, 0, // (if (param i32) (result i32) (local.get 0) + OpcodeI32Const, 2, OpcodeI32Add, // (then (i32.const 2) (i32.add)) + OpcodeElse, OpcodeI32Const, 0x7e, OpcodeI32Add, // (else (i32.const -2) (i32.add)) + OpcodeEnd, // ) + OpcodeEnd, // ) + }}}, + }, + expectedErrOnDisable: "read block: block with function type return invalid as feature \"multi-value\" is disabled", + }, + { + name: "if with function type - br", // a.k.a. "params-break" + module: &Module{ + TypeSection: []*FunctionType{ + i32_i32, // (func (param i32) (result i32) + i32i32_i32, // (if (param i32 i32) (result i32) + }, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, // (i32.const 1) + OpcodeI32Const, 2, // (i32.const 2) + OpcodeLocalGet, 0, OpcodeIf, 1, // (if (param i32) (result i32) (local.get 0) + OpcodeI32Add, OpcodeBr, 0, // (then (i32.add) (br 0)) + OpcodeElse, OpcodeI32Sub, OpcodeBr, 0, // (else (i32.sub) (br 0)) + OpcodeEnd, // ) + OpcodeEnd, // ) + }}}, + }, + expectedErrOnDisable: "read block: block with function type return invalid as feature \"multi-value\" is disabled", + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + t.Run("disabled", func(t *testing.T) { + err := tc.module.validateFunction(Features20191205, 0, []Index{0}, nil, nil, nil) + require.EqualError(t, err, tc.expectedErrOnDisable) + }) + t.Run("enabled", func(t *testing.T) { + err := tc.module.validateFunction(FeatureMultiValue, 0, []Index{0}, nil, nil, nil) + require.NoError(t, err) + }) + }) + } +} + +var ( + f32, f64, i32, i64 = ValueTypeF32, ValueTypeF64, ValueTypeI32, ValueTypeI64 + f32i32_v = &FunctionType{Params: []ValueType{f32, i32}} + i32_i32 = &FunctionType{Params: []ValueType{i32}, Results: []ValueType{i32}} + i32f64_v = &FunctionType{Params: []ValueType{i32, f64}} + i32i32_i32 = &FunctionType{Params: []ValueType{i32, i32}, Results: []ValueType{i32}} + i32_v = &FunctionType{Params: []ValueType{i32}} + v_v = &FunctionType{} + v_f32 = &FunctionType{Results: []ValueType{f32}} + v_f32f32 = &FunctionType{Results: []ValueType{f32, f32}} + v_f64i32 = &FunctionType{Results: []ValueType{f64, i32}} + v_f64f64 = &FunctionType{Results: []ValueType{f64, f64}} + v_i32 = &FunctionType{Results: []ValueType{i32}} + v_i32i32 = &FunctionType{Results: []ValueType{i32, i32}} + v_i32i64 = &FunctionType{Results: []ValueType{i32, i64}} + v_i64i64 = &FunctionType{Results: []ValueType{i64, i64}} +) + +// TestModule_ValidateFunction_TypeMismatchSpecTests are "type mismatch" tests when "multi-value" was merged. +// +// See https://github.com/WebAssembly/spec/commit/484180ba3d9d7638ba1cb400b699ffede796927c +func TestModule_ValidateFunction_MultiValue_TypeMismatch(t *testing.T) { + tests := []struct { + name string + module *Module + expectedErr string + enabledFeatures Features + }{ + // test/core/func.wast + + { + name: `func.wast - type-empty-f64-i32`, + module: &Module{ + TypeSection: []*FunctionType{v_f64i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{OpcodeEnd}}}, + }, + expectedErr: `not enough results + have () + want (f64, i32)`, + }, + { + name: `func.wast - type-value-void-vs-nums`, + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{OpcodeNop, OpcodeEnd}}}, + }, + expectedErr: `not enough results + have () + want (i32, i32)`, + }, + { + name: `func.wast - type-value-nums-vs-void`, + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{OpcodeI32Const, 0, OpcodeI64Const, 0, OpcodeEnd}}}, + }, + expectedErr: `too many results + have (i32, i64) + want ()`, + }, + { + name: `func.wast - type-value-num-vs-nums - v_f32f32 -> f32`, + module: &Module{ + TypeSection: []*FunctionType{v_f32f32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have (f32) + want (f32, f32)`, + }, + { + name: `func.wast - type-value-num-vs-nums - v_f32 -> f32f32`, + module: &Module{ + TypeSection: []*FunctionType{v_f32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeF32Const, 0, 0, 0, 0, OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) (f32.const 0) + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results + have (f32, f32) + want (f32)`, + }, + { + name: `func.wast - type-return-last-empty-vs-nums`, + module: &Module{ + TypeSection: []*FunctionType{v_f32f32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{OpcodeReturn, OpcodeEnd}}}, + }, + expectedErr: `not enough results + have () + want (f32, f32)`, + }, + { + name: `func.wast - type-return-last-void-vs-nums`, + module: &Module{ + TypeSection: []*FunctionType{v_i32i64}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{OpcodeNop, OpcodeReturn, OpcodeEnd}}}, // (return (nop)) + }, + expectedErr: `not enough results + have () + want (i32, i64)`, + }, + { + name: `func.wast - type-return-last-num-vs-nums`, + module: &Module{ + TypeSection: []*FunctionType{v_i64i64}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI64Const, 0, OpcodeReturn, // (return (i64.const 0)) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have (i64) + want (i64, i64)`, + }, + { + name: `func.wast - type-return-empty-vs-nums`, + // This should err because (return) precedes the values expected in the signature (i32i32): + // (module (func $type-return-empty-vs-nums (result i32 i32) + // (return) (i32.const 1) (i32.const 2) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeReturn, OpcodeI32Const, 1, OpcodeI32Const, 2, + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have () + want (i32, i32)`, + }, + { + name: `func.wast - type-return-partial-vs-nums`, + // This should err because (return) precedes one of the values expected in the signature (i32i32): + // (module (func $type-return-partial-vs-nums (result i32 i32) + // (i32.const 1) (return) (i32.const 2) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeReturn, OpcodeI32Const, 2, + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have (i32) + want (i32, i32)`, + }, + { + name: `func.wast - type-return-void-vs-nums`, + // This should err because (return) is empty due to nop, but the signature requires i32i32: + // (module (func $type-return-void-vs-nums (result i32 i32) + // (return (nop)) (i32.const 1) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeNop, OpcodeReturn, // (return (nop)) + OpcodeI32Const, 1, // (i32.const 1) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have () + want (i32, i32)`, + }, + + { + name: `func.wast - type-return-num-vs-nums`, + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI64Const, 1, OpcodeReturn, // (return (i64.const 1)) + OpcodeI32Const, 1, OpcodeI32Const, 2, // (i32.const 1) (i32.const 2) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have (i64) + want (i32, i32)`, + }, + { + name: `func.wast - type-return-first-num-vs-nums`, + // This should err because the return block doesn't return enough values. + // (module (func $type-return-first-num-vs-nums (result i32 i32) + // (return (i32.const 1)) (return (i32.const 1) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI64Const, 1, OpcodeReturn, // (return (i64.const 1)) + OpcodeI32Const, 1, OpcodeI32Const, 2, OpcodeReturn, // (return (i32.const 1) (i32.const 2)) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results + have (i64) + want (i32, i32)`, + }, + { + name: `func.wast - type-break-last-num-vs-nums`, + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 0, OpcodeBr, 0, // (br 0 (i32.const 0)) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have (i32) + want (i32, i32)`, + }, + { + name: `func.wast - type-break-void-vs-nums`, + // This should err because (br 0) returns no values, but its enclosing function requires two: + // (module (func $type-break-void-vs-nums (result i32 i32) + // (br 0) (i32.const 1) (i32.const 2) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBr, 0, // (br 0) + OpcodeI32Const, 1, OpcodeI32Const, 2, // (i32.const 1) (i32.const 2) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `func.wast - type-break-num-vs-nums`, + // This should err because (br 0) returns one value, but its enclosing function requires two: + // (module (func $type-break-num-vs-nums (result i32 i32) + // (br 0 (i32.const 1)) (i32.const 1) (i32.const 2) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeBr, 0, // (br 0 (i32.const 1)) + OpcodeI32Const, 1, OpcodeI32Const, 2, // (i32.const 1) (i32.const 2) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have (i32) + want (i32, i32)`, + }, + { + name: `func.wast - type-break-nested-empty-vs-nums`, + // This should err because (br 1) doesn't return values, but its enclosing function does: + // (module (func $type-break-nested-empty-vs-nums (result i32 i32) + // (block (br 1)) (br 0 (i32.const 1) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, OpcodeBr, 0x01, OpcodeEnd, // (block (br 1)) + OpcodeI32Const, 1, OpcodeI32Const, 2, OpcodeBr, 0, // (br 0 (i32.const 1) (i32.const 2)) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `func.wast - type-break-nested-void-vs-nums`, + // This should err because nop returns the empty type, but the enclosing function returns i32i32: + // (module (func $type-break-nested-void-vs-nums (result i32 i32) + // (block (br 1 (nop))) (br 0 (i32.const 1) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, OpcodeNop, OpcodeBr, 0x01, OpcodeEnd, // (block (br 1 (nop))) + OpcodeI32Const, 1, OpcodeI32Const, 2, OpcodeBr, 0, // (br 0 (i32.const 1) (i32.const 2)) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `func.wast - type-break-nested-num-vs-nums`, + // This should err because the block signature is v_i32, but the enclosing function is v_i32i32: + // (module (func $type-break-nested-num-vs-nums (result i32 i32) + // (block (result i32) (br 1 (i32.const 1))) (br 0 (i32.const 1) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x7f, OpcodeI32Const, 1, OpcodeBr, 1, OpcodeEnd, // (block (result i32) (br 1 (i32.const 1))) + OpcodeI32Const, 1, OpcodeI32Const, 2, OpcodeBr, 0, // (br 0 (i32.const 1) (i32.const 2)) + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have (i32) + want (i32, i32)`, + }, + + // test/core/if.wast + { + name: `if.wast - wrong signature for if type use`, + // This should err because (br 0) returns no values, but its enclosing function requires two: + // (module + // (type $sig (func)) + // (func (i32.const 1) (if (type $sig) (i32.const 0) (then))) + // ) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, // (i32.const 1) + OpcodeI32Const, 0, OpcodeIf, 0, // (if (type $sig) (i32.const 0) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results + have (i32) + want ()`, + }, + { + name: `if.wast - type-then-value-nums-vs-void`, + // This should err because (if) without a type use returns no values, but its (then) returns two: + // (module (func $type-then-value-nums-vs-void + // (if (i32.const 1) (then (i32.const 1) (i32.const 2))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x40, // (if (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 2, // (then (i32.const 1) (i32.const 2)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in if block + have (i32, i32) + want ()`, + }, + { + name: `if.wast - type-then-value-nums-vs-void-else`, + // This should err because (if) without a type use returns no values, but its (then) returns two: + // (module (func $type-then-value-nums-vs-void-else + // (if (i32.const 1) (then (i32.const 1) (i32.const 2)) (else)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x40, // (if (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 2, // (then (i32.const 1) (i32.const 2)) + OpcodeElse, // (else) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in if block + have (i32, i32) + want ()`, + }, + { + name: `if.wast - type-else-value-nums-vs-void`, + // This should err because (if) without a type use returns no values, but its (else) returns two: + // (module (func $type-else-value-nums-vs-void + // (if (i32.const 1) (then) (else (i32.const 1) (i32.const 2))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x40, // (if (i32.const 1) (then) + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 2, // (else (i32.const 1) (i32.const 2)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in else block + have (i32, i32) + want ()`, + }, + { + name: `if.wast - type-both-value-nums-vs-void`, + // This should err because (if) without a type use returns no values, each branch returns two: + // (module (func $type-both-value-nums-vs-void + // (if (i32.const 1) (then (i32.const 1) (i32.const 2)) (else (i32.const 2) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x40, // (if (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 2, // (then (i32.const 1) (i32.const 2)) + OpcodeElse, OpcodeI32Const, 2, OpcodeI32Const, 1, // (else (i32.const 2) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in if block + have (i32, i32) + want ()`, + }, + { + name: `if.wast - type-then-value-empty-vs-nums`, + // This should err because the if branch is empty, but its type use requires two i32s: + // (module (func $type-then-value-empty-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then) (else (i32.const 0) (i32.const 2))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (result i32 i32) (i32.const 1) (then) + OpcodeElse, OpcodeI32Const, 0, OpcodeI32Const, 2, // (else (i32.const 0) (i32.const 2))) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-else-value-empty-vs-nums`, + // This should err because the else branch is empty, but its type use requires two i32s: + // (module (func $type-else-value-empty-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 0) (i32.const 1)) (else)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 0, OpcodeI32Const, 2, // (then (i32.const 0) (i32.const 1)) + OpcodeElse, // (else) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in else block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-both-value-empty-vs-nums`, + // This should err because the both branches are empty, but the if type use requires two i32s: + // (module (func $type-both-value-empty-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then) (else)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (result i32 i32) (i32.const 1) (then) + OpcodeElse, // (else) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-no-else-vs-nums`, + // This should err because the else branch is missing, but its type use requires two i32s: + // (module (func $type-no-else-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in else block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-then-value-void-vs-nums`, + // This should err because the then branch evaluates to empty, but its type use requires two i32s: + // (module (func $type-then-value-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (nop)) (else (i32.const 0) (i32.const 0))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (result i32 i32) (i32.const 1) + OpcodeNop, // (then (nop)) + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-then-value-void-vs-nums`, + // This should err because the else branch evaluates to empty, but its type use requires two i32s: + // (module (func $type-else-value-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 0) (i32.const 0)) (else (nop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 0, OpcodeI32Const, 0, // (then (i32.const 0) (i32.const 0)) + OpcodeElse, OpcodeNop, // (else (nop)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in else block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-both-value-void-vs-nums`, + // This should err because the if branch evaluates to empty, but its type use requires two i32s: + // (module (func $type-both-value-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (nop)) (else (nop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeNop, // (then (nop)) + OpcodeElse, OpcodeNop, // (else (nop)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-then-value-num-vs-nums`, + // This should err because the if branch returns one value, but its type use requires two: + // (module (func $type-then-value-num-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1)) (else (i32.const 1) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, // (then (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1))) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-else-value-num-vs-nums`, + // This should err because the else branch returns one value, but its type use requires two: + // (module (func $type-else-value-num-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1) (i32.const 1)) (else (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1))) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in else block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-both-value-num-vs-nums`, + // This should err because the if branch returns one value, but its type use requires two: + // (module (func $type-both-value-num-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1)) (else (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, // (then (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1))) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-then-value-partial-vs-nums`, + // This should err because the if branch returns one value, but its type use requires two: + // (module (func $type-then-value-partial-vs-nums (result i32 i32) + // (i32.const 0) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1)) (else (i32.const 1) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 0, // (i32.const 0) - NOTE: this is outside the (if) + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, // (then (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-else-value-partial-vs-nums`, + // This should err because the else branch returns one value, but its type use requires two: + // (module (func $type-else-value-partial-vs-nums (result i32 i32) + // (i32.const 0) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1) (i32.const 1)) (else (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 0, // (i32.const 0) - NOTE: this is outside the (if) + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in else block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-both-value-partial-vs-nums`, + // This should err because the if branch returns one value, but its type use requires two: + // (module (func $type-both-value-partial-vs-nums (result i32 i32) + // (i32.const 0) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1)) (else (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 0, // (i32.const 0) - NOTE: this is outside the (if) + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, // (then (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-then-value-nums-vs-num`, + // This should err because the if branch returns two values, but its type use requires one: + // (module (func $type-then-value-nums-vs-num (result i32) + // (if (result i32) (i32.const 1) (then (i32.const 1) (i32.const 1)) (else (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in if block + have (i32, i32) + want (i32)`, + }, + { + name: `if.wast - type-else-value-nums-vs-num`, + // This should err because the else branch returns two values, but its type use requires one: + // (module (func $type-else-value-nums-vs-num (result i32) + // (if (result i32) (i32.const 1) (then (i32.const 1)) (else (i32.const 1) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32) (i32.const 1) + OpcodeI32Const, 1, // (then (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in else block + have (i32, i32) + want (i32)`, + }, + { + name: `if.wast - type-both-value-nums-vs-num`, + // This should err because the if branch returns two values, but its type use requires one: + // (module (func $type-both-value-nums-vs-num (result i32) + // (if (result i32) (i32.const 1) (then (i32.const 1) (i32.const 1)) (else (i32.const 1) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in if block + have (i32, i32) + want (i32)`, + }, + { + name: `if.wast - type-both-different-value-nums-vs-nums`, + // This should err because the if branch returns three values, but its type use requires two: + // (module (func $type-both-different-value-nums-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1) (i32.const 1) (i32.const 1)) (else (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in if block + have (i32, i32, i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-then-break-last-void-vs-nums`, + // This should err because the branch in the if returns no values, but its type use requires two: + // (module (func $type-then-break-last-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (br 0)) (else (i32.const 1) (i32.const 1))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeBr, 0, // (then (br 0)) + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1))) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-else-break-last-void-vs-nums`, + // This should err because the branch in the else returns no values, but its type use requires two: + // (module (func $type-else-break-last-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) (then (i32.const 1) (i32.const 1)) (else (br 0))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeBr, 0, // (else (br 0)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-then-break-empty-vs-nums`, + // This should err because the branch in the if returns no values, but its type use requires two: + // (module (func $type-then-break-empty-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) + // (then (br 0) (i32.const 1) (i32.const 1)) + // (else (i32.const 1) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeBr, 0, OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (br 0) (i32.const 1) (i32.const 1)) + // ^^ NOTE: consts are outside the br block + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-else-break-empty-vs-nums`, + // This should err because the branch in the else returns no values, but its type use requires two: + // (module (func $type-else-break-empty-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) + // (then (i32.const 1) (i32.const 1)) + // (else (br 0) (i32.const 1) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeBr, 0, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (br 0) (i32.const 1) (i32.const 1)) + // ^^ NOTE: consts are outside the br block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-then-break-void-vs-nums`, + // This should err because the branch in the if evaluates to no values, but its type use requires two: + // (module (func $type-then-break-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) + // (then (br 0 (nop)) (i32.const 1) (i32.const 1)) + // (else (i32.const 1) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeNop, OpcodeBr, 0, OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (br 0 (nop)) (i32.const 1) (i32.const 1)) + // ^^ NOTE: consts are outside the br block + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-else-break-void-vs-nums`, + // This should err because the branch in the else evaluates to no values, but its type use requires two: + // (module (func $type-else-break-void-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) + // (then (i32.const 1) (i32.const 1)) + // (else (br 0 (nop)) (i32.const 1) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeNop, OpcodeBr, 0, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (br 0 (nop)) (i32.const 1) (i32.const 1)) + // ^^ NOTE: consts are outside the br block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have () + want (i32, i32)`, + }, + { + name: `if.wast - type-then-break-num-vs-nums`, + // This should err because the branch in the if evaluates to one value, but its type use requires two: + // (module (func $type-then-break-num-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) + // (then (br 0 (i64.const 1)) (i32.const 1) (i32.const 1)) + // (else (i32.const 1) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI64Const, 1, OpcodeBr, 0, OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (br 0 (i64.const 1)) (i32.const 1) (i32.const 1)) + // ^^ NOTE: only one (incorrect) const is inside the br block + OpcodeElse, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (i32.const 1) (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have (i64) + want (i32, i32)`, + }, + { + name: `if.wast - type-else-break-num-vs-nums`, + // This should err because the branch in the else evaluates to one value, but its type use requires two: + // (module (func $type-else-break-num-vs-nums (result i32 i32) + // (if (result i32 i32) (i32.const 1) + // (then (i32.const 1) (i32.const 1)) + // (else (br 0 (i64.const 1)) (i32.const 1) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, OpcodeI32Const, 1, // (then (i32.const 1) (i32.const 1)) + OpcodeElse, OpcodeI64Const, 1, OpcodeBr, 0, OpcodeI32Const, 1, OpcodeI32Const, 1, // (else (br 0 (i64.const 1)) (i32.const 1) (i32.const 1)) + // ^^ NOTE: only one (incorrect) const is inside the br block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have (i64) + want (i32, i32)`, + }, + { + name: `if.wast - type-then-break-partial-vs-nums`, + // This should err because the branch in the if evaluates to one value, but its type use requires two: + // (module (func $type-then-break-partial-vs-nums (result i32 i32) + // (i32.const 1) + // (if (result i32 i32) (i32.const 1) + // (then (br 0 (i64.const 1)) (i32.const 1)) + // (else (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, // (i32.const 1) + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI64Const, 1, OpcodeBr, 0, OpcodeI32Const, 1, // (then (br 0 (i64.const 1)) (i32.const 1)) + // ^^ NOTE: only one (incorrect) const is inside the br block + OpcodeElse, OpcodeI32Const, 1, // (else (i32.const 1)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in br block + have (i64) + want (i32, i32)`, + }, + { + name: `if.wast - type-else-break-partial-vs-nums`, + // This should err because the branch in the if evaluates to one value, but its type use requires two: + // (module (func $type-else-break-partial-vs-nums (result i32 i32) + // (i32.const 1) + // (if (result i32 i32) (i32.const 1) + // (then (i32.const 1)) + // (else (br 0 (i64.const 1)) (i32.const 1)) + // ) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, // (i32.const 1) + OpcodeI32Const, 1, OpcodeIf, 0x00, // (if (result i32 i32) (i32.const 1) + OpcodeI32Const, 1, // (then (i32.const 1)) + OpcodeElse, OpcodeI64Const, 1, OpcodeBr, 0, OpcodeI32Const, 1, // (else (br 0 (i64.const 1)) (i32.const 1)) + // ^^ NOTE: only one (incorrect) const is inside the br block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in if block + have (i32) + want (i32, i32)`, + }, + { + name: `if.wast - type-param-void-vs-num`, + // This should err because the stack has no values, but the if type use requires two: + // (module (func $type-param-void-vs-num + // (if (param i32) (i32.const 1) (then (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param i32) (i32.const 1) + OpcodeDrop, // (then (drop))) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for if block + have () + want (i32)`, + }, + { + name: `if.wast - type-param-void-vs-nums`, + // This should err because the stack has no values, but the if type use requires two: + // (module (func $type-param-void-vs-nums + // (if (param i32 f64) (i32.const 1) (then (drop) (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32f64_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param i32 f64) (i32.const 1) + OpcodeI32Const, 1, OpcodeDrop, // (then (drop) (drop)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for if block + have () + want (i32, f64)`, + }, + { + name: `if.wast - type-param-num-vs-num`, + // This should err because the stack has a different value that what the if type use requires: + // (module (func $type-param-num-vs-num + // (f32.const 0) (if (param i32) (i32.const 1) (then (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param i32) (i32.const 1) + OpcodeDrop, // (then (drop)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: "cannot use f32 in if block as param[0] type i32", + }, + { + name: `if.wast - type-param-num-vs-nums`, + // This should err because the stack has one value, but the if type use requires two: + // (module (func $type-param-num-vs-nums + // (f32.const 0) (if (param f32 i32) (i32.const 1) (then (drop) (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, f32i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param f32 i32) (i32.const 1) + OpcodeDrop, OpcodeDrop, // (then (drop) (drop)) + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for if block + have (f32) + want (f32, i32)`, + }, + { + name: `if.wast - type-param-nested-void-vs-num`, + // This should err because the stack has no values, but the if type use requires one: + // (module (func $type-param-nested-void-vs-num + // (block (if (param i32) (i32.const 1) (then (drop)))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param i32) (i32.const 1) + OpcodeDrop, // (then (drop)) + OpcodeEnd, // block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for if block + have () + want (i32)`, + }, + { + name: `if.wast - type-param-void-vs-nums`, + // This should err because the stack has no values, but the if type use requires two: + // (module (func $type-param-void-vs-nums + // (block (if (param i32 f64) (i32.const 1) (then (drop) (drop)))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32f64_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param i32 f64) (i32.const 1) + OpcodeDrop, // (then (drop) (drop)) + OpcodeEnd, // block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for if block + have () + want (i32, f64)`, + }, + { + name: `if.wast - type-param-num-vs-num`, + // This should err because the stack has a different values than required by the if type use: + // (module (func $type-param-num-vs-num + // (block (f32.const 0) (if (param i32) (i32.const 1) (then (drop)))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param i32) (i32.const 1) + OpcodeDrop, // (then (drop)) + OpcodeEnd, // block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: "cannot use f32 in if block as param[0] type i32", + }, + { + name: `if.wast - type-param-num-vs-nums`, + // This should err because the stack has one value, but the if type use requires two: + // (module (func $type-param-num-vs-nums + // (block (f32.const 0) (if (param f32 i32) (i32.const 1) (then (drop) (drop)))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, f32i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeI32Const, 1, OpcodeIf, 0x01, // (if (param f32 i32) (i32.const 1) + OpcodeDrop, // (then (drop) (drop)) + OpcodeEnd, // block + OpcodeEnd, // if + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for if block + have (f32) + want (f32, i32)`, + }, + + // test/core/loop.wast + { + name: `loop.wast - wrong signature for loop type use`, + // This should err because the loop type use returns no values, but its block returns one: + // (module + // (type $sig (func)) + // (func (loop (type $sig) (i32.const 0))) + // ) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0, OpcodeI32Const, 0, // (loop (type $sig) (i32.const 0)) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in loop block + have (i32) + want ()`, + }, + { + name: `loop.wast - type-value-nums-vs-void`, + // This should err because the empty block type requires no values, but the loop returns two: + // (module (func $type-value-nums-vs-void + // (loop (i32.const 1) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x40, OpcodeI32Const, 1, OpcodeI32Const, 2, // (loop (i32.const 1) (i32.const 2)) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in loop block + have (i32, i32) + want ()`, + }, + { + name: `loop.wast - type-value-empty-vs-nums`, + // This should err because the loop type use returns two values, but the block returns none: + // (module (func $type-value-empty-vs-nums (result i32 i32) + // (loop (result i32 i32)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x0, // (loop (result i32 i32)) - matches existing func type + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in loop block + have () + want (i32, i32)`, + }, + { + name: `loop.wast - type-value-void-vs-nums`, + // This should err because the loop type use returns two values, but the block returns none: + // (module (func $type-value-void-vs-nums (result i32 i32) + // (loop (result i32 i32) (nop)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x0, // (loop (result i32 i32) - matches existing func type + OpcodeNop, // (nop) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in loop block + have () + want (i32, i32)`, + }, + { + name: `loop.wast - type-value-num-vs-nums`, + // This should err because the loop type use returns two values, but the block returns one: + // (module (func $type-value-num-vs-nums (result i32 i32) + // (loop (result i32 i32) (i32.const 0)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x0, // (loop (result i32 i32) - matches existing func type + OpcodeI32Const, 0, // (i32.const 0) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in loop block + have (i32) + want (i32, i32)`, + }, + { + name: `loop.wast - type-value-partial-vs-nums`, + // This should err because the loop type use returns two values, but the block returns one: + // (module (func $type-value-partial-vs-nums (result i32 i32) + // (i32.const 1) (loop (result i32 i32) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeI32Const, 1, // (i32.const 1) - NOTE: outside the loop! + OpcodeLoop, 0x0, // (loop (result i32 i32) - matches existing func type + OpcodeI32Const, 2, // (i32.const 2) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough results in loop block + have (i32) + want (i32, i32)`, + }, + { + name: `loop.wast - type-value-nums-vs-num`, + // This should err because the loop type use returns one value, but the block returns two: + // (module (func $type-value-nums-vs-num (result i32) + // (loop (result i32) (i32.const 1) (i32.const 2)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_i32}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x0, // (loop (result i32) - matches existing func type + OpcodeI32Const, 1, OpcodeI32Const, 2, // (i32.const 1) (i32.const 2)) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `too many results in loop block + have (i32, i32) + want (i32)`, + }, + { + name: `loop.wast - type-param-void-vs-num`, + // This should err because the loop type use requires one param, but the stack has none: + // (module (func $type-param-void-vs-num + // (loop (param i32) (drop)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x1, // (loop (param i32) + OpcodeDrop, // (drop) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for loop block + have () + want (i32)`, + }, + { + name: `loop.wast - type-param-void-vs-nums`, + // This should err because the loop type use requires two params, but the stack has none: + // (module (func $type-param-void-vs-nums + // (loop (param i32 f64) (drop) (drop)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32f64_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeLoop, 0x1, // (loop (param i32 f64) + OpcodeDrop, // (drop) + OpcodeDrop, // (drop) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for loop block + have () + want (i32, f64)`, + }, + { + name: `loop.wast - type-param-num-vs-num`, + // This should err because the loop type use requires a different param type than what's on the stack: + // (module (func $type-param-num-vs-num + // (f32.const 0) (loop (param i32) (drop)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeLoop, 0x1, // (loop (param i32) + OpcodeDrop, // (drop) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: "cannot use f32 in loop block as param[0] type i32", + }, + { + name: `loop.wast - type-param-num-vs-num`, + // This should err because the loop type use requires a more parameters than what's on the stack: + // (module (func $type-param-num-vs-nums + // (f32.const 0) (loop (param f32 i32) (drop) (drop)) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, f32i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeLoop, 0x1, // (loop (param f32 i32) + OpcodeDrop, OpcodeDrop, // (drop) (drop) + OpcodeEnd, // loop + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for loop block + have (f32) + want (f32, i32)`, + }, + { + name: `loop.wast - type-param-nested-void-vs-num`, + // This should err because the loop type use requires a more parameters than what's on the stack: + // (module (func $type-param-nested-void-vs-num + // (block (loop (param i32) (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeLoop, 0x1, // (loop (param i32) + OpcodeDrop, // (drop) + OpcodeEnd, // loop + OpcodeEnd, // block + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for loop block + have () + want (i32)`, + }, + { + name: `loop.wast - type-param-void-vs-nums`, + // This should err because the loop type use requires a more parameters than what's on the stack: + // (module (func $type-param-void-vs-nums + // (block (loop (param i32 f64) (drop) (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32f64_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeLoop, 0x1, // (loop (param i32 f64) + OpcodeDrop, OpcodeDrop, // (drop) (drop) + OpcodeEnd, // loop + OpcodeEnd, // block + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for loop block + have () + want (i32, f64)`, + }, + { + name: `loop.wast - type-param-void-vs-nums`, + // This should err because the loop type use requires a different param type than what's on the stack: + // (module (func $type-param-num-vs-num + // (block (f32.const 0) (loop (param i32) (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeLoop, 0x1, // (loop (param i32) + OpcodeDrop, // (drop) + OpcodeEnd, // loop + OpcodeEnd, // block + OpcodeEnd, // func + }}}, + }, + expectedErr: "cannot use f32 in loop block as param[0] type i32", + }, + { + name: `loop.wast - type-param-void-vs-nums`, + // This should err because the loop type use requires a more parameters than what's on the stack: + // (module (func $type-param-num-vs-nums + // (block (f32.const 0) (loop (param f32 i32) (drop) (drop))) + // )) + module: &Module{ + TypeSection: []*FunctionType{v_v, f32i32_v}, + FunctionSection: []Index{0}, + CodeSection: []*Code{{Body: []byte{ + OpcodeBlock, 0x40, // (block + OpcodeF32Const, 0, 0, 0, 0, // (f32.const 0) + OpcodeLoop, 0x1, // (loop (param f32 i32) + OpcodeDrop, OpcodeDrop, // (drop) (drop) + OpcodeEnd, // loop + OpcodeEnd, // block + OpcodeEnd, // func + }}}, + }, + expectedErr: `not enough params for loop block + have (f32) + want (f32, i32)`, + }, + } + + for _, tt := range tests { + tc := tt + + t.Run(tc.name, func(t *testing.T) { + err := tc.module.validateFunction(FeatureMultiValue, 0, []Index{0}, nil, nil, nil) + require.EqualError(t, err, tc.expectedErr) + }) + } +} diff --git a/internal/wasm/gofunc.go b/internal/wasm/gofunc.go index 83717419..37bbe980 100644 --- a/internal/wasm/gofunc.go +++ b/internal/wasm/gofunc.go @@ -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 } diff --git a/internal/wasm/gofunc_test.go b/internal/wasm/gofunc_test.go index e8357951..f7026b79 100644 --- a/internal/wasm/gofunc_test.go +++ b/internal/wasm/gofunc_test.go @@ -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) }) } diff --git a/internal/wasm/host.go b/internal/wasm/host.go index b3398126..d80a97c6 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -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) } diff --git a/internal/wasm/host_test.go b/internal/wasm/host_test.go index 69284ae0..52aebead 100644 --- a/internal/wasm/host_test.go +++ b/internal/wasm/host_test.go @@ -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) }) } diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index ee32eb36..ddc2cad5 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -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. diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go index 771b19b1..adcf0eee 100644 --- a/internal/wasm/interpreter/interpreter.go +++ b/internal/wasm/interpreter/interpreter.go @@ -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 { diff --git a/internal/wasm/interpreter/interpreter_test.go b/internal/wasm/interpreter/interpreter_test.go index a139a2f4..714beb25 100644 --- a/internal/wasm/interpreter/interpreter_test.go +++ b/internal/wasm/interpreter/interpreter_test.go @@ -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) diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go index 63f64f90..8fcfc1cf 100644 --- a/internal/wasm/jit/engine.go +++ b/internal/wasm/jit/engine.go @@ -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) } diff --git a/internal/wasm/jit/engine_test.go b/internal/wasm/jit/engine_test.go index 2b08c73e..4261b096 100644 --- a/internal/wasm/jit/engine_test.go +++ b/internal/wasm/jit/engine_test.go @@ -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) diff --git a/internal/wasm/jit/jit_impl_amd64.go b/internal/wasm/jit/jit_impl_amd64.go index 707a7672..259b5e11 100644 --- a/internal/wasm/jit/jit_impl_amd64.go +++ b/internal/wasm/jit/jit_impl_amd64.go @@ -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 { diff --git a/internal/wasm/jit/jit_impl_arm64.go b/internal/wasm/jit/jit_impl_arm64.go index 94941783..21f4ad8d 100644 --- a/internal/wasm/jit/jit_impl_arm64.go +++ b/internal/wasm/jit/jit_impl_arm64.go @@ -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. diff --git a/internal/wasm/jit/jit_stack_test.go b/internal/wasm/jit/jit_stack_test.go index 2e02ec01..7c4fb80c 100644 --- a/internal/wasm/jit/jit_stack_test.go +++ b/internal/wasm/jit/jit_stack_test.go @@ -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. diff --git a/internal/wasm/module.go b/internal/wasm/module.go index 08335230..ed51a881 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -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) } } diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 71c50953..5322b29a 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -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", diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 6a8b3cd8..7e046b32 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -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{}, diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go index 60eb11de..5f654c16 100644 --- a/internal/wasm/store_test.go +++ b/internal/wasm/store_test.go @@ -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. diff --git a/internal/wasm/text/decoder.go b/internal/wasm/text/decoder.go index 59d0dce2..4ba86017 100644 --- a/internal/wasm/text/decoder.go +++ b/internal/wasm/text/decoder.go @@ -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 diff --git a/internal/wasm/text/decoder_test.go b/internal/wasm/text/decoder_test.go index 278f56cd..71ab9506 100644 --- a/internal/wasm/text/decoder_test.go +++ b/internal/wasm/text/decoder_test.go @@ -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) }) } diff --git a/internal/wasm/text/errors.go b/internal/wasm/text/errors.go index 555e21c9..d084e16d 100644 --- a/internal/wasm/text/errors.go +++ b/internal/wasm/text/errors.go @@ -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 { diff --git a/internal/wasm/text/func_parser.go b/internal/wasm/text/func_parser.go index e24f0068..b7d8da4c 100644 --- a/internal/wasm/text/func_parser.go +++ b/internal/wasm/text/func_parser.go @@ -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) { diff --git a/internal/wasm/text/func_parser_test.go b/internal/wasm/text/func_parser_test.go index 1b7241ed..66cabc18 100644 --- a/internal/wasm/text/func_parser_test.go +++ b/internal/wasm/text/func_parser_test.go @@ -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) }) } diff --git a/internal/wasm/text/type_parser.go b/internal/wasm/text/type_parser.go index f6c3aabc..9c2234ea 100644 --- a/internal/wasm/text/type_parser.go +++ b/internal/wasm/text/type_parser.go @@ -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 "" } diff --git a/internal/wasm/text/type_parser_test.go b/internal/wasm/text/type_parser_test.go index 1be69f8f..4923c96b 100644 --- a/internal/wasm/text/type_parser_test.go +++ b/internal/wasm/text/type_parser_test.go @@ -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 { diff --git a/internal/wasm/text/typeuse_parser.go b/internal/wasm/text/typeuse_parser.go index 8b113960..97acefd2 100644 --- a/internal/wasm/text/typeuse_parser.go +++ b/internal/wasm/text/typeuse_parser.go @@ -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 "" } diff --git a/internal/wasm/text/typeuse_parser_test.go b/internal/wasm/text/typeuse_parser_test.go index 6f235f65..c354e90f 100644 --- a/internal/wasm/text/typeuse_parser_test.go +++ b/internal/wasm/text/typeuse_parser_test.go @@ -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"}, } diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go index 69dd900f..48e34529 100644 --- a/internal/wazeroir/compiler.go +++ b/internal/wazeroir/compiler.go @@ -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} diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go new file mode 100644 index 00000000..4e03b08d --- /dev/null +++ b/internal/wazeroir/compiler_test.go @@ -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() { +} diff --git a/internal/wazeroir/format.go b/internal/wazeroir/format.go index 0fb9d171..68f999e8 100644 --- a/internal/wazeroir/format.go +++ b/internal/wazeroir/format.go @@ -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: diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go index 4bf45901..29fd707c 100644 --- a/internal/wazeroir/operations.go +++ b/internal/wazeroir/operations.go @@ -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 { diff --git a/tests/post1_0/multi-value/multi_value_test.go b/tests/post1_0/multi-value/multi_value_test.go new file mode 100644 index 00000000..f5e1c9c1 --- /dev/null +++ b/tests/post1_0/multi-value/multi_value_test.go @@ -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) + } + }) + } +} diff --git a/tests/post1_0/multi-value/testdata/br.wasm b/tests/post1_0/multi-value/testdata/br.wasm new file mode 100644 index 0000000000000000000000000000000000000000..dc7e3c40f6736875cf91ea43947da8cb982c12b0 GIT binary patch literal 548 zcmX|;UrvKS5Ql%eK>4#t%Gpg#xbuXb(|0_yYF^pbR_<3Be@~p`e=yWMn z#A!S&{U}Xg3i2sYZC1av*CcyO^8HVC-Bd2}*q+zGqWq;v2$h&5S(LAvg!I&) z#e5m)9LMI{M@CN2Zsh720Acd!G;3?i7xaGgH|N(|J9pkxBvhE literal 0 HcmV?d00001 diff --git a/tests/post1_0/multi-value/testdata/br.wat b/tests/post1_0/multi-value/testdata/br.wat new file mode 100644 index 00000000..321e9b51 --- /dev/null +++ b/tests/post1_0/multi-value/testdata/br.wat @@ -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)))) + ) +) diff --git a/tests/post1_0/multi-value/testdata/call.wasm b/tests/post1_0/multi-value/testdata/call.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ba95a55c740f26f2136bfe3efac764d6de2f0aac GIT binary patch literal 502 zcmZ8eO-{l<6#i!V-)XfPoKCmYm?-lvMM_u)YD|MlW&fhO9W@N+T$a&& zmdnLDQVZurx{&#(gnL4bS@xKY4xBh6Zm48`q@Mf2d>ipQzxm|CM2L{7#6tW_Is%Ae z#NnXle!LaE?|#4pO|qB=T%6(-cXSql4q`3V13nR6Q^m$)D%=Lp z5=y-`iDD&iqX&K$Jxs%Nq zX1-7?Wo_Hyj(jZ2$@R*vTdC>>n=#)v|6*=#Fq+SzMgKdL%=4(<8HE8{^L+F&MxNqM zj!}-$IiZR|q^l|&akUu9)g6UswXP3e4<~43rh@^R=H+q{T}5xuDqJiV%V^w5iMF$N zGaGi!!=N*w10I#)X*3>hse?O;6q3f_LrP}-*m}YszBfi0=&h)`96xdXDQVQtkS@|!3BFa@?d+XsxDI|%`3Uw^5>9BZM3aK~kYOeI zp7W`nK@U09jM>08mFpm9z&T{ZcU^Uv=xxa|%fW{_rf`cAYd{x;Ryt|ETsyh^P6 z`%es&M3IufQ%GgGP-ii-x(@=o~6y?9Sw}UCG9SWwxdV?y3I@M zxgLchs;7y4w+xOy41E$^3{m#_bTwa$CWF!Cu)lbE70nmyU(JWrpHAird4*XD!$3tO-8N}iZsDfv6Ph^Q^!4ykQk}ta zHXik(#W0_6q}Xx=#mVhku9$H5b=32!`>Ews^Q(@h07=82987G@8wnXKu`?e~OjO9D?t7sPgC>2Bd@(5tikbw>Z zS(wPd0`(dk*Wu!A}pXrhHScCm+jx&@DLfI~cne#V>|Z5e@OtT0yJX9CPhl3uHz_AK*bKJc2cJb#3^zTXeHaTVp;Jl z?AWsg9)ukd3wB)RmtxJG@%_#@GnO@omnj3lVi5rmG?f<-xXL5wswio#Rg0!dRf&pb zuF8n1{k#Dx3RGDXlqE-z=Q+*(rYVUKP^4)O+SNf^t8v}f4%4jL<{fvO_wjt4bY{J7 zXL{C$865kHQK8=2vhn5O6vinJIPqT~Jw6j>JRl$F_^JW*Tcsx%ILh$5&Az7}H%Y)vu=5cnFbe6L#wCq2u z%^|i!1H`_3gB47%k}7A41Q+2qjOd@z^LQZY2wBKOO$czqr)J27@DP5-=;fEEAAf#6 zA9%vrnLN=rx;#Pl9~gc8{rvT(U+04cguV08t8+9`N6zlBC*k1aB6<8m-!VIkgxeen!Zms~>Dr74-zq$O+8kd746lfHCiL*P|y5vK`kYr0-cPCvxU z6$?1@%XOOG8s_9WeK${T_zSv73>+G2&|z@SZgh5Qq7Dl-1RC^pE{Bw$?C3CIE=f!x=DxsAi(VOjWzeE(WryW>8gWb5>Dfd3niD zRaI#d0aFVP{z75XK zjc{46^+dAw)U0kc{1BaZf*b8wQ!W)alQg70RziQ&3jXEJzRy%%V8nj}af zw5iU6x(prvT4`W+aBI9a(8xs5yTjwn@kO%!c04zEzJ-5qLl$Pe$;~>B&}*-^=W(zp zV)VPe)j)zyF@tlcb``_)$Ji_PInqd14LsBWwUE_2+M2XlB*^YdSuUO zaXU6@hkMk?j+XSyDtyR$;D{fj-dN`yR}H+PFZB3cJvY`n(r&t>r&jFhkzR!(A9}2_ zaO~=p9)&B0nI4B35!(gZJS|gKmnl)72`!nT)Ljswy3{iV(1#GR1F2Y&H- zXzNk@@h1OiBm5N3%49Lxt>o*A3sbDcO9eODWZBmL$0h0Aza#Sbfd4f}C^&H8p@lX& r=z`!o5Qz|}6mumOJ@nzcbwDS}v-u(@lGEUoxC36xJK&AF0}lTJ-v_^? literal 0 HcmV?d00001 diff --git a/tests/post1_0/multi-value/testdata/if.wat b/tests/post1_0/multi-value/testdata/if.wat new file mode 100644 index 00000000..bc05aac1 --- /dev/null +++ b/tests/post1_0/multi-value/testdata/if.wat @@ -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) + ) +) diff --git a/tests/post1_0/multi-value/testdata/loop.wasm b/tests/post1_0/multi-value/testdata/loop.wasm new file mode 100644 index 0000000000000000000000000000000000000000..523072aee782f95f9d3a7fdfda7577b581826092 GIT binary patch literal 773 zcmaJ<-EPw`6#i@{&Cf}@b{hia06^HE|MdD{p8;wH*~48ml>AXDa-u`7czd#=*h^6+4}*|Tl-;2@{j=_>#BhFTMXT1TW?B;^Sn)FTuE>Ll_;Lp3>Q4@DPan&2CjB z+qS?k{3Aya^bu-0Mn6dz3$#B5#mebL%lesm|Bw!BBBl|P%X!7ip_(qC24ma+Q*PO9 z^pz&38SPdzJ>_fHP*P6yc_c|+Mj(Ab^a|3`&`6*+<>`?^m1jBZlSh0sC;T2?*O zQiVrIRM8V5Oc!ygYrKFCzYIb2tW&%6HGlI#kBOdJ16SOYO=4YCSC7Xk`ouTHH={vH z#-#C?pH(;b9~9q0{6CNI(^=$AfiS3-%hl^2dAkw5fIHtd%}%}?@e_rFf&&)=c<>P* k6cG1hOJvIs&IboHtCzQTNn3tP-iQV&-i3kori!TJ%_MUu3KPfb$5N2Lx8pJA+*_~4D)@^`eR}{B5X`gPZ z?m5j}(|mxAe#O>xedD^a0Lzoh5$!)}jhJGX6Cxi6x{LX1cP{Y=+|P~0s4ihi$s!qy z3Ht$NZ_)!fLlW+Vt+L8nmC&b^1P_N;%#FdDdUbV)15@|wuDX18>%OF=#$=cNam#pcbu>EXQyo5~Ko!voQ!cb(2oxfG0~0SskpKVy literal 0 HcmV?d00001 diff --git a/tests/post1_0/multi-value/testdata/multi_value.wat b/tests/post1_0/multi-value/testdata/multi_value.wat new file mode 100644 index 00000000..331b0dd9 --- /dev/null +++ b/tests/post1_0/multi-value/testdata/multi_value.wat @@ -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)) +) diff --git a/tests/post1_0/post1_0_test.go b/tests/post1_0/sign-extension-ops/sign_extension_ops_test.go similarity index 90% rename from tests/post1_0/post1_0_test.go rename to tests/post1_0/sign-extension-ops/sign_extension_ops_test.go index 87ad99e4..aa30724f 100644 --- a/tests/post1_0/post1_0_test.go +++ b/tests/post1_0/sign-extension-ops/sign_extension_ops_test.go @@ -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'. diff --git a/tests/spectest/spec_test.go b/tests/spectest/spec_test.go index 5698d8ff..9ec8c724 100644 --- a/tests/spectest/spec_test.go +++ b/tests/spectest/spec_test.go @@ -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. diff --git a/vs/bench_fac_iter_test.go b/vs/bench_fac_test.go similarity index 64% rename from vs/bench_fac_iter_test.go rename to vs/bench_fac_test.go index 52b07cbc..b8493d2c 100644 --- a/vs/bench_fac_iter_test.go +++ b/vs/bench_fac_test.go @@ -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 } diff --git a/vs/codec_test.go b/vs/codec_test.go index dae83434..37c02cc3 100644 --- a/vs/codec_test.go +++ b/vs/codec_test.go @@ -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) diff --git a/vs/testdata/example.wat b/vs/testdata/example.wat index 03ed9485..4d4145b6 100644 --- a/vs/testdata/example.wat +++ b/vs/testdata/example.wat @@ -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)) ) diff --git a/vs/testdata/fac.wasm b/vs/testdata/fac.wasm index 32cd441e927bd1f27778b9016ef2daa5d8949d40..b178383fb81c9eeba6fb362570555afc589cd356 100644 GIT binary patch literal 137 zcmW-au?oUK5JYEoPft9=E3ATrg|+A}+(%gF2nr!w5UkyAcN2!;%``Kp-$nrJLQfO3 z-mT`OaV<0r3QCOBcO4-6n{IJ&rQHI0IHNdJGczr-@Rw2WJfJ(Kglm{ZHvTXvVpD*K9fWjX2L@5_;6=T